UNPKG

@gechiui/block-editor

Version:
1,264 lines (1,118 loc) 37.8 kB
/** * External dependencies */ import { castArray, findKey, first, isObject, last, some } from 'lodash'; /** * GeChiUI dependencies */ import { cloneBlock, __experimentalCloneSanitizedBlock, createBlock, doBlocksMatchTemplate, getBlockType, getDefaultBlockName, hasBlockSupport, switchToBlockType, synchronizeBlocksWithTemplate } from '@gechiui/blocks'; import { speak } from '@gechiui/a11y'; import { __, _n, sprintf } from '@gechiui/i18n'; import { create, insert, remove, toHTMLString } from '@gechiui/rich-text'; import deprecated from '@gechiui/deprecated'; /** * Action which will insert a default block insert action if there * are no other blocks at the root of the editor. This action should be used * in actions which may result in no blocks remaining in the editor (removal, * replacement, etc). */ const ensureDefaultBlock = () => _ref => { let { select, dispatch } = _ref; // 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. const count = select.getBlockCount(); if (count > 0) { return; } // If there's an custom appender, don't insert default block. // We have to remember to manually move the focus elsewhere to // prevent it from being lost though. const { __unstableHasCustomAppender } = select.getSettings(); if (__unstableHasCustomAppender) { return; } dispatch.insertDefaultBlock(); }; /** * 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. */ export const resetBlocks = blocks => _ref2 => { let { dispatch } = _ref2; 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. */ export const validateBlocksToTemplate = blocks => _ref3 => { let { select, dispatch } = _ref3; 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' || 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} GCBlockSelection * * @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 `gc.richText.create`. */ /* eslint-disable jsdoc/valid-types */ /** * Returns an action object used in signalling that selection state should be * reset to the specified selection. * * @param {GCBlockSelection} selectionStart The selection start. * @param {GCBlockSelection} selectionEnd The selection end. * @param {0|-1|null} initialPosition Initial block position. * * @return {Object} Action object. */ export 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. */ export function receiveBlocks(blocks) { deprecated('gc.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 * uniqueByBlock is true. * @param {boolean} uniqueByBlock true if each block in clientIds array has a unique set of attributes * @return {Object} Action object. */ export function updateBlockAttributes(clientIds, attributes) { let uniqueByBlock = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; return { type: 'UPDATE_BLOCK_ATTRIBUTES', clientIds: castArray(clientIds), attributes, uniqueByBlock }; } /** * 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. */ export 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. */ export function selectBlock(clientId) { let initialPosition = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; /* eslint-enable jsdoc/valid-types */ return { type: 'SELECT_BLOCK', initialPosition, clientId }; } /** * Yields action objects used in signalling that the block preceding the given * clientId should be selected. * * @param {string} clientId Block client ID. */ export const selectPreviousBlock = clientId => _ref4 => { let { select, dispatch } = _ref4; const previousBlockClientId = select.getPreviousBlockClientId(clientId); if (previousBlockClientId) { dispatch.selectBlock(previousBlockClientId, -1); } }; /** * Yields action objects used in signalling that the block following the given * clientId should be selected. * * @param {string} clientId Block client ID. */ export const selectNextBlock = clientId => _ref5 => { let { select, dispatch } = _ref5; const nextBlockClientId = select.getNextBlockClientId(clientId); if (nextBlockClientId) { dispatch.selectBlock(nextBlockClientId); } }; /** * Action that starts block multi-selection. * * @return {Object} Action object. */ export function startMultiSelect() { return { type: 'START_MULTI_SELECT' }; } /** * Action that stops block multi-selection. * * @return {Object} Action object. */ export 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. */ export const multiSelect = (start, end) => _ref6 => { let { select, dispatch } = _ref6; 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 }); const blockCount = select.getSelectedBlockCount(); speak(sprintf( /* translators: %s: number of selected blocks */ _n('%s block selected.', '%s blocks selected.', blockCount), blockCount), 'assertive'); }; /** * Action that clears the block selection. * * @return {Object} Action object. */ export 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. */ export function toggleSelection() { let isSelectionEnabled = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; return { type: 'TOGGLE_SELECTION', isSelectionEnabled }; } function getBlocksWithDefaultStylesApplied(blocks, blockEditorSettings) { var _blockEditorSettings$, _blockEditorSettings$2; const preferredStyleVariations = (_blockEditorSettings$ = blockEditorSettings === null || blockEditorSettings === void 0 ? void 0 : (_blockEditorSettings$2 = blockEditorSettings.__experimentalPreferredStyleVariations) === null || _blockEditorSettings$2 === void 0 ? void 0 : _blockEditorSettings$2.value) !== null && _blockEditorSettings$ !== void 0 ? _blockEditorSettings$ : {}; return blocks.map(block => { var _block$attributes; const blockName = block.name; if (!hasBlockSupport(blockName, 'defaultStylePicker', true)) { return block; } if (!preferredStyleVariations[blockName]) { return block; } const className = (_block$attributes = block.attributes) === null || _block$attributes === void 0 ? void 0 : _block$attributes.className; if (className !== null && className !== void 0 && className.includes('is-style-')) { return block; } const { attributes = {} } = block; const blockStyle = preferredStyleVariations[blockName]; return { ...block, attributes: { ...attributes, className: `${className || ''} is-style-${blockStyle}`.trim() } }; }); } /* 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. */ export const replaceBlocks = function (clientIds, blocks, indexToSelect) { let initialPosition = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; let meta = arguments.length > 4 ? arguments[4] : undefined; return _ref7 => { let { select, dispatch } = _ref7; /* eslint-enable jsdoc/valid-types */ clientIds = castArray(clientIds); blocks = getBlocksWithDefaultStylesApplied(castArray(blocks), select.getSettings()); const rootClientId = select.getBlockRootClientId(first(clientIds)); // 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; } } dispatch({ type: 'REPLACE_BLOCKS', clientIds, blocks, time: Date.now(), indexToSelect, initialPosition, meta }); 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. */ export 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) => _ref8 => { let { select, dispatch } = _ref8; // If one of the blocks is locked or the parent is locked, we cannot move any block. const canMoveBlocks = select.canMoveBlocks(clientIds, rootClientId); if (!canMoveBlocks) { return; } dispatch({ type, clientIds: castArray(clientIds), rootClientId }); }; export const moveBlocksDown = createOnMove('MOVE_BLOCKS_DOWN'); export const 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. */ export const moveBlocksToPosition = function (clientIds) { let fromRootClientId = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; let toRootClientId = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; let index = arguments.length > 3 ? arguments[3] : undefined; return _ref9 => { let { select, dispatch } = _ref9; const canMoveBlocks = select.canMoveBlocks(clientIds, fromRootClientId); // 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, fromRootClientId); // 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. */ export function moveBlockToPosition(clientId) { let fromRootClientId = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; let toRootClientId = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; let index = arguments.length > 3 ? arguments[3] : undefined; return moveBlocksToPosition([clientId], fromRootClientId, toRootClientId, index); } /** * Action that inserts a single block, optionally at a specific index respective a root 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. */ export 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. * * @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. */ export const insertBlocks = function (blocks, index, rootClientId) { let updateSelection = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true; let initialPosition = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 0; let meta = arguments.length > 5 ? arguments[5] : undefined; return _ref10 => { let { select, dispatch } = _ref10; /* eslint-enable jsdoc/valid-types */ if (isObject(initialPosition)) { meta = initialPosition; initialPosition = 0; deprecated("meta argument in gc.data.dispatch('core/block-editor')", { since: '10.1', plugin: 'Gutenberg', hint: 'The meta argument is now the 6th argument of the function' }); } blocks = getBlocksWithDefaultStylesApplied(castArray(blocks), select.getSettings()); 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 Wether or not to show an inserter button. * * @return {Object} Action object. */ export function showInsertionPoint(rootClientId, index) { let __unstableOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; const { __unstableWithInserter } = __unstableOptions; return { type: 'SHOW_INSERTION_POINT', rootClientId, index, __unstableWithInserter }; } /** * Action that hides the insertion point. * * @return {Object} Action object. */ export function hideInsertionPoint() { return { type: 'HIDE_INSERTION_POINT' }; } /** * Action that resets the template validity. * * @param {boolean} isValid template validity flag. * * @return {Object} Action object. */ export function setTemplateValidity(isValid) { return { type: 'SET_TEMPLATE_VALIDITY', isValid }; } /** * Action that synchronizes the template with the list of blocks. * * @return {Object} Action object. */ export const synchronizeTemplate = () => _ref11 => { let { select, dispatch } = _ref11; dispatch({ type: 'SYNCHRONIZE_TEMPLATE' }); const blocks = select.getBlocks(); const template = select.getTemplate(); const updatedBlockList = synchronizeBlocksWithTemplate(blocks, template); dispatch.resetBlocks(updatedBlockList); }; /** * 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. */ export const mergeBlocks = (firstBlockClientId, secondBlockClientId) => _ref12 => { let { select, dispatch } = _ref12; const blocks = [firstBlockClientId, secondBlockClientId]; dispatch({ type: 'MERGE_BLOCKS', blocks }); const [clientIdA, clientIdB] = blocks; const blockA = select.getBlock(clientIdA); const blockAType = getBlockType(blockA.name); // Only focus the previous block if it's not mergeable if (blockAType && !blockAType.merge) { dispatch.selectBlock(blockA.clientId); return; } const blockB = select.getBlock(clientIdB); const blockBType = 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 intance 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.'); } } // A robust way to retain selection position through various transforms // is to insert a special character at the position and then recover it. const START_OF_SELECTED_AREA = '\u0086'; // Clone the blocks so we don't insert the character in a "live" block. const cloneA = cloneBlock(blockA); const cloneB = cloneBlock(blockB); if (canRestoreTextSelection) { const selectedBlock = clientId === clientIdA ? cloneA : cloneB; const html = selectedBlock.attributes[attributeKey]; const { multiline: multilineTag, __unstableMultilineWrapperTags: multilineWrapperTags, __unstablePreserveWhiteSpace: preserveWhiteSpace } = attributeDefinition; const value = insert(create({ html, multilineTag, multilineWrapperTags, preserveWhiteSpace }), START_OF_SELECTED_AREA, offset, offset); selectedBlock.attributes[attributeKey] = toHTMLString({ value, multilineTag, preserveWhiteSpace }); } // We can only merge blocks with similar types // thus, we transform the block to merge first const blocksWithTheSameType = blockA.name === blockB.name ? [cloneB] : 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 = findKey(updatedAttributes, v => typeof v === 'string' && v.indexOf(START_OF_SELECTED_AREA) !== -1); const convertedHtml = updatedAttributes[newAttributeKey]; const { multiline: multilineTag, __unstableMultilineWrapperTags: multilineWrapperTags, __unstablePreserveWhiteSpace: preserveWhiteSpace } = blockAType.attributes[newAttributeKey]; const convertedValue = create({ html: convertedHtml, multilineTag, multilineWrapperTags, preserveWhiteSpace }); const newOffset = convertedValue.text.indexOf(START_OF_SELECTED_AREA); const newValue = remove(convertedValue, newOffset, newOffset + 1); const newHtml = toHTMLString({ value: newValue, multilineTag, preserveWhiteSpace }); 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 should be * selected when a block is removed. */ export const removeBlocks = function (clientIds) { let selectPrevious = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; return _ref13 => { let { select, dispatch } = _ref13; if (!clientIds || !clientIds.length) { return; } clientIds = castArray(clientIds); const rootClientId = select.getBlockRootClientId(clientIds[0]); const canRemoveBlocks = select.canRemoveBlocks(clientIds, rootClientId); if (!canRemoveBlocks) { return; } if (selectPrevious) { dispatch.selectPreviousBlock(clientIds[0]); } dispatch({ type: 'REMOVE_BLOCKS', clientIds }); // 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()); }; }; /** * 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. */ export 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. */ export function replaceInnerBlocks(rootClientId, blocks) { let updateSelection = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; let initialPosition = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 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. */ export 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. */ export function startTyping() { return { type: 'START_TYPING' }; } /** * Returns an action object used in signalling that the user has stopped typing. * * @return {Object} Action object. */ export 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. */ export function startDraggingBlocks() { let clientIds = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; return { type: 'START_DRAGGING_BLOCKS', clientIds }; } /** * Returns an action object used in signalling that the user has stopped dragging blocks. * * @return {Object} Action object. */ export function stopDraggingBlocks() { return { type: 'STOP_DRAGGING_BLOCKS' }; } /** * Returns an action object used in signalling that the caret has entered formatted text. * * @return {Object} Action object. */ export function enterFormattedText() { return { type: 'ENTER_FORMATTED_TEXT' }; } /** * Returns an action object used in signalling that the user caret has exited formatted text. * * @return {Object} Action object. */ export function exitFormattedText() { return { type: 'EXIT_FORMATTED_TEXT' }; } /** * Action that changes the position of the user caret. * * @param {string} 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. */ export function selectionChange(clientId, attributeKey, startOffset, endOffset) { return { type: 'SELECTION_CHANGE', clientId, attributeKey, startOffset, endOffset }; } /** * 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 * * @return {Object} Action object */ export function insertDefaultBlock(attributes, rootClientId, index) { // Abort if there is no default block type (if it has been unregistered). const defaultBlockName = getDefaultBlockName(); if (!defaultBlockName) { return; } const block = createBlock(defaultBlockName, attributes); return insertBlock(block, index, rootClientId); } /** * Action that changes the nested settings of a given block. * * @param {string} clientId Client ID of the block whose nested setting are * being received. * @param {Object} settings Object with the new settings for the nested block. * * @return {Object} Action object */ export 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 */ export function updateSettings(settings) { return { type: 'UPDATE_SETTINGS', settings }; } /** * 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. */ export 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. */ export 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. */ export 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. */ export const __unstableMarkAutomaticChange = () => _ref14 => { let { dispatch } = _ref14; dispatch({ type: 'MARK_AUTOMATIC_CHANGE' }); const { requestIdleCallback = cb => setTimeout(cb, 100) } = window; requestIdleCallback(() => { dispatch({ type: 'MARK_AUTOMATIC_CHANGE_FINAL' }); }); }; /** * Action that enables or disables the navigation mode. * * @param {string} isNavigationMode Enable/Disable navigation mode. */ export const setNavigationMode = function () { let isNavigationMode = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; return _ref15 => { let { dispatch } = _ref15; dispatch({ type: 'SET_NAVIGATION_MODE', isNavigationMode }); if (isNavigationMode) { speak(__('您当前处于导航模式。 使用Tab键和方向键浏览区块。 使用向左和向右箭头键在嵌套级别之间移动。 要退出导航模式并编辑选定的块,请按Enter。')); } else { speak(__('您正位于编辑模式。要返回导航模式,按Escape。')); } }; }; /** * Action that enables or disables the block moving mode. * * @param {string|null} hasBlockMovingClientId Enable/Disable block moving mode. */ export const setBlockMovingClientId = function () { let hasBlockMovingClientId = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; return _ref16 => { let { dispatch } = _ref16; dispatch({ type: 'SET_BLOCK_MOVING_MODE', hasBlockMovingClientId }); if (hasBlockMovingClientId) { speak(__('使用Tab键和箭头键选择新的块位置。使用左右箭头键在嵌套级别之间移动。选定位置后按Enter或空格键移动块。')); } }; }; /** * Action that duplicates a list of blocks. * * @param {string[]} clientIds * @param {boolean} updateSelection */ export const duplicateBlocks = function (clientIds) { let updateSelection = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; return _ref17 => { let { select, dispatch } = _ref17; if (!clientIds || !clientIds.length) { return; } // Return early if blocks don't exist. const blocks = select.getBlocksByClientId(clientIds); if (some(blocks, block => !block)) { return; } // Return early if blocks don't support multiple usage. const blockNames = blocks.map(block => block.name); if (blockNames.some(blockName => !hasBlockSupport(blockName, 'multiple', true))) { return; } const rootClientId = select.getBlockRootClientId(clientIds[0]); const lastSelectedIndex = select.getBlockIndex(last(castArray(clientIds))); const clonedBlocks = blocks.map(block => __experimentalCloneSanitizedBlock(block)); dispatch.insertBlocks(clonedBlocks, lastSelectedIndex + 1, rootClientId, updateSelection); if (clonedBlocks.length > 1 && updateSelection) { dispatch.multiSelect(first(clonedBlocks).clientId, last(clonedBlocks).clientId); } return clonedBlocks.map(block => block.clientId); }; }; /** * Action that inserts an empty block before a given block. * * @param {string} clientId */ export const insertBeforeBlock = clientId => _ref18 => { let { select, dispatch } = _ref18; if (!clientId) { return; } const rootClientId = select.getBlockRootClientId(clientId); const isLocked = select.getTemplateLock(rootClientId); if (isLocked) { return; } const firstSelectedIndex = select.getBlockIndex(clientId); return dispatch.insertDefaultBlock({}, rootClientId, firstSelectedIndex); }; /** * Action that inserts an empty block after a given block. * * @param {string} clientId */ export const insertAfterBlock = clientId => _ref19 => { let { select, dispatch } = _ref19; if (!clientId) { return; } const rootClientId = select.getBlockRootClientId(clientId); const isLocked = select.getTemplateLock(rootClientId); if (isLocked) { return; } const firstSelectedIndex = select.getBlockIndex(clientId); return dispatch.insertDefaultBlock({}, rootClientId, firstSelectedIndex + 1); }; /** * Action that toggles the highlighted block state. * * @param {string} clientId The block's clientId. * @param {boolean} isHighlighted The highlight state. */ export function toggleBlockHighlight(clientId, isHighlighted) { return { type: 'TOGGLE_BLOCK_HIGHLIGHT', clientId, isHighlighted }; } /** * Action that "flashes" the block with a given `clientId` by rhythmically highlighting it. * * @param {string} clientId Target block client ID. */ export const flashBlock = clientId => async _ref20 => { let { dispatch } = _ref20; dispatch(toggleBlockHighlight(clientId, true)); await new Promise(resolve => setTimeout(resolve, 150)); dispatch(toggleBlockHighlight(clientId, false)); }; /** * Action that sets whether a block has controlled inner blocks. * * @param {string} clientId The block's clientId. * @param {boolean} hasControlledInnerBlocks True if the block's inner blocks are controlled. */ export function setHasControlledInnerBlocks(clientId, hasControlledInnerBlocks) { return { type: 'SET_HAS_CONTROLLED_INNER_BLOCKS', hasControlledInnerBlocks, clientId }; } //# sourceMappingURL=actions.js.map