diff --git a/docs/contributors/principles/the-block.md b/docs/contributors/principles/the-block.md index 29e1486f8c1f8..bdb0c920c77c1 100644 --- a/docs/contributors/principles/the-block.md +++ b/docs/contributors/principles/the-block.md @@ -23,5 +23,5 @@ The placeholder content in the content area of the block can be thought of as a ### The block toolbar is the place for critical options that can’t be incorporated into placeholder UI. Basic block settings won’t always make sense in the context of the placeholder / content UI. As a secondary option, options that are critical to the functionality of a block can live in the block toolbar. The block toolbar is one step removed from direct manipulation, but is still highly contextual and visible on all screen sizes, so it is a great secondary option. -### The block sidebar should only be used for advanced, tertiary controls. -The sidebar is not visible by default on a small / mobile screen, and may also be collapsed even in a desktop view. Therefore, it should not be relied on for anything that is necessary for the basic operation of the block. Pick good defaults, make important actions available in the block toolbar, and think of the sidebar as something that only power users may discover. +### The Settings Sidebar should only be used for advanced, tertiary controls. +The Settings Sidebar is not visible by default on a small / mobile screen, and may also be collapsed even in a desktop view. Therefore, it should not be relied on for anything that is necessary for the basic operation of the block. Pick good defaults, make important actions available in the block toolbar, and think of the sidebar as something that only power users may discover. diff --git a/docs/designers-developers/designers/block-design.md b/docs/designers-developers/designers/block-design.md index 9bf9c0fefb841..cc99f4f74bd18 100644 --- a/docs/designers-developers/designers/block-design.md +++ b/docs/designers-developers/designers/block-design.md @@ -11,17 +11,17 @@ Since the block itself represents what will actually appear on the site, interac 1. The placeholder content in the content area of the block can be thought of as a guide or interface for users to follow a set of instructions or “fill in the blanks”. For example, a block that embeds content from a 3rd-party service might contain controls for signing in to that service in the placeholder. 2. After the user has added content, selecting the block can reveal additional controls to adjust or edit that content. For example, a signup block might reveal a control for hiding/showing subscriber count. However, this should be done in minimal ways, so as to avoid dramatically changing the size and display of a block when a user selects it (this could be disorienting or annoying). -### The block toolbar is a secondary place for required options & controls +### The Block Toolbar is a secondary place for required options & controls -Basic block settings won’t always make sense in the context of the placeholder/content UI. As a secondary option, options that are critical to the functionality of a block can live in the block toolbar. The block toolbar is still highly contextual and visible on all screen sizes. One notable constraint with the block toolbar is that it is icon-based UI, so any controls that live in the block toolbar need to be ones that can effectively be communicated via an icon or icon group. +Basic block settings won’t always make sense in the context of the placeholder/content UI. As a secondary option, options that are critical to the functionality of a block can live in the block toolbar. The Block Toolbar is still highly contextual and visible on all screen sizes. One notable constraint with the Block Toolbar is that it is icon-based UI, so any controls that live in the Block Toolbar need to be ones that can effectively be communicated via an icon or icon group. -### The block sidebar should only be used for advanced, tertiary controls +### The Settings Sidebar should only be used for advanced, tertiary controls -The sidebar is not visible by default on a small / mobile screen, and may also be collapsed in a desktop view. Therefore, it should not be relied on for anything that is necessary for the basic operation of the block. Pick good defaults, make important actions available in the block toolbar, and think of the sidebar as something that most users should not need to open. +The Settings Sidebar is not visible by default on a small / mobile screen, and may also be collapsed in a desktop view. Therefore, it should not be relied on for anything that is necessary for the basic operation of the block. Pick good defaults, make important actions available in the block toolbar, and think of the Settings Sidebar as something that most users should not need to open. -In addition, use sections and headers in the block sidebar if there are more than a handful of options, in order to allow users to easily scan and understand the options available. +In addition, use sections and headers in the Settings Sidebar if there are more than a handful of options, in order to allow users to easily scan and understand the options available. -Each block sidebar comes with an "Advanced" section by default. This area houses an "Additional CSS Class" field, and should be used to house other power user controls. +Each Settings Sidebar comes with an "Advanced" section by default. This area houses an "Additional CSS Class" field, and should be used to house other power user controls. ## Setup state vs. live preview state diff --git a/docs/designers-developers/developers/data/data-core-block-editor.md b/docs/designers-developers/developers/data/data-core-block-editor.md index 6e496446d6be2..1b42d76df3e3b 100644 --- a/docs/designers-developers/developers/data/data-core-block-editor.md +++ b/docs/designers-developers/developers/data/data-core-block-editor.md @@ -80,24 +80,6 @@ _Returns_ - `number`: Number of blocks in the post. -# **getBlockDependantsCacheBust** - -Returns a new reference when the inner blocks of a given block client ID -change. This is used exclusively as a memoized selector dependant, relying -on this selector's shared return value and recursively those of its inner -blocks defined as dependencies. This abuses mechanics of the selector -memoization to return from the original selector function only when -dependants change. - -_Parameters_ - -- _state_ `Object`: Editor state. -- _clientId_ `string`: Block client ID. - -_Returns_ - -- `*`: A value whose reference will change only when inner blocks of the given block client ID change. - # **getBlockHierarchyRootClientId** Given a block client ID, returns the root of the hierarchy from which the block is nested, return the block itself for root level blocks. diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md b/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbar-and-sidebar.md similarity index 89% rename from docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md rename to docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbar-and-sidebar.md index bfabb7379b40a..028b0e8e56a0f 100644 --- a/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md +++ b/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbar-and-sidebar.md @@ -1,8 +1,8 @@ -# Block Controls: Toolbars and Inspector +# Block Controls: Block Toolbar and Settings Sidebar To simplify block customization and ensure a consistent experience for users, there are a number of built-in UI patterns to help generate the editor preview. Like with the `RichText` component covered in the previous chapter, the `wp.editor` global includes a few other common components to render editing interfaces. In this chapter, we'll explore toolbars and the block inspector. -## Toolbar +## Block Toolbar ![Screenshot of the rich text toolbar applied to a paragraph block inside the block editor](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/assets/toolbar-text.png) @@ -169,10 +169,10 @@ Note that `BlockControls` is only visible when the block is currently selected a ![Screenshot of the inspector panel focused on the settings for a paragraph block](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/assets/inspector.png) -The inspector is used to display less-often-used settings or settings that require more screen space. The inspector should be used for **block-level settings only**. +The Settings Sidebar is used to display less-often-used settings or settings that require more screen space. The Settings Sidebar should be used for **block-level settings only**. -If you have settings that affects only selected content inside a block (example: the "bold" setting for selected text inside a paragraph): **do not place it inside the inspector**. The inspector is displayed even when editing a block in HTML mode, so it should only contain block-level settings. +If you have settings that affects only selected content inside a block (example: the "bold" setting for selected text inside a paragraph): **do not place it inside the Settings Sidebar**. The Settings Sidebar is displayed even when editing a block in HTML mode, so it should only contain block-level settings. -The inspector region is shown in place of the post settings sidebar when a block is selected. +The Block Tab is shown in place of the Document Tab when a block is selected. -Similar to rendering a toolbar, if you include an `InspectorControls` element in the return value of your block type's `edit` function, those controls will be shown in the inspector region. +Similar to rendering a toolbar, if you include an `InspectorControls` element in the return value of your block type's `edit` function, those controls will be shown in the Settings Sidebar region. diff --git a/docs/manifest-devhub.json b/docs/manifest-devhub.json index 94f2172703553..559d963aa2a49 100644 --- a/docs/manifest-devhub.json +++ b/docs/manifest-devhub.json @@ -426,9 +426,9 @@ "parent": "block-tutorial" }, { - "title": "Block Controls: Toolbars and Inspector", - "slug": "block-controls-toolbars-and-inspector", - "markdown_source": "../docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md", + "title": "Block Controls: Block Toolbar and Settings Sidebar", + "slug": "block-controls-toolbar-and-sidebar", + "markdown_source": "../docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbar-and-sidebar.md", "parent": "block-tutorial" }, { diff --git a/docs/manifest.json b/docs/manifest.json index 6cee0109f4749..03f85343b014a 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1056,9 +1056,9 @@ "parent": "block-tutorial" }, { - "title": "Block Controls: Toolbars and Inspector", - "slug": "block-controls-toolbars-and-inspector", - "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md", + "title": "Block Controls: Block Toolbar and Settings Sidebar", + "slug": "block-controls-toolbar-and-sidebar", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbar-and-sidebar.md", "parent": "block-tutorial" }, { diff --git a/docs/toc.json b/docs/toc.json index 4d0c1b38905e9..c26aec92dd5f8 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -82,7 +82,7 @@ { "docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md": [] }, { "docs/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets.md": [] }, { "docs/designers-developers/developers/tutorials/block-tutorial/introducing-attributes-and-editable-fields.md": [] }, - { "docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbars-and-inspector.md": [] }, + { "docs/designers-developers/developers/tutorials/block-tutorial/block-controls-toolbar-and-sidebar.md": [] }, { "docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md": [] }, { "docs/designers-developers/developers/tutorials/block-tutorial/generate-blocks-with-wp-cli.md": [] } ] }, diff --git a/gutenberg.php b/gutenberg.php index 977083ad62201..9ae6f7d37b412 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -3,7 +3,7 @@ * Plugin Name: Gutenberg * Plugin URI: https://github.com/WordPress/gutenberg * Description: Printing since 1440. This is the development plugin for the new block editor in core. - * Version: 6.0.0 + * Version: 6.1.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index 54148d3de9777..39f81f74f0aa5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "6.0.0", + "version": "6.1.0-rc.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 1087de7094828..6f3c286a1ef63 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "6.0.0", + "version": "6.1.0-rc.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", diff --git a/packages/block-editor/src/hooks/anchor.js b/packages/block-editor/src/hooks/anchor.js index 3f21ed3ef3b0a..3651346a96caf 100644 --- a/packages/block-editor/src/hooks/anchor.js +++ b/packages/block-editor/src/hooks/anchor.js @@ -7,7 +7,7 @@ import { assign, has } from 'lodash'; * WordPress dependencies */ import { addFilter } from '@wordpress/hooks'; -import { TextControl } from '@wordpress/components'; +import { TextControl, ExternalLink } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { hasBlockSupport } from '@wordpress/blocks'; import { createHigherOrderComponent } from '@wordpress/compose'; @@ -70,8 +70,17 @@ export const withInspectorControl = createHigherOrderComponent( ( BlockEdit ) => + { __( 'Enter a word or two — without spaces — to make a unique web address just for this heading, called an “anchor.” Then, you’ll be able to link directly to this section of your page.' ) } + + + { __( 'Learn more about anchors' ) } + + + ) } value={ props.attributes.anchor || '' } onChange={ ( nextValue ) => { nextValue = nextValue.replace( ANCHOR_REGEX, '-' ); diff --git a/packages/block-editor/src/hooks/anchor.scss b/packages/block-editor/src/hooks/anchor.scss new file mode 100644 index 0000000000000..5987b154f41a4 --- /dev/null +++ b/packages/block-editor/src/hooks/anchor.scss @@ -0,0 +1,4 @@ +.html-anchor-control .components-external-link { + display: block; + margin-top: $grid-size; +} diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index d9d0ecbdd28b6..b4cd52a08886e 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -13,6 +13,9 @@ import { isEqual, isEmpty, get, + identity, + difference, + omitBy, } from 'lodash'; /** @@ -54,6 +57,23 @@ function mapBlockOrder( blocks, rootClientId = '' ) { return result; } +/** + * Given an array of blocks, returns an object where each key contains + * the clientId of the block and the value is the parent of the block. + * + * @param {Array} blocks Blocks to map. + * @param {?string} rootClientId Assumed root client ID. + * + * @return {Object} Block order map object. + */ +function mapBlockParents( blocks, rootClientId = '' ) { + return blocks.reduce( ( result, block ) => Object.assign( + result, + { [ block.clientId ]: rootClientId }, + mapBlockParents( block.innerBlocks, block.clientId ) + ), {} ); +} + /** * Helper method to iterate through all blocks, recursing into inner blocks, * applying a transformation function to each one. @@ -64,7 +84,7 @@ function mapBlockOrder( blocks, rootClientId = '' ) { * * @return {Object} Flattened object. */ -function flattenBlocks( blocks, transform ) { +function flattenBlocks( blocks, transform = identity ) { const result = {}; const stack = [ ...blocks ]; @@ -175,6 +195,160 @@ export function isUpdatingSameBlockAttribute( action, lastAction ) { ); } +/** + * Higher-order reducer intended to reset the cache key of all blocks + * whenever the post meta values change. + * + * @param {Function} reducer Original reducer function. + * + * @return {Function} Enhanced reducer function. + */ +const withPostMetaUpdateCacheReset = ( reducer ) => ( state, action ) => { + const newState = reducer( state, action ); + const previousMetaValues = get( state, [ 'settings', '__experimentalMetaSource', 'value' ] ); + const nextMetaValues = get( newState.settings.__experimentalMetaSource, [ 'value' ] ); + // If post meta values change, reset the cache key for all blocks + if ( previousMetaValues !== nextMetaValues ) { + newState.blocks = { + ...newState.blocks, + cache: mapValues( newState.blocks.cache, () => ( {} ) ), + }; + } + + return newState; +}; + +/** + * Utility returning an object with an empty object value for each key. + * + * @param {Array} objectKeys Keys to fill. + * @return {Object} Object filled with empty object as values for each clientId. + */ +const fillKeysWithEmptyObject = ( objectKeys ) => { + return objectKeys.reduce( ( result, key ) => { + result[ key ] = {}; + return result; + }, {} ); +}; + +/** + * Higher-order reducer intended to compute a cache key for each block in the post. + * A new instance of the cache key (empty object) is created each time the block object + * needs to be refreshed (for any change in the block or its children). + * + * @param {Function} reducer Original reducer function. + * + * @return {Function} Enhanced reducer function. + */ +const withBlockCache = ( reducer ) => ( state = {}, action ) => { + const newState = reducer( state, action ); + + if ( newState === state ) { + return state; + } + newState.cache = state.cache ? state.cache : {}; + + const getBlocksWithParentsClientIds = ( clientIds ) => { + return clientIds.reduce( ( result, clientId ) => { + let current = clientId; + do { + result.push( current ); + current = state.parents[ current ]; + } while ( current ); + return result; + }, [] ); + }; + + switch ( action.type ) { + case 'RESET_BLOCKS': + newState.cache = mapValues( flattenBlocks( action.blocks ), () => ( {} ) ); + break; + case 'RECEIVE_BLOCKS': + case 'INSERT_BLOCKS': { + const updatedBlockUids = keys( flattenBlocks( action.blocks ) ); + if ( action.rootClientId ) { + updatedBlockUids.push( action.rootClientId ); + } + newState.cache = { + ...newState.cache, + ...fillKeysWithEmptyObject( + getBlocksWithParentsClientIds( updatedBlockUids ), + ), + }; + break; + } + case 'UPDATE_BLOCK': + case 'UPDATE_BLOCK_ATTRIBUTES': + newState.cache = { + ...newState.cache, + ...fillKeysWithEmptyObject( + getBlocksWithParentsClientIds( [ action.clientId ] ), + ), + }; + break; + case 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN': + newState.cache = { + ...omit( newState.cache, action.replacedClientIds ), + ...fillKeysWithEmptyObject( + getBlocksWithParentsClientIds( keys( flattenBlocks( action.blocks ) ) ), + ), + }; + break; + case 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN': + newState.cache = { + ...omit( newState.cache, action.removedClientIds ), + ...fillKeysWithEmptyObject( + difference( getBlocksWithParentsClientIds( action.clientIds ), action.clientIds ), + ), + }; + break; + case 'MOVE_BLOCK_TO_POSITION': { + const updatedBlockUids = [ action.clientId ]; + if ( action.fromRootClientId ) { + updatedBlockUids.push( action.fromRootClientId ); + } + if ( action.toRootClientId ) { + updatedBlockUids.push( action.toRootClientId ); + } + newState.cache = { + ...newState.cache, + ...fillKeysWithEmptyObject( + getBlocksWithParentsClientIds( updatedBlockUids ) + ), + }; + break; + } + case 'MOVE_BLOCKS_UP': + case 'MOVE_BLOCKS_DOWN': { + const updatedBlockUids = []; + if ( action.rootClientId ) { + updatedBlockUids.push( action.rootClientId ); + } + newState.cache = { + ...newState.cache, + ...fillKeysWithEmptyObject( + getBlocksWithParentsClientIds( updatedBlockUids ) + ), + }; + break; + } + case 'SAVE_REUSABLE_BLOCK_SUCCESS': { + const updatedBlockUids = keys( omitBy( newState.attributes, ( attributes, clientId ) => { + return newState.byClientId[ clientId ].name !== 'core/block' || attributes.ref !== action.updatedId; + } ) ); + + newState.cache = { + ...newState.cache, + ...fillKeysWithEmptyObject( + getBlocksWithParentsClientIds( updatedBlockUids ) + ), + }; + } + } + + return newState; +}; + /** * Higher-order reducer intended to augment the blocks reducer, assigning an * `isPersistentChange` property value corresponding to whether a change in @@ -264,16 +438,39 @@ function withIgnoredBlockChange( reducer ) { * @return {Function} Enhanced reducer function. */ const withInnerBlocksRemoveCascade = ( reducer ) => ( state, action ) => { - if ( state && action.type === 'REMOVE_BLOCKS' ) { - const clientIds = [ ...action.clientIds ]; + const getAllChildren = ( clientIds ) => { + let result = clientIds; + for ( let i = 0; i < result.length; i++ ) { + if ( ! state.order[ result[ i ] ] ) { + continue; + } - // For each removed client ID, include its inner blocks to remove, - // recursing into those so long as inner blocks exist. - for ( let i = 0; i < clientIds.length; i++ ) { - clientIds.push( ...state.order[ clientIds[ i ] ] ); + if ( result === clientIds ) { + result = [ ...result ]; + } + + result.push( ...state.order[ result[ i ] ] ); } + return result; + }; - action = { ...action, clientIds }; + if ( state ) { + switch ( action.type ) { + case 'REMOVE_BLOCKS': + action = { + ...action, + type: 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN', + removedClientIds: getAllChildren( action.clientIds ), + }; + break; + case 'REPLACE_BLOCKS': + action = { + ...action, + type: 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN', + replacedClientIds: getAllChildren( action.clientIds ), + }; + break; + } } return reducer( state, action ); @@ -306,6 +503,14 @@ const withBlockReset = ( reducer ) => ( state, action ) => { ...omit( state.order, visibleClientIds ), ...mapBlockOrder( action.blocks ), }, + parents: { + ...omit( state.parents, visibleClientIds ), + ...mapBlockParents( action.blocks ), + }, + cache: { + ...omit( state.cache, visibleClientIds ), + ...mapValues( flattenBlocks( action.blocks ), () => ( {} ) ), + }, }; } @@ -391,10 +596,11 @@ const withSaveReusableBlock = ( reducer ) => ( state, action ) => { */ export const blocks = flow( combineReducers, + withSaveReusableBlock, // needs to be before withBlockCache + withBlockCache, // needs to be before withInnerBlocksRemoveCascade withInnerBlocksRemoveCascade, withReplaceInnerBlocks, // needs to be after withInnerBlocksRemoveCascade withBlockReset, - withSaveReusableBlock, withPersistentBlockChange, withIgnoredBlockChange, )( { @@ -435,18 +641,18 @@ export const blocks = flow( ...getFlattenedBlocksWithoutAttributes( action.blocks ), }; - case 'REPLACE_BLOCKS': + case 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN': if ( ! action.blocks ) { return state; } return { - ...omit( state, action.clientIds ), + ...omit( state, action.replacedClientIds ), ...getFlattenedBlocksWithoutAttributes( action.blocks ), }; - case 'REMOVE_BLOCKS': - return omit( state, action.clientIds ); + case 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN': + return omit( state, action.removedClientIds ); } return state; @@ -511,18 +717,18 @@ export const blocks = flow( ...getFlattenedBlockAttributes( action.blocks ), }; - case 'REPLACE_BLOCKS': + case 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN': if ( ! action.blocks ) { return state; } return { - ...omit( state, action.clientIds ), + ...omit( state, action.replacedClientIds ), ...getFlattenedBlockAttributes( action.blocks ), }; - case 'REMOVE_BLOCKS': - return omit( state, action.clientIds ); + case 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN': + return omit( state, action.removedClientIds ); } return state; @@ -609,7 +815,7 @@ export const blocks = flow( }; } - case 'REPLACE_BLOCKS': { + case 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN': { const { clientIds } = action; if ( ! action.blocks ) { return state; @@ -618,7 +824,7 @@ export const blocks = flow( const mappedBlocks = mapBlockOrder( action.blocks ); return flow( [ - ( nextState ) => omit( nextState, clientIds ), + ( nextState ) => omit( nextState, action.replacedClientIds ), ( nextState ) => ( { ...nextState, ...omit( mappedBlocks, '' ), @@ -642,20 +848,59 @@ export const blocks = flow( ] )( state ); } - case 'REMOVE_BLOCKS': + case 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN': return flow( [ // Remove inner block ordering for removed blocks - ( nextState ) => omit( nextState, action.clientIds ), + ( nextState ) => omit( nextState, action.removedClientIds ), // Remove deleted blocks from other blocks' orderings ( nextState ) => mapValues( nextState, ( subState ) => ( - without( subState, ...action.clientIds ) + without( subState, ...action.removedClientIds ) ) ), ] )( state ); } return state; }, + + // While technically redundant data as the inverse of `order`, it serves as + // an optimization for the selectors which derive the ancestry of a block. + parents( state = {}, action ) { + switch ( action.type ) { + case 'RESET_BLOCKS': + return mapBlockParents( action.blocks ); + + case 'RECEIVE_BLOCKS': + return { + ...state, + ...mapBlockParents( action.blocks ), + }; + + case 'INSERT_BLOCKS': + return { + ...state, + ...mapBlockParents( action.blocks, action.rootClientId || '' ), + }; + + case 'MOVE_BLOCK_TO_POSITION': { + return { + ...state, + [ action.clientId ]: action.toRootClientId || '', + }; + } + + case 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN': + return { + ...omit( state, action.replacedClientIds ), + ...mapBlockParents( action.blocks, state[ action.clientIds[ 0 ] ] ), + }; + + case 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN': + return omit( state, action.removedClientIds ); + } + + return state; + }, } ); /** @@ -983,15 +1228,17 @@ export const blockListSettings = ( state = {}, action ) => { return state; }; -export default combineReducers( { - blocks, - isTyping, - isCaretWithinFormattedText, - blockSelection, - blocksMode, - blockListSettings, - insertionPoint, - template, - settings, - preferences, -} ); +export default withPostMetaUpdateCacheReset( + combineReducers( { + blocks, + isTyping, + isCaretWithinFormattedText, + blockSelection, + blocksMode, + blockListSettings, + insertionPoint, + template, + settings, + preferences, + } ) +); diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 6633611fbd9b1..85a7442cd8613 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -72,28 +72,6 @@ const EMPTY_ARRAY = []; */ const EMPTY_OBJECT = {}; -/** - * Returns a new reference when the inner blocks of a given block client ID - * change. This is used exclusively as a memoized selector dependant, relying - * on this selector's shared return value and recursively those of its inner - * blocks defined as dependencies. This abuses mechanics of the selector - * memoization to return from the original selector function only when - * dependants change. - * - * @param {Object} state Editor state. - * @param {string} clientId Block client ID. - * - * @return {*} A value whose reference will change only when inner blocks of - * the given block client ID change. - */ -export const getBlockDependantsCacheBust = createSelector( - () => [], - ( state, clientId ) => map( - getBlockOrder( state, clientId ), - ( innerBlockClientId ) => getBlock( state, innerBlockClientId ), - ), -); - /** * Returns a block's name given its client ID, or null if no block exists with * the client ID. @@ -192,8 +170,12 @@ export const getBlock = createSelector( }; }, ( state, clientId ) => [ - ...getBlockAttributes.getDependants( state, clientId ), - getBlockDependantsCacheBust( state, clientId ), + // Normally, we'd have both `getBlockAttributes` dependancies and + // `getBlocks` (children) dependancies here but for performance reasons + // we use a denormalized cache key computed in the reducer that takes both + // the attributes and inner blocks into account. The value of the cache key + // is being changed whenever one of these dependencies is out of date. + state.blocks.cache[ clientId ], ] ); @@ -458,22 +440,11 @@ export function getSelectedBlock( state ) { * * @return {?string} Root client ID, if exists */ -export const getBlockRootClientId = createSelector( - ( state, clientId ) => { - const { order } = state.blocks; - - for ( const rootClientId in order ) { - if ( includes( order[ rootClientId ], clientId ) ) { - return rootClientId; - } - } - - return null; - }, - ( state ) => [ - state.blocks.order, - ] -); +export function getBlockRootClientId( state, clientId ) { + return state.blocks.parents[ clientId ] !== undefined ? + state.blocks.parents[ clientId ] : + null; +} /** * Given a block client ID, returns the root of the hierarchy from which the block is nested, return the block itself for root level blocks. @@ -483,21 +454,15 @@ export const getBlockRootClientId = createSelector( * * @return {string} Root client ID */ -export const getBlockHierarchyRootClientId = createSelector( - ( state, clientId ) => { - let rootClientId = clientId; - let current = clientId; - while ( rootClientId ) { - current = rootClientId; - rootClientId = getBlockRootClientId( state, current ); - } - - return current; - }, - ( state ) => [ - state.blocks.order, - ] -); +export function getBlockHierarchyRootClientId( state, clientId ) { + let current = clientId; + let parent; + do { + parent = current; + current = state.blocks.parents[ current ]; + } while ( current ); + return parent; +} /** * Returns the client ID of the block adjacent one at the given reference diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js index 7543fdad57dd8..99f0ecb16282a 100644 --- a/packages/block-editor/src/store/test/reducer.js +++ b/packages/block-editor/src/store/test/reducer.js @@ -193,27 +193,35 @@ describe( 'state', () => { it( 'can replace a child block', () => { const existingState = deepFreeze( { byClientId: { - clicken: { + chicken: { clientId: 'chicken', name: 'core/test-parent-block', isValid: true, }, - 'clicken-child': { + 'chicken-child': { clientId: 'chicken-child', name: 'core/test-child-block', isValid: true, }, }, attributes: { - clicken: {}, - 'clicken-child': { + chicken: {}, + 'chicken-child': { attr: true, }, }, order: { - '': [ 'clicken' ], - clicken: [ 'clicken-child' ], - 'clicken-child': [], + '': [ 'chicken' ], + chicken: [ 'chicken-child' ], + 'chicken-child': [], + }, + parents: { + chicken: '', + 'chicken-child': 'chicken', + }, + cache: { + chicken: {}, + 'chicken-child': {}, }, } ); @@ -226,7 +234,7 @@ describe( 'state', () => { const action = { type: 'REPLACE_INNER_BLOCKS', - rootClientId: 'clicken', + rootClientId: 'chicken', blocks: [ newChildBlock ], }; @@ -236,7 +244,7 @@ describe( 'state', () => { isPersistentChange: true, isIgnoredChange: false, byClientId: { - clicken: { + chicken: { clientId: 'chicken', name: 'core/test-parent-block', isValid: true, @@ -248,35 +256,50 @@ describe( 'state', () => { }, }, attributes: { - clicken: {}, + chicken: {}, [ newChildBlockId ]: { attr: false, attr2: 'perfect', }, }, order: { - '': [ 'clicken' ], - clicken: [ newChildBlockId ], + '': [ 'chicken' ], + chicken: [ newChildBlockId ], [ newChildBlockId ]: [], }, + parents: { + [ newChildBlockId ]: 'chicken', + chicken: '', + }, + cache: { + chicken: {}, + [ newChildBlockId ]: {}, + }, } ); + expect( state.cache.chicken ).not.toBe( existingState.cache.chicken ); } ); it( 'can insert a child block', () => { const existingState = deepFreeze( { byClientId: { - clicken: { + chicken: { clientId: 'chicken', name: 'core/test-parent-block', isValid: true, }, }, attributes: { - clicken: {}, + chicken: {}, }, order: { - '': [ 'clicken' ], - clicken: [], + '': [ 'chicken' ], + chicken: [], + }, + parents: { + chicken: '', + }, + cache: { + chicken: {}, }, } ); @@ -289,7 +312,7 @@ describe( 'state', () => { const action = { type: 'REPLACE_INNER_BLOCKS', - rootClientId: 'clicken', + rootClientId: 'chicken', blocks: [ newChildBlock ], }; @@ -299,7 +322,7 @@ describe( 'state', () => { isPersistentChange: true, isIgnoredChange: false, byClientId: { - clicken: { + chicken: { clientId: 'chicken', name: 'core/test-parent-block', isValid: true, @@ -311,53 +334,72 @@ describe( 'state', () => { }, }, attributes: { - clicken: {}, + chicken: {}, [ newChildBlockId ]: { attr: false, attr2: 'perfect', }, }, order: { - '': [ 'clicken' ], - clicken: [ newChildBlockId ], + '': [ 'chicken' ], + chicken: [ newChildBlockId ], [ newChildBlockId ]: [], }, + parents: { + [ newChildBlockId ]: 'chicken', + chicken: '', + }, + cache: { + chicken: {}, + [ newChildBlockId ]: {}, + }, } ); + expect( state.cache.chicken ).not.toBe( existingState.cache.chicken ); } ); it( 'can replace multiple child blocks', () => { const existingState = deepFreeze( { byClientId: { - clicken: { + chicken: { clientId: 'chicken', name: 'core/test-parent-block', isValid: true, }, - 'clicken-child': { + 'chicken-child': { clientId: 'chicken-child', name: 'core/test-child-block', isValid: true, }, - 'clicken-child-2': { + 'chicken-child-2': { clientId: 'chicken-child', name: 'core/test-child-block', isValid: true, }, }, attributes: { - clicken: {}, - 'clicken-child': { + chicken: {}, + 'chicken-child': { attr: true, }, - 'clicken-child-2': { + 'chicken-child-2': { attr2: 'ok', }, }, order: { - '': [ 'clicken' ], - clicken: [ 'clicken-child', 'clicken-child-2' ], - 'clicken-child': [], - 'clicken-child-2': [], + '': [ 'chicken' ], + chicken: [ 'chicken-child', 'chicken-child-2' ], + 'chicken-child': [], + 'chicken-child-2': [], + }, + parents: { + chicken: '', + 'chicken-child': 'chicken', + 'chicken-child-2': 'chicken', + }, + cache: { + chicken: {}, + 'chicken-child': {}, + 'chicken-child-2': {}, }, } ); @@ -381,7 +423,7 @@ describe( 'state', () => { const action = { type: 'REPLACE_INNER_BLOCKS', - rootClientId: 'clicken', + rootClientId: 'chicken', blocks: [ newChildBlock1, newChildBlock2, newChildBlock3 ], }; @@ -391,7 +433,7 @@ describe( 'state', () => { isPersistentChange: true, isIgnoredChange: false, byClientId: { - clicken: { + chicken: { clientId: 'chicken', name: 'core/test-parent-block', isValid: true, @@ -413,7 +455,7 @@ describe( 'state', () => { }, }, attributes: { - clicken: {}, + chicken: {}, [ newChildBlockId1 ]: { attr: false, attr2: 'perfect', @@ -427,44 +469,66 @@ describe( 'state', () => { }, }, order: { - '': [ 'clicken' ], - clicken: [ newChildBlockId1, newChildBlockId2, newChildBlockId3 ], + '': [ 'chicken' ], + chicken: [ newChildBlockId1, newChildBlockId2, newChildBlockId3 ], [ newChildBlockId1 ]: [], [ newChildBlockId2 ]: [], [ newChildBlockId3 ]: [], }, + parents: { + chicken: '', + [ newChildBlockId1 ]: 'chicken', + [ newChildBlockId2 ]: 'chicken', + [ newChildBlockId3 ]: 'chicken', + }, + cache: { + chicken: {}, + [ newChildBlockId1 ]: {}, + [ newChildBlockId2 ]: {}, + [ newChildBlockId3 ]: {}, + }, } ); } ); it( 'can replace a child block that has other children', () => { const existingState = deepFreeze( { byClientId: { - clicken: { + chicken: { clientId: 'chicken', name: 'core/test-parent-block', isValid: true, }, - 'clicken-child': { + 'chicken-child': { clientId: 'chicken-child', name: 'core/test-child-block', isValid: true, }, - 'clicken-grand-child': { + 'chicken-grand-child': { clientId: 'chicken-child', name: 'core/test-block', isValid: true, }, }, attributes: { - clicken: {}, - 'clicken-child': {}, - 'clicken-grand-child': {}, + chicken: {}, + 'chicken-child': {}, + 'chicken-grand-child': {}, }, order: { - '': [ 'clicken' ], - clicken: [ 'clicken-child' ], - 'clicken-child': [ 'clicken-grand-child' ], - 'clicken-grand-child': [], + '': [ 'chicken' ], + chicken: [ 'chicken-child' ], + 'chicken-child': [ 'chicken-grand-child' ], + 'chicken-grand-child': [], + }, + parents: { + chicken: '', + 'chicken-child': 'chicken', + 'chicken-grand-child': 'chicken-child', + }, + cache: { + chicken: {}, + 'chicken-child': {}, + 'chicken-grand-child': {}, }, } ); @@ -474,7 +538,7 @@ describe( 'state', () => { const action = { type: 'REPLACE_INNER_BLOCKS', - rootClientId: 'clicken', + rootClientId: 'chicken', blocks: [ newChildBlock ], }; @@ -484,7 +548,7 @@ describe( 'state', () => { isPersistentChange: true, isIgnoredChange: false, byClientId: { - clicken: { + chicken: { clientId: 'chicken', name: 'core/test-parent-block', isValid: true, @@ -496,15 +560,26 @@ describe( 'state', () => { }, }, attributes: { - clicken: {}, + chicken: {}, [ newChildBlockId ]: {}, }, order: { - '': [ 'clicken' ], - clicken: [ newChildBlockId ], + '': [ 'chicken' ], + chicken: [ newChildBlockId ], [ newChildBlockId ]: [], }, + parents: { + chicken: '', + [ newChildBlockId ]: 'chicken', + }, + cache: { + chicken: {}, + [ newChildBlockId ]: {}, + }, } ); + + // the cache key of the parent should be updated + expect( existingState.cache.chicken ).not.toBe( state.cache.chicken ); } ); } ); @@ -515,8 +590,10 @@ describe( 'state', () => { byClientId: {}, attributes: {}, order: {}, + parents: {}, isPersistentChange: true, isIgnoredChange: false, + cache: {}, } ); } ); @@ -536,6 +613,9 @@ describe( 'state', () => { '': [ 'bananas' ], bananas: [], } ); + expect( state.cache ).toEqual( { + bananas: {}, + } ); } ); } ); @@ -555,6 +635,10 @@ describe( 'state', () => { apples: [], bananas: [ 'apples' ], } ); + expect( state.cache ).toEqual( { + bananas: {}, + apples: {}, + } ); } ); it( 'should insert block', () => { @@ -583,6 +667,12 @@ describe( 'state', () => { chicken: [], ribs: [], } ); + expect( state.cache ).toEqual( { + chicken: {}, + ribs: {}, + } ); + // The cache key is the same because the block has not been modified. + expect( original.cache.chicken ).toBe( state.cache.chicken ); } ); it( 'should replace the block', () => { @@ -612,6 +702,51 @@ describe( 'state', () => { '': [ 'wings' ], wings: [], } ); + expect( state.parents ).toEqual( { + wings: '', + } ); + expect( state.cache ).toEqual( { + wings: {}, + } ); + } ); + it( 'should replace the block and remove references to its inner blocks', () => { + const original = blocks( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + clientId: 'chicken', + name: 'core/test-block', + attributes: {}, + innerBlocks: [ + { + clientId: 'child', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], + }, + ], + } ], + } ); + const state = blocks( original, { + type: 'REPLACE_BLOCKS', + clientIds: [ 'chicken' ], + blocks: [ { + clientId: 'wings', + name: 'core/freeform', + innerBlocks: [], + } ], + } ); + + expect( Object.keys( state.byClientId ) ).toHaveLength( 1 ); + expect( state.order ).toEqual( { + '': [ 'wings' ], + wings: [], + } ); + expect( state.parents ).toEqual( { + wings: '', + } ); + expect( state.cache ).toEqual( { + wings: {}, + } ); } ); it( 'should replace the nested block', () => { @@ -634,6 +769,14 @@ describe( 'state', () => { [ wrapperBlock.clientId ]: [ replacementBlock.clientId ], [ replacementBlock.clientId ]: [], } ); + expect( state.parents ).toEqual( { + [ wrapperBlock.clientId ]: '', + [ replacementBlock.clientId ]: wrapperBlock.clientId, + } ); + expect( state.cache ).toEqual( { + [ wrapperBlock.clientId ]: {}, + [ replacementBlock.clientId ]: {}, + } ); } ); it( 'should replace the block even if the new block clientId is the same', () => { @@ -664,6 +807,10 @@ describe( 'state', () => { '': [ 'chicken' ], chicken: [], } ); + expect( replacedState.cache ).toEqual( { + chicken: {}, + } ); + expect( originalState.cache.chicken ).not.toBe( replacedState.cache.chicken ); const nestedBlock = { clientId: 'chicken', @@ -729,6 +876,11 @@ describe( 'state', () => { expect( state.attributes.chicken ).toEqual( { content: 'ribs', } ); + + expect( state.cache ).toEqual( { + chicken: {}, + } ); + expect( state.cache.chicken ).not.toBe( original.cache.chicken ); } ); it( 'should update the reusable block reference if the temporary id is swapped', () => { @@ -760,6 +912,10 @@ describe( 'state', () => { expect( state.attributes.chicken ).toEqual( { ref: 3, } ); + expect( state.cache ).toEqual( { + chicken: {}, + } ); + expect( state.cache.chicken ).not.toBe( original.cache.chicken ); } ); it( 'should move the block up', () => { @@ -783,6 +939,8 @@ describe( 'state', () => { } ); expect( state.order[ '' ] ).toEqual( [ 'ribs', 'chicken' ] ); + expect( state.cache.ribs ).toBe( original.cache.ribs ); + expect( state.cache.chicken ).toBe( original.cache.chicken ); } ); it( 'should move the nested block up', () => { @@ -805,6 +963,9 @@ describe( 'state', () => { [ movedBlock.clientId ]: [], [ siblingBlock.clientId ]: [], } ); + expect( state.cache[ wrapperBlock.clientId ] ).not.toBe( original.cache[ wrapperBlock.clientId ] ); + expect( state.cache[ movedBlock.clientId ] ).toBe( original.cache[ movedBlock.clientId ] ); + expect( state.cache[ siblingBlock.clientId ] ).toBe( original.cache[ siblingBlock.clientId ] ); } ); it( 'should move multiple blocks up', () => { @@ -1024,6 +1185,9 @@ describe( 'state', () => { expect( state.order[ '' ] ).toEqual( [ 'ribs' ] ); expect( state.order ).not.toHaveProperty( 'chicken' ); + expect( state.parents ).toEqual( { + ribs: '', + } ); expect( state.byClientId ).toEqual( { ribs: { clientId: 'ribs', @@ -1033,6 +1197,9 @@ describe( 'state', () => { expect( state.attributes ).toEqual( { ribs: {}, } ); + expect( state.cache ).toEqual( { + ribs: {}, + } ); } ); it( 'should remove multiple blocks', () => { @@ -1063,6 +1230,9 @@ describe( 'state', () => { expect( state.order[ '' ] ).toEqual( [ 'ribs' ] ); expect( state.order ).not.toHaveProperty( 'chicken' ); expect( state.order ).not.toHaveProperty( 'veggies' ); + expect( state.parents ).toEqual( { + ribs: '', + } ); expect( state.byClientId ).toEqual( { ribs: { clientId: 'ribs', @@ -1095,6 +1265,7 @@ describe( 'state', () => { expect( state.order ).toEqual( { '': [], } ); + expect( state.parents ).toEqual( {} ); } ); it( 'should insert at the specified index', () => { @@ -1521,7 +1692,8 @@ describe( 'state', () => { describe( 'isIgnoredChange', () => { it( 'should consider received blocks as ignored change', () => { - const state = blocks( undefined, { + const resetState = blocks( undefined, { type: 'random action' } ); + const state = blocks( resetState, { type: 'RECEIVE_BLOCKS', blocks: [ { clientId: 'kumquat', diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index c038a40621eff..7bc0390317ece 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -19,7 +19,6 @@ import { RawHTML } from '@wordpress/element'; import * as selectors from '../selectors'; const { - getBlockDependantsCacheBust, getBlockName, getBlock, getBlocks, @@ -136,235 +135,6 @@ describe( 'selectors', () => { setFreeformContentHandlerName( undefined ); } ); - describe( 'getBlockDependantsCacheBust', () => { - const rootBlock = { clientId: 123, name: 'core/paragraph' }; - const rootBlockAttributes = {}; - const rootOrder = [ 123 ]; - - it( 'returns an unchanging reference', () => { - const rootBlockOrder = []; - - const state = { - blocks: { - byClientId: { - 123: rootBlock, - }, - attributes: { - 123: rootBlockAttributes, - }, - order: { - '': rootOrder, - 123: rootBlockOrder, - }, - }, - }; - - const nextState = { - blocks: { - byClientId: { - 123: rootBlock, - }, - attributes: { - 123: rootBlockAttributes, - }, - order: { - '': rootOrder, - 123: rootBlockOrder, - }, - }, - }; - - expect( - getBlockDependantsCacheBust( state, 123 ) - ).toBe( getBlockDependantsCacheBust( nextState, 123 ) ); - } ); - - it( 'returns a new reference on added inner block', () => { - const state = { - blocks: { - byClientId: { - 123: rootBlock, - }, - attributes: { - 123: rootBlockAttributes, - }, - order: { - '': rootOrder, - 123: [], - }, - }, - }; - - const nextState = { - blocks: { - byClientId: { - 123: rootBlock, - 456: { clientId: 456, name: 'core/paragraph' }, - }, - attributes: { - 123: rootBlockAttributes, - 456: {}, - }, - order: { - '': rootOrder, - 123: [ 456 ], - 456: [], - }, - }, - }; - - expect( - getBlockDependantsCacheBust( state, 123 ) - ).not.toBe( getBlockDependantsCacheBust( nextState, 123 ) ); - } ); - - it( 'returns an unchanging reference on unchanging inner block', () => { - const rootBlockOrder = [ 456 ]; - const childBlock = { clientId: 456, name: 'core/paragraph' }; - const childBlockAttributes = {}; - const childBlockOrder = []; - - const state = { - blocks: { - byClientId: { - 123: rootBlock, - 456: childBlock, - }, - attributes: { - 123: rootBlockAttributes, - 456: childBlockAttributes, - }, - order: { - '': rootOrder, - 123: rootBlockOrder, - 456: childBlockOrder, - }, - }, - }; - - const nextState = { - blocks: { - byClientId: { - 123: rootBlock, - 456: childBlock, - }, - attributes: { - 123: rootBlockAttributes, - 456: childBlockAttributes, - }, - order: { - '': rootOrder, - 123: rootBlockOrder, - 456: childBlockOrder, - }, - }, - }; - - expect( - getBlockDependantsCacheBust( state, 123 ) - ).toBe( getBlockDependantsCacheBust( nextState, 123 ) ); - } ); - - it( 'returns a new reference on updated inner block', () => { - const rootBlockOrder = [ 456 ]; - const childBlockOrder = []; - - const state = { - blocks: { - byClientId: { - 123: rootBlock, - 456: { clientId: 456, name: 'core/paragraph' }, - }, - attributes: { - 123: rootBlockAttributes, - 456: {}, - }, - order: { - '': rootOrder, - 123: rootBlockOrder, - 456: childBlockOrder, - }, - }, - }; - - const nextState = { - blocks: { - byClientId: { - 123: rootBlock, - 456: { clientId: 456, name: 'core/paragraph' }, - }, - attributes: { - 123: rootBlockAttributes, - 456: { content: [ 'foo' ] }, - }, - order: { - '': rootOrder, - 123: rootBlockOrder, - 456: childBlockOrder, - }, - }, - }; - - expect( - getBlockDependantsCacheBust( state, 123 ) - ).not.toBe( getBlockDependantsCacheBust( nextState, 123 ) ); - } ); - - it( 'returns a new reference on updated grandchild inner block', () => { - const rootBlockOrder = [ 456 ]; - const childBlock = { clientId: 456, name: 'core/paragraph' }; - const childBlockAttributes = {}; - const childBlockOrder = [ 789 ]; - const grandChildBlockOrder = []; - - const state = { - blocks: { - byClientId: { - 123: rootBlock, - 456: childBlock, - 789: { clientId: 789, name: 'core/paragraph' }, - }, - attributes: { - 123: rootBlockAttributes, - 456: childBlockAttributes, - 789: {}, - }, - order: { - '': rootOrder, - 123: rootBlockOrder, - 456: childBlockOrder, - 789: grandChildBlockOrder, - }, - }, - }; - - const nextState = { - blocks: { - byClientId: { - 123: rootBlock, - 456: childBlock, - 789: { clientId: 789, name: 'core/paragraph' }, - }, - attributes: { - 123: rootBlockAttributes, - 456: childBlockAttributes, - 789: { content: [ 'foo' ] }, - }, - order: { - '': rootOrder, - 123: rootBlockOrder, - 456: childBlockOrder, - 789: grandChildBlockOrder, - }, - }, - }; - - expect( - getBlockDependantsCacheBust( state, 123 ) - ).not.toBe( getBlockDependantsCacheBust( nextState, 123 ) ); - } ); - } ); - describe( 'getBlockName', () => { it( 'returns null if no block by clientId', () => { const state = { @@ -372,6 +142,7 @@ describe( 'selectors', () => { byClientId: {}, attributes: {}, order: {}, + parents: {}, }, }; @@ -396,6 +167,9 @@ describe( 'selectors', () => { '': [ 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ], 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': [], }, + parents: { + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': '', + }, }, }; @@ -419,6 +193,12 @@ describe( 'selectors', () => { '': [ 123 ], 123: [], }, + parents: { + 123: '', + }, + cache: { + 123: {}, + }, }, }; @@ -436,6 +216,8 @@ describe( 'selectors', () => { byClientId: {}, attributes: {}, order: {}, + parents: {}, + cache: {}, }, }; @@ -458,6 +240,14 @@ describe( 'selectors', () => { 123: [ 456 ], 456: [], }, + parents: { + 123: '', + 456: 123, + }, + cache: { + 123: {}, + 456: {}, + }, }, }; @@ -507,6 +297,12 @@ describe( 'selectors', () => { '': [ 123 ], 123: [], }, + parents: { + 123: '', + }, + cache: { + 123: {}, + }, }, }; @@ -538,6 +334,14 @@ describe( 'selectors', () => { order: { '': [ 123, 23 ], }, + parents: { + 123: '', + 23: '', + }, + cache: { + 123: {}, + 23: {}, + }, }, }; @@ -603,6 +407,20 @@ describe( 'selectors', () => { 'uuid-26': [ ], 'uuid-28': [ 'uuid-30' ], }, + parents: { + 'uuid-6': '', + 'uuid-8': '', + 'uuid-10': '', + 'uuid-22': '', + 'uuid-12': 'uuid-10', + 'uuid-14': 'uuid-10', + 'uuid-16': 'uuid-12', + 'uuid-18': 'uuid-14', + 'uuid-24': 'uuid-18', + 'uuid-26': 'uuid-24', + 'uuid-28': 'uuid-24', + 'uuid-30': 'uuid-28', + }, }, }; expect( getClientIdsOfDescendants( state, [ 'uuid-10' ] ) ).toEqual( [ @@ -673,6 +491,20 @@ describe( 'selectors', () => { 'uuid-26': [ ], 'uuid-28': [ 'uuid-30' ], }, + parents: { + 'uuid-6': '', + 'uuid-8': '', + 'uuid-10': '', + 'uuid-22': '', + 'uuid-12': 'uuid-10', + 'uuid-14': 'uuid-10', + 'uuid-16': 'uuid-12', + 'uuid-18': 'uuid-14', + 'uuid-24': 'uuid-18', + 'uuid-26': 'uuid-24', + 'uuid-28': 'uuid-24', + 'uuid-30': 'uuid-28', + }, }, }; expect( getClientIdsWithDescendants( state ) ).toEqual( [ @@ -730,6 +562,11 @@ describe( 'selectors', () => { '': [ 123 ], 123: [ 456, 789 ], }, + parents: { + 123: '', + 456: 123, + 789: 123, + }, }, }; @@ -788,6 +625,10 @@ describe( 'selectors', () => { order: { '': [ 123, 456 ], }, + parents: { + 123: '', + 456: '', + }, }, }; @@ -805,6 +646,7 @@ describe( 'selectors', () => { byClientId: {}, attributes: {}, order: {}, + parents: {}, }, }; expect( getGlobalBlockCount( emptyState ) ).toBe( 0 ); @@ -862,6 +704,10 @@ describe( 'selectors', () => { 23: [], 123: [], }, + parents: { + 23: '', + 123: '', + }, }, blockSelection: { start: {}, end: {} }, }; @@ -885,6 +731,10 @@ describe( 'selectors', () => { 23: [], 123: [], }, + parents: { + 123: '', + 23: '', + }, }, blockSelection: { start: { clientId: 23 }, end: { clientId: 123 } }, }; @@ -908,6 +758,13 @@ describe( 'selectors', () => { 23: [], 123: [], }, + parents: { + 123: '', + 23: '', + }, + cache: { + 23: {}, + }, }, blockSelection: { start: { clientId: 23 }, end: { clientId: 23 } }, }; @@ -926,6 +783,7 @@ describe( 'selectors', () => { const state = { blocks: { order: {}, + parents: {}, }, }; @@ -939,10 +797,16 @@ describe( 'selectors', () => { '': [ 123, 23 ], 123: [ 456, 56 ], }, + parents: { + 123: '', + 23: '', + 456: 123, + 56: 123, + }, }, }; - expect( getBlockRootClientId( state, 56 ) ).toBe( '123' ); + expect( getBlockRootClientId( state, 56 ) ).toBe( 123 ); } ); } ); @@ -951,37 +815,51 @@ describe( 'selectors', () => { const state = { blocks: { order: {}, + parents: {}, }, }; - expect( getBlockHierarchyRootClientId( state, 56 ) ).toBe( 56 ); + expect( getBlockHierarchyRootClientId( state, '56' ) ).toBe( '56' ); } ); it( 'should return root ClientId relative the block ClientId', () => { const state = { blocks: { order: { - '': [ 123, 23 ], - 123: [ 456, 56 ], + '': [ 'a', 'b' ], + a: [ 'c', 'd' ], + }, + parents: { + a: '', + b: '', + c: 'a', + d: 'a', }, }, }; - expect( getBlockHierarchyRootClientId( state, 56 ) ).toBe( '123' ); + expect( getBlockHierarchyRootClientId( state, 'c' ) ).toBe( 'a' ); } ); it( 'should return the top level root ClientId relative the block ClientId', () => { const state = { blocks: { order: { - '': [ '123', '23' ], - 123: [ '456', '56' ], - 56: [ '12' ], + '': [ 'a', 'b' ], + a: [ 'c', 'd' ], + d: [ 'e' ], + }, + parents: { + a: '', + b: '', + c: 'a', + d: 'a', + e: 'd', }, }, }; - expect( getBlockHierarchyRootClientId( state, '12' ) ).toBe( '123' ); + expect( getBlockHierarchyRootClientId( state, 'e' ) ).toBe( 'a' ); } ); } ); @@ -992,6 +870,10 @@ describe( 'selectors', () => { order: { '': [ 123, 23 ], }, + parents: { + 123: '', + 23: '', + }, }, blockSelection: { start: {}, end: {} }, }; @@ -1005,6 +887,13 @@ describe( 'selectors', () => { order: { '': [ 5, 4, 3, 2, 1 ], }, + parents: { + 1: '', + 2: '', + 3: '', + 4: '', + 5: '', + }, }, blockSelection: { start: { clientId: 2 }, end: { clientId: 2 } }, }; @@ -1018,6 +907,13 @@ describe( 'selectors', () => { order: { '': [ 5, 4, 3, 2, 1 ], }, + parents: { + 1: '', + 2: '', + 3: '', + 4: '', + 5: '', + }, }, blockSelection: { start: { clientId: 2 }, end: { clientId: 4 } }, }; @@ -1032,6 +928,17 @@ describe( 'selectors', () => { '': [ 5, 4, 3, 2, 1 ], 4: [ 9, 8, 7, 6 ], }, + parents: { + 1: '', + 2: '', + 3: '', + 4: '', + 5: '', + 6: 4, + 7: 4, + 8: 4, + 9: 4, + }, }, blockSelection: { start: { clientId: 7 }, end: { clientId: 9 } }, }; @@ -1047,6 +954,10 @@ describe( 'selectors', () => { order: { '': [ 123, 23 ], }, + parents: { + 23: '', + 123: '', + }, }, blockSelection: { start: {}, end: {} }, }; @@ -1060,6 +971,13 @@ describe( 'selectors', () => { order: { '': [ 5, 4, 3, 2, 1 ], }, + parents: { + 1: '', + 2: '', + 3: '', + 4: '', + 5: '', + }, }, blockSelection: { start: { clientId: 2 }, end: { clientId: 4 } }, }; @@ -1074,6 +992,17 @@ describe( 'selectors', () => { '': [ 5, 4, 3, 2, 1 ], 4: [ 9, 8, 7, 6 ], }, + parents: { + 1: '', + 2: '', + 3: '', + 4: '', + 5: '', + 6: 4, + 7: 4, + 8: 4, + 9: 4, + }, }, blockSelection: { start: { clientId: 7 }, end: { clientId: 9 } }, }; @@ -1089,6 +1018,7 @@ describe( 'selectors', () => { byClientId: {}, attributes: {}, order: {}, + parents: {}, }, blockSelection: { start: {}, end: {} }, }; @@ -1142,6 +1072,10 @@ describe( 'selectors', () => { order: { '': [ 123, 23 ], }, + parents: { + 23: '', + 123: '', + }, }, }; @@ -1155,6 +1089,11 @@ describe( 'selectors', () => { '': [ 123, 23 ], 123: [ 456 ], }, + parents: { + 23: '', + 123: '', + 456: 123, + }, }, }; @@ -1169,6 +1108,10 @@ describe( 'selectors', () => { order: { '': [ 123, 23 ], }, + parents: { + 23: '', + 123: '', + }, }, }; @@ -1182,6 +1125,12 @@ describe( 'selectors', () => { '': [ 123, 23 ], 123: [ 456, 56 ], }, + parents: { + 23: '', + 123: '', + 56: 123, + 456: 123, + }, }, }; @@ -1196,6 +1145,10 @@ describe( 'selectors', () => { order: { '': [ 123, 23 ], }, + parents: { + 23: '', + 123: '', + }, }, }; @@ -1209,6 +1162,12 @@ describe( 'selectors', () => { '': [ 123, 23 ], 123: [ 456, 56 ], }, + parents: { + 23: '', + 123: '', + 456: 123, + 56: 123, + }, }, }; @@ -1221,6 +1180,10 @@ describe( 'selectors', () => { order: { '': [ 123, 23 ], }, + parents: { + 23: '', + 123: '', + }, }, }; @@ -1234,6 +1197,12 @@ describe( 'selectors', () => { '': [ 123, 23 ], 123: [ 456, 56 ], }, + parents: { + 23: '', + 123: '', + 456: 123, + 56: 123, + }, }, }; @@ -1248,6 +1217,10 @@ describe( 'selectors', () => { order: { '': [ 123, 23 ], }, + parents: { + 23: '', + 123: '', + }, }, }; @@ -1261,6 +1234,12 @@ describe( 'selectors', () => { '': [ 123, 23 ], 123: [ 456, 56 ], }, + parents: { + 23: '', + 123: '', + 456: 123, + 56: 123, + }, }, }; @@ -1273,6 +1252,10 @@ describe( 'selectors', () => { order: { '': [ 123, 23 ], }, + parents: { + 23: '', + 123: '', + }, }, }; @@ -1286,6 +1269,12 @@ describe( 'selectors', () => { '': [ 123, 23 ], 123: [ 456, 56 ], }, + parents: { + 23: '', + 123: '', + 456: 123, + 56: 123, + }, }, }; @@ -1327,6 +1316,11 @@ describe( 'selectors', () => { order: { 4: [ 3, 2, 1 ], }, + parents: { + 1: 4, + 2: 4, + 3: 4, + }, }, }; @@ -1340,6 +1334,11 @@ describe( 'selectors', () => { order: { 4: [ 3, 2, 1 ], }, + parents: { + 1: 4, + 2: 4, + 3: 4, + }, }, }; @@ -1352,6 +1351,13 @@ describe( 'selectors', () => { order: { 6: [ 5, 4, 3, 2, 1 ], }, + parents: { + 1: 6, + 2: 6, + 3: 6, + 4: 6, + 5: 6, + }, }, blockSelection: { start: { clientId: 2 }, end: { clientId: 4 } }, }; @@ -1365,6 +1371,12 @@ describe( 'selectors', () => { 3: [ 2, 1 ], 6: [ 5, 4 ], }, + parents: { + 1: 3, + 2: 3, + 4: 6, + 5: 6, + }, }, blockSelection: { start: { clientId: 5 }, end: { clientId: 4 } }, }; @@ -1380,6 +1392,13 @@ describe( 'selectors', () => { order: { '': [ 5, 4, 3, 2, 1 ], }, + parents: { + 1: '', + 2: '', + 3: '', + 4: '', + 5: '', + }, }, }; @@ -1393,6 +1412,13 @@ describe( 'selectors', () => { order: { '': [ 5, 4, 3, 2, 1 ], }, + parents: { + 1: '', + 2: '', + 3: '', + 4: '', + 5: '', + }, }, }; @@ -1406,6 +1432,13 @@ describe( 'selectors', () => { order: { '': [ 5, 4, 3, 2, 1 ], }, + parents: { + 1: '', + 2: '', + 3: '', + 4: '', + 5: '', + }, }, }; @@ -1419,6 +1452,13 @@ describe( 'selectors', () => { order: { '': [ 5, 4, 3, 2, 1 ], }, + parents: { + 1: '', + 2: '', + 3: '', + 4: '', + 5: '', + }, }, }; @@ -1467,6 +1507,13 @@ describe( 'selectors', () => { order: { '': [ 5, 4, 3, 2, 1 ], }, + parents: { + 1: '', + 2: '', + 3: '', + 4: '', + 5: '', + }, }, blockSelection: { start: { clientId: 2 }, end: { clientId: 4 } }, }; @@ -1486,6 +1533,13 @@ describe( 'selectors', () => { order: { '': [ 5, 4, 3, 2, 1 ], }, + parents: { + 1: '', + 2: '', + 3: '', + 4: '', + 5: '', + }, }, blockSelection: { start: { clientId: 2 }, end: { clientId: 4 } }, }; @@ -1598,6 +1652,10 @@ describe( 'selectors', () => { clientId1: [ 'clientId2' ], clientId2: [], }, + parents: { + clientId1: '', + clientId2: 'clientId1', + }, }, insertionPoint: { rootClientId: undefined, @@ -1628,6 +1686,9 @@ describe( 'selectors', () => { '': [ 'clientId1' ], clientId1: [], }, + parents: { + clientId1: '', + }, }, insertionPoint: null, }; @@ -1658,6 +1719,10 @@ describe( 'selectors', () => { clientId1: [ 'clientId2' ], clientId2: [], }, + parents: { + clientId1: '', + clientId2: 'clientId1', + }, }, insertionPoint: null, }; @@ -1688,6 +1753,10 @@ describe( 'selectors', () => { clientId1: [], clientId2: [], }, + parents: { + clientId1: '', + clientId2: '', + }, }, insertionPoint: null, }; @@ -1718,6 +1787,10 @@ describe( 'selectors', () => { clientId1: [], clientId2: [], }, + parents: { + clientId1: '', + clientId2: '', + }, }, insertionPoint: null, }; @@ -1921,6 +1994,8 @@ describe( 'selectors', () => { block1: {}, }, order: {}, + parents: {}, + cache: {}, }, settings: { __experimentalReusableBlocks: [ @@ -1991,6 +2066,12 @@ describe( 'selectors', () => { order: { '': [ 'block1ref' ], }, + parents: { + block1ref: '', + }, + cache: { + block1ref: {}, + }, }, settings: { __experimentalReusableBlocks: [ @@ -2051,6 +2132,16 @@ describe( 'selectors', () => { referredBlock2: [ 'childReferredBlock2' ], childReferredBlock2: [ 'grandchildReferredBlock2' ], }, + parents: { + block2ref: '', + childReferredBlock2: 'referredBlock2', + grandchildReferredBlock2: 'childReferredBlock2', + }, + cache: { + block2ref: {}, + childReferredBlock2: {}, + grandchildReferredBlock2: {}, + }, }, settings: { @@ -2094,6 +2185,8 @@ describe( 'selectors', () => { block2: {}, }, order: {}, + parents: {}, + cache: {}, }, settings: { __experimentalReusableBlocks: [ @@ -2137,6 +2230,16 @@ describe( 'selectors', () => { order: { '': [ 'block3', 'block4' ], }, + parents: { + block3: '', + block4: '', + }, + cache: { + block1: {}, + block2: {}, + block3: {}, + block4: {}, + }, }, settings: { __experimentalReusableBlocks: [ @@ -2196,6 +2299,9 @@ describe( 'selectors', () => { order: { '': [ 'block1' ], }, + cache: { + block1: {}, + }, }, preferences: { insertUsage: {}, @@ -2214,6 +2320,8 @@ describe( 'selectors', () => { byClientId: {}, attributes: {}, order: {}, + parents: {}, + cache: {}, }, preferences: { insertUsage: {}, @@ -2232,6 +2340,8 @@ describe( 'selectors', () => { byClientId: {}, attributes: {}, order: {}, + parents: {}, + cache: {}, }, preferences: { insertUsage: { @@ -2259,6 +2369,12 @@ describe( 'selectors', () => { order: { '': [ 'block1' ], }, + parents: { + block1: '', + }, + cache: { + block1: {}, + }, }, preferences: { insertUsage: {}, diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 23d70816397da..8083cf0cbc6bf 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -32,3 +32,4 @@ @import "./components/url-popover/style.scss"; @import "./components/warning/style.scss"; @import "./components/writing-flow/style.scss"; +@import "./hooks/anchor.scss"; diff --git a/packages/blocks/src/api/index.native.js b/packages/blocks/src/api/index.native.js index b99fccc531c21..3b3be8f28c3a4 100644 --- a/packages/blocks/src/api/index.native.js +++ b/packages/blocks/src/api/index.native.js @@ -1,4 +1,5 @@ export { + cloneBlock, createBlock, switchToBlockType, } from './factory'; diff --git a/packages/edit-post/src/components/sidebar/settings-header/index.js b/packages/edit-post/src/components/sidebar/settings-header/index.js index eeb95a872166f..04132d6f2664e 100644 --- a/packages/edit-post/src/components/sidebar/settings-header/index.js +++ b/packages/edit-post/src/components/sidebar/settings-header/index.js @@ -18,9 +18,9 @@ const SettingsHeader = ( { openDocumentSettings, openBlockSettings, sidebarName [ __( 'Document' ), '' ]; const [ blockAriaLabel, blockActiveClass ] = sidebarName === 'edit-post/block' ? - // translators: ARIA label for the Block sidebar tab, selected. + // translators: ARIA label for the Settings Sidebar tab, selected. [ __( 'Block (selected)' ), 'is-active' ] : - // translators: ARIA label for the Block sidebar tab, not selected. + // translators: ARIA label for the Settings Sidebar tab, not selected. [ __( 'Block' ), '' ]; return ( diff --git a/packages/editor/src/components/post-permalink/style.scss b/packages/editor/src/components/post-permalink/style.scss index 5b13e75843994..0162eb164cccc 100644 --- a/packages/editor/src/components/post-permalink/style.scss +++ b/packages/editor/src/components/post-permalink/style.scss @@ -1,8 +1,9 @@ .editor-post-permalink { display: inline-flex; align-items: center; + flex-wrap: wrap; background: $white; - padding: 5px; + padding: $grid-size $grid-size 0; font-family: $default-font; font-size: $default-font-size; height: 40px; @@ -25,11 +26,21 @@ // Put toolbar snugly to edge on mobile. margin-left: -$block-padding - $border-width; // This hides the border off the edge of the screen. margin-right: -$block-padding - $border-width; + @include break-mobile() { + padding: $grid-size-small; + } @include break-small() { margin-left: -$border-width; margin-right: -$border-width; } + // Increase specificity to override margins set on label element. + &.editor-post-permalink > * { + margin-bottom: $grid-size; + @include break-mobile() { + margin-bottom: 0; + } + } button { // Prevent button shrinking in IE11 when other items have a 100% flex basis. // This should be safe to apply in all browsers because we don't want these @@ -56,7 +67,7 @@ color: $dark-gray-200; text-decoration: underline; margin-right: 10px; - width: 100%; + flex-grow: 1; overflow: hidden; position: relative; white-space: nowrap; diff --git a/packages/editor/src/components/post-title/style.scss b/packages/editor/src/components/post-title/style.scss index 8f758e9947ae7..3bf8c5ad9f321 100644 --- a/packages/editor/src/components/post-title/style.scss +++ b/packages/editor/src/components/post-title/style.scss @@ -106,11 +106,19 @@ .editor-post-title .editor-post-permalink { font-size: $default-font-size; color: $dark-gray-900; - position: absolute; - top: -$block-toolbar-height + $border-width + $border-width + 1px; // Shift this element upward the same height as the block toolbar, minus the border size - left: 0; - right: 0; - + height: auto; + position: relative; + left: $block-left-border-width; + top: -2px; + width: calc(100% - #{$block-left-border-width}); + + @include break-mobile() { + position: absolute; + top: -$block-toolbar-height + $border-width + $border-width + 1px; // Shift this element upward the same height as the block toolbar, minus the border size + right: 0; + flex-wrap: nowrap; + width: auto; + } @include break-small() { left: $block-side-ui-clearance; right: $block-side-ui-clearance;