@wordpress/blocks
Version:
Block API for WordPress.
442 lines (393 loc) • 12.6 kB
JavaScript
/**
* External dependencies
*/
import { colord, extend } from 'colord';
import namesPlugin from 'colord/plugins/names';
import a11yPlugin from 'colord/plugins/a11y';
/**
* WordPress dependencies
*/
import { Component, isValidElement } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
import { RichTextData } from '@wordpress/rich-text';
import deprecated from '@wordpress/deprecated';
/**
* Internal dependencies
*/
import { BLOCK_ICON_DEFAULT } from './constants';
import { getBlockType, getDefaultBlockName } from './registration';
extend( [ namesPlugin, a11yPlugin ] );
/**
* Array of icon colors containing a color to be used if the icon color
* was not explicitly set but the icon background color was.
*
* @type {Object}
*/
const ICON_COLORS = [ '#191e23', '#f8f9f9' ];
/**
* Determines whether the block's attributes are equal to the default attributes
* which means the block is unmodified.
*
* @param {WPBlock} block Block Object.
* @param {?string} role Optional role to filter attributes for modification check.
*
* @return {boolean} Whether the block is an unmodified block.
*/
export function isUnmodifiedBlock( block, role ) {
const blockAttributes = getBlockType( block.name )?.attributes ?? {};
// Filter attributes by role if a role is provided.
const attributesToCheck = role
? Object.entries( blockAttributes ).filter( ( [ key, definition ] ) => {
// A special case for the metadata attribute.
// It can include block bindings that serve as a source of content,
// without directly modifying content attributes.
if ( role === 'content' && key === 'metadata' ) {
return true;
}
return (
definition.role === role ||
definition.__experimentalRole === role
);
} )
: Object.entries( blockAttributes );
return attributesToCheck.every( ( [ key, definition ] ) => {
const value = block.attributes[ key ];
// Every attribute that has a default must match the default.
if ( definition.hasOwnProperty( 'default' ) ) {
return value === definition.default;
}
// The rich text type is a bit different from the rest because it
// has an implicit default value of an empty RichTextData instance,
// so check the length of the value.
if ( definition.type === 'rich-text' ) {
return ! value?.length;
}
// Every attribute that doesn't have a default should be undefined.
return value === undefined;
} );
}
/**
* Determines whether the block is a default block and its attributes are equal
* to the default attributes which means the block is unmodified.
*
* @param {WPBlock} block Block Object
* @param {?string} role Optional role to filter attributes for modification check.
*
* @return {boolean} Whether the block is an unmodified default block.
*/
export function isUnmodifiedDefaultBlock( block, role ) {
return (
block.name === getDefaultBlockName() && isUnmodifiedBlock( block, role )
);
}
/**
* Function that checks if the parameter is a valid icon.
*
* @param {*} icon Parameter to be checked.
*
* @return {boolean} True if the parameter is a valid icon and false otherwise.
*/
export function isValidIcon( icon ) {
return (
!! icon &&
( typeof icon === 'string' ||
isValidElement( icon ) ||
typeof icon === 'function' ||
icon instanceof Component )
);
}
/**
* Function that receives an icon as set by the blocks during the registration
* and returns a new icon object that is normalized so we can rely on just on possible icon structure
* in the codebase.
*
* @param {WPBlockTypeIconRender} icon Render behavior of a block type icon;
* one of a Dashicon slug, an element, or a
* component.
*
* @return {WPBlockTypeIconDescriptor} Object describing the icon.
*/
export function normalizeIconObject( icon ) {
icon = icon || BLOCK_ICON_DEFAULT;
if ( isValidIcon( icon ) ) {
return { src: icon };
}
if ( 'background' in icon ) {
const colordBgColor = colord( icon.background );
const getColorContrast = ( iconColor ) =>
colordBgColor.contrast( iconColor );
const maxContrast = Math.max( ...ICON_COLORS.map( getColorContrast ) );
return {
...icon,
foreground: icon.foreground
? icon.foreground
: ICON_COLORS.find(
( iconColor ) =>
getColorContrast( iconColor ) === maxContrast
),
shadowColor: colordBgColor.alpha( 0.3 ).toRgbString(),
};
}
return icon;
}
/**
* Normalizes block type passed as param. When string is passed then
* it converts it to the matching block type object.
* It passes the original object otherwise.
*
* @param {string|Object} blockTypeOrName Block type or name.
*
* @return {?Object} Block type.
*/
export function normalizeBlockType( blockTypeOrName ) {
if ( typeof blockTypeOrName === 'string' ) {
return getBlockType( blockTypeOrName );
}
return blockTypeOrName;
}
/**
* Get the label for the block, usually this is either the block title,
* or the value of the block's `label` function when that's specified.
*
* @param {Object} blockType The block type.
* @param {Object} attributes The values of the block's attributes.
* @param {Object} context The intended use for the label.
*
* @return {string} The block label.
*/
export function getBlockLabel( blockType, attributes, context = 'visual' ) {
const { __experimentalLabel: getLabel, title } = blockType;
const label = getLabel && getLabel( attributes, { context } );
if ( ! label ) {
return title;
}
if ( label.toPlainText ) {
return label.toPlainText();
}
// Strip any HTML (i.e. RichText formatting) before returning.
return stripHTML( label );
}
/**
* Get a label for the block for use by screenreaders, this is more descriptive
* than the visual label and includes the block title and the value of the
* `getLabel` function if it's specified.
*
* @param {?Object} blockType The block type.
* @param {Object} attributes The values of the block's attributes.
* @param {?number} position The position of the block in the block list.
* @param {string} [direction='vertical'] The direction of the block layout.
*
* @return {string} The block label.
*/
export function getAccessibleBlockLabel(
blockType,
attributes,
position,
direction = 'vertical'
) {
// `title` is already localized, `label` is a user-supplied value.
const title = blockType?.title;
const label = blockType
? getBlockLabel( blockType, attributes, 'accessibility' )
: '';
const hasPosition = position !== undefined;
// getBlockLabel returns the block title as a fallback when there's no label,
// if it did return the title, this function needs to avoid adding the
// title twice within the accessible label. Use this `hasLabel` boolean to
// handle that.
const hasLabel = label && label !== title;
if ( hasPosition && direction === 'vertical' ) {
if ( hasLabel ) {
return sprintf(
/* translators: accessibility text. 1: The block title. 2: The block row number. 3: The block label.. */
__( '%1$s Block. Row %2$d. %3$s' ),
title,
position,
label
);
}
return sprintf(
/* translators: accessibility text. 1: The block title. 2: The block row number. */
__( '%1$s Block. Row %2$d' ),
title,
position
);
} else if ( hasPosition && direction === 'horizontal' ) {
if ( hasLabel ) {
return sprintf(
/* translators: accessibility text. 1: The block title. 2: The block column number. 3: The block label.. */
__( '%1$s Block. Column %2$d. %3$s' ),
title,
position,
label
);
}
return sprintf(
/* translators: accessibility text. 1: The block title. 2: The block column number. */
__( '%1$s Block. Column %2$d' ),
title,
position
);
}
if ( hasLabel ) {
return sprintf(
/* translators: accessibility text. 1: The block title. 2: The block label. */
__( '%1$s Block. %2$s' ),
title,
label
);
}
return sprintf(
/* translators: accessibility text. %s: The block title. */
__( '%s Block' ),
title
);
}
export function getDefault( attributeSchema ) {
if ( attributeSchema.default !== undefined ) {
return attributeSchema.default;
}
if ( attributeSchema.type === 'rich-text' ) {
return new RichTextData();
}
}
/**
* Check if a block is registered.
*
* @param {string} name The block's name.
*
* @return {boolean} Whether the block is registered.
*/
export function isBlockRegistered( name ) {
return getBlockType( name ) !== undefined;
}
/**
* Ensure attributes contains only values defined by block type, and merge
* default values for missing attributes.
*
* @param {string} name The block's name.
* @param {Object} attributes The block's attributes.
* @return {Object} The sanitized attributes.
*/
export function __experimentalSanitizeBlockAttributes( name, attributes ) {
// Get the type definition associated with a registered block.
const blockType = getBlockType( name );
if ( undefined === blockType ) {
throw new Error( `Block type '${ name }' is not registered.` );
}
return Object.entries( blockType.attributes ).reduce(
( accumulator, [ key, schema ] ) => {
const value = attributes[ key ];
if ( undefined !== value ) {
if ( schema.type === 'rich-text' ) {
if ( value instanceof RichTextData ) {
accumulator[ key ] = value;
} else if ( typeof value === 'string' ) {
accumulator[ key ] =
RichTextData.fromHTMLString( value );
}
} else if (
schema.type === 'string' &&
value instanceof RichTextData
) {
accumulator[ key ] = value.toHTMLString();
} else {
accumulator[ key ] = value;
}
} else {
const _default = getDefault( schema );
if ( undefined !== _default ) {
accumulator[ key ] = _default;
}
}
if ( [ 'node', 'children' ].indexOf( schema.source ) !== -1 ) {
// Ensure value passed is always an array, which we're expecting in
// the RichText component to handle the deprecated value.
if ( typeof accumulator[ key ] === 'string' ) {
accumulator[ key ] = [ accumulator[ key ] ];
} else if ( ! Array.isArray( accumulator[ key ] ) ) {
accumulator[ key ] = [];
}
}
return accumulator;
},
{}
);
}
/**
* Filter block attributes by `role` and return their names.
*
* @param {string} name Block attribute's name.
* @param {string} role The role of a block attribute.
*
* @return {string[]} The attribute names that have the provided role.
*/
export function getBlockAttributesNamesByRole( name, role ) {
const attributes = getBlockType( name )?.attributes;
if ( ! attributes ) {
return [];
}
const attributesNames = Object.keys( attributes );
if ( ! role ) {
return attributesNames;
}
return attributesNames.filter( ( attributeName ) => {
const attribute = attributes[ attributeName ];
if ( attribute?.role === role ) {
return true;
}
if ( attribute?.__experimentalRole === role ) {
deprecated( '__experimentalRole attribute', {
since: '6.7',
version: '6.8',
alternative: 'role attribute',
hint: `Check the block.json of the ${ name } block.`,
} );
return true;
}
return false;
} );
}
export const __experimentalGetBlockAttributesNamesByRole = ( ...args ) => {
deprecated( '__experimentalGetBlockAttributesNamesByRole', {
since: '6.7',
version: '6.8',
alternative: 'getBlockAttributesNamesByRole',
} );
return getBlockAttributesNamesByRole( ...args );
};
/**
* Checks if a block is a content block by examining its attributes.
* A block is considered a content block if it has at least one attribute
* with a role of 'content'.
*
* @param {string} name The name of the block to check.
* @return {boolean} Whether the block is a content block.
*/
export function isContentBlock( name ) {
const attributes = getBlockType( name )?.attributes;
if ( ! attributes ) {
return false;
}
return !! Object.keys( attributes )?.some( ( attributeKey ) => {
const attribute = attributes[ attributeKey ];
return (
attribute?.role === 'content' ||
attribute?.__experimentalRole === 'content'
);
} );
}
/**
* Return a new object with the specified keys omitted.
*
* @param {Object} object Original object.
* @param {Array} keys Keys to be omitted.
*
* @return {Object} Object with omitted keys.
*/
export function omit( object, keys ) {
return Object.fromEntries(
Object.entries( object ).filter( ( [ key ] ) => ! keys.includes( key ) )
);
}