@wordpress/block-editor
Version:
1,697 lines (1,529 loc) • 97.8 kB
JavaScript
/**
* 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'