UNPKG

@wordpress/blocks

Version:
473 lines (383 loc) 16.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createBlock = createBlock; exports.createBlocksFromInnerBlocksTemplate = createBlocksFromInnerBlocksTemplate; exports.__experimentalCloneSanitizedBlock = __experimentalCloneSanitizedBlock; exports.cloneBlock = cloneBlock; exports.getPossibleBlockTransformations = getPossibleBlockTransformations; exports.findTransform = findTransform; exports.getBlockTransforms = getBlockTransforms; exports.switchToBlockType = switchToBlockType; exports.getBlockFromExample = exports.isContainerGroupBlock = exports.isWildcardBlockTransform = void 0; var _uuid = require("uuid"); var _lodash = require("lodash"); var _hooks = require("@wordpress/hooks"); var _registration = require("./registration"); var _utils = require("./utils"); /** * External dependencies */ /** * WordPress dependencies */ /** * Internal dependencies */ /** * 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. */ function createBlock(name, attributes = {}, innerBlocks = []) { const sanitizedAttributes = (0, _utils.__experimentalSanitizeBlockAttributes)(name, attributes); const clientId = (0, _uuid.v4)(); // 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. */ 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. */ function __experimentalCloneSanitizedBlock(block, mergeAttributes = {}, newInnerBlocks) { const clientId = (0, _uuid.v4)(); const sanitizedAttributes = (0, _utils.__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. */ function cloneBlock(block, mergeAttributes = {}, newInnerBlocks) { const clientId = (0, _uuid.v4)(); 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 ((0, _lodash.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 = (0, _lodash.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) && !(0, _lodash.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 = (0, _lodash.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 ((0, _lodash.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 ((0, _lodash.isEmpty)(blocks)) { return []; } const allBlockTypes = (0, _registration.getBlockTypes)(); // filter all blocks to find those with a 'from' transform. const blockTypesWithPossibleFromTransforms = (0, _lodash.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 ((0, _lodash.isEmpty)(blocks)) { return []; } const sourceBlock = (0, _lodash.first)(blocks); const blockType = (0, _registration.getBlockType)(sourceBlock.name); const transformsTo = getBlockTransforms('to', blockType.name); // filter all 'to' transforms to find those that are possible. const possibleTransforms = (0, _lodash.filter)(transformsTo, transform => { return transform && isPossibleTransformForSource(transform, 'to', blocks); }); // Build a list of block names using the possible 'to' transforms. const blockNames = (0, _lodash.flatMap)(possibleTransforms, transformation => transformation.blocks); // Map block names to block types. return blockNames.map(name => (0, _registration.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 */ 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 */ exports.isWildcardBlockTransform = isWildcardBlockTransform; const isContainerGroupBlock = name => name === (0, _registration.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. */ exports.isContainerGroupBlock = isContainerGroupBlock; function getPossibleBlockTransformations(blocks) { if ((0, _lodash.isEmpty)(blocks)) { return []; } const blockTypesForFromTransforms = getBlockTypesForPossibleFromTransforms(blocks); const blockTypesForToTransforms = getBlockTypesForPossibleToTransforms(blocks); return (0, _lodash.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. */ function findTransform(transforms, predicate) { // The hooks library already has built-in mechanisms for managing priority // queue, so leverage via locally-defined instance. const hooks = (0, _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. */ function getBlockTransforms(direction, blockTypeOrName) { // When retrieving transforms for all block types, recurse into self. if (blockTypeOrName === undefined) { return (0, _lodash.flatMap)((0, _registration.getBlockTypes)(), ({ name }) => getBlockTransforms(direction, name)); } // Validate that block type exists and has array of direction. const blockType = (0, _utils.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 ? (0, _lodash.filter)(transforms[direction], t => { if (t.type === 'raw') { return true; } if (!t.blocks || !t.blocks.length) { return false; } if (isWildcardBlockTransform(t)) { return true; } return (0, _lodash.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. */ function switchToBlockType(blocks, name) { const blocksArray = (0, _lodash.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 ((0, _lodash.has)(transformation, '__experimentalConvert')) { transformationResults = transformation.__experimentalConvert(blocksArray); } else { transformationResults = transformation.transform(blocksArray.map(currentBlock => currentBlock.attributes), blocksArray.map(currentBlock => currentBlock.innerBlocks)); } } else if ((0, _lodash.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 (!(0, _lodash.isObjectLike)(transformationResults)) { return null; } // If the transformation function returned a single object, we want to work // with an array instead. transformationResults = (0, _lodash.castArray)(transformationResults); // Ensure that every block object returned by the transformation has a // valid block type. if (transformationResults.some(result => !(0, _registration.getBlockType)(result.name))) { return null; } const firstSwitchedBlock = (0, _lodash.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 (0, _hooks.applyFilters)('blocks.switchToBlockType.transformedBlock', transformedBlock, blocks); }); } /** * Create a block object from the example API. * * @param {string} name * @param {Object} example * * @return {Object} block. */ const getBlockFromExample = (name, example) => { return createBlock(name, example.attributes, (0, _lodash.map)(example.innerBlocks, innerBlock => getBlockFromExample(innerBlock.name, innerBlock))); }; exports.getBlockFromExample = getBlockFromExample; //# sourceMappingURL=factory.js.map