UNPKG

@wordpress/block-editor

Version:
4 lines 124 kB
{ "version": 3, "sources": ["../../src/store/reducer.js"], "sourcesContent": ["/**\n * External dependencies\n */\nimport fastDeepEqual from 'fast-deep-equal/es6/index.js';\n\n/**\n * WordPress dependencies\n */\nimport { pipe } from '@wordpress/compose';\nimport { combineReducers, select } from '@wordpress/data';\nimport deprecated from '@wordpress/deprecated';\nimport {\n\tstore as blocksStore,\n\tprivateApis as blocksPrivateApis,\n} from '@wordpress/blocks';\n\n/**\n * Internal dependencies\n */\nimport { PREFERENCES_DEFAULTS, SETTINGS_DEFAULTS } from './defaults';\nimport { insertAt, moveTo } from './array';\nimport { sectionRootClientIdKey, isIsolatedEditorKey } from './private-keys';\nimport { unlock } from '../lock-unlock';\n\nconst { isContentBlock } = unlock( blocksPrivateApis );\n\nconst identity = ( x ) => x;\n\n/**\n * Given an array of blocks, returns an object where each key is a nesting\n * context, the value of which is an array of block client IDs existing within\n * that nesting context.\n *\n * @param {Array} blocks Blocks to map.\n * @param {?string} rootClientId Assumed root client ID.\n *\n * @return {Object} Block order map object.\n */\nfunction mapBlockOrder( blocks, rootClientId = '' ) {\n\tconst result = new Map();\n\tconst current = [];\n\tresult.set( rootClientId, current );\n\tblocks.forEach( ( block ) => {\n\t\tconst { clientId, innerBlocks } = block;\n\t\tcurrent.push( clientId );\n\t\tmapBlockOrder( innerBlocks, clientId ).forEach(\n\t\t\t( order, subClientId ) => {\n\t\t\t\tresult.set( subClientId, order );\n\t\t\t}\n\t\t);\n\t} );\n\treturn result;\n}\n\n/**\n * Given an array of blocks, returns an object where each key contains\n * the clientId of the block and the value is the parent of the block.\n *\n * @param {Array} blocks Blocks to map.\n * @param {?string} rootClientId Assumed root client ID.\n *\n * @return {Object} Block order map object.\n */\nfunction mapBlockParents( blocks, rootClientId = '' ) {\n\tconst result = [];\n\tconst stack = [ [ rootClientId, blocks ] ];\n\twhile ( stack.length ) {\n\t\tconst [ parent, currentBlocks ] = stack.shift();\n\t\tcurrentBlocks.forEach( ( { innerBlocks, ...block } ) => {\n\t\t\tresult.push( [ block.clientId, parent ] );\n\t\t\tif ( innerBlocks?.length ) {\n\t\t\t\tstack.push( [ block.clientId, innerBlocks ] );\n\t\t\t}\n\t\t} );\n\t}\n\treturn result;\n}\n\n/**\n * Helper method to iterate through all blocks, recursing into inner blocks,\n * applying a transformation function to each one.\n * Returns a flattened object with the transformed blocks.\n *\n * @param {Array} blocks Blocks to flatten.\n * @param {Function} transform Transforming function to be applied to each block.\n *\n * @return {Array} Flattened object.\n */\nfunction flattenBlocks( blocks, transform = identity ) {\n\tconst result = [];\n\n\tconst stack = [ ...blocks ];\n\twhile ( stack.length ) {\n\t\tconst { innerBlocks, ...block } = stack.shift();\n\t\tstack.push( ...innerBlocks );\n\t\tresult.push( [ block.clientId, transform( block ) ] );\n\t}\n\n\treturn result;\n}\n\nfunction getFlattenedClientIds( blocks ) {\n\tconst result = {};\n\tconst stack = [ ...blocks ];\n\twhile ( stack.length ) {\n\t\tconst { innerBlocks, ...block } = stack.shift();\n\t\tstack.push( ...innerBlocks );\n\t\tresult[ block.clientId ] = true;\n\t}\n\n\treturn result;\n}\n\n/**\n * Given an array of blocks, returns an object containing all blocks, without\n * attributes, recursing into inner blocks. Keys correspond to the block client\n * ID, the value of which is the attributes object.\n *\n * @param {Array} blocks Blocks to flatten.\n *\n * @return {Array} Flattened block attributes object.\n */\nfunction getFlattenedBlocksWithoutAttributes( blocks ) {\n\treturn flattenBlocks( blocks, ( block ) => {\n\t\tconst { attributes, ...restBlock } = block;\n\t\treturn restBlock;\n\t} );\n}\n\n/**\n * Given an array of blocks, returns an object containing all block attributes,\n * recursing into inner blocks. Keys correspond to the block client ID, the\n * value of which is the attributes object.\n *\n * @param {Array} blocks Blocks to flatten.\n *\n * @return {Array} Flattened block attributes object.\n */\nfunction getFlattenedBlockAttributes( blocks ) {\n\treturn flattenBlocks( blocks, ( block ) => block.attributes );\n}\n\n/**\n * Returns true if the two object arguments have the same keys, or false\n * otherwise.\n *\n * @param {Object} a First object.\n * @param {Object} b Second object.\n *\n * @return {boolean} Whether the two objects have the same keys.\n */\nexport function hasSameKeys( a, b ) {\n\treturn fastDeepEqual( Object.keys( a ), Object.keys( b ) );\n}\n\n/**\n * Returns true if, given the currently dispatching action and the previously\n * dispatched action, the two actions are updating the same block attribute, or\n * false otherwise.\n *\n * @param {Object} action Currently dispatching action.\n * @param {Object} lastAction Previously dispatched action.\n *\n * @return {boolean} Whether actions are updating the same block attribute.\n */\nexport function isUpdatingSameBlockAttribute( action, lastAction ) {\n\treturn (\n\t\taction.type === 'UPDATE_BLOCK_ATTRIBUTES' &&\n\t\tlastAction !== undefined &&\n\t\tlastAction.type === 'UPDATE_BLOCK_ATTRIBUTES' &&\n\t\tfastDeepEqual( action.clientIds, lastAction.clientIds ) &&\n\t\thasSameKeys( action.attributes, lastAction.attributes )\n\t);\n}\n\nfunction updateBlockTreeForBlocks( state, blocks ) {\n\tconst treeToUpdate = state.tree;\n\tconst stack = [ ...blocks ];\n\tconst flattenedBlocks = [ ...blocks ];\n\twhile ( stack.length ) {\n\t\tconst block = stack.shift();\n\t\tstack.push( ...block.innerBlocks );\n\t\tflattenedBlocks.push( ...block.innerBlocks );\n\t}\n\t// Create objects before mutating them, that way it's always defined.\n\tfor ( const block of flattenedBlocks ) {\n\t\ttreeToUpdate.set( block.clientId, {} );\n\t}\n\tfor ( const block of flattenedBlocks ) {\n\t\ttreeToUpdate.set(\n\t\t\tblock.clientId,\n\t\t\tObject.assign( treeToUpdate.get( block.clientId ), {\n\t\t\t\t...state.byClientId.get( block.clientId ),\n\t\t\t\tattributes: state.attributes.get( block.clientId ),\n\t\t\t\tinnerBlocks: block.innerBlocks.map( ( subBlock ) =>\n\t\t\t\t\ttreeToUpdate.get( subBlock.clientId )\n\t\t\t\t),\n\t\t\t} )\n\t\t);\n\t}\n}\n\nfunction updateParentInnerBlocksInTree(\n\tstate,\n\tupdatedClientIds,\n\tupdateChildrenOfUpdatedClientIds = false\n) {\n\tconst treeToUpdate = state.tree;\n\tconst uncontrolledParents = new Set( [] );\n\tconst controlledParents = new Set();\n\tfor ( const clientId of updatedClientIds ) {\n\t\tlet current = updateChildrenOfUpdatedClientIds\n\t\t\t? clientId\n\t\t\t: state.parents.get( clientId );\n\t\tdo {\n\t\t\tif ( state.controlledInnerBlocks[ current ] ) {\n\t\t\t\t// Should stop on controlled blocks.\n\t\t\t\t// If we reach a controlled parent, break out of the loop.\n\t\t\t\tcontrolledParents.add( current );\n\t\t\t\tbreak;\n\t\t\t} else {\n\t\t\t\t// Else continue traversing up through parents.\n\t\t\t\tuncontrolledParents.add( current );\n\t\t\t\tcurrent = state.parents.get( current );\n\t\t\t}\n\t\t} while ( current !== undefined );\n\t}\n\n\t// To make sure the order of assignments doesn't matter,\n\t// we first create empty objects and mutates the inner blocks later.\n\tfor ( const clientId of uncontrolledParents ) {\n\t\ttreeToUpdate.set( clientId, { ...treeToUpdate.get( clientId ) } );\n\t}\n\tfor ( const clientId of uncontrolledParents ) {\n\t\ttreeToUpdate.get( clientId ).innerBlocks = (\n\t\t\tstate.order.get( clientId ) || []\n\t\t).map( ( subClientId ) => treeToUpdate.get( subClientId ) );\n\t}\n\n\t// Controlled parent blocks, need a dedicated key for their inner blocks\n\t// to be used when doing getBlocks( controlledBlockClientId ).\n\tfor ( const clientId of controlledParents ) {\n\t\ttreeToUpdate.set( 'controlled||' + clientId, {\n\t\t\tinnerBlocks: ( state.order.get( clientId ) || [] ).map(\n\t\t\t\t( subClientId ) => treeToUpdate.get( subClientId )\n\t\t\t),\n\t\t} );\n\t}\n}\n\n/**\n * Higher-order reducer intended to compute full block objects key for each block in the post.\n * This is a denormalization to optimize the performance of the getBlock selectors and avoid\n * recomputing the block objects and avoid heavy memoization.\n *\n * @param {Function} reducer Original reducer function.\n *\n * @return {Function} Enhanced reducer function.\n */\nconst withBlockTree =\n\t( reducer ) =>\n\t( state = {}, action ) => {\n\t\tconst newState = reducer( state, action );\n\n\t\tif ( newState === state ) {\n\t\t\treturn state;\n\t\t}\n\n\t\tnewState.tree = state.tree ? state.tree : new Map();\n\t\tswitch ( action.type ) {\n\t\t\tcase 'RECEIVE_BLOCKS':\n\t\t\tcase 'INSERT_BLOCKS': {\n\t\t\t\tnewState.tree = new Map( newState.tree );\n\t\t\t\tupdateBlockTreeForBlocks( newState, action.blocks );\n\t\t\t\tupdateParentInnerBlocksInTree(\n\t\t\t\t\tnewState,\n\t\t\t\t\taction.rootClientId ? [ action.rootClientId ] : [ '' ],\n\t\t\t\t\ttrue\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase 'UPDATE_BLOCK':\n\t\t\t\tnewState.tree = new Map( newState.tree );\n\t\t\t\tnewState.tree.set( action.clientId, {\n\t\t\t\t\t...newState.tree.get( action.clientId ),\n\t\t\t\t\t...newState.byClientId.get( action.clientId ),\n\t\t\t\t\tattributes: newState.attributes.get( action.clientId ),\n\t\t\t\t} );\n\t\t\t\tupdateParentInnerBlocksInTree(\n\t\t\t\t\tnewState,\n\t\t\t\t\t[ action.clientId ],\n\t\t\t\t\tfalse\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase 'SYNC_DERIVED_BLOCK_ATTRIBUTES':\n\t\t\tcase 'UPDATE_BLOCK_ATTRIBUTES': {\n\t\t\t\tnewState.tree = new Map( newState.tree );\n\t\t\t\taction.clientIds.forEach( ( clientId ) => {\n\t\t\t\t\tnewState.tree.set( clientId, {\n\t\t\t\t\t\t...newState.tree.get( clientId ),\n\t\t\t\t\t\tattributes: newState.attributes.get( clientId ),\n\t\t\t\t\t} );\n\t\t\t\t} );\n\t\t\t\tupdateParentInnerBlocksInTree(\n\t\t\t\t\tnewState,\n\t\t\t\t\taction.clientIds,\n\t\t\t\t\tfalse\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN': {\n\t\t\t\tconst inserterClientIds = getFlattenedClientIds(\n\t\t\t\t\taction.blocks\n\t\t\t\t);\n\t\t\t\tnewState.tree = new Map( newState.tree );\n\t\t\t\taction.replacedClientIds.forEach( ( clientId ) => {\n\t\t\t\t\tnewState.tree.delete( clientId );\n\t\t\t\t\t// Controlled inner blocks are only removed\n\t\t\t\t\t// if the block doesn't move to another position\n\t\t\t\t\t// otherwise their content will be lost.\n\t\t\t\t\tif ( ! inserterClientIds[ clientId ] ) {\n\t\t\t\t\t\tnewState.tree.delete( 'controlled||' + clientId );\n\t\t\t\t\t}\n\t\t\t\t} );\n\n\t\t\t\tupdateBlockTreeForBlocks( newState, action.blocks );\n\t\t\t\tupdateParentInnerBlocksInTree(\n\t\t\t\t\tnewState,\n\t\t\t\t\taction.blocks.map( ( b ) => b.clientId ),\n\t\t\t\t\tfalse\n\t\t\t\t);\n\n\t\t\t\t// If there are no replaced blocks, it means we're removing blocks so we need to update their parent.\n\t\t\t\tconst parentsOfRemovedBlocks = [];\n\t\t\t\tfor ( const clientId of action.clientIds ) {\n\t\t\t\t\tconst parentId = state.parents.get( clientId );\n\t\t\t\t\tif (\n\t\t\t\t\t\tparentId !== undefined &&\n\t\t\t\t\t\t( parentId === '' ||\n\t\t\t\t\t\t\tnewState.byClientId.get( parentId ) )\n\t\t\t\t\t) {\n\t\t\t\t\t\tparentsOfRemovedBlocks.push( parentId );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tupdateParentInnerBlocksInTree(\n\t\t\t\t\tnewState,\n\t\t\t\t\tparentsOfRemovedBlocks,\n\t\t\t\t\ttrue\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN':\n\t\t\t\tconst parentsOfRemovedBlocks = [];\n\t\t\t\tfor ( const clientId of action.clientIds ) {\n\t\t\t\t\tconst parentId = state.parents.get( clientId );\n\t\t\t\t\tif (\n\t\t\t\t\t\tparentId !== undefined &&\n\t\t\t\t\t\t( parentId === '' ||\n\t\t\t\t\t\t\tnewState.byClientId.get( parentId ) )\n\t\t\t\t\t) {\n\t\t\t\t\t\tparentsOfRemovedBlocks.push( parentId );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tnewState.tree = new Map( newState.tree );\n\t\t\t\taction.removedClientIds.forEach( ( clientId ) => {\n\t\t\t\t\tnewState.tree.delete( clientId );\n\t\t\t\t\tnewState.tree.delete( 'controlled||' + clientId );\n\t\t\t\t} );\n\t\t\t\tupdateParentInnerBlocksInTree(\n\t\t\t\t\tnewState,\n\t\t\t\t\tparentsOfRemovedBlocks,\n\t\t\t\t\ttrue\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase 'MOVE_BLOCKS_TO_POSITION': {\n\t\t\t\tconst updatedBlockUids = [];\n\t\t\t\tif ( action.fromRootClientId ) {\n\t\t\t\t\tupdatedBlockUids.push( action.fromRootClientId );\n\t\t\t\t} else {\n\t\t\t\t\tupdatedBlockUids.push( '' );\n\t\t\t\t}\n\t\t\t\tif ( action.toRootClientId ) {\n\t\t\t\t\tupdatedBlockUids.push( action.toRootClientId );\n\t\t\t\t}\n\t\t\t\tnewState.tree = new Map( newState.tree );\n\t\t\t\tupdateParentInnerBlocksInTree(\n\t\t\t\t\tnewState,\n\t\t\t\t\tupdatedBlockUids,\n\t\t\t\t\ttrue\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase 'MOVE_BLOCKS_UP':\n\t\t\tcase 'MOVE_BLOCKS_DOWN': {\n\t\t\t\tconst updatedBlockUids = [\n\t\t\t\t\taction.rootClientId ? action.rootClientId : '',\n\t\t\t\t];\n\t\t\t\tnewState.tree = new Map( newState.tree );\n\t\t\t\tupdateParentInnerBlocksInTree(\n\t\t\t\t\tnewState,\n\t\t\t\t\tupdatedBlockUids,\n\t\t\t\t\ttrue\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase 'SAVE_REUSABLE_BLOCK_SUCCESS': {\n\t\t\t\tconst updatedBlockUids = [];\n\t\t\t\tnewState.attributes.forEach( ( attributes, clientId ) => {\n\t\t\t\t\tif (\n\t\t\t\t\t\tnewState.byClientId.get( clientId ).name ===\n\t\t\t\t\t\t\t'core/block' &&\n\t\t\t\t\t\tattributes.ref === action.updatedId\n\t\t\t\t\t) {\n\t\t\t\t\t\tupdatedBlockUids.push( clientId );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t\tnewState.tree = new Map( newState.tree );\n\t\t\t\tupdatedBlockUids.forEach( ( clientId ) => {\n\t\t\t\t\tnewState.tree.set( clientId, {\n\t\t\t\t\t\t...newState.byClientId.get( clientId ),\n\t\t\t\t\t\tattributes: newState.attributes.get( clientId ),\n\t\t\t\t\t\tinnerBlocks: newState.tree.get( clientId ).innerBlocks,\n\t\t\t\t\t} );\n\t\t\t\t} );\n\t\t\t\tupdateParentInnerBlocksInTree(\n\t\t\t\t\tnewState,\n\t\t\t\t\tupdatedBlockUids,\n\t\t\t\t\tfalse\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn newState;\n\t};\n\n/**\n * Higher-order reducer intended to augment the blocks reducer, assigning an\n * `isPersistentChange` property value corresponding to whether a change in\n * state can be considered as persistent. All changes are considered persistent\n * except when updating the same block attribute as in the previous action.\n *\n * @param {Function} reducer Original reducer function.\n *\n * @return {Function} Enhanced reducer function.\n */\nfunction withPersistentBlockChange( reducer ) {\n\tlet lastAction;\n\tlet markNextChangeAsNotPersistent = false;\n\tlet explicitPersistent;\n\n\treturn ( state, action ) => {\n\t\tlet nextState = reducer( state, action );\n\n\t\tlet nextIsPersistentChange;\n\t\tif ( action.type === 'SET_EXPLICIT_PERSISTENT' ) {\n\t\t\texplicitPersistent = action.isPersistentChange;\n\t\t\tnextIsPersistentChange = state.isPersistentChange ?? true;\n\t\t}\n\n\t\tif ( explicitPersistent !== undefined ) {\n\t\t\tnextIsPersistentChange = explicitPersistent;\n\t\t\treturn nextIsPersistentChange === nextState.isPersistentChange\n\t\t\t\t? nextState\n\t\t\t\t: {\n\t\t\t\t\t\t...nextState,\n\t\t\t\t\t\tisPersistentChange: nextIsPersistentChange,\n\t\t\t\t };\n\t\t}\n\n\t\tconst isExplicitPersistentChange =\n\t\t\taction.type === 'MARK_LAST_CHANGE_AS_PERSISTENT' ||\n\t\t\tmarkNextChangeAsNotPersistent;\n\n\t\t// Defer to previous state value (or default) unless changing or\n\t\t// explicitly marking as persistent.\n\t\tif ( state === nextState && ! isExplicitPersistentChange ) {\n\t\t\tmarkNextChangeAsNotPersistent =\n\t\t\t\taction.type === 'MARK_NEXT_CHANGE_AS_NOT_PERSISTENT';\n\n\t\t\tnextIsPersistentChange = state?.isPersistentChange ?? true;\n\t\t\tif ( state.isPersistentChange === nextIsPersistentChange ) {\n\t\t\t\treturn state;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\t...nextState,\n\t\t\t\tisPersistentChange: nextIsPersistentChange,\n\t\t\t};\n\t\t}\n\n\t\tnextState = {\n\t\t\t...nextState,\n\t\t\tisPersistentChange: isExplicitPersistentChange\n\t\t\t\t? ! markNextChangeAsNotPersistent\n\t\t\t\t: ! isUpdatingSameBlockAttribute( action, lastAction ),\n\t\t};\n\n\t\t// In comparing against the previous action, consider only those which\n\t\t// would have qualified as one which would have been ignored or not\n\t\t// have resulted in a changed state.\n\t\tlastAction = action;\n\t\tmarkNextChangeAsNotPersistent =\n\t\t\taction.type === 'MARK_NEXT_CHANGE_AS_NOT_PERSISTENT';\n\n\t\treturn nextState;\n\t};\n}\n\n/**\n * Higher-order reducer intended to augment the blocks reducer, assigning an\n * `isIgnoredChange` property value corresponding to whether a change in state\n * can be considered as ignored. A change is considered ignored when the result\n * of an action not incurred by direct user interaction.\n *\n * @param {Function} reducer Original reducer function.\n *\n * @return {Function} Enhanced reducer function.\n */\nfunction withIgnoredBlockChange( reducer ) {\n\t/**\n\t * Set of action types for which a blocks state change should be ignored.\n\t *\n\t * @type {Set}\n\t */\n\tconst IGNORED_ACTION_TYPES = new Set( [ 'RECEIVE_BLOCKS' ] );\n\n\treturn ( state, action ) => {\n\t\tconst nextState = reducer( state, action );\n\n\t\tif ( nextState !== state ) {\n\t\t\tnextState.isIgnoredChange = IGNORED_ACTION_TYPES.has( action.type );\n\t\t}\n\n\t\treturn nextState;\n\t};\n}\n\n/**\n * Higher-order reducer targeting the combined blocks reducer, augmenting\n * block client IDs in remove action to include cascade of inner blocks.\n *\n * @param {Function} reducer Original reducer function.\n *\n * @return {Function} Enhanced reducer function.\n */\nconst withInnerBlocksRemoveCascade = ( reducer ) => ( state, action ) => {\n\t// Gets all children which need to be removed.\n\tconst getAllChildren = ( clientIds ) => {\n\t\tlet result = clientIds;\n\t\tfor ( let i = 0; i < result.length; i++ ) {\n\t\t\tif (\n\t\t\t\t! state.order.get( result[ i ] ) ||\n\t\t\t\t( action.keepControlledInnerBlocks &&\n\t\t\t\t\taction.keepControlledInnerBlocks[ result[ i ] ] )\n\t\t\t) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( result === clientIds ) {\n\t\t\t\tresult = [ ...result ];\n\t\t\t}\n\n\t\t\tresult.push( ...state.order.get( result[ i ] ) );\n\t\t}\n\t\treturn result;\n\t};\n\n\tif ( state ) {\n\t\tswitch ( action.type ) {\n\t\t\tcase 'REMOVE_BLOCKS':\n\t\t\t\taction = {\n\t\t\t\t\t...action,\n\t\t\t\t\ttype: 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN',\n\t\t\t\t\tremovedClientIds: getAllChildren( action.clientIds ),\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase 'REPLACE_BLOCKS':\n\t\t\t\taction = {\n\t\t\t\t\t...action,\n\t\t\t\t\ttype: 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN',\n\t\t\t\t\treplacedClientIds: getAllChildren( action.clientIds ),\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn reducer( state, action );\n};\n\n/**\n * Higher-order reducer which targets the combined blocks reducer and handles\n * the `RESET_BLOCKS` action. When dispatched, this action will replace all\n * blocks that exist in the post, leaving blocks that exist only in state (e.g.\n * reusable blocks and blocks controlled by inner blocks controllers) alone.\n *\n * @param {Function} reducer Original reducer function.\n *\n * @return {Function} Enhanced reducer function.\n */\nconst withBlockReset = ( reducer ) => ( state, action ) => {\n\tif ( action.type === 'RESET_BLOCKS' ) {\n\t\tconst newState = {\n\t\t\t...state,\n\t\t\tbyClientId: new Map(\n\t\t\t\tgetFlattenedBlocksWithoutAttributes( action.blocks )\n\t\t\t),\n\t\t\tattributes: new Map( getFlattenedBlockAttributes( action.blocks ) ),\n\t\t\torder: mapBlockOrder( action.blocks ),\n\t\t\tparents: new Map( mapBlockParents( action.blocks ) ),\n\t\t\tcontrolledInnerBlocks: {},\n\t\t};\n\n\t\tnewState.tree = new Map( state?.tree );\n\t\tupdateBlockTreeForBlocks( newState, action.blocks );\n\t\tnewState.tree.set( '', {\n\t\t\tinnerBlocks: action.blocks.map( ( subBlock ) =>\n\t\t\t\tnewState.tree.get( subBlock.clientId )\n\t\t\t),\n\t\t} );\n\n\t\treturn newState;\n\t}\n\n\treturn reducer( state, action );\n};\n\n/**\n * Higher-order reducer which targets the combined blocks reducer and handles\n * the `REPLACE_INNER_BLOCKS` action. When dispatched, this action the state\n * should become equivalent to the execution of a `REMOVE_BLOCKS` action\n * containing all the child's of the root block followed by the execution of\n * `INSERT_BLOCKS` with the new blocks.\n *\n * @param {Function} reducer Original reducer function.\n *\n * @return {Function} Enhanced reducer function.\n */\nconst withReplaceInnerBlocks = ( reducer ) => ( state, action ) => {\n\tif ( action.type !== 'REPLACE_INNER_BLOCKS' ) {\n\t\treturn reducer( state, action );\n\t}\n\n\t// Finds every nested inner block controller. We must check the action blocks\n\t// and not just the block parent state because some inner block controllers\n\t// should be deleted if specified, whereas others should not be deleted. If\n\t// a controlled should not be deleted, then we need to avoid deleting its\n\t// inner blocks from the block state because its inner blocks will not be\n\t// attached to the block in the action.\n\tconst nestedControllers = {};\n\tif ( Object.keys( state.controlledInnerBlocks ).length ) {\n\t\tconst stack = [ ...action.blocks ];\n\t\twhile ( stack.length ) {\n\t\t\tconst { innerBlocks, ...block } = stack.shift();\n\t\t\tstack.push( ...innerBlocks );\n\t\t\tif ( !! state.controlledInnerBlocks[ block.clientId ] ) {\n\t\t\t\tnestedControllers[ block.clientId ] = true;\n\t\t\t}\n\t\t}\n\t}\n\n\t// The `keepControlledInnerBlocks` prop will keep the inner blocks of the\n\t// marked block in the block state so that they can be reattached to the\n\t// marked block when we re-insert everything a few lines below.\n\tlet stateAfterBlocksRemoval = state;\n\tif ( state.order.get( action.rootClientId ) ) {\n\t\tstateAfterBlocksRemoval = reducer( stateAfterBlocksRemoval, {\n\t\t\ttype: 'REMOVE_BLOCKS',\n\t\t\tkeepControlledInnerBlocks: nestedControllers,\n\t\t\tclientIds: state.order.get( action.rootClientId ),\n\t\t} );\n\t}\n\tlet stateAfterInsert = stateAfterBlocksRemoval;\n\tif ( action.blocks.length ) {\n\t\tstateAfterInsert = reducer( stateAfterInsert, {\n\t\t\t...action,\n\t\t\ttype: 'INSERT_BLOCKS',\n\t\t\tindex: 0,\n\t\t} );\n\n\t\t// We need to re-attach the controlled inner blocks to the blocks tree and\n\t\t// preserve their block order. Otherwise, an inner block controller's blocks\n\t\t// will be deleted entirely from its entity.\n\t\tconst stateAfterInsertOrder = new Map( stateAfterInsert.order );\n\t\tObject.keys( nestedControllers ).forEach( ( key ) => {\n\t\t\tif ( state.order.get( key ) ) {\n\t\t\t\tstateAfterInsertOrder.set( key, state.order.get( key ) );\n\t\t\t}\n\t\t} );\n\t\tstateAfterInsert.order = stateAfterInsertOrder;\n\t\tstateAfterInsert.tree = new Map( stateAfterInsert.tree );\n\t\tObject.keys( nestedControllers ).forEach( ( _key ) => {\n\t\t\tconst key = `controlled||${ _key }`;\n\t\t\tif ( state.tree.has( key ) ) {\n\t\t\t\tstateAfterInsert.tree.set( key, state.tree.get( key ) );\n\t\t\t}\n\t\t} );\n\t}\n\treturn stateAfterInsert;\n};\n\n/**\n * Higher-order reducer which targets the combined blocks reducer and handles\n * the `SAVE_REUSABLE_BLOCK_SUCCESS` action. This action can't be handled by\n * regular reducers and needs a higher-order reducer since it needs access to\n * both `byClientId` and `attributes` simultaneously.\n *\n * @param {Function} reducer Original reducer function.\n *\n * @return {Function} Enhanced reducer function.\n */\nconst withSaveReusableBlock = ( reducer ) => ( state, action ) => {\n\tif ( state && action.type === 'SAVE_REUSABLE_BLOCK_SUCCESS' ) {\n\t\tconst { id, updatedId } = action;\n\n\t\t// If a temporary reusable block is saved, we swap the temporary id with the final one.\n\t\tif ( id === updatedId ) {\n\t\t\treturn state;\n\t\t}\n\n\t\tstate = { ...state };\n\t\tstate.attributes = new Map( state.attributes );\n\t\tstate.attributes.forEach( ( attributes, clientId ) => {\n\t\t\tconst { name } = state.byClientId.get( clientId );\n\t\t\tif ( name === 'core/block' && attributes.ref === id ) {\n\t\t\t\tstate.attributes.set( clientId, {\n\t\t\t\t\t...attributes,\n\t\t\t\t\tref: updatedId,\n\t\t\t\t} );\n\t\t\t}\n\t\t} );\n\t}\n\n\treturn reducer( state, action );\n};\n/**\n * Higher-order reducer which removes blocks from state when switching parent block controlled state.\n *\n * @param {Function} reducer Original reducer function.\n *\n * @return {Function} Enhanced reducer function.\n */\nconst withResetControlledBlocks = ( reducer ) => ( state, action ) => {\n\tif ( action.type === 'SET_HAS_CONTROLLED_INNER_BLOCKS' ) {\n\t\t// when switching a block from controlled to uncontrolled or inverse,\n\t\t// we need to remove its content first.\n\t\tconst tempState = reducer( state, {\n\t\t\ttype: 'REPLACE_INNER_BLOCKS',\n\t\t\trootClientId: action.clientId,\n\t\t\tblocks: [],\n\t\t} );\n\t\treturn reducer( tempState, action );\n\t}\n\n\treturn reducer( state, action );\n};\n\n/**\n * Reducer returning the blocks state.\n *\n * @param {Object} state Current state.\n * @param {Object} action Dispatched action.\n *\n * @return {Object} Updated state.\n */\nexport const blocks = pipe(\n\tcombineReducers,\n\twithSaveReusableBlock, // Needs to be before withBlockCache.\n\twithBlockTree, // Needs to be before withInnerBlocksRemoveCascade.\n\twithInnerBlocksRemoveCascade,\n\twithReplaceInnerBlocks, // Needs to be after withInnerBlocksRemoveCascade.\n\twithBlockReset,\n\twithPersistentBlockChange,\n\twithIgnoredBlockChange,\n\twithResetControlledBlocks\n)( {\n\t// The state is using a Map instead of a plain object for performance reasons.\n\t// You can run the \"./test/performance.js\" unit test to check the impact\n\t// code changes can have on this reducer.\n\tbyClientId( state = new Map(), action ) {\n\t\tswitch ( action.type ) {\n\t\t\tcase 'RECEIVE_BLOCKS':\n\t\t\tcase 'INSERT_BLOCKS': {\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\tgetFlattenedBlocksWithoutAttributes( action.blocks ).forEach(\n\t\t\t\t\t( [ key, value ] ) => {\n\t\t\t\t\t\tnewState.set( key, value );\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t\treturn newState;\n\t\t\t}\n\t\t\tcase 'UPDATE_BLOCK': {\n\t\t\t\t// Ignore updates if block isn't known.\n\t\t\t\tif ( ! state.has( action.clientId ) ) {\n\t\t\t\t\treturn state;\n\t\t\t\t}\n\n\t\t\t\t// Do nothing if only attributes change.\n\t\t\t\tconst { attributes, ...changes } = action.updates;\n\t\t\t\tif ( Object.values( changes ).length === 0 ) {\n\t\t\t\t\treturn state;\n\t\t\t\t}\n\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\tnewState.set( action.clientId, {\n\t\t\t\t\t...state.get( action.clientId ),\n\t\t\t\t\t...changes,\n\t\t\t\t} );\n\t\t\t\treturn newState;\n\t\t\t}\n\n\t\t\tcase 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN': {\n\t\t\t\tif ( ! action.blocks ) {\n\t\t\t\t\treturn state;\n\t\t\t\t}\n\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\taction.replacedClientIds.forEach( ( clientId ) => {\n\t\t\t\t\tnewState.delete( clientId );\n\t\t\t\t} );\n\n\t\t\t\tgetFlattenedBlocksWithoutAttributes( action.blocks ).forEach(\n\t\t\t\t\t( [ key, value ] ) => {\n\t\t\t\t\t\tnewState.set( key, value );\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t\treturn newState;\n\t\t\t}\n\n\t\t\tcase 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN': {\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\taction.removedClientIds.forEach( ( clientId ) => {\n\t\t\t\t\tnewState.delete( clientId );\n\t\t\t\t} );\n\t\t\t\treturn newState;\n\t\t\t}\n\t\t}\n\n\t\treturn state;\n\t},\n\n\t// The state is using a Map instead of a plain object for performance reasons.\n\t// You can run the \"./test/performance.js\" unit test to check the impact\n\t// code changes can have on this reducer.\n\tattributes( state = new Map(), action ) {\n\t\tswitch ( action.type ) {\n\t\t\tcase 'RECEIVE_BLOCKS':\n\t\t\tcase 'INSERT_BLOCKS': {\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\tgetFlattenedBlockAttributes( action.blocks ).forEach(\n\t\t\t\t\t( [ key, value ] ) => {\n\t\t\t\t\t\tnewState.set( key, value );\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t\treturn newState;\n\t\t\t}\n\n\t\t\tcase 'UPDATE_BLOCK': {\n\t\t\t\t// Ignore updates if block isn't known or there are no attribute changes.\n\t\t\t\tif (\n\t\t\t\t\t! state.get( action.clientId ) ||\n\t\t\t\t\t! action.updates.attributes\n\t\t\t\t) {\n\t\t\t\t\treturn state;\n\t\t\t\t}\n\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\tnewState.set( action.clientId, {\n\t\t\t\t\t...state.get( action.clientId ),\n\t\t\t\t\t...action.updates.attributes,\n\t\t\t\t} );\n\t\t\t\treturn newState;\n\t\t\t}\n\n\t\t\tcase 'SYNC_DERIVED_BLOCK_ATTRIBUTES':\n\t\t\tcase 'UPDATE_BLOCK_ATTRIBUTES': {\n\t\t\t\t// Avoid a state change if none of the block IDs are known.\n\t\t\t\tif ( action.clientIds.every( ( id ) => ! state.get( id ) ) ) {\n\t\t\t\t\treturn state;\n\t\t\t\t}\n\n\t\t\t\tlet hasChange = false;\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\tfor ( const clientId of action.clientIds ) {\n\t\t\t\t\tconst updatedAttributeEntries = Object.entries(\n\t\t\t\t\t\t!! action.options?.uniqueByBlock\n\t\t\t\t\t\t\t? action.attributes[ clientId ]\n\t\t\t\t\t\t\t: action.attributes ?? {}\n\t\t\t\t\t);\n\t\t\t\t\tif ( updatedAttributeEntries.length === 0 ) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tlet hasUpdatedAttributes = false;\n\t\t\t\t\tconst existingAttributes = state.get( clientId );\n\t\t\t\t\tconst newAttributes = {};\n\t\t\t\t\tupdatedAttributeEntries.forEach( ( [ key, value ] ) => {\n\t\t\t\t\t\tif ( existingAttributes[ key ] !== value ) {\n\t\t\t\t\t\t\thasUpdatedAttributes = true;\n\t\t\t\t\t\t\tnewAttributes[ key ] = value;\n\t\t\t\t\t\t}\n\t\t\t\t\t} );\n\t\t\t\t\thasChange = hasChange || hasUpdatedAttributes;\n\t\t\t\t\tif ( hasUpdatedAttributes ) {\n\t\t\t\t\t\tnewState.set( clientId, {\n\t\t\t\t\t\t\t...existingAttributes,\n\t\t\t\t\t\t\t...newAttributes,\n\t\t\t\t\t\t} );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn hasChange ? newState : state;\n\t\t\t}\n\n\t\t\tcase 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN': {\n\t\t\t\tif ( ! action.blocks ) {\n\t\t\t\t\treturn state;\n\t\t\t\t}\n\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\taction.replacedClientIds.forEach( ( clientId ) => {\n\t\t\t\t\tnewState.delete( clientId );\n\t\t\t\t} );\n\t\t\t\tgetFlattenedBlockAttributes( action.blocks ).forEach(\n\t\t\t\t\t( [ key, value ] ) => {\n\t\t\t\t\t\tnewState.set( key, value );\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t\treturn newState;\n\t\t\t}\n\n\t\t\tcase 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN': {\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\taction.removedClientIds.forEach( ( clientId ) => {\n\t\t\t\t\tnewState.delete( clientId );\n\t\t\t\t} );\n\t\t\t\treturn newState;\n\t\t\t}\n\t\t}\n\n\t\treturn state;\n\t},\n\n\t// The state is using a Map instead of a plain object for performance reasons.\n\t// You can run the \"./test/performance.js\" unit test to check the impact\n\t// code changes can have on this reducer.\n\torder( state = new Map(), action ) {\n\t\tswitch ( action.type ) {\n\t\t\tcase 'RECEIVE_BLOCKS': {\n\t\t\t\tconst blockOrder = mapBlockOrder( action.blocks );\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\tblockOrder.forEach( ( order, clientId ) => {\n\t\t\t\t\tif ( clientId !== '' ) {\n\t\t\t\t\t\tnewState.set( clientId, order );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t\tnewState.set(\n\t\t\t\t\t'',\n\t\t\t\t\t( state.get( '' ) ?? [] ).concat( blockOrder[ '' ] )\n\t\t\t\t);\n\t\t\t\treturn newState;\n\t\t\t}\n\t\t\tcase 'INSERT_BLOCKS': {\n\t\t\t\tconst { rootClientId = '' } = action;\n\t\t\t\tconst subState = state.get( rootClientId ) || [];\n\t\t\t\tconst mappedBlocks = mapBlockOrder(\n\t\t\t\t\taction.blocks,\n\t\t\t\t\trootClientId\n\t\t\t\t);\n\t\t\t\tconst { index = subState.length } = action;\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\tmappedBlocks.forEach( ( order, clientId ) => {\n\t\t\t\t\tnewState.set( clientId, order );\n\t\t\t\t} );\n\t\t\t\tnewState.set(\n\t\t\t\t\trootClientId,\n\t\t\t\t\tinsertAt(\n\t\t\t\t\t\tsubState,\n\t\t\t\t\t\tmappedBlocks.get( rootClientId ),\n\t\t\t\t\t\tindex\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\treturn newState;\n\t\t\t}\n\n\t\t\tcase 'MOVE_BLOCKS_TO_POSITION': {\n\t\t\t\tconst {\n\t\t\t\t\tfromRootClientId = '',\n\t\t\t\t\ttoRootClientId = '',\n\t\t\t\t\tclientIds,\n\t\t\t\t} = action;\n\t\t\t\tconst { index = state.get( toRootClientId ).length } = action;\n\n\t\t\t\t// Moving inside the same parent block.\n\t\t\t\tif ( fromRootClientId === toRootClientId ) {\n\t\t\t\t\tconst subState = state.get( toRootClientId );\n\t\t\t\t\tconst fromIndex = subState.indexOf( clientIds[ 0 ] );\n\t\t\t\t\tconst newState = new Map( state );\n\t\t\t\t\tnewState.set(\n\t\t\t\t\t\ttoRootClientId,\n\t\t\t\t\t\tmoveTo(\n\t\t\t\t\t\t\tstate.get( toRootClientId ),\n\t\t\t\t\t\t\tfromIndex,\n\t\t\t\t\t\t\tindex,\n\t\t\t\t\t\t\tclientIds.length\n\t\t\t\t\t\t)\n\t\t\t\t\t);\n\t\t\t\t\treturn newState;\n\t\t\t\t}\n\n\t\t\t\t// Moving from a parent block to another.\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\tnewState.set(\n\t\t\t\t\tfromRootClientId,\n\t\t\t\t\tstate\n\t\t\t\t\t\t.get( fromRootClientId )\n\t\t\t\t\t\t?.filter( ( id ) => ! clientIds.includes( id ) ) ?? []\n\t\t\t\t);\n\t\t\t\tnewState.set(\n\t\t\t\t\ttoRootClientId,\n\t\t\t\t\tinsertAt( state.get( toRootClientId ), clientIds, index )\n\t\t\t\t);\n\t\t\t\treturn newState;\n\t\t\t}\n\n\t\t\tcase 'MOVE_BLOCKS_UP': {\n\t\t\t\tconst { clientIds, rootClientId = '' } = action;\n\t\t\t\tconst firstClientId = clientIds[ 0 ];\n\t\t\t\tconst subState = state.get( rootClientId );\n\n\t\t\t\tif ( ! subState.length || firstClientId === subState[ 0 ] ) {\n\t\t\t\t\treturn state;\n\t\t\t\t}\n\n\t\t\t\tconst firstIndex = subState.indexOf( firstClientId );\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\tnewState.set(\n\t\t\t\t\trootClientId,\n\t\t\t\t\tmoveTo(\n\t\t\t\t\t\tsubState,\n\t\t\t\t\t\tfirstIndex,\n\t\t\t\t\t\tfirstIndex - 1,\n\t\t\t\t\t\tclientIds.length\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\treturn newState;\n\t\t\t}\n\n\t\t\tcase 'MOVE_BLOCKS_DOWN': {\n\t\t\t\tconst { clientIds, rootClientId = '' } = action;\n\t\t\t\tconst firstClientId = clientIds[ 0 ];\n\t\t\t\tconst lastClientId = clientIds[ clientIds.length - 1 ];\n\t\t\t\tconst subState = state.get( rootClientId );\n\n\t\t\t\tif (\n\t\t\t\t\t! subState.length ||\n\t\t\t\t\tlastClientId === subState[ subState.length - 1 ]\n\t\t\t\t) {\n\t\t\t\t\treturn state;\n\t\t\t\t}\n\n\t\t\t\tconst firstIndex = subState.indexOf( firstClientId );\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\tnewState.set(\n\t\t\t\t\trootClientId,\n\t\t\t\t\tmoveTo(\n\t\t\t\t\t\tsubState,\n\t\t\t\t\t\tfirstIndex,\n\t\t\t\t\t\tfirstIndex + 1,\n\t\t\t\t\t\tclientIds.length\n\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t\treturn newState;\n\t\t\t}\n\n\t\t\tcase 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN': {\n\t\t\t\tconst { clientIds } = action;\n\t\t\t\tif ( ! action.blocks ) {\n\t\t\t\t\treturn state;\n\t\t\t\t}\n\n\t\t\t\tconst mappedBlocks = mapBlockOrder( action.blocks );\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\taction.replacedClientIds.forEach( ( clientId ) => {\n\t\t\t\t\tnewState.delete( clientId );\n\t\t\t\t} );\n\t\t\t\tmappedBlocks.forEach( ( order, clientId ) => {\n\t\t\t\t\tif ( clientId !== '' ) {\n\t\t\t\t\t\tnewState.set( clientId, order );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t\tnewState.forEach( ( order, clientId ) => {\n\t\t\t\t\tconst newSubOrder = Object.values( order ).reduce(\n\t\t\t\t\t\t( result, subClientId ) => {\n\t\t\t\t\t\t\tif ( subClientId === clientIds[ 0 ] ) {\n\t\t\t\t\t\t\t\treturn [ ...result, ...mappedBlocks.get( '' ) ];\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif ( clientIds.indexOf( subClientId ) === -1 ) {\n\t\t\t\t\t\t\t\tresult.push( subClientId );\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\treturn result;\n\t\t\t\t\t\t},\n\t\t\t\t\t\t[]\n\t\t\t\t\t);\n\t\t\t\t\tnewState.set( clientId, newSubOrder );\n\t\t\t\t} );\n\t\t\t\treturn newState;\n\t\t\t}\n\n\t\t\tcase 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN': {\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\t// Remove inner block ordering for removed blocks.\n\t\t\t\taction.removedClientIds.forEach( ( clientId ) => {\n\t\t\t\t\tnewState.delete( clientId );\n\t\t\t\t} );\n\t\t\t\tnewState.forEach( ( order, clientId ) => {\n\t\t\t\t\tconst newSubOrder =\n\t\t\t\t\t\torder?.filter(\n\t\t\t\t\t\t\t( id ) => ! action.removedClientIds.includes( id )\n\t\t\t\t\t\t) ?? [];\n\t\t\t\t\tif ( newSubOrder.length !== order.length ) {\n\t\t\t\t\t\tnewState.set( clientId, newSubOrder );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t\treturn newState;\n\t\t\t}\n\t\t}\n\n\t\treturn state;\n\t},\n\n\t// While technically redundant data as the inverse of `order`, it serves as\n\t// an optimization for the selectors which derive the ancestry of a block.\n\tparents( state = new Map(), action ) {\n\t\tswitch ( action.type ) {\n\t\t\tcase 'RECEIVE_BLOCKS': {\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\tmapBlockParents( action.blocks ).forEach(\n\t\t\t\t\t( [ key, value ] ) => {\n\t\t\t\t\t\tnewState.set( key, value );\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t\treturn newState;\n\t\t\t}\n\t\t\tcase 'INSERT_BLOCKS': {\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\tmapBlockParents(\n\t\t\t\t\taction.blocks,\n\t\t\t\t\taction.rootClientId || ''\n\t\t\t\t).forEach( ( [ key, value ] ) => {\n\t\t\t\t\tnewState.set( key, value );\n\t\t\t\t} );\n\t\t\t\treturn newState;\n\t\t\t}\n\t\t\tcase 'MOVE_BLOCKS_TO_POSITION': {\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\taction.clientIds.forEach( ( id ) => {\n\t\t\t\t\tnewState.set( id, action.toRootClientId || '' );\n\t\t\t\t} );\n\t\t\t\treturn newState;\n\t\t\t}\n\n\t\t\tcase 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN': {\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\taction.replacedClientIds.forEach( ( clientId ) => {\n\t\t\t\t\tnewState.delete( clientId );\n\t\t\t\t} );\n\t\t\t\tmapBlockParents(\n\t\t\t\t\taction.blocks,\n\t\t\t\t\tstate.get( action.clientIds[ 0 ] )\n\t\t\t\t).forEach( ( [ key, value ] ) => {\n\t\t\t\t\tnewState.set( key, value );\n\t\t\t\t} );\n\t\t\t\treturn newState;\n\t\t\t}\n\t\t\tcase 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN': {\n\t\t\t\tconst newState = new Map( state );\n\t\t\t\taction.removedClientIds.forEach( ( clientId ) => {\n\t\t\t\t\tnewState.delete( clientId );\n\t\t\t\t} );\n\t\t\t\treturn newState;\n\t\t\t}\n\t\t}\n\n\t\treturn state;\n\t},\n\n\tcontrolledInnerBlocks(\n\t\tstate = {},\n\t\t{ type, clientId, hasControlledInnerBlocks }\n\t) {\n\t\tif ( type === 'SET_HAS_CONTROLLED_INNER_BLOCKS' ) {\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\t[ clientId ]: hasControlledInnerBlocks,\n\t\t\t};\n\t\t}\n\t\treturn state;\n\t},\n} );\n\n/**\n * Reducer returning visibility status of block interface.\n *\n * @param {boolean} state Current state.\n * @param {Object} action Dispatched action.\n *\n * @return {boolean} Updated state.\n */\nexport function isBlockInterfaceHidden( state = false, action ) {\n\tswitch ( action.type ) {\n\t\tcase 'HIDE_BLOCK_INTERFACE':\n\t\t\treturn true;\n\n\t\tcase 'SHOW_BLOCK_INTERFACE':\n\t\t\treturn false;\n\t}\n\n\treturn state;\n}\n\n/**\n * Reducer returning typing state.\n *\n * @param {boolean} state Current state.\n * @param {Object} action Dispatched action.\n *\n * @return {boolean} Updated state.\n */\nexport function isTyping( state = false, action ) {\n\tswitch ( action.type ) {\n\t\tcase 'START_TYPING':\n\t\t\treturn true;\n\n\t\tcase 'STOP_TYPING':\n\t\t\treturn false;\n\t}\n\n\treturn state;\n}\n\n/**\n * Reducer returning dragging state. It is possible for a user to be dragging\n * data from outside of the editor, so this state is separate from `draggedBlocks`.\n *\n * @param {boolean} state Current state.\n * @param {Object} action Dispatched action.\n *\n * @return {boolean} Updated state.\n */\nexport function isDragging( state = false, action ) {\n\tswitch ( action.type ) {\n\t\tcase 'START_DRAGGING':\n\t\t\treturn true;\n\n\t\tcase 'STOP_DRAGGING':\n\t\t\treturn false;\n\t}\n\n\treturn state;\n}\n\n/**\n * Reducer returning dragged block client id.\n *\n * @param {string[]} state Current state.\n * @param {Object} action Dispatched action.\n *\n * @return {string[]} Updated state.\n */\nexport function draggedBlocks( state = [], action ) {\n\tswitch ( action.type ) {\n\t\tcase 'START_DRAGGING_BLOCKS':\n\t\t\treturn action.clientIds;\n\n\t\tcase 'STOP_DRAGGING_BLOCKS':\n\t\t\treturn [];\n\t}\n\n\treturn state;\n}\n\n/**\n * Reducer tracking the visible blocks.\n *\n * @param {Record<string,boolean>} state Current state.\n * @param {Object} action Dispatched action.\n *\n * @return {Record<string,boolean>} Block visibility.\n */\nexport function blockVisibility( state = {}, action ) {\n\tif ( action.type === 'SET_BLOCK_VISIBILITY' ) {\n\t\treturn {\n\t\t\t...state,\n\t\t\t...action.updates,\n\t\t};\n\t}\n\n\treturn state;\n}\n\n/**\n * Internal helper reducer for selectionStart and selectionEnd. Can hold a block\n * selection, represented by an object with property clientId.\n *\n * @param {Object} state Current state.\n * @param {Object} action Dispatched action.\n *\n * @return {Object} Updated state.\n */\nfunction selectionHelper( state = {}, action ) {\n\tswitch ( action.type ) {\n\t\tcase 'CLEAR_SELECTED_BLOCK': {\n\t\t\tif ( state.clientId ) {\n\t\t\t\treturn {};\n\t\t\t}\n\n\t\t\treturn state;\n\t\t}\n\t\tcase 'SELECT_BLOCK':\n\t\t\tif ( action.clientId === state.clientId ) {\n\t\t\t\treturn state;\n\t\t\t}\n\n\t\t\treturn { clientId: action.clientId };\n\t\tcase 'REPLACE_INNER_BLOCKS':\n\t\tcase 'INSERT_BLOCKS': {\n\t\t\tif ( ! action.updateSelection || ! action.blocks.length ) {\n\t\t\t\treturn state;\n\t\t\t}\n\n\t\t\treturn { clientId: action.blocks[ 0 ].clientId };\n\t\t}\n\t\tcase 'REMOVE_BLOCKS':\n\t\t\tif (\n\t\t\t\t! action.clientIds ||\n\t\t\t\t! action.clientIds.length ||\n\t\t\t\taction.clientIds.indexOf( state.clientId ) === -1\n\t\t\t) {\n\t\t\t\treturn state;\n\t\t\t}\n\n\t\t\treturn {};\n\t\tcase 'REPLACE_BLOCKS': {\n\t\t\tif ( action.clientIds.indexOf( state.clientId ) === -1 ) {\n\t\t\t\treturn state;\n\t\t\t}\n\n\t\t\tconst blockToSelect =\n\t\t\t\taction.blocks[ action.indexToSelect ] ||\n\t\t\t\taction.blocks[ action.blocks.length - 1 ];\n\n\t\t\tif ( ! blockToSelect ) {\n\t\t\t\treturn {};\n\t\t\t}\n\n\t\t\tif ( blockToSelect.clientId === state.clientId ) {\n\t\t\t\treturn state;\n\t\t\t}\n\n\t\t\treturn { clientId: blockToSelect.clientId };\n\t\t}\n\t}\n\n\treturn state;\n}\n\n/**\n * Reducer returning the selection state.\n *\n * @param {boolean} state Current state.\n * @param {Object} action Dispatched action.\n *\n * @return {boolean} Updated state.\n */\nexport function selection( state = {}, action ) {\n\tswitch ( action.type ) {\n\t\tcase 'SELECTION_CHANGE':\n\t\t\tif ( action.clientId ) {\n\t\t\t\treturn {\n\t\t\t\t\tselectionStart: {\n\t\t\t\t\t\tclientId: action.clientId,\n\t\t\t\t\t\tattributeKey: action.attributeKey,\n\t\t\t\t\t\toffset: action.startOffset,\n\t\t\t\t\t},\n\t\t\t\t\tselectionEnd: {\n\t\t\t\t\t\tclientId: action.clientId,\n\t\t\t\t\t\tattributeKey: action.attributeKey,\n\t\t\t\t\t\toffset: action.endOffset,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tselectionStart: action.start || state.selectionStart,\n\t\t\t\tselectionEnd: action.end || state.selectionEnd,\n\t\t\t};\n\t\tcase 'RESET_SELECTION':\n\t\t\tconst { selectionStart, selectionEnd } = action;\n\t\t\treturn {\n\t\t\t\tselectionStart,\n\t\t\t\tselectionEnd,\n\t\t\t};\n\t\tcase 'MULTI_SELECT':\n\t\t\tconst { start, end } = action;\n\n\t\t\tif (\n\t\t\t\tstart === state.selectionStart?.clientId &&\n\t\t\t\tend === state.selectionEnd?.clientId\n\t\t\t) {\n\t\t\t\treturn state;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tselectionStart: { clientId: start },\n\t\t\t\tselectionEnd: { clientId: end },\n\t\t\t};\n\t\tcase 'RESET_BLOCKS':\n\t\t\tconst startClientId = state?.selectionStart?.clientId;\n\t\t\tconst endClientId = state?.selectionEnd?.clientId;\n\n\t\t\t// Do nothing if there's no selected block.\n\t\t\tif ( ! startClientId && ! endClientId ) {\n\t\t\t\treturn state;\n\t\t\t}\n\n\t\t\t// If the start of the selection won't exist after reset, remove selection.\n\t\t\tif (\n\t\t\t\t! action.blocks.some(\n\t\t\t\t\t( block ) => block.clientId === startClientId\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\treturn {\n\t\t\t\t\tselectionStart: {},\n\t\t\t\t\tselectionEnd: {},\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// If the end of the selection won't exist after reset, collapse selection.\n\t\t\tif (\n\t\t\t\t! action.blocks.some(\n\t\t\t\t\t( block ) => block.clientId === endClientId\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\treturn {\n\t\t\t\t\t...state,\n\t\t\t\t\tselectionEnd: state.selectionStart,\n\t\t\t\t};\n\t\t\t}\n\t}\n\n\tconst selectionStart = selectionHelper( state.selectionStart, action );\n\tconst selectionEnd = selectionHelper( state.selectionEnd, action );\n\n\tif (\n\t\tselectionStart === state.selectionStart &&\n\t\tselectionEnd === state.selectionEnd\n\t) {\n\t\treturn state;\n\t}\n\n\treturn {\n\t\tselectionStart,\n\t\tselectionEnd,\n\t};\n}\n\n/**\n * Reducer returning whether the user is multi-selecting.\n *\n * @param {boolean} state Current state.\n * @param {Object} action Dispatched action.\n *\n * @return {boolean} Updated state.\n */\nexport function isMultiSelecting( state = false, action ) {\n\tswitch ( action.type ) {\n\t\tcase 'START_MULTI_SELECT':\n\t\t\treturn true;\n\n\t\tcase 'STOP_MULTI_SELECT':\n\t\t\treturn false;\n\t}\n\n\treturn state;\n}\n\n/**\n * Reducer returning whether selection is enabled.\n *\n * @param {boolean} state Current state.\n * @param {Object} action Dispatched action.\n *\n * @return {boolean} Updated state.\n */\nexport function isSelectionEnabled( state = true, action ) {\n\tswitch ( action.type ) {\n\t\tcase 'TOGGLE_SELECTION':\n\t\t\treturn action.isSelectionEnabled;\n\t}\n\n\treturn state;\n}\n\n/**\n * Reducer returning the data needed to display a prompt when certain blocks\n * are removed, or `false` if no such prompt is requested.\n *\n * @param {boolean} state Current state.\n * @param {Object} action Dispatched action.\n *\n * @return {Object|false} Data for removal prompt display, if any.\n */\nfunction removalPromptData( state = false, action ) {\n\tswitch ( action.type ) {\n\t\tcase 'DISPLAY_BLOCK_REMOVAL_PROMPT':\n\t\t\tconst { clientIds, selectPrevious, message } = action;\n\t\t\treturn {\n\t\t\t\tclientIds,\n\t\t\t\tselectPrevious,\n\t\t\t\tmessage,\n\t\t\t};\n\t\tcase 'CLEAR_BLOCK_REMOVAL_PROMPT':\n\t\t\treturn false;\n\t}\n\n\treturn state;\n}\n\n/**\n * Reducer returning any rules that a block editor may provide in order to\n * prevent a user from accidentally removing certain blocks. These rules are\n * then used to display a confirmation prompt to the user. For instance, in the\n * Site Editor, the Query Loop block is important enough to warrant such\n * confirmation.\n *\n * The data is a record whose keys are block types (e.g. 'core/query') and\n * whose values are the explanation to be shown to users (e.g. 'Query Loop\n * displays a list of posts or pages.').\n *\n * @param {boolean} state Current state.\n * @param {Object} action Dispatched action.\n *\n * @return {Record<string,string>} Updated state.\n */\nfunction blockRemovalRules( state = false, action ) {\n\tswitch ( action.type ) {\n\t\tcase 'SET_BLOCK_REMOVAL_RULES':\n\t\t\treturn action.rules;\n\t}\n\n\treturn state;\n}\n\n/**\n * Reducer returning the initial block selection.\n *\n * Currently this in only used to restore the selection after block deletion and\n * pasting new content.This reducer should eventually be removed in favour of setting\n * selection directly.\n *\n * @param {boolean} state Current state.\n * @param {Object} action Dispatched action.\n *\n * @return {number|null} Initial position: 0, -1 or null.\n */\nexport function initialPosition( state = null, action ) {\n\tif (\n\t\taction.type === 'REPLACE_BLOCKS' &&\n\t\taction.initialPosition !== undefined\n\t) {\n\t\treturn action.initialPosition;\n\t} else if (\n\t\t[\n\t\t\t'MULTI_SELECT',\n\t\t\t'SELECT_BLOCK',\n\t\t\t'RESET_SELECTION',\n\t\t\t'INSERT_BLOCKS',\n\t\t\t'REPLACE_INNER_BLOCKS',\n\t\t].includes( action.type )\n\t) {\n\t\treturn action.initialPosition;\n\t}\n\n\treturn state;\n}\n\nexport function blocksMode( state = {}, action ) {\n\tif ( action.type === 'TOGGLE_BLOCK_MODE' ) {\n\t\tconst { clientId } = action;\n\t\treturn {\n\t\t\t...state,\n\t\t\t[ clientId ]:\n\t\t\t\tstate[ clientId ] && state[ clientId ] === 'html'\n\t\t\t\t\t? 'visual'\n\t\t\t\t\t: 'html',\n\t\t};\n\t}\n\n\treturn state;\n}\n\n/**\n * Reducer returning the block insertion point visibility, either null if there\n * is not an explicit insertion point assigned, or an object of its `index` and\n * `rootClientId`.\n *\n * @param {Object} state Current state.\n * @param {Object} action Dispatched action.\n *\n * @return {Object} Updated state.\n */\nexport function insertionCue( state = null, action ) {\n\tswitch ( action.type ) {\n\t\tcase 'SHOW_INSERTION_POINT': {\n\t\t\tconst {\n\t\t\t\trootClientId,\n\t\t\t\tindex,\n\t\t\t\t__unstableWithInserter,\n\t\t\t\toperation,\n\t\t\t\tnearestSide,\n\t\t\t} = action;\n\t\t\tconst nextState = {\n\t\t\t\trootClientId,\n\t\t\t\tindex,\n\t\t\t\t__unstableWithInserter,\n\t\t\t\toperation,\n\t\t\t\tnearestSide,\n\t\t\t};\n\n\t\t\t// Bail out updates if the states are the same.\n\t\t\treturn fastDeepEqual( state, nextState ) ? state : nextState;\n\t\t}\n\n\t\tcase 'HIDE_INSERTION_POINT':\n\t\t\treturn null;\n\t}\n\n\treturn state;\n}\n\n/**\n * Reducer returning whether the post blocks match the defined template or not.\n *\n * @param {Object} state Current state.\n * @param {Object} action Dispatched