@wordpress/blocks
Version:
Block API for WordPress.
456 lines (419 loc) • 16.5 kB
JavaScript
/**
* External dependencies
*/
import { v4 as uuid } from 'uuid';
/**
* WordPress dependencies
*/
import { createHooks, applyFilters } from '@wordpress/hooks';
/**
* Internal dependencies
*/
import { getBlockType, getBlockTypes, getGroupingBlockName } from './registration';
import { isBlockRegistered, normalizeBlockType, __experimentalSanitizeBlockAttributes } from './utils';
/**
* Returns a block object given its type and attributes.
*
* @param {string} name Block name.
* @param {Object} attributes Block attributes.
* @param {?Array} innerBlocks Nested blocks.
*
* @return {Object} Block object.
*/
export function createBlock(name, attributes = {}, innerBlocks = []) {
if (!isBlockRegistered(name)) {
return createBlock('core/missing', {
originalName: name,
originalContent: '',
originalUndelimitedContent: ''
});
}
const sanitizedAttributes = __experimentalSanitizeBlockAttributes(name, attributes);
const clientId = uuid();
// Blocks are stored with a unique ID, the assigned type name, the block
// attributes, and their inner blocks.
return {
clientId,
name,
isValid: true,
attributes: sanitizedAttributes,
innerBlocks
};
}
/**
* Given an array of InnerBlocks templates or Block Objects,
* returns an array of created Blocks from them.
* It handles the case of having InnerBlocks as Blocks by
* converting them to the proper format to continue recursively.
*
* @param {Array} innerBlocksOrTemplate Nested blocks or InnerBlocks templates.
*
* @return {Object[]} Array of Block objects.
*/
export function createBlocksFromInnerBlocksTemplate(innerBlocksOrTemplate = []) {
return innerBlocksOrTemplate.map(innerBlock => {
const innerBlockTemplate = Array.isArray(innerBlock) ? innerBlock : [innerBlock.name, innerBlock.attributes, innerBlock.innerBlocks];
const [name, attributes, innerBlocks = []] = innerBlockTemplate;
return createBlock(name, attributes, createBlocksFromInnerBlocksTemplate(innerBlocks));
});
}
/**
* Given a block object, returns a copy of the block object while sanitizing its attributes,
* optionally merging new attributes and/or replacing its inner blocks.
*
* @param {Object} block Block instance.
* @param {Object} mergeAttributes Block attributes.
* @param {?Array} newInnerBlocks Nested blocks.
*
* @return {Object} A cloned block.
*/
export function __experimentalCloneSanitizedBlock(block, mergeAttributes = {}, newInnerBlocks) {
const {
name
} = block;
if (!isBlockRegistered(name)) {
return createBlock('core/missing', {
originalName: name,
originalContent: '',
originalUndelimitedContent: ''
});
}
const clientId = uuid();
const sanitizedAttributes = __experimentalSanitizeBlockAttributes(name, {
...block.attributes,
...mergeAttributes
});
return {
...block,
clientId,
attributes: sanitizedAttributes,
innerBlocks: newInnerBlocks || block.innerBlocks.map(innerBlock => __experimentalCloneSanitizedBlock(innerBlock))
};
}
/**
* Given a block object, returns a copy of the block object,
* optionally merging new attributes and/or replacing its inner blocks.
*
* @param {Object} block Block instance.
* @param {Object} mergeAttributes Block attributes.
* @param {?Array} newInnerBlocks Nested blocks.
*
* @return {Object} A cloned block.
*/
export function cloneBlock(block, mergeAttributes = {}, newInnerBlocks) {
const clientId = uuid();
return {
...block,
clientId,
attributes: {
...block.attributes,
...mergeAttributes
},
innerBlocks: newInnerBlocks || block.innerBlocks.map(innerBlock => cloneBlock(innerBlock))
};
}
/**
* Returns a boolean indicating whether a transform is possible based on
* various bits of context.
*
* @param {Object} transform The transform object to validate.
* @param {string} direction Is this a 'from' or 'to' transform.
* @param {Array} blocks The blocks to transform from.
*
* @return {boolean} Is the transform possible?
*/
const isPossibleTransformForSource = (transform, direction, blocks) => {
if (!blocks.length) {
return false;
}
// If multiple blocks are selected, only multi block transforms
// or wildcard transforms are allowed.
const isMultiBlock = blocks.length > 1;
const firstBlockName = blocks[0].name;
const isValidForMultiBlocks = isWildcardBlockTransform(transform) || !isMultiBlock || transform.isMultiBlock;
if (!isValidForMultiBlocks) {
return false;
}
// Check non-wildcard transforms to ensure that transform is valid
// for a block selection of multiple blocks of different types.
if (!isWildcardBlockTransform(transform) && !blocks.every(block => block.name === firstBlockName)) {
return false;
}
// Only consider 'block' type transforms as valid.
const isBlockType = transform.type === 'block';
if (!isBlockType) {
return false;
}
// Check if the transform's block name matches the source block (or is a wildcard)
// only if this is a transform 'from'.
const sourceBlock = blocks[0];
const hasMatchingName = direction !== 'from' || transform.blocks.indexOf(sourceBlock.name) !== -1 || isWildcardBlockTransform(transform);
if (!hasMatchingName) {
return false;
}
// Don't allow single Grouping blocks to be transformed into
// a Grouping block.
if (!isMultiBlock && direction === 'from' && isContainerGroupBlock(sourceBlock.name) && isContainerGroupBlock(transform.blockName)) {
return false;
}
// If the transform has a `isMatch` function specified, check that it returns true.
if (!maybeCheckTransformIsMatch(transform, blocks)) {
return false;
}
return true;
};
/**
* Returns block types that the 'blocks' can be transformed into, based on
* 'from' transforms on other blocks.
*
* @param {Array} blocks The blocks to transform from.
*
* @return {Array} Block types that the blocks can be transformed into.
*/
const getBlockTypesForPossibleFromTransforms = blocks => {
if (!blocks.length) {
return [];
}
const allBlockTypes = getBlockTypes();
// filter all blocks to find those with a 'from' transform.
const blockTypesWithPossibleFromTransforms = allBlockTypes.filter(blockType => {
const fromTransforms = getBlockTransforms('from', blockType.name);
return !!findTransform(fromTransforms, transform => {
return isPossibleTransformForSource(transform, 'from', blocks);
});
});
return blockTypesWithPossibleFromTransforms;
};
/**
* Returns block types that the 'blocks' can be transformed into, based on
* the source block's own 'to' transforms.
*
* @param {Array} blocks The blocks to transform from.
*
* @return {Array} Block types that the source can be transformed into.
*/
const getBlockTypesForPossibleToTransforms = blocks => {
if (!blocks.length) {
return [];
}
const sourceBlock = blocks[0];
const blockType = getBlockType(sourceBlock.name);
const transformsTo = blockType ? getBlockTransforms('to', blockType.name) : [];
// filter all 'to' transforms to find those that are possible.
const possibleTransforms = transformsTo.filter(transform => {
return transform && isPossibleTransformForSource(transform, 'to', blocks);
});
// Build a list of block names using the possible 'to' transforms.
const blockNames = possibleTransforms.map(transformation => transformation.blocks).flat();
// Map block names to block types.
return blockNames.map(getBlockType);
};
/**
* Determines whether transform is a "block" type
* and if so whether it is a "wildcard" transform
* ie: targets "any" block type
*
* @param {Object} t the Block transform object
*
* @return {boolean} whether transform is a wildcard transform
*/
export const isWildcardBlockTransform = t => t && t.type === 'block' && Array.isArray(t.blocks) && t.blocks.includes('*');
/**
* Determines whether the given Block is the core Block which
* acts as a container Block for other Blocks as part of the
* Grouping mechanics
*
* @param {string} name the name of the Block to test against
*
* @return {boolean} whether or not the Block is the container Block type
*/
export const isContainerGroupBlock = name => name === getGroupingBlockName();
/**
* Returns an array of block types that the set of blocks received as argument
* can be transformed into.
*
* @param {Array} blocks Blocks array.
*
* @return {Array} Block types that the blocks argument can be transformed to.
*/
export function getPossibleBlockTransformations(blocks) {
if (!blocks.length) {
return [];
}
const blockTypesForFromTransforms = getBlockTypesForPossibleFromTransforms(blocks);
const blockTypesForToTransforms = getBlockTypesForPossibleToTransforms(blocks);
return [...new Set([...blockTypesForFromTransforms, ...blockTypesForToTransforms])];
}
/**
* Given an array of transforms, returns the highest-priority transform where
* the predicate function returns a truthy value. A higher-priority transform
* is one with a lower priority value (i.e. first in priority order). Returns
* null if the transforms set is empty or the predicate function returns a
* falsey value for all entries.
*
* @param {Object[]} transforms Transforms to search.
* @param {Function} predicate Function returning true on matching transform.
*
* @return {?Object} Highest-priority transform candidate.
*/
export function findTransform(transforms, predicate) {
// The hooks library already has built-in mechanisms for managing priority
// queue, so leverage via locally-defined instance.
const hooks = createHooks();
for (let i = 0; i < transforms.length; i++) {
const candidate = transforms[i];
if (predicate(candidate)) {
hooks.addFilter('transform', 'transform/' + i.toString(), result => result ? result : candidate, candidate.priority);
}
}
// Filter name is arbitrarily chosen but consistent with above aggregation.
return hooks.applyFilters('transform', null);
}
/**
* Returns normal block transforms for a given transform direction, optionally
* for a specific block by name, or an empty array if there are no transforms.
* If no block name is provided, returns transforms for all blocks. A normal
* transform object includes `blockName` as a property.
*
* @param {string} direction Transform direction ("to", "from").
* @param {string|Object} blockTypeOrName Block type or name.
*
* @return {Array} Block transforms for direction.
*/
export function getBlockTransforms(direction, blockTypeOrName) {
// When retrieving transforms for all block types, recurse into self.
if (blockTypeOrName === undefined) {
return getBlockTypes().map(({
name
}) => getBlockTransforms(direction, name)).flat();
}
// Validate that block type exists and has array of direction.
const blockType = normalizeBlockType(blockTypeOrName);
const {
name: blockName,
transforms
} = blockType || {};
if (!transforms || !Array.isArray(transforms[direction])) {
return [];
}
const usingMobileTransformations = transforms.supportedMobileTransforms && Array.isArray(transforms.supportedMobileTransforms);
const filteredTransforms = usingMobileTransformations ? transforms[direction].filter(t => {
if (t.type === 'raw') {
return true;
}
if (t.type === 'prefix') {
return true;
}
if (!t.blocks || !t.blocks.length) {
return false;
}
if (isWildcardBlockTransform(t)) {
return true;
}
return t.blocks.every(transformBlockName => transforms.supportedMobileTransforms.includes(transformBlockName));
}) : transforms[direction];
// Map transforms to normal form.
return filteredTransforms.map(transform => ({
...transform,
blockName,
usingMobileTransformations
}));
}
/**
* Checks that a given transforms isMatch method passes for given source blocks.
*
* @param {Object} transform A transform object.
* @param {Array} blocks Blocks array.
*
* @return {boolean} True if given blocks are a match for the transform.
*/
function maybeCheckTransformIsMatch(transform, blocks) {
if (typeof transform.isMatch !== 'function') {
return true;
}
const sourceBlock = blocks[0];
const attributes = transform.isMultiBlock ? blocks.map(block => block.attributes) : sourceBlock.attributes;
const block = transform.isMultiBlock ? blocks : sourceBlock;
return transform.isMatch(attributes, block);
}
/**
* Switch one or more blocks into one or more blocks of the new block type.
*
* @param {Array|Object} blocks Blocks array or block object.
* @param {string} name Block name.
*
* @return {?Array} Array of blocks or null.
*/
export function switchToBlockType(blocks, name) {
const blocksArray = Array.isArray(blocks) ? blocks : [blocks];
const isMultiBlock = blocksArray.length > 1;
const firstBlock = blocksArray[0];
const sourceName = firstBlock.name;
// Find the right transformation by giving priority to the "to"
// transformation.
const transformationsFrom = getBlockTransforms('from', name);
const transformationsTo = getBlockTransforms('to', sourceName);
const transformation = findTransform(transformationsTo, t => t.type === 'block' && (isWildcardBlockTransform(t) || t.blocks.indexOf(name) !== -1) && (!isMultiBlock || t.isMultiBlock) && maybeCheckTransformIsMatch(t, blocksArray)) || findTransform(transformationsFrom, t => t.type === 'block' && (isWildcardBlockTransform(t) || t.blocks.indexOf(sourceName) !== -1) && (!isMultiBlock || t.isMultiBlock) && maybeCheckTransformIsMatch(t, blocksArray));
// Stop if there is no valid transformation.
if (!transformation) {
return null;
}
let transformationResults;
if (transformation.isMultiBlock) {
if ('__experimentalConvert' in transformation) {
transformationResults = transformation.__experimentalConvert(blocksArray);
} else {
transformationResults = transformation.transform(blocksArray.map(currentBlock => currentBlock.attributes), blocksArray.map(currentBlock => currentBlock.innerBlocks));
}
} else if ('__experimentalConvert' in transformation) {
transformationResults = transformation.__experimentalConvert(firstBlock);
} else {
transformationResults = transformation.transform(firstBlock.attributes, firstBlock.innerBlocks);
}
// Ensure that the transformation function returned an object or an array
// of objects.
if (transformationResults === null || typeof transformationResults !== 'object') {
return null;
}
// If the transformation function returned a single object, we want to work
// with an array instead.
transformationResults = Array.isArray(transformationResults) ? transformationResults : [transformationResults];
// Ensure that every block object returned by the transformation has a
// valid block type.
if (transformationResults.some(result => !getBlockType(result.name))) {
return null;
}
const hasSwitchedBlock = transformationResults.some(result => result.name === name);
// Ensure that at least one block object returned by the transformation has
// the expected "destination" block type.
if (!hasSwitchedBlock) {
return null;
}
const ret = transformationResults.map((result, index, results) => {
/**
* Filters an individual transform result from block transformation.
* All of the original blocks are passed, since transformations are
* many-to-many, not one-to-one.
*
* @param {Object} transformedBlock The transformed block.
* @param {Object[]} blocks Original blocks transformed.
* @param {Object[]} index Index of the transformed block on the array of results.
* @param {Object[]} results An array all the blocks that resulted from the transformation.
*/
return applyFilters('blocks.switchToBlockType.transformedBlock', result, blocks, index, results);
});
return ret;
}
/**
* Create a block object from the example API.
*
* @param {string} name
* @param {Object} example
*
* @return {Object} block.
*/
export const getBlockFromExample = (name, example) => {
var _example$innerBlocks;
return createBlock(name, example.attributes, ((_example$innerBlocks = example.innerBlocks) !== null && _example$innerBlocks !== void 0 ? _example$innerBlocks : []).map(innerBlock => getBlockFromExample(innerBlock.name, innerBlock)));
};
//# sourceMappingURL=factory.js.map