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 03929e3671685d..fb3f337dd59cd2 100644 --- a/docs/designers-developers/developers/data/data-core-block-editor.md +++ b/docs/designers-developers/developers/data/data-core-block-editor.md @@ -980,6 +980,17 @@ specified client ID is to be removed. * selectPrevious: True if the previous block should be selected when a block is removed. +### replaceInnerBlocks + +Returns an action object used in signalling that the inner blocks with the +specified client ID should be replaced. + +*Parameters* + + * rootClientId: Client ID of the block whose InnerBlocks will re replaced. + * blocks: Block objects to insert as new InnerBlocks + * updateSelection: If true block selection will be updated. If false, block selection will not change. Defaults to true. + ### toggleBlockMode Returns an action object used to toggle the block editing mode between diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index a2a1bc6e6e628c..8fe1c600857a0b 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -420,6 +420,26 @@ export function removeBlock( clientId, selectPrevious ) { return removeBlocks( [ clientId ], selectPrevious ); } +/** + * Returns an action object used in signalling that the inner blocks with the + * specified client ID should be replaced. + * + * @param {string} rootClientId Client ID of the block whose InnerBlocks will re replaced. + * @param {Object[]} blocks Block objects to insert as new InnerBlocks + * @param {?boolean} updateSelection If true block selection will be updated. If false, block selection will not change. Defaults to true. + * + * @return {Object} Action object. + */ +export function replaceInnerBlocks( rootClientId, blocks, updateSelection ) { + return { + type: 'REPLACE_INNER_BLOCKS', + rootClientId, + blocks, + updateSelection, + time: Date.now(), + }; +} + /** * Returns an action object used to toggle the block editing mode between * visual and HTML modes. diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 007c5f4a8f6d4b..51fa67ce82ba9b 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -296,6 +296,38 @@ const withBlockReset = ( reducer ) => ( state, action ) => { return reducer( state, action ); }; +/** + * Higher-order reducer which targets the combined blocks reducer and handles + * the `REPLACE_INNER_BLOCKS` action. When dispatched, this action the state should become equivalent + * to the execution of a `REMOVE_BLOCKS` action containing all the child's of the root block followed by + * the execution of `INSERT_BLOCKS` with the new blocks. + * + * @param {Function} reducer Original reducer function. + * + * @return {Function} Enhanced reducer function. + */ +const withReplaceInnerBlocks = ( reducer ) => ( state, action ) => { + if ( action.type !== 'REPLACE_INNER_BLOCKS' ) { + return reducer( state, action ); + } + let stateAfterBlocksRemoval = state; + if ( state.order[ action.rootClientId ] ) { + stateAfterBlocksRemoval = reducer( stateAfterBlocksRemoval, { + type: 'REMOVE_BLOCKS', + clientIds: state.order[ action.rootClientId ], + } ); + } + let stateAfterInsert = stateAfterBlocksRemoval; + if ( action.blocks.length ) { + stateAfterInsert = reducer( stateAfterInsert, { + ...action, + type: 'INSERT_BLOCKS', + index: 0, + } ); + } + return stateAfterInsert; +}; + /** * Higher-order reducer which targets the combined blocks reducer and handles * the `SAVE_REUSABLE_BLOCK_SUCCESS` action. This action can't be handled by @@ -343,6 +375,7 @@ const withSaveReusableBlock = ( reducer ) => ( state, action ) => { */ export const blocks = flow( combineReducers, + withReplaceInnerBlocks, // needs to be before withInnerBlocksRemoveCascade withInnerBlocksRemoveCascade, withBlockReset, withSaveReusableBlock, @@ -713,6 +746,7 @@ export function blockSelection( state = { end: action.clientId, initialPosition: action.initialPosition, }; + case 'REPLACE_INNER_BLOCKS': // REPLACE_INNER_BLOCKS and INSERT_BLOCKS should follow the same logic. case 'INSERT_BLOCKS': { if ( action.updateSelection ) { return { diff --git a/packages/editor/src/components/inner-blocks/index.js b/packages/editor/src/components/inner-blocks/index.js index eba0122e688561..b7a81e48e11c09 100644 --- a/packages/editor/src/components/inner-blocks/index.js +++ b/packages/editor/src/components/inner-blocks/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { pick, isEqual, map } from 'lodash'; +import { pick, isEqual } from 'lodash'; import classnames from 'classnames'; /** @@ -144,20 +144,14 @@ InnerBlocks = compose( [ } ), withDispatch( ( dispatch, ownProps ) => { const { - replaceBlocks, - insertBlocks, + replaceInnerBlocks, updateBlockListSettings, } = dispatch( 'core/block-editor' ); const { block, clientId, templateInsertUpdatesSelection = true } = ownProps; return { replaceInnerBlocks( blocks ) { - const clientIds = map( block.innerBlocks, 'clientId' ); - if ( clientIds.length ) { - replaceBlocks( clientIds, blocks ); - } else { - insertBlocks( blocks, undefined, clientId, templateInsertUpdatesSelection ); - } + replaceInnerBlocks( clientId, blocks, block.innerBlocks.length === 0 && templateInsertUpdatesSelection ); }, updateNestedSettings( settings ) { dispatch( updateBlockListSettings( clientId, settings ) );