/**
* WordPress dependencies
*/
import { select, dispatch } from '@wordpress/data';
import { _x } from '@wordpress/i18n';
import warning from '@wordpress/warning';
/**
* Internal dependencies
*/
import i18nBlockSchema from './i18n-block.json';
import { store as blocksStore } from '../store';
import { unlock } from '../lock-unlock';
import type {
BlockType,
Block,
BlockVariation,
BlockVariationScope,
BlockStyle,
BlockBindingsSource,
Icon,
} from '../types';
function isObject( object: unknown ): object is Record< string, unknown > {
return object !== null && typeof object === 'object';
}
/**
* Sets the server side block definition of blocks.
*
* Ignored from documentation due to being marked as unstable.
*
* @ignore
*
* @param definitions Server-side block definitions
*/
// eslint-disable-next-line camelcase
export function unstable__bootstrapServerSideBlockDefinitions(
definitions: Record< string, Record< string, unknown > >
): void {
const { addBootstrappedBlockType } = unlock( dispatch( blocksStore ) );
for ( const [ name, blockType ] of Object.entries( definitions ) ) {
addBootstrappedBlockType( name, blockType );
}
}
/**
* Gets block settings from metadata loaded from `block.json` file
*
* @param metadata Block metadata loaded from `block.json`.
* @param metadata.textdomain Textdomain to use with translations.
*
* @return Block settings.
*/
function getBlockSettingsFromMetadata( {
textdomain,
...metadata
}: Record< string, unknown > & { textdomain?: string } ) {
const allowedFields = [
'apiVersion',
'title',
'category',
'parent',
'ancestor',
'icon',
'description',
'keywords',
'attributes',
'providesContext',
'usesContext',
'selectors',
'supports',
'styles',
'example',
'variations',
'blockHooks',
'allowedBlocks',
];
const settings = Object.fromEntries(
Object.entries( metadata ).filter( ( [ key ] ) =>
allowedFields.includes( key )
)
);
if ( textdomain ) {
Object.keys( i18nBlockSchema ).forEach( ( key ) => {
if ( ! settings[ key ] ) {
return;
}
settings[ key ] = translateBlockSettingUsingI18nSchema(
( i18nBlockSchema as Record< string, unknown > )[ key ],
settings[ key ],
textdomain
);
} );
}
return settings;
}
/**
* Registers a new block provided a unique name and an object defining its
* behavior. Once registered, the block is made available as an option to any
* editor interface where blocks are implemented.
*
* For more in-depth information on registering a custom block see the
* [Create a block tutorial](https://developer.wordpress.org/block-editor/getting-started/create-block/).
*
* @param blockNameOrMetadata Block type name or its metadata.
* @param settings Block settings.
*
* @example
* ```js
* import { __ } from '@wordpress/i18n';
* import { registerBlockType } from '@wordpress/blocks'
*
* registerBlockType( 'namespace/block-name', {
* title: __( 'My First Block' ),
* edit: () =>
{ __( 'Hello from the editor!' ) }
,
* save: () => Hello from the saved content!
,
* } );
* ```
*
* @return The block, if it has been successfully registered;
* otherwise `undefined`.
*/
export function registerBlockType(
blockNameOrMetadata: string | Record< string, unknown >,
settings: Partial< BlockType >
): BlockType | undefined {
const name = isObject( blockNameOrMetadata )
? blockNameOrMetadata.name
: blockNameOrMetadata;
if ( typeof name !== 'string' ) {
warning( 'Block names must be strings.' );
return;
}
if ( ! /^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/.test( name ) ) {
warning(
'Block names must contain a namespace prefix, include only lowercase alphanumeric characters or dashes, and start with a letter. Example: my-plugin/my-custom-block'
);
return;
}
if ( select( blocksStore ).getBlockType( name ) ) {
warning( 'Block "' + name + '" is already registered.' );
return;
}
const { addBootstrappedBlockType, addUnprocessedBlockType } = unlock(
dispatch( blocksStore )
);
if ( isObject( blockNameOrMetadata ) ) {
const metadata = getBlockSettingsFromMetadata( blockNameOrMetadata );
addBootstrappedBlockType( name, metadata );
}
addUnprocessedBlockType( name, settings );
return select( blocksStore ).getBlockType( name );
}
/**
* Translates block settings provided with metadata using the i18n schema.
*
* @param i18nSchema I18n schema for the block setting.
* @param settingValue Value for the block setting.
* @param textdomain Textdomain to use with translations.
*
* @return Translated setting.
*/
function translateBlockSettingUsingI18nSchema(
i18nSchema: unknown,
settingValue: unknown,
textdomain: string
): unknown {
if ( typeof i18nSchema === 'string' && typeof settingValue === 'string' ) {
// eslint-disable-next-line @wordpress/i18n-no-variables, @wordpress/i18n-text-domain
return _x( settingValue, i18nSchema, textdomain );
}
if (
Array.isArray( i18nSchema ) &&
i18nSchema.length &&
Array.isArray( settingValue )
) {
return settingValue.map( ( value ) =>
translateBlockSettingUsingI18nSchema(
i18nSchema[ 0 ],
value,
textdomain
)
);
}
if (
isObject( i18nSchema ) &&
Object.entries( i18nSchema ).length &&
isObject( settingValue )
) {
return Object.keys( settingValue ).reduce(
( accumulator: Record< string, unknown >, key ) => {
if ( ! ( i18nSchema as Record< string, unknown > )[ key ] ) {
accumulator[ key ] = settingValue[ key ];
return accumulator;
}
accumulator[ key ] = translateBlockSettingUsingI18nSchema(
( i18nSchema as Record< string, unknown > )[ key ],
settingValue[ key ],
textdomain
);
return accumulator;
},
{}
);
}
return settingValue;
}
/**
* Registers a new block collection to group blocks in the same namespace in the inserter.
*
* @param namespace The namespace to group blocks by in the inserter; corresponds to the block namespace.
* @param settings The block collection settings.
* @param settings.title The title to display in the block inserter.
* @param settings.icon The icon to display in the block inserter.
*
* @example
* ```js
* import { __ } from '@wordpress/i18n';
* import { registerBlockCollection, registerBlockType } from '@wordpress/blocks';
*
* // Register the collection.
* registerBlockCollection( 'my-collection', {
* title: __( 'Custom Collection' ),
* } );
*
* // Register a block in the same namespace to add it to the collection.
* registerBlockType( 'my-collection/block-name', {
* title: __( 'My First Block' ),
* edit: () => { __( 'Hello from the editor!' ) }
,
* save: () => 'Hello from the saved content!
,
* } );
* ```
*/
export function registerBlockCollection(
namespace: string,
{ title, icon }: { title: string; icon?: Icon }
): void {
dispatch( blocksStore ).addBlockCollection( namespace, title, icon );
}
/**
* Unregisters a block collection
*
* @param namespace The namespace to group blocks by in the inserter; corresponds to the block namespace
*
* @example
* ```js
* import { unregisterBlockCollection } from '@wordpress/blocks';
*
* unregisterBlockCollection( 'my-collection' );
* ```
*/
export function unregisterBlockCollection( namespace: string ): void {
dispatch( blocksStore ).removeBlockCollection( namespace );
}
/**
* Unregisters a block.
*
* @param name Block name.
*
* @example
* ```js
* import { __ } from '@wordpress/i18n';
* import { unregisterBlockType } from '@wordpress/blocks';
*
* const ExampleComponent = () => {
* return (
*
* );
* };
* ```
*
* @return The previous block value, if it has been successfully
* unregistered; otherwise `undefined`.
*/
export function unregisterBlockType( name: string ): BlockType | undefined {
const oldBlock = select( blocksStore ).getBlockType( name );
if ( ! oldBlock ) {
warning( 'Block "' + name + '" is not registered.' );
return;
}
dispatch( blocksStore ).removeBlockTypes( name );
return oldBlock;
}
/**
* Assigns name of block for handling non-block content.
*
* @param blockName Block name.
*/
export function setFreeformContentHandlerName( blockName: string ): void {
dispatch( blocksStore ).setFreeformFallbackBlockName( blockName );
}
/**
* Retrieves name of block handling non-block content, or undefined if no
* handler has been defined.
*
* @return Block name.
*/
export function getFreeformContentHandlerName(): string | null {
return select( blocksStore ).getFreeformFallbackBlockName();
}
/**
* Retrieves name of block used for handling grouping interactions.
*
* @return Block name.
*/
export function getGroupingBlockName(): string | null {
return select( blocksStore ).getGroupingBlockName();
}
/**
* Assigns name of block handling unregistered block types.
*
* @param blockName Block name.
*/
export function setUnregisteredTypeHandlerName( blockName: string ): void {
dispatch( blocksStore ).setUnregisteredFallbackBlockName( blockName );
}
/**
* Retrieves name of block handling unregistered block types, or undefined if no
* handler has been defined.
*
* @return Block name.
*/
export function getUnregisteredTypeHandlerName(): string | null {
return select( blocksStore ).getUnregisteredFallbackBlockName();
}
/**
* Assigns the default block name.
*
* @param name Block name.
*
* @example
* ```js
* import { setDefaultBlockName } from '@wordpress/blocks';
*
* const ExampleComponent = () => {
*
* return (
*
* );
* };
* ```
*/
export function setDefaultBlockName( name: string ): void {
dispatch( blocksStore ).setDefaultBlockName( name );
}
/**
* Assigns name of block for handling block grouping interactions.
*
* This function lets you select a different block to group other blocks in instead of the
* default `core/group` block. This function must be used in a component or when the DOM is fully
* loaded. See https://developer.wordpress.org/block-editor/reference-guides/packages/packages-dom-ready/
*
* @param name Block name.
*
* @example
* ```js
* import { setGroupingBlockName } from '@wordpress/blocks';
*
* const ExampleComponent = () => {
*
* return (
*
* );
* };
* ```
*/
export function setGroupingBlockName( name: string ): void {
dispatch( blocksStore ).setGroupingBlockName( name );
}
/**
* Retrieves the default block name.
*
* @return Block name.
*/
export function getDefaultBlockName(): string | null {
return select( blocksStore ).getDefaultBlockName();
}
/**
* Returns a registered block type.
*
* @param name Block name.
*
* @return Block type.
*/
export function getBlockType( name: string ): BlockType | undefined {
return select( blocksStore )?.getBlockType( name );
}
/**
* Returns all registered blocks.
*
* @return Block settings.
*/
export function getBlockTypes(): BlockType[] {
return select( blocksStore ).getBlockTypes();
}
/**
* Returns the block support value for a feature, if defined.
*
* @param nameOrType Block name or type object
* @param feature Feature to retrieve
* @param defaultSupports Default value to return if not
* explicitly defined
*
* @return Block support value
*/
export function getBlockSupport(
nameOrType: string | BlockType,
feature: string,
defaultSupports?: unknown
): unknown {
return select( blocksStore ).getBlockSupport(
nameOrType,
feature,
defaultSupports
);
}
/**
* Returns true if the block defines support for a feature, or false otherwise.
*
* @param nameOrType Block name or type object.
* @param feature Feature to test.
* @param defaultSupports Whether feature is supported by
* default if not explicitly defined.
*
* @return Whether block supports feature.
*/
export function hasBlockSupport(
nameOrType: string | BlockType,
feature: string,
defaultSupports?: boolean
): boolean {
return select( blocksStore ).hasBlockSupport(
nameOrType,
feature,
defaultSupports
);
}
/**
* Determines whether or not the given block is a reusable block. This is a
* special block type that is used to point to a global block stored via the
* API.
*
* @param blockOrType Block or Block Type to test.
*
* @return Whether the given block is a reusable block.
*/
export function isReusableBlock(
blockOrType: Block | BlockType | null | undefined
): boolean {
return blockOrType?.name === 'core/block';
}
/**
* Determines whether or not the given block is a template part. This is a
* special block type that allows composing a page template out of reusable
* design elements.
*
* @param blockOrType Block or Block Type to test.
*
* @return Whether the given block is a template part.
*/
export function isTemplatePart(
blockOrType: Block | BlockType | null | undefined
): boolean {
return blockOrType?.name === 'core/template-part';
}
/**
* Returns an array with the child blocks of a given block.
*
* @param blockName Name of block (example: “latest-posts”).
*
* @return Array of child block names.
*/
export const getChildBlockNames = ( blockName: string ): string[] => {
return select( blocksStore ).getChildBlockNames( blockName );
};
/**
* Returns a boolean indicating if a block has child blocks or not.
*
* @param blockName Name of block (example: “latest-posts”).
*
* @return True if a block contains child blocks and false otherwise.
*/
export const hasChildBlocks = ( blockName: string ): boolean => {
return select( blocksStore ).hasChildBlocks( blockName );
};
/**
* Returns a boolean indicating if a block has at least one child block with inserter support.
*
* @param blockName Block type name.
*
* @return True if a block contains at least one child blocks with inserter support
* and false otherwise.
*/
export const hasChildBlocksWithInserterSupport = (
blockName: string
): boolean => {
return select( blocksStore ).hasChildBlocksWithInserterSupport( blockName );
};
/**
* Registers a new block style for the given block types.
*
* For more information on connecting the styles with CSS
* [the official documentation](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/#styles).
*
* @param blockNames Name of blocks e.g. “core/latest-posts” or `[“core/group”, “core/columns”]`.
* @param styleVariation Object containing `name` which is the class name applied to the block and `label` which identifies the variation to the user.
*
* @example
* ```js
* import { __ } from '@wordpress/i18n';
* import { registerBlockStyle } from '@wordpress/blocks';
* import { Button } from '@wordpress/components';
*
*
* const ExampleComponent = () => {
* return (
*
* );
* };
* ```
*/
export const registerBlockStyle = (
blockNames: string | string[],
styleVariation: BlockStyle | BlockStyle[]
): void => {
dispatch( blocksStore ).addBlockStyles( blockNames, styleVariation );
};
/**
* Unregisters a block style for the given block.
*
* @param blockName Name of block (example: “core/latest-posts”).
* @param styleVariationName Name of class applied to the block.
*
* @example
* ```js
* import { __ } from '@wordpress/i18n';
* import { unregisterBlockStyle } from '@wordpress/blocks';
* import { Button } from '@wordpress/components';
*
* const ExampleComponent = () => {
* return (
*
* );
* };
* ```
*/
export const unregisterBlockStyle = (
blockName: string,
styleVariationName: string
): void => {
dispatch( blocksStore ).removeBlockStyles( blockName, styleVariationName );
};
/**
* Returns an array with the variations of a given block type.
* Ignored from documentation as the recommended usage is via useSelect from @wordpress/data.
*
* @ignore
*
* @param blockName Name of block (example: “core/columns”).
* @param scope Block variation scope name.
*
* @return Block variations.
*/
export const getBlockVariations = (
blockName: string,
scope?: BlockVariationScope
): BlockVariation[] | void => {
return select( blocksStore ).getBlockVariations( blockName, scope );
};
/**
* Registers a new block variation for the given block type.
*
* For more information on block variations see
* [the official documentation ](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-variations/).
*
* @param blockName Name of the block (example: “core/columns”).
* @param variation Object describing a block variation.
*
* @example
* ```js
* import { __ } from '@wordpress/i18n';
* import { registerBlockVariation } from '@wordpress/blocks';
* import { Button } from '@wordpress/components';
*
* const ExampleComponent = () => {
* return (
*
* );
* };
* ```
*/
export const registerBlockVariation = (
blockName: string,
variation: BlockVariation
): void => {
if ( typeof variation.name !== 'string' ) {
warning( 'Variation names must be unique strings.' );
}
dispatch( blocksStore ).addBlockVariations( blockName, variation );
};
/**
* Unregisters a block variation defined for the given block type.
*
* @param blockName Name of the block (example: “core/columns”).
* @param variationName Name of the variation defined for the block.
*
* @example
* ```js
* import { __ } from '@wordpress/i18n';
* import { unregisterBlockVariation } from '@wordpress/blocks';
* import { Button } from '@wordpress/components';
*
* const ExampleComponent = () => {
* return (
*
* );
* };
* ```
*/
export const unregisterBlockVariation = (
blockName: string,
variationName: string
): void => {
dispatch( blocksStore ).removeBlockVariations( blockName, variationName );
};
/**
* Registers a new block bindings source with an object defining its
* behavior. Once registered, the source is available to be connected
* to the supported block attributes.
*
* @since 6.7.0 Introduced in WordPress core.
*
* @param source Object describing a block bindings source.
*
* @example
* ```js
* import { _x } from '@wordpress/i18n';
* import { registerBlockBindingsSource } from '@wordpress/blocks'
*
* registerBlockBindingsSource( {
* name: 'plugin/my-custom-source',
* label: _x( 'My Custom Source', 'block bindings source' ),
* usesContext: [ 'postType' ],
* getValues: getSourceValues,
* setValues: updateMyCustomValuesInBatch,
* canUserEditValue: () => true,
* } );
* ```
*/
export const registerBlockBindingsSource = (
source: BlockBindingsSource
): void => {
const {
name,
label,
usesContext,
getValues,
setValues,
canUserEditValue,
getFieldsList,
} = source;
const existingSource = unlock(
select( blocksStore )
).getBlockBindingsSource( name );
/*
* Check if the source has been already registered on the client.
* If any property expected to be "client-only" is defined, return a warning.
*/
const serverProps = [ 'label', 'usesContext' ];
for ( const prop in existingSource ) {
if ( ! serverProps.includes( prop ) && existingSource[ prop ] ) {
warning(
'Block bindings source "' + name + '" is already registered.'
);
return;
}
}
// Check the `name` property is correct.
if ( ! name ) {
warning( 'Block bindings source must contain a name.' );
return;
}
if ( typeof name !== 'string' ) {
warning( 'Block bindings source name must be a string.' );
return;
}
if ( /[A-Z]+/.test( name ) ) {
warning(
'Block bindings source name must not contain uppercase characters.'
);
return;
}
if ( ! /^[a-z0-9/-]+$/.test( name ) ) {
warning(
'Block bindings source name must contain only valid characters: lowercase characters, hyphens, or digits. Example: my-plugin/my-custom-source.'
);
return;
}
if ( ! /^[a-z0-9-]+\/[a-z0-9-]+$/.test( name ) ) {
warning(
'Block bindings source name must contain a namespace and valid characters. Example: my-plugin/my-custom-source.'
);
return;
}
// Check the `label` property is correct.
if ( ! label && ! existingSource?.label ) {
warning( 'Block bindings source must contain a label.' );
return;
}
if ( label && typeof label !== 'string' ) {
warning( 'Block bindings source label must be a string.' );
return;
}
if ( label && existingSource?.label && label !== existingSource?.label ) {
warning( 'Block bindings "' + name + '" source label was overridden.' );
}
// Check the `usesContext` property is correct.
if ( usesContext && ! Array.isArray( usesContext ) ) {
warning( 'Block bindings source usesContext must be an array.' );
return;
}
// Check the `getValues` property is correct.
if ( getValues && typeof getValues !== 'function' ) {
warning( 'Block bindings source getValues must be a function.' );
return;
}
// Check the `setValues` property is correct.
if ( setValues && typeof setValues !== 'function' ) {
warning( 'Block bindings source setValues must be a function.' );
return;
}
// Check the `canUserEditValue` property is correct.
if ( canUserEditValue && typeof canUserEditValue !== 'function' ) {
warning( 'Block bindings source canUserEditValue must be a function.' );
return;
}
// Check the `getFieldsList` property is correct.
if ( getFieldsList && typeof getFieldsList !== 'function' ) {
warning( 'Block bindings source getFieldsList must be a function.' );
return;
}
return unlock( dispatch( blocksStore ) ).addBlockBindingsSource( source );
};
/**
* Unregisters a block bindings source by providing its name.
*
* @since 6.7.0 Introduced in WordPress core.
*
* @param name The name of the block bindings source to unregister.
*
* @example
* ```js
* import { unregisterBlockBindingsSource } from '@wordpress/blocks';
*
* unregisterBlockBindingsSource( 'plugin/my-custom-source' );
* ```
*/
export function unregisterBlockBindingsSource( name: string ): void {
const oldSource = getBlockBindingsSource( name );
if ( ! oldSource ) {
warning( 'Block bindings source "' + name + '" is not registered.' );
return;
}
unlock( dispatch( blocksStore ) ).removeBlockBindingsSource( name );
}
/**
* Returns a registered block bindings source by its name.
*
* @since 6.7.0 Introduced in WordPress core.
*
* @param name Block bindings source name.
*
* @return Block bindings source.
*/
export function getBlockBindingsSource(
name: string
): BlockBindingsSource | undefined {
return unlock( select( blocksStore ) ).getBlockBindingsSource( name );
}
/**
* Returns all registered block bindings sources.
*
* @since 6.7.0 Introduced in WordPress core.
*
* @return Block bindings sources.
*/
export function getBlockBindingsSources(): Record<
string,
BlockBindingsSource
> {
return unlock( select( blocksStore ) ).getAllBlockBindingsSources();
}