UNPKG

@wordpress/block-editor

Version:
1,697 lines (1,529 loc) 97.8 kB
/** * WordPress dependencies */ import { getBlockType, getBlockTypes, getBlockVariations, hasBlockSupport, getPossibleBlockTransformations, switchToBlockType, store as blocksStore, } from '@wordpress/blocks'; import { Platform } from '@wordpress/element'; import { applyFilters } from '@wordpress/hooks'; import { symbol } from '@wordpress/icons'; import { create, remove, toHTMLString } from '@wordpress/rich-text'; import deprecated from '@wordpress/deprecated'; import { createSelector, createRegistrySelector } from '@wordpress/data'; import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies */ import { isFiltered, checkAllowListRecursive, checkAllowList, getAllPatternsDependants, getInsertBlockTypeDependants, getParsedPattern, getGrammar, } from './utils'; import { orderBy } from '../utils/sorting'; import { STORE_NAME } from './constants'; import { unlock } from '../lock-unlock'; import { getContentLockingParent, getTemporarilyEditingAsBlocks, getTemporarilyEditingFocusModeToRevert, getSectionRootClientId, isSectionBlock, getParentSectionBlock, isZoomOut, } from './private-selectors'; /** * 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`. */ // Module constants. const MILLISECONDS_PER_HOUR = 3600 * 1000; const MILLISECONDS_PER_DAY = 24 * 3600 * 1000; const MILLISECONDS_PER_WEEK = 7 * 24 * 3600 * 1000; /** * Shared reference to an empty array for cases where it is important to avoid * returning a new array reference on every invocation, as in a connected or * other pure component which performs `shouldComponentUpdate` check on props. * This should be used as a last resort, since the normalized data should be * maintained by the reducer result in state. * * @type {Array} */ const EMPTY_ARRAY = []; /** * Shared reference to an empty Set for cases where it is important to avoid * returning a new Set reference on every invocation, as in a connected or * other pure component which performs `shouldComponentUpdate` check on props. * This should be used as a last resort, since the normalized data should be * maintained by the reducer result in state. * * @type {Set} */ const EMPTY_SET = new Set(); const DEFAULT_INSERTER_OPTIONS = { [ isFiltered ]: true, }; /** * Returns a block's name given its client ID, or null if no block exists with * the client ID. * * @param {Object} state Editor state. * @param {string} clientId Block client ID. * * @return {string} Block name. */ export function getBlockName( state, clientId ) { const block = state.blocks.byClientId.get( clientId ); const socialLinkName = 'core/social-link'; if ( Platform.OS !== 'web' && block?.name === socialLinkName ) { const attributes = state.blocks.attributes.get( clientId ); const { service } = attributes ?? {}; return service ? `${ socialLinkName }-${ service }` : socialLinkName; } return block ? block.name : null; } /** * Returns whether a block is valid or not. * * @param {Object} state Editor state. * @param {string} clientId Block client ID. * * @return {boolean} Is Valid. */ export function isBlockValid( state, clientId ) { const block = state.blocks.byClientId.get( clientId ); return !! block && block.isValid; } /** * Returns a block's attributes given its client ID, or null if no block exists with * the client ID. * * @param {Object} state Editor state. * @param {string} clientId Block client ID. * * @return {?Object} Block attributes. */ export function getBlockAttributes( state, clientId ) { const block = state.blocks.byClientId.get( clientId ); if ( ! block ) { return null; } return state.blocks.attributes.get( clientId ); } /** * Returns a block given its client ID. This is a parsed copy of the block, * containing its `blockName`, `clientId`, and current `attributes` state. This * is not the block's registration settings, which must be retrieved from the * blocks module registration store. * * getBlock recurses through its inner blocks until all its children blocks have * been retrieved. Note that getBlock will not return the child inner blocks of * an inner block controller. This is because an inner block controller syncs * itself with its own entity, and should therefore not be included with the * blocks of a different entity. For example, say you call `getBlocks( TP )` to * get the blocks of a template part. If another template part is a child of TP, * then the nested template part's child blocks will not be returned. This way, * the template block itself is considered part of the parent, but the children * are not. * * @param {Object} state Editor state. * @param {string} clientId Block client ID. * * @return {Object} Parsed block object. */ export function getBlock( state, clientId ) { if ( ! state.blocks.byClientId.has( clientId ) ) { return null; } return state.blocks.tree.get( clientId ); } export const __unstableGetBlockWithoutInnerBlocks = createSelector( ( state, clientId ) => { const block = state.blocks.byClientId.get( clientId ); if ( ! block ) { return null; } return { ...block, attributes: getBlockAttributes( state, clientId ), }; }, ( state, clientId ) => [ state.blocks.byClientId.get( clientId ), state.blocks.attributes.get( clientId ), ] ); /** * Returns all block objects for the current post being edited as an array in * the order they appear in the post. Note that this will exclude child blocks * of nested inner block controllers. * * @param {Object} state Editor state. * @param {?string} rootClientId Optional root client ID of block list. * * @return {Object[]} Post blocks. */ export function getBlocks( state, rootClientId ) { const treeKey = ! rootClientId || ! areInnerBlocksControlled( state, rootClientId ) ? rootClientId || '' : 'controlled||' + rootClientId; return state.blocks.tree.get( treeKey )?.innerBlocks || EMPTY_ARRAY; } /** * Returns a stripped down block object containing only its client ID, * and its inner blocks' client IDs. * * @deprecated * * @param {Object} state Editor state. * @param {string} clientId Client ID of the block to get. * * @return {Object} Client IDs of the post blocks. */ export const __unstableGetClientIdWithClientIdsTree = createSelector( ( state, clientId ) => { deprecated( "wp.data.select( 'core/block-editor' ).__unstableGetClientIdWithClientIdsTree", { since: '6.3', version: '6.5', } ); return { clientId, innerBlocks: __unstableGetClientIdsTree( state, clientId ), }; }, ( state ) => [ state.blocks.order ] ); /** * Returns the block tree represented in the block-editor store from the * given root, consisting of stripped down block objects containing only * their client IDs, and their inner blocks' client IDs. * * @deprecated * * @param {Object} state Editor state. * @param {?string} rootClientId Optional root client ID of block list. * * @return {Object[]} Client IDs of the post blocks. */ export const __unstableGetClientIdsTree = createSelector( ( state, rootClientId = '' ) => { deprecated( "wp.data.select( 'core/block-editor' ).__unstableGetClientIdsTree", { since: '6.3', version: '6.5', } ); return getBlockOrder( state, rootClientId ).map( ( clientId ) => __unstableGetClientIdWithClientIdsTree( state, clientId ) ); }, ( state ) => [ state.blocks.order ] ); /** * Returns an array containing the clientIds of all descendants of the blocks * given. Returned ids are ordered first by the order of the ids given, then * by the order that they appear in the editor. * * @param {Object} state Global application state. * @param {string|string[]} rootIds Client ID(s) for which descendant blocks are to be returned. * * @return {Array} Client IDs of descendants. */ export const getClientIdsOfDescendants = createSelector( ( state, rootIds ) => { rootIds = Array.isArray( rootIds ) ? [ ...rootIds ] : [ rootIds ]; const ids = []; // Add the descendants of the root blocks first. for ( const rootId of rootIds ) { const order = state.blocks.order.get( rootId ); if ( order ) { ids.push( ...order ); } } let index = 0; // Add the descendants of the descendants, recursively. while ( index < ids.length ) { const id = ids[ index ]; const order = state.blocks.order.get( id ); if ( order ) { ids.splice( index + 1, 0, ...order ); } index++; } return ids; }, ( state ) => [ state.blocks.order ] ); /** * Returns an array containing the clientIds of the top-level blocks and * their descendants of any depth (for nested blocks). Ids are returned * in the same order that they appear in the editor. * * @param {Object} state Global application state. * * @return {Array} ids of top-level and descendant blocks. */ export const getClientIdsWithDescendants = ( state ) => getClientIdsOfDescendants( state, '' ); /** * Returns the total number of blocks, or the total number of blocks with a specific name in a post. * The number returned includes nested blocks. * * @param {Object} state Global application state. * @param {?string} blockName Optional block name, if specified only blocks of that type will be counted. * * @return {number} Number of blocks in the post, or number of blocks with name equal to blockName. */ export const getGlobalBlockCount = createSelector( ( state, blockName ) => { const clientIds = getClientIdsWithDescendants( state ); if ( ! blockName ) { return clientIds.length; } let count = 0; for ( const clientId of clientIds ) { const block = state.blocks.byClientId.get( clientId ); if ( block.name === blockName ) { count++; } } return count; }, ( state ) => [ state.blocks.order, state.blocks.byClientId ] ); /** * Returns all blocks that match a blockName. Results include nested blocks. * * @param {Object} state Global application state. * @param {string[]} blockName Block name(s) for which clientIds are to be returned. * * @return {Array} Array of clientIds of blocks with name equal to blockName. */ export const getBlocksByName = createSelector( ( state, blockName ) => { if ( ! blockName ) { return EMPTY_ARRAY; } const blockNames = Array.isArray( blockName ) ? blockName : [ blockName ]; const clientIds = getClientIdsWithDescendants( state ); const foundBlocks = clientIds.filter( ( clientId ) => { const block = state.blocks.byClientId.get( clientId ); return blockNames.includes( block.name ); } ); return foundBlocks.length > 0 ? foundBlocks : EMPTY_ARRAY; }, ( state ) => [ state.blocks.order, state.blocks.byClientId ] ); /** * Returns all global blocks that match a blockName. Results include nested blocks. * * @deprecated * * @param {Object} state Global application state. * @param {string[]} blockName Block name(s) for which clientIds are to be returned. * * @return {Array} Array of clientIds of blocks with name equal to blockName. */ export function __experimentalGetGlobalBlocksByName( state, blockName ) { deprecated( "wp.data.select( 'core/block-editor' ).__experimentalGetGlobalBlocksByName", { since: '6.5', alternative: `wp.data.select( 'core/block-editor' ).getBlocksByName`, } ); return getBlocksByName( state, blockName ); } /** * Given an array of block client IDs, returns the corresponding array of block * objects. * * @param {Object} state Editor state. * @param {string[]} clientIds Client IDs for which blocks are to be returned. * * @return {WPBlock[]} Block objects. */ export const getBlocksByClientId = createSelector( ( state, clientIds ) => ( Array.isArray( clientIds ) ? clientIds : [ clientIds ] ).map( ( clientId ) => getBlock( state, clientId ) ), ( state, clientIds ) => ( Array.isArray( clientIds ) ? clientIds : [ clientIds ] ).map( ( clientId ) => state.blocks.tree.get( clientId ) ) ); /** * Given an array of block client IDs, returns the corresponding array of block * names. * * @param {Object} state Editor state. * @param {string[]} clientIds Client IDs for which block names are to be returned. * * @return {string[]} Block names. */ export const getBlockNamesByClientId = createSelector( ( state, clientIds ) => getBlocksByClientId( state, clientIds ) .filter( Boolean ) .map( ( block ) => block.name ), ( state, clientIds ) => getBlocksByClientId( state, clientIds ) ); /** * Returns the number of blocks currently present in the post. * * @param {Object} state Editor state. * @param {?string} rootClientId Optional root client ID of block list. * * @return {number} Number of blocks in the post. */ export function getBlockCount( state, rootClientId ) { return getBlockOrder( state, rootClientId ).length; } /** * Returns the current selection start block client ID, attribute key and text * offset. * * @param {Object} state Block editor state. * * @return {WPBlockSelection} Selection start information. */ export function getSelectionStart( state ) { return state.selection.selectionStart; } /** * Returns the current selection end block client ID, attribute key and text * offset. * * @param {Object} state Block editor state. * * @return {WPBlockSelection} Selection end information. */ export function getSelectionEnd( state ) { return state.selection.selectionEnd; } /** * Returns the current block selection start. This value may be null, and it * may represent either a singular block selection or multi-selection start. * A selection is singular if its start and end match. * * @param {Object} state Global application state. * * @return {?string} Client ID of block selection start. */ export function getBlockSelectionStart( state ) { return state.selection.selectionStart.clientId; } /** * Returns the current block selection end. This value may be null, and it * may represent either a singular block selection or multi-selection end. * A selection is singular if its start and end match. * * @param {Object} state Global application state. * * @return {?string} Client ID of block selection end. */ export function getBlockSelectionEnd( state ) { return state.selection.selectionEnd.clientId; } /** * Returns the number of blocks currently selected in the post. * * @param {Object} state Global application state. * * @return {number} Number of blocks selected in the post. */ export function getSelectedBlockCount( state ) { const multiSelectedBlockCount = getMultiSelectedBlockClientIds( state ).length; if ( multiSelectedBlockCount ) { return multiSelectedBlockCount; } return state.selection.selectionStart.clientId ? 1 : 0; } /** * Returns true if there is a single selected block, or false otherwise. * * @param {Object} state Editor state. * * @return {boolean} Whether a single block is selected. */ export function hasSelectedBlock( state ) { const { selectionStart, selectionEnd } = state.selection; return ( !! selectionStart.clientId && selectionStart.clientId === selectionEnd.clientId ); } /** * Returns the currently selected block client ID, or null if there is no * selected block. * * @param {Object} state Editor state. * * @return {?string} Selected block client ID. */ export function getSelectedBlockClientId( state ) { const { selectionStart, selectionEnd } = state.selection; const { clientId } = selectionStart; if ( ! clientId || clientId !== selectionEnd.clientId ) { return null; } return clientId; } /** * Returns the currently selected block, or null if there is no selected block. * * @param {Object} state Global application state. * * @example * *```js * import { select } from '@wordpress/data' * import { store as blockEditorStore } from '@wordpress/block-editor' * * // Set initial active block client ID * let activeBlockClientId = null * * const getActiveBlockData = () => { * const activeBlock = select(blockEditorStore).getSelectedBlock() * * if (activeBlock && activeBlock.clientId !== activeBlockClientId) { * activeBlockClientId = activeBlock.clientId * * // Get active block name and attributes * const activeBlockName = activeBlock.name * const activeBlockAttributes = activeBlock.attributes * * // Log active block name and attributes * console.log(activeBlockName, activeBlockAttributes) * } * } * * // Subscribe to changes in the editor * // wp.data.subscribe(() => { * // getActiveBlockData() * // }) * * // Update active block data on click * // onclick="getActiveBlockData()" *``` * * @return {?Object} Selected block. */ export function getSelectedBlock( state ) { const clientId = getSelectedBlockClientId( state ); return clientId ? getBlock( state, clientId ) : null; } /** * Given a block client ID, returns the root block from which the block is * nested, an empty string for top-level blocks, or null if the block does not * exist. * * @param {Object} state Editor state. * @param {string} clientId Block from which to find root client ID. * * @return {?string} Root client ID, if exists */ export function getBlockRootClientId( state, clientId ) { return state.blocks.parents.get( clientId ) ?? null; } /** * Given a block client ID, returns the list of all its parents from top to bottom. * * @param {Object} state Editor state. * @param {string} clientId Block from which to find root client ID. * @param {boolean} ascending Order results from bottom to top (true) or top to bottom (false). * * @return {Array} ClientIDs of the parent blocks. */ export const getBlockParents = createSelector( ( state, clientId, ascending = false ) => { const parents = []; let current = clientId; while ( ( current = state.blocks.parents.get( current ) ) ) { parents.push( current ); } if ( ! parents.length ) { return EMPTY_ARRAY; } return ascending ? parents : parents.reverse(); }, ( state ) => [ state.blocks.parents ] ); /** * Given a block client ID and a block name, returns the list of all its parents * from top to bottom, filtered by the given name(s). For example, if passed * 'core/group' as the blockName, it will only return parents which are group * blocks. If passed `[ 'core/group', 'core/cover']`, as the blockName, it will * return parents which are group blocks and parents which are cover blocks. * * @param {Object} state Editor state. * @param {string} clientId Block from which to find root client ID. * @param {string|string[]} blockName Block name(s) to filter. * @param {boolean} ascending Order results from bottom to top (true) or top to bottom (false). * * @return {Array} ClientIDs of the parent blocks. */ export const getBlockParentsByBlockName = createSelector( ( state, clientId, blockName, ascending = false ) => { const parents = getBlockParents( state, clientId, ascending ); const hasName = Array.isArray( blockName ) ? ( name ) => blockName.includes( name ) : ( name ) => blockName === name; return parents.filter( ( id ) => hasName( getBlockName( state, id ) ) ); }, ( state ) => [ state.blocks.parents ] ); /** * Given a block client ID, returns the root of the hierarchy from which the block is nested, return the block itself for root level blocks. * * @param {Object} state Editor state. * @param {string} clientId Block from which to find root client ID. * * @return {string} Root client ID */ export function getBlockHierarchyRootClientId( state, clientId ) { let current = clientId; let parent; do { parent = current; current = state.blocks.parents.get( current ); } while ( current ); return parent; } /** * Given a block client ID, returns the lowest common ancestor with selected client ID. * * @param {Object} state Editor state. * @param {string} clientId Block from which to find common ancestor client ID. * * @return {string} Common ancestor client ID or undefined */ export function getLowestCommonAncestorWithSelectedBlock( state, clientId ) { const selectedId = getSelectedBlockClientId( state ); const clientParents = [ ...getBlockParents( state, clientId ), clientId ]; const selectedParents = [ ...getBlockParents( state, selectedId ), selectedId, ]; let lowestCommonAncestor; const maxDepth = Math.min( clientParents.length, selectedParents.length ); for ( let index = 0; index < maxDepth; index++ ) { if ( clientParents[ index ] === selectedParents[ index ] ) { lowestCommonAncestor = clientParents[ index ]; } else { break; } } return lowestCommonAncestor; } /** * Returns the client ID of the block adjacent one at the given reference * startClientId and modifier directionality. Defaults start startClientId to * the selected block, and direction as next block. Returns null if there is no * adjacent block. * * @param {Object} state Editor state. * @param {?string} startClientId Optional client ID of block from which to * search. * @param {?number} modifier Directionality multiplier (1 next, -1 * previous). * * @return {?string} Return the client ID of the block, or null if none exists. */ export function getAdjacentBlockClientId( state, startClientId, modifier = 1 ) { // Default to selected block. if ( startClientId === undefined ) { startClientId = getSelectedBlockClientId( state ); } // Try multi-selection starting at extent based on modifier. if ( startClientId === undefined ) { if ( modifier < 0 ) { startClientId = getFirstMultiSelectedBlockClientId( state ); } else { startClientId = getLastMultiSelectedBlockClientId( state ); } } // Validate working start client ID. if ( ! startClientId ) { return null; } // Retrieve start block root client ID, being careful to allow the falsey // empty string top-level root by explicitly testing against null. const rootClientId = getBlockRootClientId( state, startClientId ); if ( rootClientId === null ) { return null; } const { order } = state.blocks; const orderSet = order.get( rootClientId ); const index = orderSet.indexOf( startClientId ); const nextIndex = index + 1 * modifier; // Block was first in set and we're attempting to get previous. if ( nextIndex < 0 ) { return null; } // Block was last in set and we're attempting to get next. if ( nextIndex === orderSet.length ) { return null; } // Assume incremented index is within the set. return orderSet[ nextIndex ]; } /** * Returns the previous block's client ID from the given reference start ID. * Defaults start to the selected block. Returns null if there is no previous * block. * * @param {Object} state Editor state. * @param {?string} startClientId Optional client ID of block from which to * search. * * @return {?string} Adjacent block's client ID, or null if none exists. */ export function getPreviousBlockClientId( state, startClientId ) { return getAdjacentBlockClientId( state, startClientId, -1 ); } /** * Returns the next block's client ID from the given reference start ID. * Defaults start to the selected block. Returns null if there is no next * block. * * @param {Object} state Editor state. * @param {?string} startClientId Optional client ID of block from which to * search. * * @return {?string} Adjacent block's client ID, or null if none exists. */ export function getNextBlockClientId( state, startClientId ) { return getAdjacentBlockClientId( state, startClientId, 1 ); } /* eslint-disable jsdoc/valid-types */ /** * Returns the initial caret position for the selected block. * This position is to used to position the caret properly when the selected block changes. * If the current block is not a RichText, having initial position set to 0 means "focus block" * * @param {Object} state Global application state. * * @return {0|-1|null} Initial position. */ export function getSelectedBlocksInitialCaretPosition( state ) { /* eslint-enable jsdoc/valid-types */ return state.initialPosition; } /** * Returns the current selection set of block client IDs (multiselection or single selection). * * @param {Object} state Editor state. * * @return {Array} Multi-selected block client IDs. */ export const getSelectedBlockClientIds = createSelector( ( state ) => { const { selectionStart, selectionEnd } = state.selection; if ( ! selectionStart.clientId || ! selectionEnd.clientId ) { return EMPTY_ARRAY; } if ( selectionStart.clientId === selectionEnd.clientId ) { return [ selectionStart.clientId ]; } // Retrieve root client ID to aid in retrieving relevant nested block // order, being careful to allow the falsey empty string top-level root // by explicitly testing against null. const rootClientId = getBlockRootClientId( state, selectionStart.clientId ); if ( rootClientId === null ) { return EMPTY_ARRAY; } const blockOrder = getBlockOrder( state, rootClientId ); const startIndex = blockOrder.indexOf( selectionStart.clientId ); const endIndex = blockOrder.indexOf( selectionEnd.clientId ); if ( startIndex > endIndex ) { return blockOrder.slice( endIndex, startIndex + 1 ); } return blockOrder.slice( startIndex, endIndex + 1 ); }, ( state ) => [ state.blocks.order, state.selection.selectionStart.clientId, state.selection.selectionEnd.clientId, ] ); /** * Returns the current multi-selection set of block client IDs, or an empty * array if there is no multi-selection. * * @param {Object} state Editor state. * * @return {Array} Multi-selected block client IDs. */ export function getMultiSelectedBlockClientIds( state ) { const { selectionStart, selectionEnd } = state.selection; if ( selectionStart.clientId === selectionEnd.clientId ) { return EMPTY_ARRAY; } return getSelectedBlockClientIds( state ); } /** * Returns the current multi-selection set of blocks, or an empty array if * there is no multi-selection. * * @param {Object} state Editor state. * * @return {Array} Multi-selected block objects. */ export const getMultiSelectedBlocks = createSelector( ( state ) => { const multiSelectedBlockClientIds = getMultiSelectedBlockClientIds( state ); if ( ! multiSelectedBlockClientIds.length ) { return EMPTY_ARRAY; } return multiSelectedBlockClientIds.map( ( clientId ) => getBlock( state, clientId ) ); }, ( state ) => [ ...getSelectedBlockClientIds.getDependants( state ), state.blocks.byClientId, state.blocks.order, state.blocks.attributes, ] ); /** * Returns the client ID of the first block in the multi-selection set, or null * if there is no multi-selection. * * @param {Object} state Editor state. * * @return {?string} First block client ID in the multi-selection set. */ export function getFirstMultiSelectedBlockClientId( state ) { return getMultiSelectedBlockClientIds( state )[ 0 ] || null; } /** * Returns the client ID of the last block in the multi-selection set, or null * if there is no multi-selection. * * @param {Object} state Editor state. * * @return {?string} Last block client ID in the multi-selection set. */ export function getLastMultiSelectedBlockClientId( state ) { const selectedClientIds = getMultiSelectedBlockClientIds( state ); return selectedClientIds[ selectedClientIds.length - 1 ] || null; } /** * Returns true if a multi-selection exists, and the block corresponding to the * specified client ID is the first block of the multi-selection set, or false * otherwise. * * @param {Object} state Editor state. * @param {string} clientId Block client ID. * * @return {boolean} Whether block is first in multi-selection. */ export function isFirstMultiSelectedBlock( state, clientId ) { return getFirstMultiSelectedBlockClientId( state ) === clientId; } /** * Returns true if the client ID occurs within the block multi-selection, or * false otherwise. * * @param {Object} state Editor state. * @param {string} clientId Block client ID. * * @return {boolean} Whether block is in multi-selection set. */ export function isBlockMultiSelected( state, clientId ) { return getMultiSelectedBlockClientIds( state ).indexOf( clientId ) !== -1; } /** * Returns true if an ancestor of the block is multi-selected, or false * otherwise. * * @param {Object} state Editor state. * @param {string} clientId Block client ID. * * @return {boolean} Whether an ancestor of the block is in multi-selection * set. */ export const isAncestorMultiSelected = createSelector( ( state, clientId ) => { let ancestorClientId = clientId; let isMultiSelected = false; while ( ancestorClientId && ! isMultiSelected ) { ancestorClientId = getBlockRootClientId( state, ancestorClientId ); isMultiSelected = isBlockMultiSelected( state, ancestorClientId ); } return isMultiSelected; }, ( state ) => [ state.blocks.order, state.selection.selectionStart.clientId, state.selection.selectionEnd.clientId, ] ); /** * Returns the client ID of the block which begins the multi-selection set, or * null if there is no multi-selection. * * This is not necessarily the first client ID in the selection. * * @see getFirstMultiSelectedBlockClientId * * @param {Object} state Editor state. * * @return {?string} Client ID of block beginning multi-selection. */ export function getMultiSelectedBlocksStartClientId( state ) { const { selectionStart, selectionEnd } = state.selection; if ( selectionStart.clientId === selectionEnd.clientId ) { return null; } return selectionStart.clientId || null; } /** * Returns the client ID of the block which ends the multi-selection set, or * null if there is no multi-selection. * * This is not necessarily the last client ID in the selection. * * @see getLastMultiSelectedBlockClientId * * @param {Object} state Editor state. * * @return {?string} Client ID of block ending multi-selection. */ export function getMultiSelectedBlocksEndClientId( state ) { const { selectionStart, selectionEnd } = state.selection; if ( selectionStart.clientId === selectionEnd.clientId ) { return null; } return selectionEnd.clientId || null; } /** * Returns true if the selection is not partial. * * @param {Object} state Editor state. * * @return {boolean} Whether the selection is mergeable. */ export function __unstableIsFullySelected( state ) { const selectionAnchor = getSelectionStart( state ); const selectionFocus = getSelectionEnd( state ); return ( ! selectionAnchor.attributeKey && ! selectionFocus.attributeKey && typeof selectionAnchor.offset === 'undefined' && typeof selectionFocus.offset === 'undefined' ); } /** * Returns true if the selection is collapsed. * * @param {Object} state Editor state. * * @return {boolean} Whether the selection is collapsed. */ export function __unstableIsSelectionCollapsed( state ) { const selectionAnchor = getSelectionStart( state ); const selectionFocus = getSelectionEnd( state ); return ( !! selectionAnchor && !! selectionFocus && selectionAnchor.clientId === selectionFocus.clientId && selectionAnchor.attributeKey === selectionFocus.attributeKey && selectionAnchor.offset === selectionFocus.offset ); } export function __unstableSelectionHasUnmergeableBlock( state ) { return getSelectedBlockClientIds( state ).some( ( clientId ) => { const blockName = getBlockName( state, clientId ); const blockType = getBlockType( blockName ); return ! blockType.merge; } ); } /** * Check whether the selection is mergeable. * * @param {Object} state Editor state. * @param {boolean} isForward Whether to merge forwards. * * @return {boolean} Whether the selection is mergeable. */ export function __unstableIsSelectionMergeable( state, isForward ) { const selectionAnchor = getSelectionStart( state ); const selectionFocus = getSelectionEnd( state ); // It's not mergeable if the start and end are within the same block. if ( selectionAnchor.clientId === selectionFocus.clientId ) { return false; } // 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 = getBlockRootClientId( state, selectionAnchor.clientId ); const focusRootClientId = getBlockRootClientId( state, 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 false; } const blockOrder = getBlockOrder( state, 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 targetBlockClientId = isForward ? selectionEnd.clientId : selectionStart.clientId; const blockToMergeClientId = isForward ? selectionStart.clientId : selectionEnd.clientId; const targetBlockName = getBlockName( state, targetBlockClientId ); const targetBlockType = getBlockType( targetBlockName ); if ( ! targetBlockType.merge ) { return false; } const blockToMerge = getBlock( state, blockToMergeClientId ); // It's mergeable if the blocks are of the same type. if ( blockToMerge.name === targetBlockName ) { return true; } // If the blocks are of a different type, try to transform the block being // merged into the same type of block. const blocksToMerge = switchToBlockType( blockToMerge, targetBlockName ); return blocksToMerge && blocksToMerge.length; } /** * Get partial selected blocks with their content updated * based on the selection. * * @param {Object} state Editor state. * * @return {Object[]} Updated partial selected blocks. */ export const __unstableGetSelectedBlocksWithPartialSelection = ( state ) => { const selectionAnchor = getSelectionStart( state ); const selectionFocus = getSelectionEnd( state ); if ( selectionAnchor.clientId === selectionFocus.clientId ) { return EMPTY_ARRAY; } // Can't split if the selection is not set. if ( ! selectionAnchor.attributeKey || ! selectionFocus.attributeKey || typeof selectionAnchor.offset === 'undefined' || typeof selectionFocus.offset === 'undefined' ) { return EMPTY_ARRAY; } const anchorRootClientId = getBlockRootClientId( state, selectionAnchor.clientId ); const focusRootClientId = getBlockRootClientId( state, 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 EMPTY_ARRAY; } const blockOrder = getBlockOrder( state, anchorRootClientId ); const anchorIndex = blockOrder.indexOf( selectionAnchor.clientId ); const focusIndex = blockOrder.indexOf( selectionFocus.clientId ); // Reassign selection start and end based on order. const [ selectionStart, selectionEnd ] = anchorIndex > focusIndex ? [ selectionFocus, selectionAnchor ] : [ selectionAnchor, selectionFocus ]; const blockA = getBlock( state, selectionStart.clientId ); const blockB = getBlock( state, selectionEnd.clientId ); const htmlA = blockA.attributes[ selectionStart.attributeKey ]; const htmlB = blockB.attributes[ selectionEnd.attributeKey ]; let valueA = create( { html: htmlA } ); let valueB = create( { html: htmlB } ); valueA = remove( valueA, 0, selectionStart.offset ); valueB = remove( valueB, selectionEnd.offset, valueB.text.length ); return [ { ...blockA, attributes: { ...blockA.attributes, [ selectionStart.attributeKey ]: toHTMLString( { value: valueA, } ), }, }, { ...blockB, attributes: { ...blockB.attributes, [ selectionEnd.attributeKey ]: toHTMLString( { value: valueB, } ), }, }, ]; }; /** * Returns an array containing all block client IDs in the editor in the order * they appear. Optionally accepts a root client ID of the block list for which * the order should be returned, defaulting to the top-level block order. * * @param {Object} state Editor state. * @param {?string} rootClientId Optional root client ID of block list. * * @return {Array} Ordered client IDs of editor blocks. */ export function getBlockOrder( state, rootClientId ) { return state.blocks.order.get( rootClientId || '' ) || EMPTY_ARRAY; } /** * Returns the index at which the block corresponding to the specified client * ID occurs within the block order, or `-1` if the block does not exist. * * @param {Object} state Editor state. * @param {string} clientId Block client ID. * * @return {number} Index at which block exists in order. */ export function getBlockIndex( state, clientId ) { const rootClientId = getBlockRootClientId( state, clientId ); return getBlockOrder( state, rootClientId ).indexOf( clientId ); } /** * Returns true if the block corresponding to the specified client ID is * currently selected and no multi-selection exists, or false otherwise. * * @param {Object} state Editor state. * @param {string} clientId Block client ID. * * @return {boolean} Whether block is selected and multi-selection exists. */ export function isBlockSelected( state, clientId ) { const { selectionStart, selectionEnd } = state.selection; if ( selectionStart.clientId !== selectionEnd.clientId ) { return false; } return selectionStart.clientId === clientId; } /** * Returns true if one of the block's inner blocks is selected. * * @param {Object} state Editor state. * @param {string} clientId Block client ID. * @param {boolean} deep Perform a deep check. * * @return {boolean} Whether the block has an inner block selected */ export function hasSelectedInnerBlock( state, clientId, deep = false ) { const selectedBlockClientIds = getSelectedBlockClientIds( state ); if ( ! selectedBlockClientIds.length ) { return false; } if ( deep ) { return selectedBlockClientIds.some( ( id ) => // Pass true because we don't care about order and it's more // performant. getBlockParents( state, id, true ).includes( clientId ) ); } return selectedBlockClientIds.some( ( id ) => getBlockRootClientId( state, id ) === clientId ); } /** * Returns true if one of the block's inner blocks is dragged. * * @param {Object} state Editor state. * @param {string} clientId Block client ID. * @param {boolean} deep Perform a deep check. * * @return {boolean} Whether the block has an inner block dragged */ export function hasDraggedInnerBlock( state, clientId, deep = false ) { return getBlockOrder( state, clientId ).some( ( innerClientId ) => isBlockBeingDragged( state, innerClientId ) || ( deep && hasDraggedInnerBlock( state, innerClientId, deep ) ) ); } /** * Returns true if the block corresponding to the specified client ID is * currently selected but isn't the last of the selected blocks. Here "last" * refers to the block sequence in the document, _not_ the sequence of * multi-selection, which is why `state.selectionEnd` isn't used. * * @param {Object} state Editor state. * @param {string} clientId Block client ID. * * @return {boolean} Whether block is selected and not the last in the * selection. */ export function isBlockWithinSelection( state, clientId ) { if ( ! clientId ) { return false; } const clientIds = getMultiSelectedBlockClientIds( state ); const index = clientIds.indexOf( clientId ); return index > -1 && index < clientIds.length - 1; } /** * Returns true if a multi-selection has been made, or false otherwise. * * @param {Object} state Editor state. * * @return {boolean} Whether multi-selection has been made. */ export function hasMultiSelection( state ) { const { selectionStart, selectionEnd } = state.selection; return selectionStart.clientId !== selectionEnd.clientId; } /** * Whether in the process of multi-selecting or not. This flag is only true * while the multi-selection is being selected (by mouse move), and is false * once the multi-selection has been settled. * * @see hasMultiSelection * * @param {Object} state Global application state. * * @return {boolean} True if multi-selecting, false if not. */ export function isMultiSelecting( state ) { return state.isMultiSelecting; } /** * Selector that returns if multi-selection is enabled or not. * * @param {Object} state Global application state. * * @return {boolean} True if it should be possible to multi-select blocks, false if multi-selection is disabled. */ export function isSelectionEnabled( state ) { return state.isSelectionEnabled; } /** * Returns the block's editing mode, defaulting to "visual" if not explicitly * assigned. * * @param {Object} state Editor state. * @param {string} clientId Block client ID. * * @return {Object} Block editing mode. */ export function getBlockMode( state, clientId ) { return state.blocksMode[ clientId ] || 'visual'; } /** * Returns true if the user is typing, or false otherwise. * * @param {Object} state Global application state. * * @return {boolean} Whether user is typing. */ export function isTyping( state ) { return state.isTyping; } /** * Returns true if the user is dragging blocks, or false otherwise. * * @param {Object} state Global application state. * * @return {boolean} Whether user is dragging blocks. */ export function isDraggingBlocks( state ) { return !! state.draggedBlocks.length; } /** * Returns the client ids of any blocks being directly dragged. * * This does not include children of a parent being dragged. * * @param {Object} state Global application state. * * @return {string[]} Array of dragged block client ids. */ export function getDraggedBlockClientIds( state ) { return state.draggedBlocks; } /** * Returns whether the block is being dragged. * * Only returns true if the block is being directly dragged, * not if the block is a child of a parent being dragged. * See `isAncestorBeingDragged` for child blocks. * * @param {Object} state Global application state. * @param {string} clientId Client id for block to check. * * @return {boolean} Whether the block is being dragged. */ export function isBlockBeingDragged( state, clientId ) { return state.draggedBlocks.includes( clientId ); } /** * Returns whether a parent/ancestor of the block is being dragged. * * @param {Object} state Global application state. * @param {string} clientId Client id for block to check. * * @return {boolean} Whether the block's ancestor is being dragged. */ export function isAncestorBeingDragged( state, clientId ) { // Return early if no blocks are being dragged rather than // the more expensive check for parents. if ( ! isDraggingBlocks( state ) ) { return false; } const parents = getBlockParents( state, clientId ); return parents.some( ( parentClientId ) => isBlockBeingDragged( state, parentClientId ) ); } /** * Returns true if the caret is within formatted text, or false otherwise. * * @deprecated * * @return {boolean} Whether the caret is within formatted text. */ export function isCaretWithinFormattedText() { deprecated( 'wp.data.select( "core/block-editor" ).isCaretWithinFormattedText', { since: '6.1', version: '6.3', } ); return false; } /** * Returns the location of the insertion cue. Defaults to the last index. * * @param {Object} state Editor state. * * @return {Object} Insertion point object with `rootClientId`, `index`. */ export const getBlockInsertionPoint = createSelector( ( state ) => { let rootClientId, index; const { insertionCue, selection: { selectionEnd }, } = state; if ( insertionCue !== null ) { return insertionCue; } const { clientId } = selectionEnd; if ( clientId ) { rootClientId = getBlockRootClientId( state, clientId ) || undefined; index = getBlockIndex( state, selectionEnd.clientId ) + 1; } else { index = getBlockOrder( state ).length; } return { rootClientId, index }; }, ( state ) => [ state.insertionCue, state.selection.selectionEnd.clientId, state.blocks.parents, state.blocks.order, ] ); /** * Returns true if the block insertion point is visible. * * @param {Object} state Global application state. * * @return {?boolean} Whether the insertion point is visible or not. */ export function isBlockInsertionPointVisible( state ) { return state.insertionCue !== null; } /** * Returns whether the blocks matches the template or not. * * @param {boolean} state * @return {?boolean} Whether the template is valid or not. */ export function isValidTemplate( state ) { return state.template.isValid; } /** * Returns the defined block template * * @param {boolean} state * * @return {?Array} Block Template. */ export function getTemplate( state ) { return state.settings.template; } /** * Returns the defined block template lock. Optionally accepts a root block * client ID as context, otherwise defaulting to the global context. * * @param {Object} state Editor state. * @param {?string} rootClientId Optional block root client ID. * * @return {string|false} Block Template Lock */ export function getTemplateLock( state, rootClientId ) { if ( ! rootClientId ) { return state.settings.templateLock ?? false; } return getBlockListSettings( state, rootClientId )?.templateLock ?? false; } /** * Determines if the given block type is visible in the inserter. * Note that this is different than whether a block is allowed to be inserted. * In some cases, the block is not allowed in a given position but * it should still be visible in the inserter to be able to add it * to a different position. * * @param {Object} state Editor state. * @param {string|Object} blockNameOrType The block type object, e.g., the response * from the block directory; or a string name of * an installed block type, e.g.' core/paragraph'. * @param {?string} rootClientId Optional root client ID of block list. * * @return {boolean} Whether the given block type is allowed to be inserted. */ const isBlockVisibleInTheInserter = ( state, blockNameOrType, rootClientId = null ) => { let blockType; let blockName; if ( blockNameOrType && 'object' === typeof blockNameOrType ) { blockType = blockNameOrType; blockName = blockNameOrType.name; } else { blockType = getBlockType( blockNameOrType ); blockName = blockNameOrType; } if ( ! blockType ) { return false; } const { allowedBlockTypes } = getSettings( state ); const isBlockAllowedInEditor = checkAllowList( allowedBlockTypes, blockName, true ); if ( ! isBlockAllowedInEditor ) { return false; } // If parent blocks are not visible, child blocks should be hidden too. const parents = ( Array.isArray( blockType.parent ) ? blockType.parent : [] ).concat( Array.isArray( blockType.ancestor ) ? blockType.ancestor : [] ); if ( parents.length > 0 ) { // This is an exception to the rule that says that all blocks are visible in the inserter. // Blocks that require a given parent or ancestor are only visible if we're within that parent. if ( parents.includes( 'core/post-content' ) ) { return true; } let current = rootClientId; let hasParent = false; do { if ( parents.includes( getBlockName( state, current ) ) ) { hasParent = true; break; } current = state.blocks.parents.get( current ); } while ( current ); return hasParent; } return true; }; /** * Determines if the given block type is allowed to be inserted into the block list. * This function is not exported and not memoized because using a memoized selector * inside another memoized selector is just a waste of time. * * @param {Object} state Editor state. * @param {string|Object} blockName The block type object, e.g., the response * from the block directory; or a string name of * an installed block type, e.g.' core/paragraph'. * @param {?string} rootClientId Optional root client ID of block list. * * @return {boolean} Whether the given block type is allowed to be inserted. */ const canInsertBlockTypeUnmemoized = ( state, blockName, rootClientId = null ) => { if ( ! isBlockVisibleInTheInserter( state, blockName, rootClientId ) ) { return false; } let blockType; if ( blockName && 'object' === typeof blockName ) { blockType = blockName; blockName = blockType.name; } else { blockType = getBlockType( blockName ); } const isLocked = !! getTemplateLock( state, rootClientId ); if ( isLocked ) { return false; } const _isSectionBlock = !! isSectionBlock( state, rootClientId ); if ( _isSectionBlock ) { return false; } if ( getBlockEditingMode( state, rootClientId ?? '' ) === 'disabled' ) { return false; } const parentBlockListSettings = getBlockListSettings( state, rootClientId ); // The parent block doesn'