@wordpress/blocks
Version:
Block API for WordPress.
367 lines (345 loc) • 12.8 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) {
var _getBlockType$attribu;
const blockAttributes = (_getBlockType$attribu = getBlockType(block.name)?.attributes) !== null && _getBlockType$attribu !== void 0 ? _getBlockType$attribu : {};
// 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 blockType = getBlockType(name);
const attributes = blockType?.attributes;
// Not all blocks have attributes but they may support contentRole instead.
const supportsContentRole = blockType?.supports?.contentRole;
if (supportsContentRole) {
return true;
}
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)));
}
//# sourceMappingURL=utils.js.map