: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
selectionStart = selectionFocus;
selectionEnd = selectionAnchor;
selectionStart = selectionAnchor;
selectionEnd = selectionFocus;
const selectionA = selectionStart;
const selectionB = selectionEnd;
const blockA = select.getBlock(selectionA.clientId);
const blockB = select.getBlock(selectionB.clientId);
const blockAType = (0,external_wp_blocks_namespaceObject.getBlockType)(blockA.name);
const blockBType = (0,external_wp_blocks_namespaceObject.getBlockType)(blockB.name);
const attributeKeyA = typeof selectionA.attributeKey === 'string' ? selectionA.attributeKey : findRichTextAttributeKey(blockAType);
const attributeKeyB = typeof selectionB.attributeKey === 'string' ? selectionB.attributeKey : findRichTextAttributeKey(blockBType);
const blockAttributes = select.getBlockAttributes(selectionA.clientId);
const bindings = blockAttributes?.metadata?.bindings;
// If the attribute is bound, don't split the selection and insert a new block instead.
if (bindings?.[attributeKeyA]) {
// Show warning if user tries to insert a block into another block with bindings.
} = registry.dispatch(external_wp_notices_namespaceObject.store);
createWarningNotice((0,external_wp_i18n_namespaceObject.__)("Blocks can't be inserted into other blocks with bindings"), {
dispatch.insertAfterBlock(selectionA.clientId);
// Can't split if the selection is not set.
if (!attributeKeyA || !attributeKeyB || typeof selectionAnchor.offset === 'undefined' || typeof selectionFocus.offset === 'undefined') {
// We can do some short-circuiting if the selection is collapsed.
if (selectionA.clientId === selectionB.clientId && attributeKeyA === attributeKeyB && selectionA.offset === selectionB.offset) {
// If an unmodified default block is selected, replace it. We don't
// want to be converting into a default block.
if ((0,external_wp_blocks_namespaceObject.isUnmodifiedDefaultBlock)(blockA)) {
dispatch.replaceBlocks([selectionA.clientId], blocks, blocks.length - 1, -1);
// If selection is at the start or end, we can simply insert an
// empty block, provided this block has no inner blocks.
else if (!select.getBlockOrder(selectionA.clientId).length) {
const defaultBlockName = (0,external_wp_blocks_namespaceObject.getDefaultBlockName)();
return select.canInsertBlockType(defaultBlockName, anchorRootClientId) ? (0,external_wp_blocks_namespaceObject.createBlock)(defaultBlockName) : (0,external_wp_blocks_namespaceObject.createBlock)(select.getBlockName(selectionA.clientId));
const length = blockAttributes[attributeKeyA].length;
if (selectionA.offset === 0 && length) {
dispatch.insertBlocks([createEmpty()], select.getBlockIndex(selectionA.clientId), anchorRootClientId, false);
if (selectionA.offset === length) {
dispatch.insertBlocks([createEmpty()], select.getBlockIndex(selectionA.clientId) + 1, anchorRootClientId);
const htmlA = blockA.attributes[attributeKeyA];
const htmlB = blockB.attributes[attributeKeyB];
let valueA = (0,external_wp_richText_namespaceObject.create)({
let valueB = (0,external_wp_richText_namespaceObject.create)({
valueA = (0,external_wp_richText_namespaceObject.remove)(valueA, selectionA.offset, valueA.text.length);
valueB = (0,external_wp_richText_namespaceObject.remove)(valueB, 0, selectionB.offset);
// Preserve the original client ID.
// If both start and end are the same, should only copy innerBlocks
innerBlocks: blockA.clientId === blockB.clientId ? [] : blockA.innerBlocks,
[attributeKeyA]: (0,external_wp_richText_namespaceObject.toHTMLString)({
// Only preserve the original client ID if the end is different.
clientId: blockA.clientId === blockB.clientId ? (0,external_wp_blocks_namespaceObject.createBlock)(blockB.name).clientId : blockB.clientId,
[attributeKeyB]: (0,external_wp_richText_namespaceObject.toHTMLString)({
// When splitting a block, attempt to convert the tail block to the
// default block type. For example, when splitting a heading block, the
// tail block will be converted to a paragraph block. Note that for
// blocks such as a list item and button, this will be skipped because
// the default block type cannot be inserted.
const defaultBlockName = (0,external_wp_blocks_namespaceObject.getDefaultBlockName)();
// A block is only split when the selection is within the same
blockA.clientId === blockB.clientId && defaultBlockName && tail.name !== defaultBlockName && select.canInsertBlockType(defaultBlockName, anchorRootClientId)) {
const switched = (0,external_wp_blocks_namespaceObject.switchToBlockType)(tail, defaultBlockName);
if (switched?.length === 1) {
dispatch.replaceBlocks(select.getSelectedBlockClientIds(), [head, tail]);
const clonedBlocks = [...blocks];
const firstBlock = clonedBlocks.shift();
const headType = (0,external_wp_blocks_namespaceObject.getBlockType)(head.name);
const firstBlocks = headType.merge && firstBlock.name === headType.name ? [firstBlock] : (0,external_wp_blocks_namespaceObject.switchToBlockType)(firstBlock, headType.name);
if (firstBlocks?.length) {
const first = firstBlocks.shift();
...headType.merge(head.attributes, first.attributes)
attributeKey: attributeKeyA,
offset: (0,external_wp_richText_namespaceObject.create)({
html: head.attributes[attributeKeyA]
clonedBlocks.unshift(...firstBlocks);
if (!(0,external_wp_blocks_namespaceObject.isUnmodifiedBlock)(head)) {
const lastBlock = clonedBlocks.pop();
const tailType = (0,external_wp_blocks_namespaceObject.getBlockType)(tail.name);
if (clonedBlocks.length) {
output.push(...clonedBlocks);
const lastBlocks = tailType.merge && tailType.name === lastBlock.name ? [lastBlock] : (0,external_wp_blocks_namespaceObject.switchToBlockType)(lastBlock, tailType.name);
if (lastBlocks?.length) {
const last = lastBlocks.pop();
...tailType.merge(last.attributes, tail.attributes)
output.push(...lastBlocks);
attributeKey: attributeKeyB,
offset: (0,external_wp_richText_namespaceObject.create)({
html: last.attributes[attributeKeyB]
if (!(0,external_wp_blocks_namespaceObject.isUnmodifiedBlock)(tail)) {
} else if (!(0,external_wp_blocks_namespaceObject.isUnmodifiedBlock)(tail)) {
dispatch.replaceBlocks(select.getSelectedBlockClientIds(), output, output.length - 1, 0);
dispatch.selectionChange(selection.clientId, selection.attributeKey, selection.offset, selection.offset);
* Expand the selection to cover the entire blocks, removing partial selection.
const __unstableExpandSelection = () => ({
const selectionAnchor = select.getSelectionStart();
const selectionFocus = select.getSelectionEnd();
dispatch.selectionChange({
clientId: selectionAnchor.clientId
clientId: selectionFocus.clientId
* Action that merges two blocks.
* @param {string} firstBlockClientId Client ID of the first block to merge.
* @param {string} secondBlockClientId Client ID of the second block to merge.
const mergeBlocks = (firstBlockClientId, secondBlockClientId) => ({
const clientIdA = firstBlockClientId;
const clientIdB = secondBlockClientId;
const blockA = select.getBlock(clientIdA);
const blockAType = (0,external_wp_blocks_namespaceObject.getBlockType)(blockA.name);
const blockB = select.getBlock(clientIdB);
if (!blockAType.merge && (0,external_wp_blocks_namespaceObject.getBlockSupport)(blockA.name, '__experimentalOnMerge')) {
// If there's no merge function defined, attempt merging inner
const blocksWithTheSameType = (0,external_wp_blocks_namespaceObject.switchToBlockType)(blockB, blockAType.name);
// Only focus the previous block if it's not mergeable.
if (blocksWithTheSameType?.length !== 1) {
dispatch.selectBlock(blockA.clientId);
const [blockWithSameType] = blocksWithTheSameType;
if (blockWithSameType.innerBlocks.length < 1) {
dispatch.selectBlock(blockA.clientId);
dispatch.insertBlocks(blockWithSameType.innerBlocks, undefined, clientIdA);
dispatch.removeBlock(clientIdB);
dispatch.selectBlock(blockWithSameType.innerBlocks[0].clientId);
// Attempt to merge the next block if it's the same type and
// same attributes. This is useful when merging a paragraph into
// a list, and the next block is also a list. If we don't merge,
// it looks like one list, but it's actually two lists. The same
// applies to other blocks such as a group with the same
const nextBlockClientId = select.getNextBlockClientId(clientIdA);
if (nextBlockClientId && select.getBlockName(clientIdA) === select.getBlockName(nextBlockClientId)) {
const rootAttributes = select.getBlockAttributes(clientIdA);
const previousRootAttributes = select.getBlockAttributes(nextBlockClientId);
if (Object.keys(rootAttributes).every(key => rootAttributes[key] === previousRootAttributes[key])) {
dispatch.moveBlocksToPosition(select.getBlockOrder(nextBlockClientId), nextBlockClientId, clientIdA);
dispatch.removeBlock(nextBlockClientId, false);
if ((0,external_wp_blocks_namespaceObject.isUnmodifiedDefaultBlock)(blockA)) {
dispatch.removeBlock(clientIdA, select.isBlockSelected(clientIdA));
if ((0,external_wp_blocks_namespaceObject.isUnmodifiedDefaultBlock)(blockB)) {
dispatch.removeBlock(clientIdB, select.isBlockSelected(clientIdB));
dispatch.selectBlock(blockA.clientId);
const blockBType = (0,external_wp_blocks_namespaceObject.getBlockType)(blockB.name);
} = select.getSelectionStart();
const selectedBlockType = clientId === clientIdA ? blockAType : blockBType;
const attributeDefinition = selectedBlockType.attributes[attributeKey];
const canRestoreTextSelection = (clientId === clientIdA || clientId === clientIdB) && attributeKey !== undefined && offset !== undefined &&
// We cannot restore text selection if the RichText identifier
// is not a defined block attribute key. This can be the case if the
// fallback intance ID is used to store selection (and no RichText
// identifier is set), or when the identifier is wrong.
if (!attributeDefinition) {
if (typeof attributeKey === 'number') {
window.console.error(`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}`);
window.console.error('The RichText identifier prop does not match any attributes defined by the block.');
// Clone the blocks so we don't insert the character in a "live" block.
const cloneA = (0,external_wp_blocks_namespaceObject.cloneBlock)(blockA);
const cloneB = (0,external_wp_blocks_namespaceObject.cloneBlock)(blockB);
if (canRestoreTextSelection) {
const selectedBlock = clientId === clientIdA ? cloneA : cloneB;
const html = selectedBlock.attributes[attributeKey];
const value = (0,external_wp_richText_namespaceObject.insert)((0,external_wp_richText_namespaceObject.create)({
}), START_OF_SELECTED_AREA, offset, offset);
selectedBlock.attributes[attributeKey] = (0,external_wp_richText_namespaceObject.toHTMLString)({
// We can only merge blocks with similar types
// thus, we transform the block to merge first.
const blocksWithTheSameType = blockA.name === blockB.name ? [cloneB] : (0,external_wp_blocks_namespaceObject.switchToBlockType)(cloneB, blockA.name);
// If the block types can not match, do nothing.
if (!blocksWithTheSameType || !blocksWithTheSameType.length) {
// Calling the merge to update the attributes and remove the block to be merged.
const updatedAttributes = blockAType.merge(cloneA.attributes, blocksWithTheSameType[0].attributes);
if (canRestoreTextSelection) {
const newAttributeKey = retrieveSelectedAttribute(updatedAttributes);
const convertedHtml = updatedAttributes[newAttributeKey];
const convertedValue = (0,external_wp_richText_namespaceObject.create)({
const newOffset = convertedValue.text.indexOf(START_OF_SELECTED_AREA);
const newValue = (0,external_wp_richText_namespaceObject.remove)(convertedValue, newOffset, newOffset + 1);
const newHtml = (0,external_wp_richText_namespaceObject.toHTMLString)({
updatedAttributes[newAttributeKey] = newHtml;
dispatch.selectionChange(blockA.clientId, newAttributeKey, newOffset, newOffset);
dispatch.replaceBlocks([blockA.clientId, blockB.clientId], [{
}, ...blocksWithTheSameType.slice(1)], 0 // If we don't pass the `indexToSelect` it will default to the last block.
* Yields action objects used in signalling that the blocks corresponding to
* the set of specified client IDs are to be removed.
* @param {string|string[]} clientIds Client IDs of blocks to remove.
* @param {boolean} selectPrevious True if the previous block
* or the immediate parent
* (if no previous block exists)
* when a block is removed.
const removeBlocks = (clientIds, selectPrevious = true) => privateRemoveBlocks(clientIds, selectPrevious);
* Returns an action object used in signalling that the block with the
* specified client ID is to be removed.
* @param {string} clientId Client ID of block to remove.
* @param {boolean} selectPrevious True if the previous block should be
* selected when a block is removed.
* @return {Object} Action object.
function removeBlock(clientId, selectPrevious) {
return removeBlocks([clientId], selectPrevious);
/* eslint-disable jsdoc/valid-types */
* Returns an action object used in signalling that the inner blocks with the
* specified client ID should be replaced.
* @param {string} rootClientId Client ID of the block whose InnerBlocks will re replaced.
* @param {Object[]} blocks Block objects to insert as new InnerBlocks
* @param {?boolean} updateSelection If true block selection will be updated. If false, block selection will not change. Defaults to false.
* @param {0|-1|null} initialPosition Initial block position.
* @return {Object} Action object.
function replaceInnerBlocks(rootClientId, blocks, updateSelection = false, initialPosition = 0) {
/* eslint-enable jsdoc/valid-types */
type: 'REPLACE_INNER_BLOCKS',
initialPosition: updateSelection ? initialPosition : null,
* Returns an action object used to toggle the block editing mode between
* @param {string} clientId Block client ID.
* @return {Object} Action object.
function toggleBlockMode(clientId) {
type: 'TOGGLE_BLOCK_MODE',
* Returns an action object used in signalling that the user has begun to type.
* @return {Object} Action object.
* Returns an action object used in signalling that the user has stopped typing.
* @return {Object} Action object.
* Returns an action object used in signalling that the user has begun to drag blocks.
* @param {string[]} clientIds An array of client ids being dragged
* @return {Object} Action object.
function startDraggingBlocks(clientIds = []) {
type: 'START_DRAGGING_BLOCKS',
* Returns an action object used in signalling that the user has stopped dragging blocks.
* @return {Object} Action object.
function stopDraggingBlocks() {
type: 'STOP_DRAGGING_BLOCKS'
* Returns an action object used in signalling that the caret has entered formatted text.
* @return {Object} Action object.
function enterFormattedText() {
external_wp_deprecated_default()('wp.data.dispatch( "core/block-editor" ).enterFormattedText', {
* Returns an action object used in signalling that the user caret has exited formatted text.
* @return {Object} Action object.
function exitFormattedText() {
external_wp_deprecated_default()('wp.data.dispatch( "core/block-editor" ).exitFormattedText', {
* Action that changes the position of the user caret.
* @param {string|WPSelection} clientId The selected block client ID.