@wordpress/blocks
Version:
Block API for WordPress.
331 lines (302 loc) • 7.92 kB
text/typescript
/**
* WordPress dependencies
*/
import { createSelector, createRegistrySelector } from '@wordpress/data';
import deprecated from '@wordpress/deprecated';
/**
* Internal dependencies
*/
import { getBlockType } from './selectors';
import { getValueFromObjectPath } from './utils';
import { __EXPERIMENTAL_STYLE_PROPERTY as STYLE_PROPERTY } from '../api/constants';
import type { BlockStoreState } from './types';
import type { BlockAttribute, BlockBindingsSource, BlockType } from '../types';
const ROOT_BLOCK_SUPPORTS: string[] = [
'background',
'backgroundColor',
'color',
'linkColor',
'captionColor',
'buttonColor',
'headingColor',
'fontFamily',
'fontSize',
'fontStyle',
'fontWeight',
'lineHeight',
'padding',
'contentSize',
'wideSize',
'blockGap',
'textAlign',
'textDecoration',
'textIndent',
'textTransform',
'letterSpacing',
];
/**
* Filters the list of supported styles for a given element.
*
* @param blockSupports list of supported styles.
* @param name block name.
* @param element element name.
*
* @return filtered list of supported styles.
*/
function filterElementBlockSupports(
blockSupports: string[],
name: string | undefined,
element: string | undefined
): string[] {
return blockSupports.filter( ( support ) => {
if ( support === 'fontSize' && element === 'heading' ) {
return false;
}
// This is only available for links
if ( support === 'textDecoration' && ! name && element !== 'link' ) {
return false;
}
// This is only available for heading, button, caption and text
if (
support === 'textTransform' &&
! name &&
! (
[ 'heading', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ].includes(
element as string
) ||
element === 'button' ||
element === 'caption' ||
element === 'text'
)
) {
return false;
}
// This is only available for heading, button, caption and text
if (
support === 'letterSpacing' &&
! name &&
! (
[ 'heading', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ].includes(
element as string
) ||
element === 'button' ||
element === 'caption' ||
element === 'text'
)
) {
return false;
}
// Text indent is only available for blocks, not elements
if ( support === 'textIndent' && ! name ) {
return false;
}
// Text columns is only available for blocks.
if ( support === 'textColumns' && ! name ) {
return false;
}
return true;
} );
}
/**
* Returns the list of supported styles for a given block name and element.
*/
export const getSupportedStyles = createSelector(
(
state: BlockStoreState,
name: string | undefined,
element: string | undefined
): string[] => {
if ( ! name ) {
return filterElementBlockSupports(
ROOT_BLOCK_SUPPORTS,
name,
element
);
}
const blockType = getBlockType( state, name );
if ( ! blockType ) {
return [];
}
const supportKeys: string[] = [];
// Check for blockGap support.
// Block spacing support doesn't map directly to a single style property, so needs to be handled separately.
const supports = blockType?.supports as
| Record< string, unknown >
| undefined;
if (
( supports?.spacing as Record< string, unknown > | undefined )
?.blockGap
) {
supportKeys.push( 'blockGap' );
}
// check for shadow support
if ( supports?.shadow ) {
supportKeys.push( 'shadow' );
}
const stylePropertyMap = STYLE_PROPERTY as Record<
string,
{
support?: string[];
requiresOptOut?: boolean;
value?: string[];
}
>;
Object.keys( stylePropertyMap ).forEach( ( styleName ) => {
if ( ! stylePropertyMap[ styleName ].support ) {
return;
}
// Opting out means that, for certain support keys like background color,
// blocks have to explicitly set the support value false. If the key is
// unset, we still enable it.
if ( stylePropertyMap[ styleName ].requiresOptOut ) {
if (
supports &&
stylePropertyMap[ styleName ].support![ 0 ] in supports &&
getValueFromObjectPath(
supports,
stylePropertyMap[ styleName ].support!
) !== false
) {
supportKeys.push( styleName );
return;
}
}
if (
supports &&
getValueFromObjectPath(
supports,
stylePropertyMap[ styleName ].support!,
false
)
) {
supportKeys.push( styleName );
}
} );
return filterElementBlockSupports( supportKeys, name, element );
},
( state: BlockStoreState, name: string | undefined ) => [
state.blockTypes[ name as string ],
]
);
/**
* Returns the bootstrapped block type metadata for a give block name.
*
* @param state Data state.
* @param name Block name.
*
* @return Bootstrapped block type metadata for a block.
*/
export function getBootstrappedBlockType(
state: BlockStoreState,
name: string
): Partial< BlockType > | undefined {
return state.bootstrappedBlockTypes[ name ];
}
/**
* Returns all the unprocessed (before applying the `registerBlockType` filter)
* block type settings as passed during block registration.
*
* @param state Data state.
*
* @return Unprocessed block type settings for all blocks.
*/
export function getUnprocessedBlockTypes(
state: BlockStoreState
): Record< string, Partial< BlockType > > {
return state.unprocessedBlockTypes;
}
/**
* Returns all the block bindings sources registered.
*
* @param state Data state.
*
* @return All the registered sources and their properties.
*/
export function getAllBlockBindingsSources(
state: BlockStoreState
): Record< string, Omit< BlockBindingsSource, 'name' > > {
return state.blockBindingsSources;
}
/**
* Returns a specific block bindings source.
*
* @param state Data state.
* @param sourceName Name of the source to get.
*
* @return The specific block binding source and its properties.
*/
export function getBlockBindingsSource(
state: BlockStoreState,
sourceName: string
): Omit< BlockBindingsSource, 'name' > | undefined {
return state.blockBindingsSources[ sourceName ];
}
/**
* Compute the fields list for a specific block bindings source.
*
* @param state Data state.
* @param source Block bindings source.
* @param blockContext Block context.
*
* @return List of fields for the specific source.
*/
export const getBlockBindingsSourceFieldsList = createRegistrySelector(
( select: unknown ) =>
createSelector(
(
state: BlockStoreState,
source: BlockBindingsSource,
blockContext: Record< string, unknown >
): unknown[] => {
if ( ! source.getFieldsList ) {
return [];
}
const context: Record< string, unknown > = {};
if ( source?.usesContext?.length ) {
for ( const key of source.usesContext ) {
context[ key ] = blockContext[ key ];
}
}
return source.getFieldsList( { select, context } ) as unknown[];
},
(
state: BlockStoreState,
source: BlockBindingsSource,
blockContext: Record< string, unknown >
) => [ source.getFieldsList, source.usesContext, blockContext ]
)
);
/**
* Determines if any of the block type's attributes have
* the content role attribute.
*
* @param state Data state.
* @param blockTypeName Block type name.
* @return Whether block type has content role attribute.
*/
export const hasContentRoleAttribute = (
state: BlockStoreState,
blockTypeName: string
): boolean => {
const blockType = getBlockType( state, blockTypeName );
if ( ! blockType ) {
return false;
}
return Object.values( blockType.attributes ).some(
( { role, __experimentalRole }: BlockAttribute ) => {
if ( role === 'content' ) {
return true;
}
if ( __experimentalRole === 'content' ) {
deprecated( '__experimentalRole attribute', {
since: '6.7',
version: '6.8',
alternative: 'role attribute',
hint: `Check the block.json of the ${ blockTypeName } block.`,
} );
return true;
}
return false;
}
);
};