UNPKG

@wordpress/blocks

Version:
596 lines (537 loc) 16 kB
/** * External dependencies */ import { v4 as uuid } from 'uuid'; import { every, castArray, findIndex, isObjectLike, filter, first, flatMap, has, uniq, isFunction, isEmpty, map, } from 'lodash'; /** * WordPress dependencies */ import { createHooks, applyFilters } from '@wordpress/hooks'; /** * Internal dependencies */ import { getBlockType, getBlockTypes, getGroupingBlockName, } from './registration'; import { 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 = [] ) { 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 clientId = uuid(); const sanitizedAttributes = __experimentalSanitizeBlockAttributes( block.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 ( isEmpty( blocks ) ) { return false; } // If multiple blocks are selected, only multi block transforms // or wildcard transforms are allowed. const isMultiBlock = blocks.length > 1; const firstBlockName = first( blocks ).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 ) && ! every( blocks, { 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 = first( blocks ); 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 && isContainerGroupBlock( sourceBlock.name ) && isContainerGroupBlock( transform.blockName ) ) { return false; } // If the transform has a `isMatch` function specified, check that it returns true. if ( isFunction( transform.isMatch ) ) { const attributes = transform.isMultiBlock ? blocks.map( ( block ) => block.attributes ) : sourceBlock.attributes; if ( ! transform.isMatch( attributes ) ) { return false; } } if ( transform.usingMobileTransformations && isWildcardBlockTransform( transform ) && ! isContainerGroupBlock( sourceBlock.name ) ) { 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 ( isEmpty( blocks ) ) { return []; } const allBlockTypes = getBlockTypes(); // filter all blocks to find those with a 'from' transform. const blockTypesWithPossibleFromTransforms = filter( allBlockTypes, ( 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 ( isEmpty( blocks ) ) { return []; } const sourceBlock = first( blocks ); const blockType = getBlockType( sourceBlock.name ); const transformsTo = getBlockTransforms( 'to', blockType.name ); // filter all 'to' transforms to find those that are possible. const possibleTransforms = filter( transformsTo, ( transform ) => { return ( transform && isPossibleTransformForSource( transform, 'to', blocks ) ); } ); // Build a list of block names using the possible 'to' transforms. const blockNames = flatMap( possibleTransforms, ( transformation ) => transformation.blocks ); // Map block names to block types. return blockNames.map( ( name ) => getBlockType( name ) ); }; /** * 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 ( isEmpty( blocks ) ) { return []; } const blockTypesForFromTransforms = getBlockTypesForPossibleFromTransforms( blocks ); const blockTypesForToTransforms = getBlockTypesForPossibleToTransforms( blocks ); return uniq( [ ...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 flatMap( getBlockTypes(), ( { name } ) => getBlockTransforms( direction, name ) ); } // 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 ? filter( transforms[ direction ], ( t ) => { if ( t.type === 'raw' ) { return true; } if ( ! t.blocks || ! t.blocks.length ) { return false; } if ( isWildcardBlockTransform( t ) ) { return true; } return every( t.blocks, ( transformBlockName ) => transforms.supportedMobileTransforms.includes( transformBlockName ) ); } ) : transforms[ direction ]; // Map transforms to normal form. return filteredTransforms.map( ( transform ) => ( { ...transform, blockName, usingMobileTransformations, } ) ); } /** * 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 = castArray( 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 ) ) || findTransform( transformationsFrom, ( t ) => t.type === 'block' && ( isWildcardBlockTransform( t ) || t.blocks.indexOf( sourceName ) !== -1 ) && ( ! isMultiBlock || t.isMultiBlock ) ); // Stop if there is no valid transformation. if ( ! transformation ) { return null; } let transformationResults; if ( transformation.isMultiBlock ) { if ( has( transformation, '__experimentalConvert' ) ) { transformationResults = transformation.__experimentalConvert( blocksArray ); } else { transformationResults = transformation.transform( blocksArray.map( ( currentBlock ) => currentBlock.attributes ), blocksArray.map( ( currentBlock ) => currentBlock.innerBlocks ) ); } } else if ( has( transformation, '__experimentalConvert' ) ) { 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 ( ! isObjectLike( transformationResults ) ) { return null; } // If the transformation function returned a single object, we want to work // with an array instead. transformationResults = castArray( 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 firstSwitchedBlock = findIndex( transformationResults, ( result ) => result.name === name ); // Ensure that at least one block object returned by the transformation has // the expected "destination" block type. if ( firstSwitchedBlock < 0 ) { return null; } return transformationResults.map( ( result, index ) => { const transformedBlock = { ...result, // The first transformed block whose type matches the "destination" // type gets to keep the existing client ID of the first block. clientId: index === firstSwitchedBlock ? firstBlock.clientId : result.clientId, }; /** * 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. */ return applyFilters( 'blocks.switchToBlockType.transformedBlock', transformedBlock, blocks ); } ); } /** * Create a block object from the example API. * * @param {string} name * @param {Object} example * * @return {Object} block. */ export const getBlockFromExample = ( name, example ) => { return createBlock( name, example.attributes, map( example.innerBlocks, ( innerBlock ) => getBlockFromExample( innerBlock.name, innerBlock ) ) ); };