@wordpress/blocks
Version:
Block API for WordPress.
709 lines (652 loc) • 23.2 kB
JavaScript
/* eslint no-console: [ 'error', { allow: [ 'error', 'warn' ] } ] */
/**
* External dependencies
*/
import {
camelCase,
isArray,
isEmpty,
isFunction,
isNil,
isObject,
isPlainObject,
isString,
mapKeys,
omit,
pick,
pickBy,
some,
} from 'lodash';
/**
* WordPress dependencies
*/
import { applyFilters } from '@wordpress/hooks';
import { select, dispatch } from '@wordpress/data';
import { _x } from '@wordpress/i18n';
import { blockDefault } from '@wordpress/icons';
/**
* Internal dependencies
*/
import i18nBlockSchema from './i18n-block.json';
import { isValidIcon, normalizeIconObject } from './utils';
import { DEPRECATED_ENTRY_KEYS } from './constants';
import { store as blocksStore } from '../store';
/**
* An icon type definition. One of a Dashicon slug, an element,
* or a component.
*
* @typedef {(string|WPElement|WPComponent)} WPIcon
*
* @see https://developer.wordpress.org/resource/dashicons/
*/
/**
* Render behavior of a block type icon; one of a Dashicon slug, an element,
* or a component.
*
* @typedef {WPIcon} WPBlockTypeIconRender
*/
/**
* An object describing a normalized block type icon.
*
* @typedef {Object} WPBlockTypeIconDescriptor
*
* @property {WPBlockTypeIconRender} src Render behavior of the icon,
* one of a Dashicon slug, an
* element, or a component.
* @property {string} background Optimal background hex string
* color when displaying icon.
* @property {string} foreground Optimal foreground hex string
* color when displaying icon.
* @property {string} shadowColor Optimal shadow hex string
* color when displaying icon.
*/
/**
* Value to use to render the icon for a block type in an editor interface,
* either a Dashicon slug, an element, a component, or an object describing
* the icon.
*
* @typedef {(WPBlockTypeIconDescriptor|WPBlockTypeIconRender)} WPBlockTypeIcon
*/
/**
* Named block variation scopes.
*
* @typedef {'block'|'inserter'|'transform'} WPBlockVariationScope
*/
/**
* An object describing a variation defined for the block type.
*
* @typedef {Object} WPBlockVariation
*
* @property {string} name The unique and machine-readable name.
* @property {string} title A human-readable variation title.
* @property {string} [description] A detailed variation description.
* @property {string} [category] Block type category classification,
* used in search interfaces to arrange
* block types by category.
* @property {WPIcon} [icon] An icon helping to visualize the variation.
* @property {boolean} [isDefault] Indicates whether the current variation is
* the default one. Defaults to `false`.
* @property {Object} [attributes] Values which override block attributes.
* @property {Array[]} [innerBlocks] Initial configuration of nested blocks.
* @property {Object} [example] Example provides structured data for
* the block preview. You can set to
* `undefined` to disable the preview shown
* for the block type.
* @property {WPBlockVariationScope[]} [scope] The list of scopes where the variation
* is applicable. When not provided, it
* assumes all available scopes.
* @property {string[]} [keywords] An array of terms (which can be translated)
* that help users discover the variation
* while searching.
* @property {Function|string[]} [isActive] This can be a function or an array of block attributes.
* Function that accepts a block's attributes and the
* variation's attributes and determines if a variation is active.
* This function doesn't try to find a match dynamically based
* on all block's attributes, as in many cases some attributes are irrelevant.
* An example would be for `embed` block where we only care
* about `providerNameSlug` attribute's value.
* We can also use a `string[]` to tell which attributes
* should be compared as a shorthand. Each attributes will
* be matched and the variation will be active if all of them are matching.
*/
/**
* Defined behavior of a block type.
*
* @typedef {Object} WPBlock
*
* @property {string} name Block type's namespaced name.
* @property {string} title Human-readable block type label.
* @property {string} [description] A detailed block type description.
* @property {string} [category] Block type category classification,
* used in search interfaces to arrange
* block types by category.
* @property {WPBlockTypeIcon} [icon] Block type icon.
* @property {string[]} [keywords] Additional keywords to produce block
* type as result in search interfaces.
* @property {Object} [attributes] Block type attributes.
* @property {WPComponent} [save] Optional component describing
* serialized markup structure of a
* block type.
* @property {WPComponent} edit Component rendering an element to
* manipulate the attributes of a block
* in the context of an editor.
* @property {WPBlockVariation[]} [variations] The list of block variations.
* @property {Object} [example] Example provides structured data for
* the block preview. When not defined
* then no preview is shown.
*/
/**
* Mapping of legacy category slugs to their latest normal values, used to
* accommodate updates of the default set of block categories.
*
* @type {Record<string,string>}
*/
const LEGACY_CATEGORY_MAPPING = {
common: 'text',
formatting: 'text',
layout: 'design',
};
export const serverSideBlockDefinitions = {};
/**
* Sets the server side block definition of blocks.
*
* @param {Object} definitions Server-side block definitions
*/
// eslint-disable-next-line camelcase
export function unstable__bootstrapServerSideBlockDefinitions( definitions ) {
for ( const blockName of Object.keys( definitions ) ) {
// Don't overwrite if already set. It covers the case when metadata
// was initialized from the server.
if ( serverSideBlockDefinitions[ blockName ] ) {
// We still need to polyfill `apiVersion` for WordPress version
// lower than 5.7. If it isn't present in the definition shared
// from the server, we try to fallback to the definition passed.
// @see https://github.com/WordPress/gutenberg/pull/29279
if (
serverSideBlockDefinitions[ blockName ].apiVersion ===
undefined &&
definitions[ blockName ].apiVersion
) {
serverSideBlockDefinitions[ blockName ].apiVersion =
definitions[ blockName ].apiVersion;
}
continue;
}
serverSideBlockDefinitions[ blockName ] = mapKeys(
pickBy( definitions[ blockName ], ( value ) => ! isNil( value ) ),
( value, key ) => camelCase( key )
);
}
}
/**
* 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.
*
* @param {string} name Block name.
* @param {Object} settings Block settings.
*
* @return {?WPBlock} The block, if it has been successfully registered;
* otherwise `undefined`.
*/
export function registerBlockType( name, settings ) {
settings = {
name,
icon: blockDefault,
keywords: [],
attributes: {},
providesContext: {},
usesContext: [],
supports: {},
styles: [],
save: () => null,
...serverSideBlockDefinitions?.[ name ],
...settings,
};
if ( typeof name !== 'string' ) {
console.error( 'Block names must be strings.' );
return;
}
if ( ! /^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/.test( name ) ) {
console.error(
'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 ) ) {
console.error( 'Block "' + name + '" is already registered.' );
return;
}
const preFilterSettings = { ...settings };
settings = applyFilters( 'blocks.registerBlockType', settings, name );
if ( settings.deprecated ) {
settings.deprecated = settings.deprecated.map( ( deprecation ) =>
pick(
// Only keep valid deprecation keys.
applyFilters(
'blocks.registerBlockType',
// Merge deprecation keys with pre-filter settings
// so that filters that depend on specific keys being
// present don't fail.
{
// Omit deprecation keys here so that deprecations
// can opt out of specific keys like "supports".
...omit( preFilterSettings, DEPRECATED_ENTRY_KEYS ),
...deprecation,
},
name
),
DEPRECATED_ENTRY_KEYS
)
);
}
if ( ! isPlainObject( settings ) ) {
console.error( 'Block settings must be a valid object.' );
return;
}
if ( ! isFunction( settings.save ) ) {
console.error( 'The "save" property must be a valid function.' );
return;
}
if ( 'edit' in settings && ! isFunction( settings.edit ) ) {
console.error( 'The "edit" property must be a valid function.' );
return;
}
// Canonicalize legacy categories to equivalent fallback.
if ( LEGACY_CATEGORY_MAPPING.hasOwnProperty( settings.category ) ) {
settings.category = LEGACY_CATEGORY_MAPPING[ settings.category ];
}
if (
'category' in settings &&
! some( select( blocksStore ).getCategories(), {
slug: settings.category,
} )
) {
console.warn(
'The block "' +
name +
'" is registered with an invalid category "' +
settings.category +
'".'
);
delete settings.category;
}
if ( ! ( 'title' in settings ) || settings.title === '' ) {
console.error( 'The block "' + name + '" must have a title.' );
return;
}
if ( typeof settings.title !== 'string' ) {
console.error( 'Block titles must be strings.' );
return;
}
settings.icon = normalizeIconObject( settings.icon );
if ( ! isValidIcon( settings.icon.src ) ) {
console.error(
'The icon passed is invalid. ' +
'The icon should be a string, an element, a function, or an object following the specifications documented in https://developer.wordpress.org/block-editor/developers/block-api/block-registration/#icon-optional'
);
return;
}
dispatch( blocksStore ).addBlockTypes( settings );
return settings;
}
/**
* Translates block settings provided with metadata using the i18n schema.
*
* @param {string|string[]|Object[]} i18nSchema I18n schema for the block setting.
* @param {string|string[]|Object[]} settingValue Value for the block setting.
* @param {string} textdomain Textdomain to use with translations.
*
* @return {string|string[]|Object[]} Translated setting.
*/
function translateBlockSettingUsingI18nSchema(
i18nSchema,
settingValue,
textdomain
) {
if ( isString( i18nSchema ) && isString( settingValue ) ) {
// eslint-disable-next-line @wordpress/i18n-no-variables, @wordpress/i18n-text-domain
return _x( settingValue, i18nSchema, textdomain );
}
if (
isArray( i18nSchema ) &&
! isEmpty( i18nSchema ) &&
isArray( settingValue )
) {
return settingValue.map( ( value ) =>
translateBlockSettingUsingI18nSchema(
i18nSchema[ 0 ],
value,
textdomain
)
);
}
if (
isObject( i18nSchema ) &&
! isEmpty( i18nSchema ) &&
isObject( settingValue )
) {
return Object.keys( settingValue ).reduce( ( accumulator, key ) => {
if ( ! i18nSchema[ key ] ) {
accumulator[ key ] = settingValue[ key ];
return accumulator;
}
accumulator[ key ] = translateBlockSettingUsingI18nSchema(
i18nSchema[ key ],
settingValue[ key ],
textdomain
);
return accumulator;
}, {} );
}
return settingValue;
}
/**
* Registers a new block provided from metadata stored in `block.json` file.
* It uses `registerBlockType` internally.
*
* @see registerBlockType
*
* @param {Object} metadata Block metadata loaded from `block.json`.
* @param {string} metadata.name Block name.
* @param {string} metadata.textdomain Textdomain to use with translations.
* @param {Object} additionalSettings Additional block settings.
*
* @return {?WPBlock} The block, if it has been successfully registered;
* otherwise `undefined`.
*/
export function registerBlockTypeFromMetadata(
{ name, textdomain, ...metadata },
additionalSettings
) {
const allowedFields = [
'apiVersion',
'title',
'category',
'parent',
'icon',
'description',
'keywords',
'attributes',
'providesContext',
'usesContext',
'supports',
'styles',
'example',
'variations',
];
const settings = pick( metadata, allowedFields );
if ( textdomain ) {
Object.keys( i18nBlockSchema ).forEach( ( key ) => {
if ( ! settings[ key ] ) {
return;
}
settings[ key ] = translateBlockSettingUsingI18nSchema(
i18nBlockSchema[ key ],
settings[ key ],
textdomain
);
} );
}
unstable__bootstrapServerSideBlockDefinitions( {
[ name ]: settings,
} );
return registerBlockType( name, additionalSettings );
}
/**
* Registers a new block collection to group blocks in the same namespace in the inserter.
*
* @param {string} namespace The namespace to group blocks by in the inserter; corresponds to the block namespace.
* @param {Object} settings The block collection settings.
* @param {string} settings.title The title to display in the block inserter.
* @param {Object} [settings.icon] The icon to display in the block inserter.
*/
export function registerBlockCollection( namespace, { title, icon } ) {
dispatch( blocksStore ).addBlockCollection( namespace, title, icon );
}
/**
* Unregisters a block collection
*
* @param {string} namespace The namespace to group blocks by in the inserter; corresponds to the block namespace
*
*/
export function unregisterBlockCollection( namespace ) {
dispatch( blocksStore ).removeBlockCollection( namespace );
}
/**
* Unregisters a block.
*
* @param {string} name Block name.
*
* @return {?WPBlock} The previous block value, if it has been successfully
* unregistered; otherwise `undefined`.
*/
export function unregisterBlockType( name ) {
const oldBlock = select( blocksStore ).getBlockType( name );
if ( ! oldBlock ) {
console.error( 'Block "' + name + '" is not registered.' );
return;
}
dispatch( blocksStore ).removeBlockTypes( name );
return oldBlock;
}
/**
* Assigns name of block for handling non-block content.
*
* @param {string} blockName Block name.
*/
export function setFreeformContentHandlerName( blockName ) {
dispatch( blocksStore ).setFreeformFallbackBlockName( blockName );
}
/**
* Retrieves name of block handling non-block content, or undefined if no
* handler has been defined.
*
* @return {?string} Block name.
*/
export function getFreeformContentHandlerName() {
return select( blocksStore ).getFreeformFallbackBlockName();
}
/**
* Retrieves name of block used for handling grouping interactions.
*
* @return {?string} Block name.
*/
export function getGroupingBlockName() {
return select( blocksStore ).getGroupingBlockName();
}
/**
* Assigns name of block handling unregistered block types.
*
* @param {string} blockName Block name.
*/
export function setUnregisteredTypeHandlerName( blockName ) {
dispatch( blocksStore ).setUnregisteredFallbackBlockName( blockName );
}
/**
* Retrieves name of block handling unregistered block types, or undefined if no
* handler has been defined.
*
* @return {?string} Block name.
*/
export function getUnregisteredTypeHandlerName() {
return select( blocksStore ).getUnregisteredFallbackBlockName();
}
/**
* Assigns the default block name.
*
* @param {string} name Block name.
*/
export function setDefaultBlockName( name ) {
dispatch( blocksStore ).setDefaultBlockName( name );
}
/**
* Assigns name of block for handling block grouping interactions.
*
* @param {string} name Block name.
*/
export function setGroupingBlockName( name ) {
dispatch( blocksStore ).setGroupingBlockName( name );
}
/**
* Retrieves the default block name.
*
* @return {?string} Block name.
*/
export function getDefaultBlockName() {
return select( blocksStore ).getDefaultBlockName();
}
/**
* Returns a registered block type.
*
* @param {string} name Block name.
*
* @return {?Object} Block type.
*/
export function getBlockType( name ) {
return select( blocksStore ).getBlockType( name );
}
/**
* Returns all registered blocks.
*
* @return {Array} Block settings.
*/
export function getBlockTypes() {
return select( blocksStore ).getBlockTypes();
}
/**
* Returns the block support value for a feature, if defined.
*
* @param {(string|Object)} nameOrType Block name or type object
* @param {string} feature Feature to retrieve
* @param {*} defaultSupports Default value to return if not
* explicitly defined
*
* @return {?*} Block support value
*/
export function getBlockSupport( nameOrType, feature, defaultSupports ) {
return select( blocksStore ).getBlockSupport(
nameOrType,
feature,
defaultSupports
);
}
/**
* Returns true if the block defines support for a feature, or false otherwise.
*
* @param {(string|Object)} nameOrType Block name or type object.
* @param {string} feature Feature to test.
* @param {boolean} defaultSupports Whether feature is supported by
* default if not explicitly defined.
*
* @return {boolean} Whether block supports feature.
*/
export function hasBlockSupport( nameOrType, feature, defaultSupports ) {
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 {Object} blockOrType Block or Block Type to test.
*
* @return {boolean} Whether the given block is a reusable block.
*/
export function isReusableBlock( blockOrType ) {
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 {Object} blockOrType Block or Block Type to test.
*
* @return {boolean} Whether the given block is a template part.
*/
export function isTemplatePart( blockOrType ) {
return blockOrType.name === 'core/template-part';
}
/**
* Returns an array with the child blocks of a given block.
*
* @param {string} blockName Name of block (example: “latest-posts”).
*
* @return {Array} Array of child block names.
*/
export const getChildBlockNames = ( blockName ) => {
return select( blocksStore ).getChildBlockNames( blockName );
};
/**
* Returns a boolean indicating if a block has child blocks or not.
*
* @param {string} blockName Name of block (example: “latest-posts”).
*
* @return {boolean} True if a block contains child blocks and false otherwise.
*/
export const hasChildBlocks = ( blockName ) => {
return select( blocksStore ).hasChildBlocks( blockName );
};
/**
* Returns a boolean indicating if a block has at least one child block with inserter support.
*
* @param {string} blockName Block type name.
*
* @return {boolean} True if a block contains at least one child blocks with inserter support
* and false otherwise.
*/
export const hasChildBlocksWithInserterSupport = ( blockName ) => {
return select( blocksStore ).hasChildBlocksWithInserterSupport( blockName );
};
/**
* Registers a new block style variation for the given block.
*
* @param {string} blockName Name of block (example: “core/latest-posts”).
* @param {Object} styleVariation Object containing `name` which is the class name applied to the block and `label` which identifies the variation to the user.
*/
export const registerBlockStyle = ( blockName, styleVariation ) => {
dispatch( blocksStore ).addBlockStyles( blockName, styleVariation );
};
/**
* Unregisters a block style variation for the given block.
*
* @param {string} blockName Name of block (example: “core/latest-posts”).
* @param {string} styleVariationName Name of class applied to the block.
*/
export const unregisterBlockStyle = ( blockName, styleVariationName ) => {
dispatch( blocksStore ).removeBlockStyles( blockName, styleVariationName );
};
/**
* Returns an array with the variations of a given block type.
*
* @param {string} blockName Name of block (example: “core/columns”).
* @param {WPBlockVariationScope} [scope] Block variation scope name.
*
* @return {(WPBlockVariation[]|void)} Block variations.
*/
export const getBlockVariations = ( blockName, scope ) => {
return select( blocksStore ).getBlockVariations( blockName, scope );
};
/**
* Registers a new block variation for the given block type.
*
* @param {string} blockName Name of the block (example: “core/columns”).
* @param {WPBlockVariation} variation Object describing a block variation.
*/
export const registerBlockVariation = ( blockName, variation ) => {
dispatch( blocksStore ).addBlockVariations( blockName, variation );
};
/**
* Unregisters a block variation defined for the given block type.
*
* @param {string} blockName Name of the block (example: “core/columns”).
* @param {string} variationName Name of the variation defined for the block.
*/
export const unregisterBlockVariation = ( blockName, variationName ) => {
dispatch( blocksStore ).removeBlockVariations( blockName, variationName );
};