UNPKG

@wordpress/block-editor

Version:
1,469 lines (1,390 loc) 66.1 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.__unstableMarkAutomaticChange = exports.__unstableExpandSelection = exports.__unstableDeleteSelection = void 0; exports.__unstableMarkLastChangeAsPersistent = __unstableMarkLastChangeAsPersistent; exports.__unstableMarkNextChangeAsNotPersistent = __unstableMarkNextChangeAsNotPersistent; exports.__unstableSaveReusableBlock = __unstableSaveReusableBlock; exports.__unstableSetEditorMode = void 0; exports.__unstableSetTemporarilyEditingAsBlocks = __unstableSetTemporarilyEditingAsBlocks; exports.__unstableSplitSelection = void 0; exports.clearSelectedBlock = clearSelectedBlock; exports.duplicateBlocks = void 0; exports.enterFormattedText = enterFormattedText; exports.exitFormattedText = exitFormattedText; exports.hideInsertionPoint = exports.flashBlock = void 0; exports.hoverBlock = hoverBlock; exports.insertBeforeBlock = exports.insertAfterBlock = void 0; exports.insertBlock = insertBlock; exports.mergeBlocks = exports.insertDefaultBlock = exports.insertBlocks = void 0; exports.moveBlockToPosition = moveBlockToPosition; exports.multiSelect = exports.moveBlocksUp = exports.moveBlocksToPosition = exports.moveBlocksDown = void 0; exports.receiveBlocks = receiveBlocks; exports.registerInserterMediaCategory = void 0; exports.removeBlock = removeBlock; exports.removeBlocks = void 0; exports.replaceBlock = replaceBlock; exports.replaceBlocks = void 0; exports.replaceInnerBlocks = replaceInnerBlocks; exports.resetBlocks = void 0; exports.resetSelection = resetSelection; exports.selectBlock = selectBlock; exports.selectPreviousBlock = exports.selectNextBlock = void 0; exports.selectionChange = selectionChange; exports.setBlockEditingMode = setBlockEditingMode; exports.setBlockMovingClientId = setBlockMovingClientId; exports.setBlockVisibility = setBlockVisibility; exports.setHasControlledInnerBlocks = setHasControlledInnerBlocks; exports.setNavigationMode = void 0; exports.setTemplateValidity = setTemplateValidity; exports.showInsertionPoint = showInsertionPoint; exports.startDraggingBlocks = startDraggingBlocks; exports.startMultiSelect = startMultiSelect; exports.startTyping = startTyping; exports.stopDraggingBlocks = stopDraggingBlocks; exports.stopMultiSelect = stopMultiSelect; exports.stopTyping = stopTyping; exports.synchronizeTemplate = void 0; exports.toggleBlockHighlight = toggleBlockHighlight; exports.toggleBlockMode = toggleBlockMode; exports.toggleSelection = toggleSelection; exports.unsetBlockEditingMode = unsetBlockEditingMode; exports.updateBlock = updateBlock; exports.updateBlockAttributes = updateBlockAttributes; exports.updateBlockListSettings = updateBlockListSettings; exports.updateSettings = updateSettings; exports.validateBlocksToTemplate = void 0; var _blocks = require("@wordpress/blocks"); var _a11y = require("@wordpress/a11y"); var _i18n = require("@wordpress/i18n"); var _notices = require("@wordpress/notices"); var _richText = require("@wordpress/rich-text"); var _deprecated = _interopRequireDefault(require("@wordpress/deprecated")); var _preferences = require("@wordpress/preferences"); var _selection = require("../utils/selection"); var _privateActions = require("./private-actions"); /* eslint no-console: [ 'error', { allow: [ 'error', 'warn' ] } ] */ /** * WordPress dependencies */ /** * Internal dependencies */ /** @typedef {import('../components/use-on-block-drop/types').WPDropOperation} WPDropOperation */ const castArray = maybeArray => Array.isArray(maybeArray) ? maybeArray : [maybeArray]; /** * Action that resets blocks state to the specified array of blocks, taking precedence * over any other content reflected as an edit in state. * * @param {Array} blocks Array of blocks. */ const resetBlocks = blocks => ({ dispatch }) => { dispatch({ type: 'RESET_BLOCKS', blocks }); dispatch(validateBlocksToTemplate(blocks)); }; /** * Block validity is a function of blocks state (at the point of a * reset) and the template setting. As a compromise to its placement * across distinct parts of state, it is implemented here as a side * effect of the block reset action. * * @param {Array} blocks Array of blocks. */ exports.resetBlocks = resetBlocks; const validateBlocksToTemplate = blocks => ({ select, dispatch }) => { const template = select.getTemplate(); const templateLock = select.getTemplateLock(); // Unlocked templates are considered always valid because they act // as default values only. const isBlocksValidToTemplate = !template || templateLock !== 'all' || (0, _blocks.doBlocksMatchTemplate)(blocks, template); // Update if validity has changed. const isValidTemplate = select.isValidTemplate(); if (isBlocksValidToTemplate !== isValidTemplate) { dispatch.setTemplateValidity(isBlocksValidToTemplate); return isBlocksValidToTemplate; } }; /** * A block selection object. * * @typedef {Object} WPBlockSelection * * @property {string} clientId A block client ID. * @property {string} attributeKey A block attribute key. * @property {number} offset An attribute value offset, based on the rich * text value. See `wp.richText.create`. */ /** * A selection object. * * @typedef {Object} WPSelection * * @property {WPBlockSelection} start The selection start. * @property {WPBlockSelection} end The selection end. */ /* eslint-disable jsdoc/valid-types */ /** * Returns an action object used in signalling that selection state should be * reset to the specified selection. * * @param {WPBlockSelection} selectionStart The selection start. * @param {WPBlockSelection} selectionEnd The selection end. * @param {0|-1|null} initialPosition Initial block position. * * @return {Object} Action object. */ exports.validateBlocksToTemplate = validateBlocksToTemplate; function resetSelection(selectionStart, selectionEnd, initialPosition) { /* eslint-enable jsdoc/valid-types */ return { type: 'RESET_SELECTION', selectionStart, selectionEnd, initialPosition }; } /** * Returns an action object used in signalling that blocks have been received. * Unlike resetBlocks, these should be appended to the existing known set, not * replacing. * * @deprecated * * @param {Object[]} blocks Array of block objects. * * @return {Object} Action object. */ function receiveBlocks(blocks) { (0, _deprecated.default)('wp.data.dispatch( "core/block-editor" ).receiveBlocks', { since: '5.9', alternative: 'resetBlocks or insertBlocks' }); return { type: 'RECEIVE_BLOCKS', blocks }; } /** * Action that updates attributes of multiple blocks with the specified client IDs. * * @param {string|string[]} clientIds Block client IDs. * @param {Object} attributes Block attributes to be merged. Should be keyed by clientIds if `options.uniqueByBlock` is true. * @param {Object} options Updating options. * @param {boolean} [options.uniqueByBlock=false] Whether each block in clientIds array has a unique set of attributes. * @return {Object} Action object. */ function updateBlockAttributes(clientIds, attributes, options = { uniqueByBlock: false }) { if (typeof options === 'boolean') { options = { uniqueByBlock: options }; } return { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: castArray(clientIds), attributes, options }; } /** * Action that updates the block with the specified client ID. * * @param {string} clientId Block client ID. * @param {Object} updates Block attributes to be merged. * * @return {Object} Action object. */ function updateBlock(clientId, updates) { return { type: 'UPDATE_BLOCK', clientId, updates }; } /* eslint-disable jsdoc/valid-types */ /** * Returns an action object used in signalling that the block with the * specified client ID has been selected, optionally accepting a position * value reflecting its selection directionality. An initialPosition of -1 * reflects a reverse selection. * * @param {string} clientId Block client ID. * @param {0|-1|null} initialPosition Optional initial position. Pass as -1 to * reflect reverse selection. * * @return {Object} Action object. */ function selectBlock(clientId, initialPosition = 0) { /* eslint-enable jsdoc/valid-types */ return { type: 'SELECT_BLOCK', initialPosition, clientId }; } /** * Returns an action object used in signalling that the block with the * specified client ID has been hovered. * * @deprecated */ function hoverBlock() { (0, _deprecated.default)('wp.data.dispatch( "core/block-editor" ).hoverBlock', { since: '6.9', version: '7.1' }); return { type: 'DO_NOTHING' }; } /** * Yields action objects used in signalling that the block preceding the given * clientId (or optionally, its first parent from bottom to top) * should be selected. * * @param {string} clientId Block client ID. * @param {boolean} fallbackToParent If true, select the first parent if there is no previous block. */ const selectPreviousBlock = (clientId, fallbackToParent = false) => ({ select, dispatch }) => { const previousBlockClientId = select.getPreviousBlockClientId(clientId); if (previousBlockClientId) { dispatch.selectBlock(previousBlockClientId, -1); } else if (fallbackToParent) { const firstParentClientId = select.getBlockRootClientId(clientId); if (firstParentClientId) { dispatch.selectBlock(firstParentClientId, -1); } } }; /** * Yields action objects used in signalling that the block following the given * clientId should be selected. * * @param {string} clientId Block client ID. */ exports.selectPreviousBlock = selectPreviousBlock; const selectNextBlock = clientId => ({ select, dispatch }) => { const nextBlockClientId = select.getNextBlockClientId(clientId); if (nextBlockClientId) { dispatch.selectBlock(nextBlockClientId); } }; /** * Action that starts block multi-selection. * * @return {Object} Action object. */ exports.selectNextBlock = selectNextBlock; function startMultiSelect() { return { type: 'START_MULTI_SELECT' }; } /** * Action that stops block multi-selection. * * @return {Object} Action object. */ function stopMultiSelect() { return { type: 'STOP_MULTI_SELECT' }; } /** * Action that changes block multi-selection. * * @param {string} start First block of the multi selection. * @param {string} end Last block of the multiselection. * @param {number|null} __experimentalInitialPosition Optional initial position. Pass as null to skip focus within editor canvas. */ const multiSelect = (start, end, __experimentalInitialPosition = 0) => ({ select, dispatch }) => { const startBlockRootClientId = select.getBlockRootClientId(start); const endBlockRootClientId = select.getBlockRootClientId(end); // Only allow block multi-selections at the same level. if (startBlockRootClientId !== endBlockRootClientId) { return; } dispatch({ type: 'MULTI_SELECT', start, end, initialPosition: __experimentalInitialPosition }); const blockCount = select.getSelectedBlockCount(); (0, _a11y.speak)((0, _i18n.sprintf)(/* translators: %s: number of selected blocks */ (0, _i18n._n)('%s block selected.', '%s blocks selected.', blockCount), blockCount), 'assertive'); }; /** * Action that clears the block selection. * * @return {Object} Action object. */ exports.multiSelect = multiSelect; function clearSelectedBlock() { return { type: 'CLEAR_SELECTED_BLOCK' }; } /** * Action that enables or disables block selection. * * @param {boolean} [isSelectionEnabled=true] Whether block selection should * be enabled. * * @return {Object} Action object. */ function toggleSelection(isSelectionEnabled = true) { return { type: 'TOGGLE_SELECTION', isSelectionEnabled }; } /* eslint-disable jsdoc/valid-types */ /** * Action that replaces given blocks with one or more replacement blocks. * * @param {(string|string[])} clientIds Block client ID(s) to replace. * @param {(Object|Object[])} blocks Replacement block(s). * @param {number} indexToSelect Index of replacement block to select. * @param {0|-1|null} initialPosition Index of caret after in the selected block after the operation. * @param {?Object} meta Optional Meta values to be passed to the action object. * * @return {Object} Action object. */ const replaceBlocks = (clientIds, blocks, indexToSelect, initialPosition = 0, meta) => ({ select, dispatch, registry }) => { /* eslint-enable jsdoc/valid-types */ clientIds = castArray(clientIds); blocks = castArray(blocks); const rootClientId = select.getBlockRootClientId(clientIds[0]); // Replace is valid if the new blocks can be inserted in the root block. for (let index = 0; index < blocks.length; index++) { const block = blocks[index]; const canInsertBlock = select.canInsertBlockType(block.name, rootClientId); if (!canInsertBlock) { return; } } // We're batching these two actions because an extra `undo/redo` step can // be created, based on whether we insert a default block or not. registry.batch(() => { dispatch({ type: 'REPLACE_BLOCKS', clientIds, blocks, time: Date.now(), indexToSelect, initialPosition, meta }); // To avoid a focus loss when removing the last block, assure there is // always a default block if the last of the blocks have been removed. dispatch.ensureDefaultBlock(); }); }; /** * Action that replaces a single block with one or more replacement blocks. * * @param {(string|string[])} clientId Block client ID to replace. * @param {(Object|Object[])} block Replacement block(s). * * @return {Object} Action object. */ exports.replaceBlocks = replaceBlocks; function replaceBlock(clientId, block) { return replaceBlocks(clientId, block); } /** * Higher-order action creator which, given the action type to dispatch creates * an action creator for managing block movement. * * @param {string} type Action type to dispatch. * * @return {Function} Action creator. */ const createOnMove = type => (clientIds, rootClientId) => ({ select, dispatch }) => { // If one of the blocks is locked or the parent is locked, we cannot move any block. const canMoveBlocks = select.canMoveBlocks(clientIds); if (!canMoveBlocks) { return; } dispatch({ type, clientIds: castArray(clientIds), rootClientId }); }; const moveBlocksDown = exports.moveBlocksDown = createOnMove('MOVE_BLOCKS_DOWN'); const moveBlocksUp = exports.moveBlocksUp = createOnMove('MOVE_BLOCKS_UP'); /** * Action that moves given blocks to a new position. * * @param {?string} clientIds The client IDs of the blocks. * @param {?string} fromRootClientId Root client ID source. * @param {?string} toRootClientId Root client ID destination. * @param {number} index The index to move the blocks to. */ const moveBlocksToPosition = (clientIds, fromRootClientId = '', toRootClientId = '', index) => ({ select, dispatch }) => { const canMoveBlocks = select.canMoveBlocks(clientIds); // If one of the blocks is locked or the parent is locked, we cannot move any block. if (!canMoveBlocks) { return; } // If moving inside the same root block the move is always possible. if (fromRootClientId !== toRootClientId) { const canRemoveBlocks = select.canRemoveBlocks(clientIds); // If we're moving to another block, it means we're deleting blocks from // the original block, so we need to check if removing is possible. if (!canRemoveBlocks) { return; } const canInsertBlocks = select.canInsertBlocks(clientIds, toRootClientId); // 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. if (!canInsertBlocks) { return; } } dispatch({ type: 'MOVE_BLOCKS_TO_POSITION', fromRootClientId, toRootClientId, clientIds, index }); }; /** * Action that moves given block to a new position. * * @param {?string} clientId The client ID of the block. * @param {?string} fromRootClientId Root client ID source. * @param {?string} toRootClientId Root client ID destination. * @param {number} index The index to move the block to. */ exports.moveBlocksToPosition = moveBlocksToPosition; function moveBlockToPosition(clientId, fromRootClientId = '', toRootClientId = '', index) { return moveBlocksToPosition([clientId], fromRootClientId, toRootClientId, index); } /** * Action that inserts a single block, optionally at a specific index respective a root block list. * * Only allowed blocks are inserted. The action may fail silently for blocks that are not allowed or if * a templateLock is active on the block list. * * @param {Object} block Block object to insert. * @param {?number} index Index at which block should be inserted. * @param {?string} rootClientId Optional root client ID of block list on which to insert. * @param {?boolean} updateSelection If true block selection will be updated. If false, block selection will not change. Defaults to true. * @param {?Object} meta Optional Meta values to be passed to the action object. * * @return {Object} Action object. */ function insertBlock(block, index, rootClientId, updateSelection, meta) { return insertBlocks([block], index, rootClientId, updateSelection, 0, meta); } /* eslint-disable jsdoc/valid-types */ /** * Action that inserts an array of blocks, optionally at a specific index respective a root block list. * * Only allowed blocks are inserted. The action may fail silently for blocks that are not allowed or if * a templateLock is active on the block list. * * @param {Object[]} blocks Block objects to insert. * @param {?number} index Index at which block should be inserted. * @param {?string} rootClientId Optional root client ID of block list on which to insert. * @param {?boolean} updateSelection If true block selection will be updated. If false, block selection will not change. Defaults to true. * @param {0|-1|null} initialPosition Initial focus position. Setting it to null prevent focusing the inserted block. * @param {?Object} meta Optional Meta values to be passed to the action object. * * @return {Object} Action object. */ const insertBlocks = (blocks, index, rootClientId, updateSelection = true, initialPosition = 0, meta) => ({ select, dispatch }) => { /* eslint-enable jsdoc/valid-types */ if (initialPosition !== null && typeof initialPosition === 'object') { meta = initialPosition; initialPosition = 0; (0, _deprecated.default)("meta argument in wp.data.dispatch('core/block-editor')", { since: '5.8', hint: 'The meta argument is now the 6th argument of the function' }); } blocks = castArray(blocks); const allowedBlocks = []; for (const block of blocks) { const isValid = select.canInsertBlockType(block.name, rootClientId); if (isValid) { allowedBlocks.push(block); } } if (allowedBlocks.length) { dispatch({ type: 'INSERT_BLOCKS', blocks: allowedBlocks, index, rootClientId, time: Date.now(), updateSelection, initialPosition: updateSelection ? initialPosition : null, meta }); } }; /** * Action that shows the insertion point. * * @param {?string} rootClientId Optional root client ID of block list on * which to insert. * @param {?number} index Index at which block should be inserted. * @param {?Object} __unstableOptions Additional options. * @property {boolean} __unstableWithInserter Whether or not to show an inserter button. * @property {WPDropOperation} operation The operation to perform when applied, * either 'insert' or 'replace' for now. * * @return {Object} Action object. */ exports.insertBlocks = insertBlocks; function showInsertionPoint(rootClientId, index, __unstableOptions = {}) { const { __unstableWithInserter, operation, nearestSide } = __unstableOptions; return { type: 'SHOW_INSERTION_POINT', rootClientId, index, __unstableWithInserter, operation, nearestSide }; } /** * Action that hides the insertion point. */ const hideInsertionPoint = () => ({ select, dispatch }) => { if (!select.isBlockInsertionPointVisible()) { return; } dispatch({ type: 'HIDE_INSERTION_POINT' }); }; /** * Action that resets the template validity. * * @param {boolean} isValid template validity flag. * * @return {Object} Action object. */ exports.hideInsertionPoint = hideInsertionPoint; function setTemplateValidity(isValid) { return { type: 'SET_TEMPLATE_VALIDITY', isValid }; } /** * Action that synchronizes the template with the list of blocks. * * @return {Object} Action object. */ const synchronizeTemplate = () => ({ select, dispatch }) => { dispatch({ type: 'SYNCHRONIZE_TEMPLATE' }); const blocks = select.getBlocks(); const template = select.getTemplate(); const updatedBlockList = (0, _blocks.synchronizeBlocksWithTemplate)(blocks, template); dispatch.resetBlocks(updatedBlockList); }; /** * Delete the current selection. * * @param {boolean} isForward */ exports.synchronizeTemplate = synchronizeTemplate; const __unstableDeleteSelection = isForward => ({ registry, select, dispatch }) => { const selectionAnchor = select.getSelectionStart(); const selectionFocus = select.getSelectionEnd(); if (selectionAnchor.clientId === selectionFocus.clientId) { return; } // It's not mergeable if there's no rich text selection. if (!selectionAnchor.attributeKey || !selectionFocus.attributeKey || typeof selectionAnchor.offset === 'undefined' || typeof selectionFocus.offset === 'undefined') { return false; } const anchorRootClientId = select.getBlockRootClientId(selectionAnchor.clientId); const focusRootClientId = select.getBlockRootClientId(selectionFocus.clientId); // It's not mergeable if the selection doesn't start and end in the same // block list. Maybe in the future it should be allowed. if (anchorRootClientId !== focusRootClientId) { return; } const blockOrder = select.getBlockOrder(anchorRootClientId); const anchorIndex = blockOrder.indexOf(selectionAnchor.clientId); const focusIndex = blockOrder.indexOf(selectionFocus.clientId); // Reassign selection start and end based on order. let selectionStart, selectionEnd; if (anchorIndex > focusIndex) { selectionStart = selectionFocus; selectionEnd = selectionAnchor; } else { selectionStart = selectionAnchor; selectionEnd = selectionFocus; } const targetSelection = isForward ? selectionEnd : selectionStart; const targetBlock = select.getBlock(targetSelection.clientId); const targetBlockType = (0, _blocks.getBlockType)(targetBlock.name); if (!targetBlockType.merge) { return; } const selectionA = selectionStart; const selectionB = selectionEnd; const blockA = select.getBlock(selectionA.clientId); const blockB = select.getBlock(selectionB.clientId); const htmlA = blockA.attributes[selectionA.attributeKey]; const htmlB = blockB.attributes[selectionB.attributeKey]; let valueA = (0, _richText.create)({ html: htmlA }); let valueB = (0, _richText.create)({ html: htmlB }); valueA = (0, _richText.remove)(valueA, selectionA.offset, valueA.text.length); valueB = (0, _richText.insert)(valueB, _selection.START_OF_SELECTED_AREA, 0, selectionB.offset); // Clone the blocks so we don't manipulate the original. const cloneA = (0, _blocks.cloneBlock)(blockA, { [selectionA.attributeKey]: (0, _richText.toHTMLString)({ value: valueA }) }); const cloneB = (0, _blocks.cloneBlock)(blockB, { [selectionB.attributeKey]: (0, _richText.toHTMLString)({ value: valueB }) }); const followingBlock = isForward ? cloneA : cloneB; // We can only merge blocks with similar types // thus, we transform the block to merge first const blocksWithTheSameType = blockA.name === blockB.name ? [followingBlock] : (0, _blocks.switchToBlockType)(followingBlock, targetBlockType.name); // If the block types can not match, do nothing if (!blocksWithTheSameType || !blocksWithTheSameType.length) { return; } let updatedAttributes; if (isForward) { const blockToMerge = blocksWithTheSameType.pop(); updatedAttributes = targetBlockType.merge(blockToMerge.attributes, cloneB.attributes); } else { const blockToMerge = blocksWithTheSameType.shift(); updatedAttributes = targetBlockType.merge(cloneA.attributes, blockToMerge.attributes); } const newAttributeKey = (0, _selection.retrieveSelectedAttribute)(updatedAttributes); const convertedHtml = updatedAttributes[newAttributeKey]; const convertedValue = (0, _richText.create)({ html: convertedHtml }); const newOffset = convertedValue.text.indexOf(_selection.START_OF_SELECTED_AREA); const newValue = (0, _richText.remove)(convertedValue, newOffset, newOffset + 1); const newHtml = (0, _richText.toHTMLString)({ value: newValue }); updatedAttributes[newAttributeKey] = newHtml; const selectedBlockClientIds = select.getSelectedBlockClientIds(); const replacement = [...(isForward ? blocksWithTheSameType : []), { // Preserve the original client ID. ...targetBlock, attributes: { ...targetBlock.attributes, ...updatedAttributes } }, ...(isForward ? [] : blocksWithTheSameType)]; registry.batch(() => { dispatch.selectionChange(targetBlock.clientId, newAttributeKey, newOffset, newOffset); dispatch.replaceBlocks(selectedBlockClientIds, replacement, 0, // If we don't pass the `indexToSelect` it will default to the last block. select.getSelectedBlocksInitialCaretPosition()); }); }; /** * Split the current selection. * @param {?Array} blocks */ exports.__unstableDeleteSelection = __unstableDeleteSelection; const __unstableSplitSelection = (blocks = []) => ({ registry, select, dispatch }) => { const selectionAnchor = select.getSelectionStart(); const selectionFocus = select.getSelectionEnd(); const anchorRootClientId = select.getBlockRootClientId(selectionAnchor.clientId); const focusRootClientId = select.getBlockRootClientId(selectionFocus.clientId); // It's not splittable if the selection doesn't start and end in the same // block list. Maybe in the future it should be allowed. if (anchorRootClientId !== focusRootClientId) { return; } const blockOrder = select.getBlockOrder(anchorRootClientId); const anchorIndex = blockOrder.indexOf(selectionAnchor.clientId); const focusIndex = blockOrder.indexOf(selectionFocus.clientId); // Reassign selection start and end based on order. let selectionStart, selectionEnd; if (anchorIndex > focusIndex) { selectionStart = selectionFocus; selectionEnd = selectionAnchor; } else { 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, _blocks.getBlockType)(blockA.name); const blockBType = (0, _blocks.getBlockType)(blockB.name); const attributeKeyA = typeof selectionA.attributeKey === 'string' ? selectionA.attributeKey : (0, _selection.findRichTextAttributeKey)(blockAType); const attributeKeyB = typeof selectionB.attributeKey === 'string' ? selectionB.attributeKey : (0, _selection.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. if (blocks.length) { const { createWarningNotice } = registry.dispatch(_notices.store); createWarningNotice((0, _i18n.__)("Blocks can't be inserted into other blocks with bindings"), { type: 'snackbar' }); return; } dispatch.insertAfterBlock(selectionA.clientId); return; } // Can't split if the selection is not set. if (!attributeKeyA || !attributeKeyB || typeof selectionAnchor.offset === 'undefined' || typeof selectionFocus.offset === 'undefined') { return; } // 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 (blocks.length) { if ((0, _blocks.isUnmodifiedDefaultBlock)(blockA)) { dispatch.replaceBlocks([selectionA.clientId], blocks, blocks.length - 1, -1); return; } } // 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) { function createEmpty() { const defaultBlockName = (0, _blocks.getDefaultBlockName)(); return select.canInsertBlockType(defaultBlockName, anchorRootClientId) ? (0, _blocks.createBlock)(defaultBlockName) : (0, _blocks.createBlock)(select.getBlockName(selectionA.clientId)); } const length = blockAttributes[attributeKeyA].length; if (selectionA.offset === 0 && length) { dispatch.insertBlocks([createEmpty()], select.getBlockIndex(selectionA.clientId), anchorRootClientId, false); return; } if (selectionA.offset === length) { dispatch.insertBlocks([createEmpty()], select.getBlockIndex(selectionA.clientId) + 1, anchorRootClientId); return; } } } const htmlA = blockA.attributes[attributeKeyA]; const htmlB = blockB.attributes[attributeKeyB]; let valueA = (0, _richText.create)({ html: htmlA }); let valueB = (0, _richText.create)({ html: htmlB }); valueA = (0, _richText.remove)(valueA, selectionA.offset, valueA.text.length); valueB = (0, _richText.remove)(valueB, 0, selectionB.offset); let head = { // Preserve the original client ID. ...blockA, // If both start and end are the same, should only copy innerBlocks // once. innerBlocks: blockA.clientId === blockB.clientId ? [] : blockA.innerBlocks, attributes: { ...blockA.attributes, [attributeKeyA]: (0, _richText.toHTMLString)({ value: valueA }) } }; let tail = { ...blockB, // Only preserve the original client ID if the end is different. clientId: blockA.clientId === blockB.clientId ? (0, _blocks.createBlock)(blockB.name).clientId : blockB.clientId, attributes: { ...blockB.attributes, [attributeKeyB]: (0, _richText.toHTMLString)({ value: valueB }) } }; // 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, _blocks.getDefaultBlockName)(); if ( // A block is only split when the selection is within the same // block. blockA.clientId === blockB.clientId && defaultBlockName && tail.name !== defaultBlockName && select.canInsertBlockType(defaultBlockName, anchorRootClientId)) { const switched = (0, _blocks.switchToBlockType)(tail, defaultBlockName); if (switched?.length === 1) { tail = switched[0]; } } if (!blocks.length) { dispatch.replaceBlocks(select.getSelectedBlockClientIds(), [head, tail]); return; } let selection; const output = []; const clonedBlocks = [...blocks]; const firstBlock = clonedBlocks.shift(); const headType = (0, _blocks.getBlockType)(head.name); const firstBlocks = headType.merge && firstBlock.name === headType.name ? [firstBlock] : (0, _blocks.switchToBlockType)(firstBlock, headType.name); if (firstBlocks?.length) { const first = firstBlocks.shift(); head = { ...head, attributes: { ...head.attributes, ...headType.merge(head.attributes, first.attributes) } }; output.push(head); selection = { clientId: head.clientId, attributeKey: attributeKeyA, offset: (0, _richText.create)({ html: head.attributes[attributeKeyA] }).text.length }; clonedBlocks.unshift(...firstBlocks); } else { if (!(0, _blocks.isUnmodifiedBlock)(head)) { output.push(head); } output.push(firstBlock); } const lastBlock = clonedBlocks.pop(); const tailType = (0, _blocks.getBlockType)(tail.name); if (clonedBlocks.length) { output.push(...clonedBlocks); } if (lastBlock) { const lastBlocks = tailType.merge && tailType.name === lastBlock.name ? [lastBlock] : (0, _blocks.switchToBlockType)(lastBlock, tailType.name); if (lastBlocks?.length) { const last = lastBlocks.pop(); output.push({ ...tail, attributes: { ...tail.attributes, ...tailType.merge(last.attributes, tail.attributes) } }); output.push(...lastBlocks); selection = { clientId: tail.clientId, attributeKey: attributeKeyB, offset: (0, _richText.create)({ html: last.attributes[attributeKeyB] }).text.length }; } else { output.push(lastBlock); if (!(0, _blocks.isUnmodifiedBlock)(tail)) { output.push(tail); } } } else if (!(0, _blocks.isUnmodifiedBlock)(tail)) { output.push(tail); } registry.batch(() => { dispatch.replaceBlocks(select.getSelectedBlockClientIds(), output, output.length - 1, 0); if (selection) { dispatch.selectionChange(selection.clientId, selection.attributeKey, selection.offset, selection.offset); } }); }; /** * Expand the selection to cover the entire blocks, removing partial selection. */ exports.__unstableSplitSelection = __unstableSplitSelection; const __unstableExpandSelection = () => ({ select, dispatch }) => { const selectionAnchor = select.getSelectionStart(); const selectionFocus = select.getSelectionEnd(); dispatch.selectionChange({ start: { clientId: selectionAnchor.clientId }, end: { 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. */ exports.__unstableExpandSelection = __unstableExpandSelection; const mergeBlocks = (firstBlockClientId, secondBlockClientId) => ({ registry, select, dispatch }) => { const clientIdA = firstBlockClientId; const clientIdB = secondBlockClientId; const blockA = select.getBlock(clientIdA); const blockAType = (0, _blocks.getBlockType)(blockA.name); if (!blockAType || select.getBlockEditingMode(clientIdA) === 'disabled' || select.getBlockEditingMode(clientIdB) === 'disabled') { return; } const blockB = select.getBlock(clientIdB); if (!blockAType.merge && (0, _blocks.getBlockSupport)(blockA.name, '__experimentalOnMerge')) { // If there's no merge function defined, attempt merging inner // blocks. const blocksWithTheSameType = (0, _blocks.switchToBlockType)(blockB, blockAType.name); // Only focus the previous block if it's not mergeable. if (blocksWithTheSameType?.length !== 1) { dispatch.selectBlock(blockA.clientId); return; } const [blockWithSameType] = blocksWithTheSameType; if (blockWithSameType.innerBlocks.length < 1) { dispatch.selectBlock(blockA.clientId); return; } registry.batch(() => { 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 // attributes. 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); } } }); return; } if ((0, _blocks.isUnmodifiedDefaultBlock)(blockA)) { dispatch.removeBlock(clientIdA, select.isBlockSelected(clientIdA)); return; } if ((0, _blocks.isUnmodifiedDefaultBlock)(blockB)) { dispatch.removeBlock(clientIdB, select.isBlockSelected(clientIdB)); return; } if (!blockAType.merge) { if ((0, _blocks.isUnmodifiedBlock)(blockB, 'content')) { dispatch.removeBlock(clientIdB, select.isBlockSelected(clientIdB)); } else { dispatch.selectBlock(blockA.clientId); } return; } const blockBType = (0, _blocks.getBlockType)(blockB.name); const { clientId, attributeKey, offset } = 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 instance ID is used to store selection (and no RichText // identifier is set), or when the identifier is wrong. !!attributeDefinition; 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}`); } else { 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, _blocks.cloneBlock)(blockA); const cloneB = (0, _blocks.cloneBlock)(blockB); if (canRestoreTextSelection) { const selectedBlock = clientId === clientIdA ? cloneA : cloneB; const html = selectedBlock.attributes[attributeKey]; const value = (0, _richText.insert)((0, _richText.create)({ html }), _selection.START_OF_SELECTED_AREA, offset, offset); selectedBlock.attributes[attributeKey] = (0, _richText.toHTMLString)({ value }); } // We can only merge blocks with similar types // thus, we transform the block to merge first. const blocksWithTheSameType = blockA.name === blockB.name ? [cloneB] : (0, _blocks.switchToBlockType)(cloneB, blockA.name); // If the block types can not match, do nothing. if (!blocksWithTheSameType || !blocksWithTheSameType.length) { return; } // 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 = (0, _selection.retrieveSelectedAttribute)(updatedAttributes); const convertedHtml = updatedAttributes[newAttributeKey]; const convertedValue = (0, _richText.create)({ html: convertedHtml }); const newOffset = convertedValue.text.indexOf(_selection.START_OF_SELECTED_AREA); const newValue = (0, _richText.remove)(convertedValue, newOffset, newOffset + 1); const newHtml = (0, _richText.toHTMLString)({ value: newValue }); updatedAttributes[newAttributeKey] = newHtml; dispatch.selectionChange(blockA.clientId, newAttributeKey, newOffset, newOffset); } dispatch.replaceBlocks([blockA.clientId, blockB.clientId], [{ ...blockA, attributes: { ...blockA.attributes, ...updatedAttributes } }, ...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) * should be selected * when a block is removed. */ exports.mergeBlocks = mergeBlocks; const removeBlocks = (clientIds, selectPrevious = true) => (0, _privateActions.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. */ exports.removeBlocks = removeBlocks; 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 */ return { type: 'REPLACE_INNER_BLOCKS', rootClientId, blocks, updateSelection, initialPosition: updateSelection ? initialPosition : null, time: Date.now() }; } /** * Returns an action object used to toggle the block editing mode between * visual and HTML modes. * * @param {string} clientId Block client ID. * * @return {Object} Action object. */ function toggleBlockMode(clientId) { return { type: 'TOGGLE_BLOCK_MODE', clientId }; } /** * Returns an action object used in signalling that the user has begun to type. * * @return {Object} Action object. */ function startTyping() { return { type: 'START_TYPING' }; } /** * Returns an action object used in signalling that the user has stopped typing. * * @return {Object} Action object. */ function stopTyping() { return { type: 'STOP_TYPING' }; } /** * 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 = []) { return { type: 'START_DRAGGING_BLOCKS', clientIds }; } /** * Returns an action object used in signalling that the user has stopped dragging blocks. * * @return {Object} Action object. */ function stopDraggingBlocks() { return { type: 'STOP_DRAGGING_BLOCKS' }; } /** * Returns an action object used in signalling that the caret has entered formatted text. * * @deprecated * * @return {Object} Action object. */ function enterFormattedText() { (0, _deprecated.default)('wp.data.dispatch( "core/block-editor" ).enterFormattedText', { since: '6.1', version: '6.3' }); return { type: 'DO_NOTHING' }; } /** * Returns an action object used in signalling that the user caret has exited formatted text. * * @deprecated * * @return {Object} Action object. */ function exitFormattedText() { (0, _deprecated.default)('wp.data.dispatch( "core/block-editor" ).exitFormattedText', { since: '6.1', version: '6.3' }); return { type: 'DO_NOTHING' }; } /** * Action that changes the position of the user caret. * * @param {string|WPSelection} clientId The selected block client ID. * @param {string} attributeKey The selected block attribute key. * @param {number} startOffset The start offset. * @param {number} endOffset The end offset. * * @return {Object} Action object. */ function selectionChange(clientId, attributeKey, startOffset, endOffset) { if (typeof clientId === 'string') { return { type: 'SELECTION_CHANGE', clientId, attributeKey, startOffset, endOffset }; } return { type: 'SELECTION_CHANGE', ...clientId }; } /** * Action that adds a new block of the default type to the block list. * * @param {?Object} attributes Optional attributes of the block to assign. * @param {?string} rootClientId Optional root client ID of block list on which * to append. * @param {?number} index Optional index where to insert the default block. */ const insertDefaultBlock = (attributes, rootClientId, index) => ({ dispatch }) => { // Abort if there is no default block type (if it has been unregistered). const defaultBlockName = (0, _blocks.getDefaultBlockName)(); if (!defaultBlockName) { return; } const block = (0, _blocks.createBlock)(defaultBlockName, attributes); return dispatch.insertBlock(block, index, rootClientId); }; /** * @typedef {Object< string, Object >} SettingsByClientId */ /** * Action that changes the nested settings of the given block(s). * * @param {string | SettingsByClientId} clientId Client ID of the block whose * nested setting are being * received, or object of settings * by client ID. * @param {Object} settings Object with the new settings * for the nested block. * * @return {Object} Action object */ exports.insertDefaultBlock = insertDefaultBlock; function updateBlockListSettings(clientId, settings) { return { type: 'UPDATE_BLOCK_LIST_SETTINGS', clientId, settings }; } /** * Action that updates the block editor settings. * * @param {Object} settings Updated settings * * @return {Object} Action object */ function updateSettings(settings) { return (0, _privateActions.__experimentalUpdateSettings)(settings, { stripExperimentalSettings: true }); } /** * Action that signals that a temporary reusable block has been saved * in order to switch its temporary id with the real id. * * @param {string} id Reusable block's id. * @param {string} updatedId Updated block's id. * * @return {Object} Action object. */ function __unstableSaveReusableBlock(id, updatedId) { return { type: 'SAVE_REUSABLE_BLOCK_SUCCESS', id, updatedId }; } /** * Action that marks the last block change explicitly as persistent. * * @return {Object} Action object. */ function __unstableMarkLastChangeAsPersistent() { return { type: 'MARK_LAST_CHANGE_AS_PERSISTENT' }; } /** * Action that signals that the next block change should be marked explicitly as not persistent. * * @return {Object} Action object. */ function __unstableMarkNextChangeAsNotPersistent() { return { type: 'MARK_NEXT_CHANGE_AS_NOT_PERSISTENT' }; } /** * Action that marks the last block change as an automatic change, meaning it was not * performed by the user, and can be undone using the `Escape` and `Backspace` keys. * This action must be called after the change was made, and any actions that are a * consequence of it, so it is recommended to be called at the next idle period to ensure all * selection changes have been recorded. */ const __unstableMarkAutomaticChange = () => ({ dispatch }) => { dispatch({ type: 'MARK_AUTOMATIC_CHANGE' }); const { requestIdleCallback = cb => setTimeout(cb, 100) } = window; requestIdleCallback(() => { dispatch({ type: