UNPKG

@wordpress/block-editor

Version:
4 lines 89.6 kB
{ "version": 3, "sources": ["../../src/store/actions.js"], "sourcesContent": ["/* eslint no-console: [ 'error', { allow: [ 'error', 'warn' ] } ] */\n/**\n * WordPress dependencies\n */\nimport {\n\tcloneBlock,\n\t__experimentalCloneSanitizedBlock,\n\tcreateBlock,\n\tdoBlocksMatchTemplate,\n\tgetBlockType,\n\tgetDefaultBlockName,\n\thasBlockSupport,\n\tswitchToBlockType,\n\tsynchronizeBlocksWithTemplate,\n\tgetBlockSupport,\n\tisUnmodifiedDefaultBlock,\n\tisUnmodifiedBlock,\n} from '@wordpress/blocks';\nimport { speak } from '@wordpress/a11y';\nimport { __, _n, sprintf } from '@wordpress/i18n';\nimport { store as noticesStore } from '@wordpress/notices';\nimport { create, insert, remove, toHTMLString } from '@wordpress/rich-text';\nimport deprecated from '@wordpress/deprecated';\nimport { store as preferencesStore } from '@wordpress/preferences';\n\n/**\n * Internal dependencies\n */\nimport {\n\tretrieveSelectedAttribute,\n\tfindRichTextAttributeKey,\n\tSTART_OF_SELECTED_AREA,\n} from '../utils/selection';\nimport {\n\t__experimentalUpdateSettings,\n\tprivateRemoveBlocks,\n\teditContentOnlySection,\n} from './private-actions';\n\n/** @typedef {import('../components/use-on-block-drop/types').WPDropOperation} WPDropOperation */\n\nconst castArray = ( maybeArray ) =>\n\tArray.isArray( maybeArray ) ? maybeArray : [ maybeArray ];\n\n/**\n * Action that resets blocks state to the specified array of blocks, taking precedence\n * over any other content reflected as an edit in state.\n *\n * @param {Array} blocks Array of blocks.\n */\nexport const resetBlocks =\n\t( blocks ) =>\n\t( { dispatch } ) => {\n\t\tdispatch( { type: 'RESET_BLOCKS', blocks } );\n\t\tdispatch( validateBlocksToTemplate( blocks ) );\n\t};\n\n/**\n * Block validity is a function of blocks state (at the point of a\n * reset) and the template setting. As a compromise to its placement\n * across distinct parts of state, it is implemented here as a side\n * effect of the block reset action.\n *\n * @param {Array} blocks Array of blocks.\n */\nexport const validateBlocksToTemplate =\n\t( blocks ) =>\n\t( { select, dispatch } ) => {\n\t\tconst template = select.getTemplate();\n\t\tconst templateLock = select.getTemplateLock();\n\n\t\t// Unlocked templates are considered always valid because they act\n\t\t// as default values only.\n\t\tconst isBlocksValidToTemplate =\n\t\t\t! template ||\n\t\t\ttemplateLock !== 'all' ||\n\t\t\tdoBlocksMatchTemplate( blocks, template );\n\n\t\t// Update if validity has changed.\n\t\tconst isValidTemplate = select.isValidTemplate();\n\n\t\tif ( isBlocksValidToTemplate !== isValidTemplate ) {\n\t\t\tdispatch.setTemplateValidity( isBlocksValidToTemplate );\n\t\t\treturn isBlocksValidToTemplate;\n\t\t}\n\t};\n\n/**\n * A block selection object.\n *\n * @typedef {Object} WPBlockSelection\n *\n * @property {string} clientId A block client ID.\n * @property {string} attributeKey A block attribute key.\n * @property {number} offset An attribute value offset, based on the rich\n * text value. See `wp.richText.create`.\n */\n\n/**\n * A selection object.\n *\n * @typedef {Object} WPSelection\n *\n * @property {WPBlockSelection} start The selection start.\n * @property {WPBlockSelection} end The selection end.\n */\n\n/**\n * Returns an action object used in signalling that selection state should be\n * reset to the specified selection.\n *\n * @param {WPBlockSelection} selectionStart The selection start.\n * @param {WPBlockSelection} selectionEnd The selection end.\n * @param {0|-1|null} initialPosition Initial block position.\n *\n * @return {Object} Action object.\n */\nexport function resetSelection(\n\tselectionStart,\n\tselectionEnd,\n\tinitialPosition\n) {\n\treturn {\n\t\ttype: 'RESET_SELECTION',\n\t\tselectionStart,\n\t\tselectionEnd,\n\t\tinitialPosition,\n\t};\n}\n\n/**\n * Returns an action object used in signalling that blocks have been received.\n * Unlike resetBlocks, these should be appended to the existing known set, not\n * replacing.\n *\n * @deprecated\n *\n * @param {Object[]} blocks Array of block objects.\n *\n * @return {Object} Action object.\n */\nexport function receiveBlocks( blocks ) {\n\tdeprecated( 'wp.data.dispatch( \"core/block-editor\" ).receiveBlocks', {\n\t\tsince: '5.9',\n\t\talternative: 'resetBlocks or insertBlocks',\n\t} );\n\n\treturn {\n\t\ttype: 'RECEIVE_BLOCKS',\n\t\tblocks,\n\t};\n}\n\n/**\n * Action that updates attributes of multiple blocks with the specified client IDs.\n *\n * @param {string|string[]} clientIds Block client IDs.\n * @param {Object} attributes Block attributes to be merged. Should be keyed by clientIds if `options.uniqueByBlock` is true.\n * @param {Object} options Updating options.\n * @param {boolean} [options.uniqueByBlock=false] Whether each block in clientIds array has a unique set of attributes.\n * @return {Object} Action object.\n */\nexport function updateBlockAttributes(\n\tclientIds,\n\tattributes,\n\toptions = { uniqueByBlock: false }\n) {\n\tif ( typeof options === 'boolean' ) {\n\t\toptions = { uniqueByBlock: options };\n\t}\n\n\treturn {\n\t\ttype: 'UPDATE_BLOCK_ATTRIBUTES',\n\t\tclientIds: castArray( clientIds ),\n\t\tattributes,\n\t\toptions,\n\t};\n}\n\n/**\n * Action that updates the block with the specified client ID.\n *\n * @param {string} clientId Block client ID.\n * @param {Object} updates Block attributes to be merged.\n *\n * @return {Object} Action object.\n */\nexport function updateBlock( clientId, updates ) {\n\treturn {\n\t\ttype: 'UPDATE_BLOCK',\n\t\tclientId,\n\t\tupdates,\n\t};\n}\n\n/**\n * Returns an action object used in signalling that the block with the\n * specified client ID has been selected, optionally accepting a position\n * value reflecting its selection directionality. An initialPosition of -1\n * reflects a reverse selection.\n *\n * @param {string} clientId Block client ID.\n * @param {0|-1|null} initialPosition Optional initial position. Pass -1 to reflect reverse selection\n * or `null` to prevent focusing the block.\n *\n * @return {Object} Action object.\n */\nexport function selectBlock( clientId, initialPosition = 0 ) {\n\treturn {\n\t\ttype: 'SELECT_BLOCK',\n\t\tinitialPosition,\n\t\tclientId,\n\t};\n}\n\n/**\n * Returns an action object used in signalling that the block with the\n * specified client ID has been hovered.\n *\n * @deprecated\n */\nexport function hoverBlock() {\n\tdeprecated( 'wp.data.dispatch( \"core/block-editor\" ).hoverBlock', {\n\t\tsince: '6.9',\n\t\tversion: '7.1',\n\t} );\n\treturn {\n\t\ttype: 'DO_NOTHING',\n\t};\n}\n\n/**\n * Yields action objects used in signalling that the block preceding the given\n * clientId (or optionally, its first parent from bottom to top)\n * should be selected.\n *\n * @param {string} clientId Block client ID.\n * @param {boolean} fallbackToParent If true, select the first parent if there is no previous block.\n */\nexport const selectPreviousBlock =\n\t( clientId, fallbackToParent = false ) =>\n\t( { select, dispatch } ) => {\n\t\tconst previousBlockClientId =\n\t\t\tselect.getPreviousBlockClientId( clientId );\n\t\tif ( previousBlockClientId ) {\n\t\t\tdispatch.selectBlock( previousBlockClientId, -1 );\n\t\t} else if ( fallbackToParent ) {\n\t\t\tconst firstParentClientId = select.getBlockRootClientId( clientId );\n\t\t\tif ( firstParentClientId ) {\n\t\t\t\tdispatch.selectBlock( firstParentClientId, -1 );\n\t\t\t}\n\t\t}\n\t};\n\n/**\n * Yields action objects used in signalling that the block following the given\n * clientId should be selected.\n *\n * @param {string} clientId Block client ID.\n */\nexport const selectNextBlock =\n\t( clientId ) =>\n\t( { select, dispatch } ) => {\n\t\tconst nextBlockClientId = select.getNextBlockClientId( clientId );\n\t\tif ( nextBlockClientId ) {\n\t\t\tdispatch.selectBlock( nextBlockClientId );\n\t\t}\n\t};\n\n/**\n * Action that starts block multi-selection.\n *\n * @return {Object} Action object.\n */\nexport function startMultiSelect() {\n\treturn {\n\t\ttype: 'START_MULTI_SELECT',\n\t};\n}\n\n/**\n * Action that stops block multi-selection.\n *\n * @return {Object} Action object.\n */\nexport function stopMultiSelect() {\n\treturn {\n\t\ttype: 'STOP_MULTI_SELECT',\n\t};\n}\n\n/**\n * Action that changes block multi-selection.\n *\n * @param {string} start First block of the multi selection.\n * @param {string} end Last block of the multiselection.\n * @param {number|null} __experimentalInitialPosition Optional initial position. Pass as null to skip focus within editor canvas.\n */\nexport const multiSelect =\n\t( start, end, __experimentalInitialPosition = 0 ) =>\n\t( { select, dispatch } ) => {\n\t\tconst startBlockRootClientId = select.getBlockRootClientId( start );\n\t\tconst endBlockRootClientId = select.getBlockRootClientId( end );\n\n\t\t// Only allow block multi-selections at the same level.\n\t\tif ( startBlockRootClientId !== endBlockRootClientId ) {\n\t\t\treturn;\n\t\t}\n\n\t\tdispatch( {\n\t\t\ttype: 'MULTI_SELECT',\n\t\t\tstart,\n\t\t\tend,\n\t\t\tinitialPosition: __experimentalInitialPosition,\n\t\t} );\n\n\t\tconst blockCount = select.getSelectedBlockCount();\n\n\t\tspeak(\n\t\t\tsprintf(\n\t\t\t\t/* translators: %s: number of selected blocks */\n\t\t\t\t_n( '%s block selected.', '%s blocks selected.', blockCount ),\n\t\t\t\tblockCount\n\t\t\t),\n\t\t\t'assertive'\n\t\t);\n\t};\n\n/**\n * Action that clears the block selection.\n *\n * @return {Object} Action object.\n */\nexport function clearSelectedBlock() {\n\treturn {\n\t\ttype: 'CLEAR_SELECTED_BLOCK',\n\t};\n}\n\n/**\n * Action that enables or disables block selection.\n *\n * @param {boolean} [isSelectionEnabled=true] Whether block selection should\n * be enabled.\n *\n * @return {Object} Action object.\n */\nexport function toggleSelection( isSelectionEnabled = true ) {\n\treturn {\n\t\ttype: 'TOGGLE_SELECTION',\n\t\tisSelectionEnabled,\n\t};\n}\n\n/**\n * Action that replaces given blocks with one or more replacement blocks.\n *\n * @param {(string|string[])} clientIds Block client ID(s) to replace.\n * @param {(Object|Object[])} blocks Replacement block(s).\n * @param {number} indexToSelect Index of replacement block to select.\n * @param {0|-1|null} initialPosition Index of caret after in the selected block after the operation.\n * @param {?Object} meta Optional Meta values to be passed to the action object.\n *\n * @return {Object} Action object.\n */\nexport const replaceBlocks =\n\t( clientIds, blocks, indexToSelect, initialPosition = 0, meta ) =>\n\t( { select, dispatch, registry } ) => {\n\t\tclientIds = castArray( clientIds );\n\t\tblocks = castArray( blocks );\n\t\tconst rootClientId = select.getBlockRootClientId( clientIds[ 0 ] );\n\t\t// Replace is valid if the new blocks can be inserted in the root block.\n\t\tfor ( let index = 0; index < blocks.length; index++ ) {\n\t\t\tconst block = blocks[ index ];\n\t\t\tconst canInsertBlock = select.canInsertBlockType(\n\t\t\t\tblock.name,\n\t\t\t\trootClientId\n\t\t\t);\n\t\t\tif ( ! canInsertBlock ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\t// We're batching these two actions because an extra `undo/redo` step can\n\t\t// be created, based on whether we insert a default block or not.\n\t\tregistry.batch( () => {\n\t\t\tdispatch( {\n\t\t\t\ttype: 'REPLACE_BLOCKS',\n\t\t\t\tclientIds,\n\t\t\t\tblocks,\n\t\t\t\ttime: Date.now(),\n\t\t\t\tindexToSelect,\n\t\t\t\tinitialPosition,\n\t\t\t\tmeta,\n\t\t\t} );\n\t\t\t// To avoid a focus loss when removing the last block, assure there is\n\t\t\t// always a default block if the last of the blocks have been removed.\n\t\t\tdispatch.ensureDefaultBlock();\n\t\t} );\n\t};\n\n/**\n * Action that replaces a single block with one or more replacement blocks.\n *\n * @param {(string|string[])} clientId Block client ID to replace.\n * @param {(Object|Object[])} block Replacement block(s).\n *\n * @return {Object} Action object.\n */\nexport function replaceBlock( clientId, block ) {\n\treturn replaceBlocks( clientId, block );\n}\n\n/**\n * Higher-order action creator which, given the action type to dispatch creates\n * an action creator for managing block movement.\n *\n * @param {string} type Action type to dispatch.\n *\n * @return {Function} Action creator.\n */\nconst createOnMove =\n\t( type ) =>\n\t( clientIds, rootClientId ) =>\n\t( { select, dispatch } ) => {\n\t\t// If one of the blocks is locked or the parent is locked, we cannot move any block.\n\t\tconst canMoveBlocks = select.canMoveBlocks( clientIds );\n\t\tif ( ! canMoveBlocks ) {\n\t\t\treturn;\n\t\t}\n\n\t\tdispatch( { type, clientIds: castArray( clientIds ), rootClientId } );\n\t};\n\nexport const moveBlocksDown = createOnMove( 'MOVE_BLOCKS_DOWN' );\nexport const moveBlocksUp = createOnMove( 'MOVE_BLOCKS_UP' );\n\n/**\n * Action that moves given blocks to a new position.\n *\n * @param {?string} clientIds The client IDs of the blocks.\n * @param {?string} fromRootClientId Root client ID source.\n * @param {?string} toRootClientId Root client ID destination.\n * @param {number} index The index to move the blocks to.\n */\nexport const moveBlocksToPosition =\n\t( clientIds, fromRootClientId = '', toRootClientId = '', index ) =>\n\t( { select, dispatch } ) => {\n\t\tconst canMoveBlocks = select.canMoveBlocks( clientIds );\n\n\t\t// If one of the blocks is locked or the parent is locked, we cannot move any block.\n\t\tif ( ! canMoveBlocks ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// If moving inside the same root block the move is always possible.\n\t\tif ( fromRootClientId !== toRootClientId ) {\n\t\t\tconst canRemoveBlocks = select.canRemoveBlocks( clientIds );\n\n\t\t\t// If we're moving to another block, it means we're deleting blocks from\n\t\t\t// the original block, so we need to check if removing is possible.\n\t\t\tif ( ! canRemoveBlocks ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst canInsertBlocks = select.canInsertBlocks(\n\t\t\t\tclientIds,\n\t\t\t\ttoRootClientId\n\t\t\t);\n\n\t\t\t// If moving to other parent block, the move is possible if we can insert a block of the same type inside the new parent block.\n\t\t\tif ( ! canInsertBlocks ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tdispatch( {\n\t\t\ttype: 'MOVE_BLOCKS_TO_POSITION',\n\t\t\tfromRootClientId,\n\t\t\ttoRootClientId,\n\t\t\tclientIds,\n\t\t\tindex,\n\t\t} );\n\t};\n\n/**\n * Action that moves given block to a new position.\n *\n * @param {?string} clientId The client ID of the block.\n * @param {?string} fromRootClientId Root client ID source.\n * @param {?string} toRootClientId Root client ID destination.\n * @param {number} index The index to move the block to.\n */\nexport function moveBlockToPosition(\n\tclientId,\n\tfromRootClientId = '',\n\ttoRootClientId = '',\n\tindex\n) {\n\treturn moveBlocksToPosition(\n\t\t[ clientId ],\n\t\tfromRootClientId,\n\t\ttoRootClientId,\n\t\tindex\n\t);\n}\n\n/**\n * Action that inserts a single block, optionally at a specific index respective a root block list.\n *\n * Only allowed blocks are inserted. The action may fail silently for blocks that are not allowed or if\n * a templateLock is active on the block list.\n *\n * @param {Object} block Block object to insert.\n * @param {?number} index Index at which block should be inserted.\n * @param {?string} rootClientId Optional root client ID of block list on which to insert.\n * @param {?boolean} updateSelection If true block selection will be updated. If false, block selection will not change. Defaults to true.\n * @param {?Object} meta Optional Meta values to be passed to the action object.\n *\n * @return {Object} Action object.\n */\nexport function insertBlock(\n\tblock,\n\tindex,\n\trootClientId,\n\tupdateSelection,\n\tmeta\n) {\n\treturn insertBlocks(\n\t\t[ block ],\n\t\tindex,\n\t\trootClientId,\n\t\tupdateSelection,\n\t\t0,\n\t\tmeta\n\t);\n}\n\n/**\n * Action that inserts an array of blocks, optionally at a specific index respective a root block list.\n *\n * Only allowed blocks are inserted. The action may fail silently for blocks that are not allowed or if\n * a templateLock is active on the block list.\n *\n * @param {Object[]} blocks Block objects to insert.\n * @param {?number} index Index at which block should be inserted.\n * @param {?string} rootClientId Optional root client ID of block list on which to insert.\n * @param {?boolean} updateSelection If true block selection will be updated. If false, block selection will not change. Defaults to true.\n * @param {0|-1|null} initialPosition Initial focus position. Setting it to null prevent focusing the inserted block.\n * @param {?Object} meta Optional Meta values to be passed to the action object.\n *\n * @return {Object} Action object.\n */\nexport const insertBlocks =\n\t(\n\t\tblocks,\n\t\tindex,\n\t\trootClientId,\n\t\tupdateSelection = true,\n\t\tinitialPosition = 0,\n\t\tmeta\n\t) =>\n\t( { select, dispatch } ) => {\n\t\tif ( initialPosition !== null && typeof initialPosition === 'object' ) {\n\t\t\tmeta = initialPosition;\n\t\t\tinitialPosition = 0;\n\t\t\tdeprecated(\n\t\t\t\t\"meta argument in wp.data.dispatch('core/block-editor')\",\n\t\t\t\t{\n\t\t\t\t\tsince: '5.8',\n\t\t\t\t\thint: 'The meta argument is now the 6th argument of the function',\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\n\t\tblocks = castArray( blocks );\n\t\tconst allowedBlocks = [];\n\t\tfor ( const block of blocks ) {\n\t\t\tconst isValid = select.canInsertBlockType(\n\t\t\t\tblock.name,\n\t\t\t\trootClientId\n\t\t\t);\n\t\t\tif ( isValid ) {\n\t\t\t\tallowedBlocks.push( block );\n\t\t\t}\n\t\t}\n\t\tif ( allowedBlocks.length ) {\n\t\t\tdispatch( {\n\t\t\t\ttype: 'INSERT_BLOCKS',\n\t\t\t\tblocks: allowedBlocks,\n\t\t\t\tindex,\n\t\t\t\trootClientId,\n\t\t\t\ttime: Date.now(),\n\t\t\t\tupdateSelection,\n\t\t\t\tinitialPosition: updateSelection ? initialPosition : null,\n\t\t\t\tmeta,\n\t\t\t} );\n\t\t}\n\t};\n\n/**\n * Action that shows the insertion point.\n *\n * @param {?string} rootClientId Optional root client ID of block list on\n * which to insert.\n * @param {?number} index Index at which block should be inserted.\n * @param {?Object} __unstableOptions Additional options.\n * @property {boolean} __unstableWithInserter Whether or not to show an inserter button.\n * @property {WPDropOperation} operation The operation to perform when applied,\n * either 'insert' or 'replace' for now.\n *\n * @return {Object} Action object.\n */\nexport function showInsertionPoint(\n\trootClientId,\n\tindex,\n\t__unstableOptions = {}\n) {\n\tconst { __unstableWithInserter, operation, nearestSide } =\n\t\t__unstableOptions;\n\treturn {\n\t\ttype: 'SHOW_INSERTION_POINT',\n\t\trootClientId,\n\t\tindex,\n\t\t__unstableWithInserter,\n\t\toperation,\n\t\tnearestSide,\n\t};\n}\n/**\n * Action that hides the insertion point.\n */\nexport const hideInsertionPoint =\n\t() =>\n\t( { select, dispatch } ) => {\n\t\tif ( ! select.isBlockInsertionPointVisible() ) {\n\t\t\treturn;\n\t\t}\n\t\tdispatch( {\n\t\t\ttype: 'HIDE_INSERTION_POINT',\n\t\t} );\n\t};\n\n/**\n * Action that resets the template validity.\n *\n * @param {boolean} isValid template validity flag.\n *\n * @return {Object} Action object.\n */\nexport function setTemplateValidity( isValid ) {\n\treturn {\n\t\ttype: 'SET_TEMPLATE_VALIDITY',\n\t\tisValid,\n\t};\n}\n\n/**\n * Action that synchronizes the template with the list of blocks.\n *\n * @return {Object} Action object.\n */\nexport const synchronizeTemplate =\n\t() =>\n\t( { select, dispatch } ) => {\n\t\tdispatch( { type: 'SYNCHRONIZE_TEMPLATE' } );\n\t\tconst blocks = select.getBlocks();\n\t\tconst template = select.getTemplate();\n\t\tconst updatedBlockList = synchronizeBlocksWithTemplate(\n\t\t\tblocks,\n\t\t\ttemplate\n\t\t);\n\n\t\tdispatch.resetBlocks( updatedBlockList );\n\t};\n\n/**\n * Delete the current selection.\n *\n * @param {boolean} isForward\n */\nexport const __unstableDeleteSelection =\n\t( isForward ) =>\n\t( { registry, select, dispatch } ) => {\n\t\tconst selectionAnchor = select.getSelectionStart();\n\t\tconst selectionFocus = select.getSelectionEnd();\n\n\t\tif ( selectionAnchor.clientId === selectionFocus.clientId ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// It's not mergeable if there's no rich text selection.\n\t\tif (\n\t\t\t! selectionAnchor.attributeKey ||\n\t\t\t! selectionFocus.attributeKey ||\n\t\t\ttypeof selectionAnchor.offset === 'undefined' ||\n\t\t\ttypeof selectionFocus.offset === 'undefined'\n\t\t) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst anchorRootClientId = select.getBlockRootClientId(\n\t\t\tselectionAnchor.clientId\n\t\t);\n\t\tconst focusRootClientId = select.getBlockRootClientId(\n\t\t\tselectionFocus.clientId\n\t\t);\n\n\t\t// It's not mergeable if the selection doesn't start and end in the same\n\t\t// block list. Maybe in the future it should be allowed.\n\t\tif ( anchorRootClientId !== focusRootClientId ) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst blockOrder = select.getBlockOrder( anchorRootClientId );\n\t\tconst anchorIndex = blockOrder.indexOf( selectionAnchor.clientId );\n\t\tconst focusIndex = blockOrder.indexOf( selectionFocus.clientId );\n\n\t\t// Reassign selection start and end based on order.\n\t\tlet selectionStart, selectionEnd;\n\n\t\tif ( anchorIndex > focusIndex ) {\n\t\t\tselectionStart = selectionFocus;\n\t\t\tselectionEnd = selectionAnchor;\n\t\t} else {\n\t\t\tselectionStart = selectionAnchor;\n\t\t\tselectionEnd = selectionFocus;\n\t\t}\n\n\t\tconst targetSelection = isForward ? selectionEnd : selectionStart;\n\t\tconst targetBlock = select.getBlock( targetSelection.clientId );\n\t\tconst targetBlockType = getBlockType( targetBlock.name );\n\n\t\tif ( ! targetBlockType.merge ) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst selectionA = selectionStart;\n\t\tconst selectionB = selectionEnd;\n\n\t\tconst blockA = select.getBlock( selectionA.clientId );\n\t\tconst blockB = select.getBlock( selectionB.clientId );\n\n\t\tconst htmlA = blockA.attributes[ selectionA.attributeKey ];\n\t\tconst htmlB = blockB.attributes[ selectionB.attributeKey ];\n\n\t\tlet valueA = create( { html: htmlA } );\n\t\tlet valueB = create( { html: htmlB } );\n\n\t\tvalueA = remove( valueA, selectionA.offset, valueA.text.length );\n\t\tvalueB = insert( valueB, START_OF_SELECTED_AREA, 0, selectionB.offset );\n\n\t\t// Clone the blocks so we don't manipulate the original.\n\t\tconst cloneA = cloneBlock( blockA, {\n\t\t\t[ selectionA.attributeKey ]: toHTMLString( { value: valueA } ),\n\t\t} );\n\t\tconst cloneB = cloneBlock( blockB, {\n\t\t\t[ selectionB.attributeKey ]: toHTMLString( { value: valueB } ),\n\t\t} );\n\n\t\tconst followingBlock = isForward ? cloneA : cloneB;\n\n\t\t// We can only merge blocks with similar types\n\t\t// thus, we transform the block to merge first\n\t\tconst blocksWithTheSameType =\n\t\t\tblockA.name === blockB.name\n\t\t\t\t? [ followingBlock ]\n\t\t\t\t: switchToBlockType( followingBlock, targetBlockType.name );\n\n\t\t// If the block types can not match, do nothing\n\t\tif ( ! blocksWithTheSameType || ! blocksWithTheSameType.length ) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet updatedAttributes;\n\n\t\tif ( isForward ) {\n\t\t\tconst blockToMerge = blocksWithTheSameType.pop();\n\t\t\tupdatedAttributes = targetBlockType.merge(\n\t\t\t\tblockToMerge.attributes,\n\t\t\t\tcloneB.attributes\n\t\t\t);\n\t\t} else {\n\t\t\tconst blockToMerge = blocksWithTheSameType.shift();\n\t\t\tupdatedAttributes = targetBlockType.merge(\n\t\t\t\tcloneA.attributes,\n\t\t\t\tblockToMerge.attributes\n\t\t\t);\n\t\t}\n\n\t\tconst newAttributeKey = retrieveSelectedAttribute( updatedAttributes );\n\n\t\tconst convertedHtml = updatedAttributes[ newAttributeKey ];\n\t\tconst convertedValue = create( { html: convertedHtml } );\n\t\tconst newOffset = convertedValue.text.indexOf( START_OF_SELECTED_AREA );\n\t\tconst newValue = remove( convertedValue, newOffset, newOffset + 1 );\n\t\tconst newHtml = toHTMLString( { value: newValue } );\n\n\t\tupdatedAttributes[ newAttributeKey ] = newHtml;\n\n\t\tconst selectedBlockClientIds = select.getSelectedBlockClientIds();\n\t\tconst replacement = [\n\t\t\t...( isForward ? blocksWithTheSameType : [] ),\n\t\t\t{\n\t\t\t\t// Preserve the original client ID.\n\t\t\t\t...targetBlock,\n\t\t\t\tattributes: {\n\t\t\t\t\t...targetBlock.attributes,\n\t\t\t\t\t...updatedAttributes,\n\t\t\t\t},\n\t\t\t},\n\t\t\t...( isForward ? [] : blocksWithTheSameType ),\n\t\t];\n\n\t\tregistry.batch( () => {\n\t\t\tdispatch.selectionChange(\n\t\t\t\ttargetBlock.clientId,\n\t\t\t\tnewAttributeKey,\n\t\t\t\tnewOffset,\n\t\t\t\tnewOffset\n\t\t\t);\n\n\t\t\tdispatch.replaceBlocks(\n\t\t\t\tselectedBlockClientIds,\n\t\t\t\treplacement,\n\t\t\t\t0, // If we don't pass the `indexToSelect` it will default to the last block.\n\t\t\t\tselect.getSelectedBlocksInitialCaretPosition()\n\t\t\t);\n\t\t} );\n\t};\n\n/**\n * Split the current selection.\n * @param {?Array} blocks\n */\nexport const __unstableSplitSelection =\n\t( blocks = [] ) =>\n\t( { registry, select, dispatch } ) => {\n\t\tconst selectionAnchor = select.getSelectionStart();\n\t\tconst selectionFocus = select.getSelectionEnd();\n\t\tconst anchorRootClientId = select.getBlockRootClientId(\n\t\t\tselectionAnchor.clientId\n\t\t);\n\t\tconst focusRootClientId = select.getBlockRootClientId(\n\t\t\tselectionFocus.clientId\n\t\t);\n\n\t\t// It's not splittable if the selection doesn't start and end in the same\n\t\t// block list. Maybe in the future it should be allowed.\n\t\tif ( anchorRootClientId !== focusRootClientId ) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst blockOrder = select.getBlockOrder( anchorRootClientId );\n\t\tconst anchorIndex = blockOrder.indexOf( selectionAnchor.clientId );\n\t\tconst focusIndex = blockOrder.indexOf( selectionFocus.clientId );\n\n\t\t// Reassign selection start and end based on order.\n\t\tlet selectionStart, selectionEnd;\n\n\t\tif ( anchorIndex > focusIndex ) {\n\t\t\tselectionStart = selectionFocus;\n\t\t\tselectionEnd = selectionAnchor;\n\t\t} else {\n\t\t\tselectionStart = selectionAnchor;\n\t\t\tselectionEnd = selectionFocus;\n\t\t}\n\n\t\tconst selectionA = selectionStart;\n\t\tconst selectionB = selectionEnd;\n\t\tconst blockA = select.getBlock( selectionA.clientId );\n\t\tconst blockB = select.getBlock( selectionB.clientId );\n\t\tconst blockAType = getBlockType( blockA.name );\n\t\tconst blockBType = getBlockType( blockB.name );\n\t\tconst attributeKeyA =\n\t\t\ttypeof selectionA.attributeKey === 'string'\n\t\t\t\t? selectionA.attributeKey\n\t\t\t\t: findRichTextAttributeKey( blockAType );\n\t\tconst attributeKeyB =\n\t\t\ttypeof selectionB.attributeKey === 'string'\n\t\t\t\t? selectionB.attributeKey\n\t\t\t\t: findRichTextAttributeKey( blockBType );\n\t\tconst blockAttributes = select.getBlockAttributes(\n\t\t\tselectionA.clientId\n\t\t);\n\t\tconst bindings = blockAttributes?.metadata?.bindings;\n\n\t\t// If the attribute is bound, don't split the selection and insert a new block instead.\n\t\tif ( bindings?.[ attributeKeyA ] ) {\n\t\t\t// Show warning if user tries to insert a block into another block with bindings.\n\t\t\tif ( blocks.length ) {\n\t\t\t\tconst { createWarningNotice } =\n\t\t\t\t\tregistry.dispatch( noticesStore );\n\t\t\t\tcreateWarningNotice(\n\t\t\t\t\t__(\n\t\t\t\t\t\t\"Blocks can't be inserted into other blocks with bindings\"\n\t\t\t\t\t),\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: 'snackbar',\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tdispatch.insertAfterBlock( selectionA.clientId );\n\t\t\treturn;\n\t\t}\n\n\t\t// Can't split if the selection is not set.\n\t\tif (\n\t\t\t! attributeKeyA ||\n\t\t\t! attributeKeyB ||\n\t\t\ttypeof selectionAnchor.offset === 'undefined' ||\n\t\t\ttypeof selectionFocus.offset === 'undefined'\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\t// We can do some short-circuiting if the selection is collapsed.\n\t\tif (\n\t\t\tselectionA.clientId === selectionB.clientId &&\n\t\t\tattributeKeyA === attributeKeyB &&\n\t\t\tselectionA.offset === selectionB.offset\n\t\t) {\n\t\t\t// If an unmodified default block is selected, replace it. We don't\n\t\t\t// want to be converting into a default block.\n\t\t\tif ( blocks.length ) {\n\t\t\t\tif ( isUnmodifiedDefaultBlock( blockA, 'content' ) ) {\n\t\t\t\t\tdispatch.replaceBlocks(\n\t\t\t\t\t\t[ selectionA.clientId ],\n\t\t\t\t\t\tblocks,\n\t\t\t\t\t\tblocks.length - 1,\n\t\t\t\t\t\t-1\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// If selection is at the start or end, we can simply insert an\n\t\t\t// empty block, provided this block has no inner blocks.\n\t\t\telse if ( ! select.getBlockOrder( selectionA.clientId ).length ) {\n\t\t\t\tfunction createEmpty() {\n\t\t\t\t\tconst defaultBlockName = getDefaultBlockName();\n\t\t\t\t\treturn select.canInsertBlockType(\n\t\t\t\t\t\tdefaultBlockName,\n\t\t\t\t\t\tanchorRootClientId\n\t\t\t\t\t)\n\t\t\t\t\t\t? createBlock( defaultBlockName )\n\t\t\t\t\t\t: createBlock(\n\t\t\t\t\t\t\t\tselect.getBlockName( selectionA.clientId )\n\t\t\t\t\t\t );\n\t\t\t\t}\n\n\t\t\t\tconst length = blockAttributes[ attributeKeyA ].length;\n\n\t\t\t\tif ( selectionA.offset === 0 && length ) {\n\t\t\t\t\tdispatch.insertBlocks(\n\t\t\t\t\t\t[ createEmpty() ],\n\t\t\t\t\t\tselect.getBlockIndex( selectionA.clientId ),\n\t\t\t\t\t\tanchorRootClientId,\n\t\t\t\t\t\tfalse\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif ( selectionA.offset === length ) {\n\t\t\t\t\tdispatch.insertBlocks(\n\t\t\t\t\t\t[ createEmpty() ],\n\t\t\t\t\t\tselect.getBlockIndex( selectionA.clientId ) + 1,\n\t\t\t\t\t\tanchorRootClientId\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst htmlA = blockA.attributes[ attributeKeyA ];\n\t\tconst htmlB = blockB.attributes[ attributeKeyB ];\n\n\t\tlet valueA = create( { html: htmlA } );\n\t\tlet valueB = create( { html: htmlB } );\n\n\t\tvalueA = remove( valueA, selectionA.offset, valueA.text.length );\n\t\tvalueB = remove( valueB, 0, selectionB.offset );\n\n\t\tlet head = {\n\t\t\t// Preserve the original client ID.\n\t\t\t...blockA,\n\t\t\t// If both start and end are the same, should only copy innerBlocks\n\t\t\t// once.\n\t\t\tinnerBlocks:\n\t\t\t\tblockA.clientId === blockB.clientId ? [] : blockA.innerBlocks,\n\t\t\tattributes: {\n\t\t\t\t...blockA.attributes,\n\t\t\t\t[ attributeKeyA ]: toHTMLString( { value: valueA } ),\n\t\t\t},\n\t\t};\n\n\t\tlet tail = {\n\t\t\t...blockB,\n\t\t\t// Only preserve the original client ID if the end is different.\n\t\t\tclientId:\n\t\t\t\tblockA.clientId === blockB.clientId\n\t\t\t\t\t? createBlock( blockB.name ).clientId\n\t\t\t\t\t: blockB.clientId,\n\t\t\tattributes: {\n\t\t\t\t...blockB.attributes,\n\t\t\t\t[ attributeKeyB ]: toHTMLString( { value: valueB } ),\n\t\t\t},\n\t\t};\n\n\t\t// When splitting a block, attempt to convert the tail block to the\n\t\t// default block type. For example, when splitting a heading block, the\n\t\t// tail block will be converted to a paragraph block. Note that for\n\t\t// blocks such as a list item and button, this will be skipped because\n\t\t// the default block type cannot be inserted.\n\t\tconst defaultBlockName = getDefaultBlockName();\n\t\tif (\n\t\t\t// A block is only split when the selection is within the same\n\t\t\t// block.\n\t\t\tblockA.clientId === blockB.clientId &&\n\t\t\tdefaultBlockName &&\n\t\t\ttail.name !== defaultBlockName &&\n\t\t\tselect.canInsertBlockType( defaultBlockName, anchorRootClientId )\n\t\t) {\n\t\t\tconst switched = switchToBlockType( tail, defaultBlockName );\n\t\t\tif ( switched?.length === 1 ) {\n\t\t\t\ttail = switched[ 0 ];\n\t\t\t}\n\t\t}\n\n\t\tif ( ! blocks.length ) {\n\t\t\tdispatch.replaceBlocks( select.getSelectedBlockClientIds(), [\n\t\t\t\thead,\n\t\t\t\ttail,\n\t\t\t] );\n\t\t\treturn;\n\t\t}\n\n\t\tlet selection;\n\t\tconst output = [];\n\t\tconst clonedBlocks = [ ...blocks ];\n\t\tconst firstBlock = clonedBlocks.shift();\n\t\tconst headType = getBlockType( head.name );\n\t\tconst firstBlocks =\n\t\t\theadType.merge && firstBlock.name === headType.name\n\t\t\t\t? [ firstBlock ]\n\t\t\t\t: switchToBlockType( firstBlock, headType.name );\n\n\t\tif ( firstBlocks?.length ) {\n\t\t\tconst first = firstBlocks.shift();\n\t\t\thead = {\n\t\t\t\t...head,\n\t\t\t\tattributes: {\n\t\t\t\t\t...head.attributes,\n\t\t\t\t\t...headType.merge( head.attributes, first.attributes ),\n\t\t\t\t},\n\t\t\t};\n\t\t\toutput.push( head );\n\t\t\tselection = {\n\t\t\t\tclientId: head.clientId,\n\t\t\t\tattributeKey: attributeKeyA,\n\t\t\t\toffset: create( { html: head.attributes[ attributeKeyA ] } )\n\t\t\t\t\t.text.length,\n\t\t\t};\n\t\t\tclonedBlocks.unshift( ...firstBlocks );\n\t\t} else {\n\t\t\tif ( ! isUnmodifiedBlock( head ) ) {\n\t\t\t\toutput.push( head );\n\t\t\t}\n\t\t\toutput.push( firstBlock );\n\t\t}\n\n\t\tconst lastBlock = clonedBlocks.pop();\n\t\tconst tailType = getBlockType( tail.name );\n\n\t\tif ( clonedBlocks.length ) {\n\t\t\toutput.push( ...clonedBlocks );\n\t\t}\n\n\t\tif ( lastBlock ) {\n\t\t\tconst lastBlocks =\n\t\t\t\ttailType.merge && tailType.name === lastBlock.name\n\t\t\t\t\t? [ lastBlock ]\n\t\t\t\t\t: switchToBlockType( lastBlock, tailType.name );\n\n\t\t\tif ( lastBlocks?.length ) {\n\t\t\t\tconst last = lastBlocks.pop();\n\t\t\t\toutput.push( {\n\t\t\t\t\t...tail,\n\t\t\t\t\tattributes: {\n\t\t\t\t\t\t...tail.attributes,\n\t\t\t\t\t\t...tailType.merge( last.attributes, tail.attributes ),\n\t\t\t\t\t},\n\t\t\t\t} );\n\t\t\t\toutput.push( ...lastBlocks );\n\t\t\t\tselection = {\n\t\t\t\t\tclientId: tail.clientId,\n\t\t\t\t\tattributeKey: attributeKeyB,\n\t\t\t\t\toffset: create( {\n\t\t\t\t\t\thtml: last.attributes[ attributeKeyB ],\n\t\t\t\t\t} ).text.length,\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\toutput.push( lastBlock );\n\t\t\t\tif ( ! isUnmodifiedBlock( tail ) ) {\n\t\t\t\t\toutput.push( tail );\n\t\t\t\t}\n\t\t\t}\n\t\t} else if ( ! isUnmodifiedBlock( tail ) ) {\n\t\t\toutput.push( tail );\n\t\t}\n\n\t\tregistry.batch( () => {\n\t\t\tdispatch.replaceBlocks(\n\t\t\t\tselect.getSelectedBlockClientIds(),\n\t\t\t\toutput,\n\t\t\t\toutput.length - 1,\n\t\t\t\t0\n\t\t\t);\n\t\t\tif ( selection ) {\n\t\t\t\tdispatch.selectionChange(\n\t\t\t\t\tselection.clientId,\n\t\t\t\t\tselection.attributeKey,\n\t\t\t\t\tselection.offset,\n\t\t\t\t\tselection.offset\n\t\t\t\t);\n\t\t\t}\n\t\t} );\n\t};\n\n/**\n * Expand the selection to cover the entire blocks, removing partial selection.\n */\nexport const __unstableExpandSelection =\n\t() =>\n\t( { select, dispatch } ) => {\n\t\tconst selectionAnchor = select.getSelectionStart();\n\t\tconst selectionFocus = select.getSelectionEnd();\n\t\tdispatch.selectionChange( {\n\t\t\tstart: { clientId: selectionAnchor.clientId },\n\t\t\tend: { clientId: selectionFocus.clientId },\n\t\t} );\n\t};\n\n/**\n * Action that merges two blocks.\n *\n * @param {string} firstBlockClientId Client ID of the first block to merge.\n * @param {string} secondBlockClientId Client ID of the second block to merge.\n */\nexport const mergeBlocks =\n\t( firstBlockClientId, secondBlockClientId ) =>\n\t( { registry, select, dispatch } ) => {\n\t\tconst clientIdA = firstBlockClientId;\n\t\tconst clientIdB = secondBlockClientId;\n\t\tconst blockA = select.getBlock( clientIdA );\n\t\tconst blockAType = getBlockType( blockA.name );\n\n\t\tif (\n\t\t\t! blockAType ||\n\t\t\tselect.getBlockEditingMode( clientIdA ) === 'disabled' ||\n\t\t\tselect.getBlockEditingMode( clientIdB ) === 'disabled'\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst blockB = select.getBlock( clientIdB );\n\n\t\tif (\n\t\t\t! blockAType.merge &&\n\t\t\tgetBlockSupport( blockA.name, '__experimentalOnMerge' )\n\t\t) {\n\t\t\t// If there's no merge function defined, attempt merging inner\n\t\t\t// blocks.\n\t\t\tconst blocksWithTheSameType = switchToBlockType(\n\t\t\t\tblockB,\n\t\t\t\tblockAType.name\n\t\t\t);\n\t\t\t// Only focus the previous block if it's not mergeable.\n\t\t\tif ( blocksWithTheSameType?.length !== 1 ) {\n\t\t\t\tdispatch.selectBlock( blockA.clientId );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst [ blockWithSameType ] = blocksWithTheSameType;\n\t\t\tif ( blockWithSameType.innerBlocks.length < 1 ) {\n\t\t\t\tdispatch.selectBlock( blockA.clientId );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tregistry.batch( () => {\n\t\t\t\tdispatch.insertBlocks(\n\t\t\t\t\tblockWithSameType.innerBlocks,\n\t\t\t\t\tundefined,\n\t\t\t\t\tclientIdA\n\t\t\t\t);\n\t\t\t\tdispatch.removeBlock( clientIdB );\n\t\t\t\tdispatch.selectBlock(\n\t\t\t\t\tblockWithSameType.innerBlocks[ 0 ].clientId\n\t\t\t\t);\n\n\t\t\t\t// Attempt to merge the next block if it's the same type and\n\t\t\t\t// same attributes. This is useful when merging a paragraph into\n\t\t\t\t// a list, and the next block is also a list. If we don't merge,\n\t\t\t\t// it looks like one list, but it's actually two lists. The same\n\t\t\t\t// applies to other blocks such as a group with the same\n\t\t\t\t// attributes.\n\t\t\t\tconst nextBlockClientId =\n\t\t\t\t\tselect.getNextBlockClientId( clientIdA );\n\n\t\t\t\tif (\n\t\t\t\t\tnextBlockClientId &&\n\t\t\t\t\tselect.getBlockName( clientIdA ) ===\n\t\t\t\t\t\tselect.getBlockName( nextBlockClientId )\n\t\t\t\t) {\n\t\t\t\t\tconst rootAttributes =\n\t\t\t\t\t\tselect.getBlockAttributes( clientIdA );\n\t\t\t\t\tconst previousRootAttributes =\n\t\t\t\t\t\tselect.getBlockAttributes( nextBlockClientId );\n\n\t\t\t\t\tif (\n\t\t\t\t\t\tObject.keys( rootAttributes ).every(\n\t\t\t\t\t\t\t( key ) =>\n\t\t\t\t\t\t\t\trootAttributes[ key ] ===\n\t\t\t\t\t\t\t\tpreviousRootAttributes[ key ]\n\t\t\t\t\t\t)\n\t\t\t\t\t) {\n\t\t\t\t\t\tdispatch.moveBlocksToPosition(\n\t\t\t\t\t\t\tselect.getBlockOrder( nextBlockClientId ),\n\t\t\t\t\t\t\tnextBlockClientId,\n\t\t\t\t\t\t\tclientIdA\n\t\t\t\t\t\t);\n\t\t\t\t\t\tdispatch.removeBlock( nextBlockClientId, false );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} );\n\t\t\treturn;\n\t\t}\n\n\t\tif ( isUnmodifiedDefaultBlock( blockA ) ) {\n\t\t\tdispatch.removeBlock(\n\t\t\t\tclientIdA,\n\t\t\t\tselect.isBlockSelected( clientIdA )\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tif ( isUnmodifiedDefaultBlock( blockB ) ) {\n\t\t\tdispatch.removeBlock(\n\t\t\t\tclientIdB,\n\t\t\t\tselect.isBlockSelected( clientIdB )\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tif ( ! blockAType.merge ) {\n\t\t\tif ( isUnmodifiedBlock( blockB, 'content' ) ) {\n\t\t\t\tdispatch.removeBlock(\n\t\t\t\t\tclientIdB,\n\t\t\t\t\tselect.isBlockSelected( clientIdB )\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tdispatch.selectBlock( blockA.clientId );\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst blockBType = getBlockType( blockB.name );\n\t\tconst { clientId, attributeKey, offset } = select.getSelectionStart();\n\t\tconst selectedBlockType =\n\t\t\tclientId === clientIdA ? blockAType : blockBType;\n\t\tconst attributeDefinition =\n\t\t\tselectedBlockType.attributes[ attributeKey ];\n\t\tconst canRestoreTextSelection =\n\t\t\t( clientId === clientIdA || clientId === clientIdB ) &&\n\t\t\tattributeKey !== undefined &&\n\t\t\toffset !== undefined &&\n\t\t\t// We cannot restore text selection if the RichText identifier\n\t\t\t// is not a defined block attribute key. This can be the case if the\n\t\t\t// fallback instance ID is used to store selection (and no RichText\n\t\t\t// identifier is set), or when the identifier is wrong.\n\t\t\t!! attributeDefinition;\n\n\t\tif ( ! attributeDefinition ) {\n\t\t\tif ( typeof attributeKey === 'number' ) {\n\t\t\t\twindow.console.error(\n\t\t\t\t\t`RichText needs an identifier prop that is the block attribute key of the attribute it controls. Its type is expected to be a string, but was ${ typeof attributeKey }`\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\twindow.console.error(\n\t\t\t\t\t'The RichText identifier prop does not match any attributes defined by the block.'\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Clone the blocks so we don't insert the character in a \"live\" block.\n\t\tconst cloneA = cloneBlock( blockA );\n\t\tconst cloneB = cloneBlock( blockB );\n\n\t\tif ( canRestoreTextSelection ) {\n\t\t\tconst selectedBlock = clientId === clientIdA ? cloneA : cloneB;\n\t\t\tconst html = selectedBlock.attributes[ attributeKey ];\n\t\t\tconst value = insert(\n\t\t\t\tcreate( { html } ),\n\t\t\t\tSTART_OF_SELECTED_AREA,\n\t\t\t\toffset,\n\t\t\t\toffset\n\t\t\t);\n\n\t\t\tselectedBlock.attributes[ attributeKey ] = toHTMLString( {\n\t\t\t\tvalue,\n\t\t\t} );\n\t\t}\n\n\t\t// We can only merge blocks with similar types\n\t\t// thus, we transform the block to merge first.\n\t\tconst blocksWithTheSameType =\n\t\t\tblockA.name === blockB.name\n\t\t\t\t? [ cloneB ]\n\t\t\t\t: switchToBlockType( cloneB, blockA.name );\n\n\t\t// If the block types can not match, do nothing.\n\t\tif ( ! blocksWithTheSameType || ! blocksWithTheSameType.length ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Calling the merge to update the attributes and remove the block to be merged.\n\t\tconst updatedAttributes = blockAType.merge(\n\t\t\tcloneA.attributes,\n\t\t\tblocksWithTheSameType[ 0 ].attributes\n\t\t);\n\n\t\tif ( canRestoreTextSelection ) {\n\t\t\tconst newAttributeKey =\n\t\t\t\tretrieveSelectedAttribute( updatedAttributes );\n\t\t\tconst convertedHtml = updatedAttributes[ newAttributeKey ];\n\t\t\tconst convertedValue = create( { html: convertedHtml } );\n\t\t\tconst newOffset = convertedValue.text.indexOf(\n\t\t\t\tSTART_OF_SELECTED_AREA\n\t\t\t);\n\t\t\tconst newValue = remove( convertedValue, newOffset, newOffset + 1 );\n\t\t\tconst newHtml = toHTMLString( { value: newValue } );\n\n\t\t\tupdatedAttributes[ newAttributeKey ] = newHtml;\n\n\t\t\tdispatch.selectionChange(\n\t\t\t\tblockA.clientId,\n\t\t\t\tnewAttributeKey,\n\t\t\t\tnewOffset,\n\t\t\t\tnewOffset\n\t\t\t);\n\t\t}\n\n\t\tdispatch.replaceBlocks(\n\t\t\t[ blockA.clientId, blockB.clientId ],\n\t\t\t[\n\t\t\t\t{\n\t\t\t\t\t...blockA,\n\t\t\t\t\tattributes: {\n\t\t\t\t\t\t...blockA.attributes,\n\t\t\t\t\t\t...updatedAttributes,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t...blocksWithTheSameType.slice( 1 ),\n\t\t\t],\n\t\t\t0 // If we don't pass the `indexToSelect` it will default to the last block.\n\t\t);\n\t};\n\n/**\n * Yields action objects used in signalling that the blocks corresponding to\n * the set of specified client IDs are to be removed.\n *\n * @param {string|string[]} clientIds Client IDs of blocks to remove.\n * @param {boolean} selectPrevious True if the previous block\n * or the immediate parent\n * (if no previous block exists)\n * should be selected\n * when a block is removed.\n */\nexport const removeBlocks = ( clientIds, selectPrevious = true ) =>\n\tprivateRemoveBlocks( clientIds, selectPrevious );\n\n/**\n * Returns an action object used in signalling that the block with the\n * specified client ID is to be removed.\n *\n * @param {string} clientId Client ID of block to remove.\n * @param {boolean} selectPrevious True if the previous block should be\n * selected when a block is removed.\n *\n * @return {Object} Action object.\n */\nexport function removeBlock( clientId, selectPrevious ) {\n\treturn removeBlocks( [ clientId ], selectPrevious );\n}\n\n/**\n * Returns an action object used in signalling that the inner blocks with the\n * specified client ID should be replaced.\n *\n * @param {string} rootClientId Client ID of the block whose InnerBlocks will re replaced.\n * @param {Object[]} blocks Block objects to insert as new InnerBlocks\n * @param {?boolean} updateSelection If true block selection will be updated. If false, block selection will not change. Defaults to false.\n * @param {0|-1|null} initialPosition Initial block position.\n * @return {Object} Action object.\n */\nexport function replaceInnerBlocks(\n\trootClientId,\n\tblocks,\n\tupdateSelection = false,\n\tinitialPosition = 0\n) {\n\treturn {\n\t\ttype: 'REPLACE_INNER_BLOCKS',\n\t\trootClientId,\n\t\tblocks,\n\t\tupdateSelection,\n\t\tinitialPosition: updateSelection ? initialPosition : null,\n\t\ttime: Date.now(),\n\t};\n}\n\n/**\n * Returns an action object used to toggle the block editing mode between\n * visual and HTML modes.\n *\n * @param {string} clientId Block client ID.\n *\n * @return {Object} Action object.\n */\nexport function toggleBlockMode( clientId ) {\n\treturn {\n\t\ttype: 'TOGGLE_BLOCK_MODE',\n\t\tclientId,\n\t};\n}\n\n/**\n * Returns an action object used in signalling that the user has begun to type.\n *\n * @return {Object} Action object.\n */\nexport function startTyping() {\n\treturn {\n\t\ttype: 'START_TYPING',\n\t};\n}\n\n/**\n * Returns an action object used in signalling that the user has stopped typing.\n *\n * @return {Object} Action object.\n */\nexport function stopTyping() {\n\treturn {\n\t\ttype: 'STOP_TYPING',\n\t};\n}\n\n/**\n * Returns an action object used in signalling that the user has begun to drag blocks.\n *\n * @param {string[]} clientIds An array of client ids being dragged\n *\n * @return {Object} Action object.\n */\nexport function startDraggingBlocks( clientIds = [] ) {\n\treturn {\n\t\ttype: 'START_DRAGGING_BLOCKS',\n\t\tclientIds,\n\t};\n}\n\n/**\n * Returns an action object used in signalling that the user has stopped dragging blocks.\n *\n * @return {Object} Action object.\n */\nexport function stopDraggingBlocks() {\n\treturn {\n\t\ttype: 'STOP_DRAGGING_BLOCKS',\n\t};\n}\n\n/**\n * Returns an action object used in signalling that the caret has entered formatted text.\n *\n * @deprecated\n *\n * @return {Object} Action object.\n */\nexport function enterFormattedText() {\n\tdeprecated( 'wp.data.dispatch( \"core/block-editor\" ).enterFormattedText', {\n\t\tsince: '6.1',\n\t\tversion: '6.3',\n\t} );\n\treturn {\n\t\ttype: 'DO_NOTHING',\n\t};\n}\n\n/**\n * Returns an action object used in signalling that the user caret has exited formatted text.\n *\n * @deprecated\n *\n * @return {Object} Action object.\n */\nexport function exitFormattedText() {\n\tdeprecated( 'wp.data.dispatch( \"core/block-editor\" ).exitFormattedText', {\n\t\tsince: '6.1',\n\t\tversion: '6.3',\n\t} );\n\treturn {\n\t\ttype: 'DO_NOTHING',\n\t};\n}\n\n/**\n * Action that changes the position of the user caret.\n *\n * @param {string|WPSelection} clientId The selected block client ID.\n * @param {string} attributeKey The selected block attribute key.\n * @param {number} startOffset The start offset.\n * @param {number} endOffset The end offset.\n *\n * @return {Object} Action object.\n */\nexport function selectionChange(\n\tclientId,\n\tattributeKey,\n\tstartOffset,\n\tendOffset\n) {\n\tif ( typeof clientId === 'string' ) {\n\t\treturn {\n\t\t\ttype: 'SELECTION_CHANGE',\n\t\t\tclientId,\n\t\t\tattributeKey,\n\t\t\tstartOffset,\n\t\t\tendOffset,\n\t\t};\n\t}\n\n\treturn { type: 'SELECTION_CHANGE', ...clientId };\n}\n\n/**\n * Action that adds a new block of the default type to the block list.\n *\n * @param {?Object} attributes Optional attributes of the block to assign.\n * @param {?string} rootClientId Optional root client ID of block list on which\n * to append.\n * @param {?number} index Optional index where to insert the default block.\n */\nexport const insertDefaultBlock =\n\t( attributes, rootClientId, index ) =>\n\t( { dispatch } ) => {\n\t\t// Abort if there is no default block type (if it has been unregistered).\n\t\tconst defaultBlockName = getDefaultBlockName();\n\t\tif ( ! defaultBlockName ) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst block = createBlock( defaultBlockName, attributes );\n\n\t\treturn dispatch.insertBlock( block, index, rootClientId );\n\t};\n\n/**\n * @typedef {Object< string, Object >} SettingsByClientId\n */\n\n/**\n * Action that changes the nested settings of the given block(s).\n *\n * @param {string | SettingsByClientId} clientId Client ID of the block whose\n * nested setting are being\n * received, or object of settings\n * by client ID.\n * @param {Object} settings Object with the new settings\n * for the nested block.\n *\n * @return {Object} Action object\n */\nexport function updateBlockListSettings( clientId, settings ) {\n\treturn {\n\t\ttype: 'UPDATE_BLOCK_LIST_SETTINGS',\n\t\tclientId,\n\t\tsettings,\n\t};\n}\n\n/**\n * Action that updates the block editor settings.\n *\n * @param {Object} settings Updated settings\n *\n * @return {Object} Action object\n */\nexport function updateSettings( settings ) {\n\treturn __experimentalUpdateSettings( settings, {\n\t\tstripExperimentalSettings: true,\n\t} );\n}\n\n/**\n * Action that signals that a temporary reusable block has been saved\n * in order to switch its temporary id with the real id.\n *\n * @param {string} id Reusable block's id.\n * @param {string} updatedId Updated block's id.\n *\n * @return {Object} Action object.\n */\nexport function __unstableSaveReusableBlock( id, updatedId ) {\n\treturn {\n\t\ttype: 'SAVE_REUSABLE_BLOCK_SUCCESS',\n\t\tid,\n\t\tupdatedId,\n\t};\n}\n\n/**\n * Action that marks the last block change explicitly as persistent.\n *\n * @return {Object} Action object.\n