UNPKG

@wordpress/block-editor

Version:
671 lines (620 loc) 19.8 kB
/** * WordPress dependencies */ import { createSelector, createRegistrySelector } from '@wordpress/data'; /** * Internal dependencies */ import { getBlockOrder, getBlockParents, getBlockEditingMode, getSettings, canInsertBlockType, getBlockName, getTemplateLock, getClientIdsWithDescendants, isNavigationMode, getBlockRootClientId, } from './selectors'; import { checkAllowListRecursive, getAllPatternsDependants, getInsertBlockTypeDependants, getGrammar, } from './utils'; import { INSERTER_PATTERN_TYPES } from '../components/inserter/block-patterns-tab/utils'; import { STORE_NAME } from './constants'; import { unlock } from '../lock-unlock'; import { selectBlockPatternsKey, reusableBlocksSelectKey, sectionRootClientIdKey, } from './private-keys'; export { getBlockSettings } from './get-block-settings'; /** * Returns true if the block interface is hidden, or false otherwise. * * @param {Object} state Global application state. * * @return {boolean} Whether the block toolbar is hidden. */ export function isBlockInterfaceHidden( state ) { return state.isBlockInterfaceHidden; } /** * Gets the client ids of the last inserted blocks. * * @param {Object} state Global application state. * @return {Array|undefined} Client Ids of the last inserted block(s). */ export function getLastInsertedBlocksClientIds( state ) { return state?.lastBlockInserted?.clientIds; } export function getBlockWithoutAttributes( state, clientId ) { return state.blocks.byClientId.get( clientId ); } /** * Returns true if all of the descendants of a block with the given client ID * have an editing mode of 'disabled', or false otherwise. * * @param {Object} state Global application state. * @param {string} clientId The block client ID. * * @return {boolean} Whether the block descendants are disabled. */ export const isBlockSubtreeDisabled = ( state, clientId ) => { const isChildSubtreeDisabled = ( childClientId ) => { return ( getBlockEditingMode( state, childClientId ) === 'disabled' && getBlockOrder( state, childClientId ).every( isChildSubtreeDisabled ) ); }; return getBlockOrder( state, clientId ).every( isChildSubtreeDisabled ); }; function getEnabledClientIdsTreeUnmemoized( state, rootClientId ) { const blockOrder = getBlockOrder( state, rootClientId ); const result = []; for ( const clientId of blockOrder ) { const innerBlocks = getEnabledClientIdsTreeUnmemoized( state, clientId ); if ( getBlockEditingMode( state, clientId ) !== 'disabled' ) { result.push( { clientId, innerBlocks } ); } else { result.push( ...innerBlocks ); } } return result; } /** * Returns a tree of block objects with only clientID and innerBlocks set. * Blocks with a 'disabled' editing mode are not included. * * @param {Object} state Global application state. * @param {?string} rootClientId Optional root client ID of block list. * * @return {Object[]} Tree of block objects with only clientID and innerBlocks set. */ export const getEnabledClientIdsTree = createRegistrySelector( ( select ) => createSelector( getEnabledClientIdsTreeUnmemoized, ( state ) => [ state.blocks.order, state.derivedBlockEditingModes, state.derivedNavModeBlockEditingModes, state.blockEditingModes, state.settings.templateLock, state.blockListSettings, select( STORE_NAME ).__unstableGetEditorMode( state ), ] ) ); /** * Returns a list of a given block's ancestors, from top to bottom. Blocks with * a 'disabled' editing mode are excluded. * * @see getBlockParents * * @param {Object} state Global application state. * @param {string} clientId The block client ID. * @param {boolean} ascending Order results from bottom to top (true) or top * to bottom (false). */ export const getEnabledBlockParents = createSelector( ( state, clientId, ascending = false ) => { return getBlockParents( state, clientId, ascending ).filter( ( parent ) => getBlockEditingMode( state, parent ) !== 'disabled' ); }, ( state ) => [ state.blocks.parents, state.blockEditingModes, state.settings.templateLock, state.blockListSettings, ] ); /** * Selector that returns the data needed to display a prompt when certain * blocks are removed, or `false` if no such prompt is requested. * * @param {Object} state Global application state. * * @return {Object|false} Data for removal prompt display, if any. */ export function getRemovalPromptData( state ) { return state.removalPromptData; } /** * Returns true if removal prompt exists, or false otherwise. * * @param {Object} state Global application state. * * @return {boolean} Whether removal prompt exists. */ export function getBlockRemovalRules( state ) { return state.blockRemovalRules; } /** * Returns the client ID of the block settings menu that is currently open. * * @param {Object} state Global application state. * @return {string|null} The client ID of the block menu that is currently open. */ export function getOpenedBlockSettingsMenu( state ) { return state.openedBlockSettingsMenu; } /** * Returns all style overrides, intended to be merged with global editor styles. * * Overrides are sorted to match the order of the blocks they relate to. This * is useful to maintain correct CSS cascade order. * * @param {Object} state Global application state. * * @return {Array} An array of style ID to style override pairs. */ export const getStyleOverrides = createSelector( ( state ) => { const clientIds = getClientIdsWithDescendants( state ); const clientIdMap = clientIds.reduce( ( acc, clientId, index ) => { acc[ clientId ] = index; return acc; }, {} ); return [ ...state.styleOverrides ].sort( ( overrideA, overrideB ) => { // Once the overrides Map is spread to an array, the first element // is the key, while the second is the override itself including // the clientId to sort by. const [ , { clientId: clientIdA } ] = overrideA; const [ , { clientId: clientIdB } ] = overrideB; const aIndex = clientIdMap[ clientIdA ] ?? -1; const bIndex = clientIdMap[ clientIdB ] ?? -1; return aIndex - bIndex; } ); }, ( state ) => [ state.blocks.order, state.styleOverrides ] ); /** @typedef {import('./actions').InserterMediaCategory} InserterMediaCategory */ /** * Returns the registered inserter media categories through the public API. * * @param {Object} state Editor state. * * @return {InserterMediaCategory[]} Inserter media categories. */ export function getRegisteredInserterMediaCategories( state ) { return state.registeredInserterMediaCategories; } /** * Returns an array containing the allowed inserter media categories. * It merges the registered media categories from extenders with the * core ones. It also takes into account the allowed `mime_types`, which * can be altered by `upload_mimes` filter and restrict some of them. * * @param {Object} state Global application state. * * @return {InserterMediaCategory[]} Client IDs of descendants. */ export const getInserterMediaCategories = createSelector( ( state ) => { const { settings: { inserterMediaCategories, allowedMimeTypes, enableOpenverseMediaCategory, }, registeredInserterMediaCategories, } = state; // The allowed `mime_types` can be altered by `upload_mimes` filter and restrict // some of them. In this case we shouldn't add the category to the available media // categories list in the inserter. if ( ( ! inserterMediaCategories && ! registeredInserterMediaCategories.length ) || ! allowedMimeTypes ) { return; } const coreInserterMediaCategoriesNames = inserterMediaCategories?.map( ( { name } ) => name ) || []; const mergedCategories = [ ...( inserterMediaCategories || [] ), ...( registeredInserterMediaCategories || [] ).filter( ( { name } ) => ! coreInserterMediaCategoriesNames.includes( name ) ), ]; return mergedCategories.filter( ( category ) => { // Check if Openverse category is enabled. if ( ! enableOpenverseMediaCategory && category.name === 'openverse' ) { return false; } return Object.values( allowedMimeTypes ).some( ( mimeType ) => mimeType.startsWith( `${ category.mediaType }/` ) ); } ); }, ( state ) => [ state.settings.inserterMediaCategories, state.settings.allowedMimeTypes, state.settings.enableOpenverseMediaCategory, state.registeredInserterMediaCategories, ] ); /** * Returns whether there is at least one allowed pattern for inner blocks children. * This is useful for deferring the parsing of all patterns until needed. * * @param {Object} state Editor state. * @param {string} [rootClientId=null] Target root client ID. * * @return {boolean} If there is at least one allowed pattern. */ export const hasAllowedPatterns = createRegistrySelector( ( select ) => createSelector( ( state, rootClientId = null ) => { const { getAllPatterns } = unlock( select( STORE_NAME ) ); const patterns = getAllPatterns(); const { allowedBlockTypes } = getSettings( state ); return patterns.some( ( pattern ) => { const { inserter = true } = pattern; if ( ! inserter ) { return false; } const grammar = getGrammar( pattern ); return ( checkAllowListRecursive( grammar, allowedBlockTypes ) && grammar.every( ( { name: blockName } ) => canInsertBlockType( state, blockName, rootClientId ) ) ); } ); }, ( state, rootClientId ) => [ ...getAllPatternsDependants( select )( state ), ...getInsertBlockTypeDependants( select )( state, rootClientId ), ] ) ); function mapUserPattern( userPattern, __experimentalUserPatternCategories = [] ) { return { name: `core/block/${ userPattern.id }`, id: userPattern.id, type: INSERTER_PATTERN_TYPES.user, title: userPattern.title.raw, categories: userPattern.wp_pattern_category?.map( ( catId ) => { const category = __experimentalUserPatternCategories.find( ( { id } ) => id === catId ); return category ? category.slug : catId; } ), content: userPattern.content.raw, syncStatus: userPattern.wp_pattern_sync_status, }; } export const getPatternBySlug = createRegistrySelector( ( select ) => createSelector( ( state, patternName ) => { // Only fetch reusable blocks if we know we need them. To do: maybe // use the entity record API to retrieve the block by slug. if ( patternName?.startsWith( 'core/block/' ) ) { const _id = parseInt( patternName.slice( 'core/block/'.length ), 10 ); const block = unlock( select( STORE_NAME ) ) .getReusableBlocks() .find( ( { id } ) => id === _id ); if ( ! block ) { return null; } return mapUserPattern( block, state.settings.__experimentalUserPatternCategories ); } return [ // This setting is left for back compat. ...( state.settings.__experimentalBlockPatterns ?? [] ), ...( state.settings[ selectBlockPatternsKey ]?.( select ) ?? [] ), ].find( ( { name } ) => name === patternName ); }, ( state, patternName ) => patternName?.startsWith( 'core/block/' ) ? [ unlock( select( STORE_NAME ) ).getReusableBlocks(), state.settings.__experimentalReusableBlocks, ] : [ state.settings.__experimentalBlockPatterns, state.settings[ selectBlockPatternsKey ]?.( select ), ] ) ); export const getAllPatterns = createRegistrySelector( ( select ) => createSelector( ( state ) => { return [ ...unlock( select( STORE_NAME ) ) .getReusableBlocks() .map( ( userPattern ) => mapUserPattern( userPattern, state.settings.__experimentalUserPatternCategories ) ), // This setting is left for back compat. ...( state.settings.__experimentalBlockPatterns ?? [] ), ...( state.settings[ selectBlockPatternsKey ]?.( select ) ?? [] ), ].filter( ( x, index, arr ) => index === arr.findIndex( ( y ) => x.name === y.name ) ); }, getAllPatternsDependants( select ) ) ); const EMPTY_ARRAY = []; export const getReusableBlocks = createRegistrySelector( ( select ) => ( state ) => { const reusableBlocksSelect = state.settings[ reusableBlocksSelectKey ]; return ( ( reusableBlocksSelect ? reusableBlocksSelect( select ) : state.settings.__experimentalReusableBlocks ) ?? EMPTY_ARRAY ); } ); /** * Returns the element of the last element that had focus when focus left the editor canvas. * * @param {Object} state Block editor state. * * @return {Object} Element. */ export function getLastFocus( state ) { return state.lastFocus; } /** * Returns true if the user is dragging anything, or false otherwise. It is possible for a * user to be dragging data from outside of the editor, so this selector is separate from * the `isDraggingBlocks` selector which only returns true if the user is dragging blocks. * * @param {Object} state Global application state. * * @return {boolean} Whether user is dragging. */ export function isDragging( state ) { return state.isDragging; } /** * Retrieves the expanded block from the state. * * @param {Object} state Block editor state. * * @return {string|null} The client ID of the expanded block, if set. */ export function getExpandedBlock( state ) { return state.expandedBlock; } /** * Retrieves the client ID of the ancestor block that is content locking the block * with the provided client ID. * * @param {Object} state Global application state. * @param {string} clientId Client Id of the block. * * @return {?string} Client ID of the ancestor block that is content locking the block. */ export const getContentLockingParent = ( state, clientId ) => { let current = clientId; let result; while ( ! result && ( current = state.blocks.parents.get( current ) ) ) { if ( getTemplateLock( state, current ) === 'contentOnly' ) { result = current; } } return result; }; /** * Retrieves the client ID of the parent section block. * * @param {Object} state Global application state. * @param {string} clientId Client Id of the block. * * @return {?string} Client ID of the ancestor block that is content locking the block. */ export const getParentSectionBlock = ( state, clientId ) => { let current = clientId; let result; while ( ! result && ( current = state.blocks.parents.get( current ) ) ) { if ( isSectionBlock( state, current ) ) { result = current; } } return result; }; /** * Retrieves the client ID is a content locking parent * * @param {Object} state Global application state. * @param {string} clientId Client Id of the block. * * @return {boolean} Whether the block is a content locking parent. */ export function isSectionBlock( state, clientId ) { const blockName = getBlockName( state, clientId ); if ( blockName === 'core/block' || getTemplateLock( state, clientId ) === 'contentOnly' ) { return true; } // Template parts become sections in navigation mode. const _isNavigationMode = isNavigationMode( state ); if ( _isNavigationMode && blockName === 'core/template-part' ) { return true; } const sectionRootClientId = getSectionRootClientId( state ); const sectionClientIds = getBlockOrder( state, sectionRootClientId ); return _isNavigationMode && sectionClientIds.includes( clientId ); } /** * Retrieves the client ID of the block that is content locked but is * currently being temporarily edited as a non-locked block. * * @param {Object} state Global application state. * * @return {?string} The client ID of the block being temporarily edited as a non-locked block. */ export function getTemporarilyEditingAsBlocks( state ) { return state.temporarilyEditingAsBlocks; } /** * Returns the focus mode that should be reapplied when the user stops editing * a content locked blocks as a block without locking. * * @param {Object} state Global application state. * * @return {?string} The focus mode that should be re-set when temporarily editing as blocks stops. */ export function getTemporarilyEditingFocusModeToRevert( state ) { return state.temporarilyEditingFocusModeRevert; } /** * Returns the style attributes of multiple blocks. * * @param {Object} state Global application state. * @param {string[]} clientIds An array of block client IDs. * * @return {Object} An object where keys are client IDs and values are the corresponding block styles or undefined. */ export const getBlockStyles = createSelector( ( state, clientIds ) => clientIds.reduce( ( styles, clientId ) => { styles[ clientId ] = state.blocks.attributes.get( clientId )?.style; return styles; }, {} ), ( state, clientIds ) => [ ...clientIds.map( ( clientId ) => state.blocks.attributes.get( clientId )?.style ), ] ); /** * Retrieves the client ID of the block which contains the blocks * acting as "sections" in the editor. This is typically the "main content" * of the template/post. * * @param {Object} state Editor state. * * @return {string|undefined} The section root client ID or undefined if not set. */ export function getSectionRootClientId( state ) { return state.settings?.[ sectionRootClientIdKey ]; } /** * Returns whether the editor is considered zoomed out. * * @param {Object} state Global application state. * @return {boolean} Whether the editor is zoomed. */ export function isZoomOut( state ) { return state.zoomLevel === 'auto-scaled' || state.zoomLevel < 100; } /** * Returns whether the zoom level. * * @param {Object} state Global application state. * @return {number|"auto-scaled"} Zoom level. */ export function getZoomLevel( state ) { return state.zoomLevel; } /** * Finds the closest block where the block is allowed to be inserted. * * @param {Object} state Editor state. * @param {string[] | string} name Block name or names. * @param {string} clientId Default insertion point. * * @return {string} clientID of the closest container when the block name can be inserted. */ export function getClosestAllowedInsertionPoint( state, name, clientId = '' ) { const blockNames = Array.isArray( name ) ? name : [ name ]; const areBlockNamesAllowedInClientId = ( id ) => blockNames.every( ( currentName ) => canInsertBlockType( state, currentName, id ) ); // If we're trying to insert at the root level and it's not allowed // Try the section root instead. if ( ! clientId ) { if ( areBlockNamesAllowedInClientId( clientId ) ) { return clientId; } const sectionRootClientId = getSectionRootClientId( state ); if ( sectionRootClientId && areBlockNamesAllowedInClientId( sectionRootClientId ) ) { return sectionRootClientId; } return null; } // Traverse the block tree up until we find a place where we can insert. let current = clientId; while ( current !== null && ! areBlockNamesAllowedInClientId( current ) ) { const parentClientId = getBlockRootClientId( state, current ); current = parentClientId; } return current; } export function getClosestAllowedInsertionPointForPattern( state, pattern, clientId ) { const { allowedBlockTypes } = getSettings( state ); const isAllowed = checkAllowListRecursive( getGrammar( pattern ), allowedBlockTypes ); if ( ! isAllowed ) { return null; } const names = getGrammar( pattern ).map( ( { blockName: name } ) => name ); return getClosestAllowedInsertionPoint( state, names, clientId ); } /** * Where the point where the next block will be inserted into. * * @param {Object} state * @return {Object} where the insertion point in the block editor is or null if none is set. */ export function getInsertionPoint( state ) { return state.insertionPoint; }