From d83c9c83d65bbfd48731615d01e66593a1cd586b Mon Sep 17 00:00:00 2001 From: Doug Wollison Date: Wed, 16 Oct 2024 12:26:59 -0400 Subject: [PATCH] Allow multiple transform per block type - Block transform options now based on valid block transforms, not block types with valid transforms. - Alternative transforms, separate from the default one(s), designated by specifying a name, title, and optional icon. - Includes tranforming directly to variations that opt-in with 'switcher' scope; only listed the default transform for a block is applicable. --- .../block-api/block-transforms.md | 3 + .../block-api/block-variations.md | 1 + .../data/data-core-block-editor.md | 2 + .../block-transformations-menu.js | 59 ++-- .../src/components/block-switcher/index.js | 16 +- .../components/block-switcher/test/index.js | 2 + .../components/use-block-commands/index.js | 17 +- packages/block-editor/src/store/selectors.js | 59 +++- .../block-editor/src/store/test/selectors.js | 52 +++- packages/blocks/README.md | 28 ++ packages/blocks/src/api/factory.js | 271 +++++++++++++++--- packages/blocks/src/api/index.js | 2 + packages/blocks/src/api/registration.js | 2 +- platform-docs/docs/create-block/transforms.md | 3 + 14 files changed, 428 insertions(+), 89 deletions(-) diff --git a/docs/reference-guides/block-api/block-transforms.md b/docs/reference-guides/block-api/block-transforms.md index c2c5ed49d1b19c..3d3358261d1d1c 100644 --- a/docs/reference-guides/block-api/block-transforms.md +++ b/docs/reference-guides/block-api/block-transforms.md @@ -45,6 +45,9 @@ A transformation of type `block` is an object that takes the following parameter - **isMatch** _(function, optional)_: a callback that receives the block attributes as the first argument and the block object as the second argument and should return a boolean. Returning `false` from this function will prevent the transform from being available and displayed as an option to the user. - **isMultiBlock** _(boolean, optional)_: whether the transformation can be applied when multiple blocks are selected. If true, the `transform` function's first parameter will be an array containing each selected block's attributes, and the second an array of each selected block's inner blocks. False by default. - **priority** _(number, optional)_: controls the priority with which a transformation is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://codex.wordpress.org/Plugin_API#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set. +- **name** _(string, optional)_: a unique name for the transformation. If set, this transformation will be listed separately from the highest priority non-titled match. +- **title** _(string, optional)_: a custom title for the transformation, used in place of the block title. Only used if `name` is provided. +- **icon** _(string|Element, optional)_: a [Dashicon](https://developer.wordpress.org/resource/dashicons/) icon slug string, or an SVG WP element. Only used if `name` is provided. **Example: from Paragraph block to Heading block** diff --git a/docs/reference-guides/block-api/block-variations.md b/docs/reference-guides/block-api/block-variations.md index 8a0c6b1dd5bd6c..5c5c761d58c837 100644 --- a/docs/reference-guides/block-api/block-variations.md +++ b/docs/reference-guides/block-api/block-variations.md @@ -40,6 +40,7 @@ A block variation is defined by an object that can contain the following fields: - `block` - Used by blocks to filter specific block variations. `Columns` and `Query` blocks have such variations, which are passed to the [experimental BlockVariationPicker](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/block-variation-picker/README.md) component. This component handles displaying the variations and allows users to choose one of them. - `inserter` - Block variation is shown on the inserter. - `transform` - Block variation is shown in the component for variation transformations. + - `switcher` - Block variation is shown in the block switcher menu. - `isDefault` (optional, type `boolean`) – Defaults to `false`. Indicates whether the current variation is the default one (details below). - `isActive` (optional, type `Function|string[]`) - A function or an array of block attributes that is used to determine if the variation is active when the block is selected. The function accepts `blockAttributes` and `variationAttributes` (details below). diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 956e8dd010581a..cab352b5285336 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -472,6 +472,8 @@ _Properties_ - _name_ `string`: The type of block to create. - _title_ `string`: Title of the item, as it appears in the inserter. - _icon_ `string`: Dashicon for the item, as it appears in the inserter. +- _transform_ `Object`: The block transform config to use. +- _variation_ `string|null`: The specific varaition for the block, if applicable. - _isDisabled_ `boolean`: Whether or not the user should be prevented from inserting this item. - _frecency_ `number`: Heuristic that combines frequency and recency. diff --git a/packages/block-editor/src/components/block-switcher/block-transformations-menu.js b/packages/block-editor/src/components/block-switcher/block-transformations-menu.js index 271b7fabd51731..f4ee9f14d8515f 100644 --- a/packages/block-editor/src/components/block-switcher/block-transformations-menu.js +++ b/packages/block-editor/src/components/block-switcher/block-transformations-menu.js @@ -5,7 +5,7 @@ import { __ } from '@wordpress/i18n'; import { MenuGroup, MenuItem } from '@wordpress/components'; import { getBlockMenuDefaultClassName, - switchToBlockType, + getBlockTransformationResults, } from '@wordpress/blocks'; import { useState, useMemo } from '@wordpress/element'; @@ -39,8 +39,11 @@ function useGroupedTransforms( possibleBlockTransformations ) { ); const groupedPossibleTransforms = possibleBlockTransformations.reduce( ( accumulator, item ) => { - const { name } = item; - if ( priorityTextTranformsNames.includes( name ) ) { + const { name, variation } = item; // only default versions for text transforms + if ( + priorityTextTranformsNames.includes( name ) && + ! variation + ) { accumulator.priorityTextTransformations.push( item ); } else { accumulator.restTransformations.push( item ); @@ -88,8 +91,7 @@ const BlockTransformationsMenu = ( { onSelectVariation, blocks, } ) => { - const [ hoveredTransformItemName, setHoveredTransformItemName ] = - useState(); + const [ hoveredTransformItem, setHoveredTransformItem ] = useState(); const { priorityTextTransformations, restTransformations } = useGroupedTransforms( possibleBlockTransformations ); @@ -101,18 +103,25 @@ const BlockTransformationsMenu = ( { ); + + function getTransformPreview( { name, variation, transform } ) { + return getBlockTransformationResults( + blocks, + name, + transform, + variation + ); + } + return ( <> - { hoveredTransformItemName && ( + { hoveredTransformItem && ( ) } { !! possibleBlockVariationTransformations?.length && ( @@ -126,12 +135,10 @@ const BlockTransformationsMenu = ( { ) } { priorityTextTransformations.map( ( item ) => ( ) ) } { ! hasBothContentTransformations && restTransformItems } @@ -148,34 +155,32 @@ const BlockTransformationsMenu = ( { function RestTransformationItems( { restTransformations, onSelect, - setHoveredTransformItemName, + setHoveredTransformItem, } ) { return restTransformations.map( ( item ) => ( ) ); } -function BlockTranformationItem( { - item, - onSelect, - setHoveredTransformItemName, -} ) { - const { name, icon, title, isDisabled } = item; +function BlockTranformationItem( { item, onSelect, setHoveredTransformItem } ) { + const { name, variation, icon, title, isDisabled, transform } = item; return ( { event.preventDefault(); - onSelect( name ); + onSelect( { name, variation, transform } ); } } disabled={ isDisabled } - onMouseLeave={ () => setHoveredTransformItemName( null ) } - onMouseEnter={ () => setHoveredTransformItemName( name ) } + onMouseLeave={ () => setHoveredTransformItem( null ) } + onMouseEnter={ () => + setHoveredTransformItem( { name, variation, transform } ) + } > { title } diff --git a/packages/block-editor/src/components/block-switcher/index.js b/packages/block-editor/src/components/block-switcher/index.js index 79f33bd30d7537..2143ac0cdae08a 100644 --- a/packages/block-editor/src/components/block-switcher/index.js +++ b/packages/block-editor/src/components/block-switcher/index.js @@ -11,7 +11,7 @@ import { MenuGroup, } from '@wordpress/components'; import { - switchToBlockType, + getBlockTransformationResults, store as blocksStore, isReusableBlock, isTemplatePart, @@ -82,8 +82,14 @@ function BlockSwitcherDropdownMenuContents( { } } // Simple block tranformation based on the `Block Transforms` API. - function onBlockTransform( name ) { - const newBlocks = switchToBlockType( blocks, name ); + function onBlockTransform( { name, variation, transform } ) { + const newBlocks = getBlockTransformationResults( + blocks, + name, + transform, + variation + ); + replaceBlocks( clientIds, newBlocks ); selectForMultipleBlocks( newBlocks ); } @@ -158,8 +164,8 @@ function BlockSwitcherDropdownMenuContents( { blockVariationTransformations } blocks={ blocks } - onSelect={ ( name ) => { - onBlockTransform( name ); + onSelect={ ( transformation ) => { + onBlockTransform( transformation ); onClose(); } } onSelectVariation={ ( name ) => { diff --git a/packages/block-editor/src/components/block-switcher/test/index.js b/packages/block-editor/src/components/block-switcher/test/index.js index bc1b7d1a4a3640..fb7e7d41683de6 100644 --- a/packages/block-editor/src/components/block-switcher/test/index.js +++ b/packages/block-editor/src/components/block-switcher/test/index.js @@ -83,11 +83,13 @@ describe( 'BlockSwitcher', () => { useSelect.mockImplementation( () => ( { possibleBlockTransformations: [ { + id: 'core/heading', name: 'core/heading', title: headingBlockType.title, frecency: 1, }, { + id: 'core/paragraph', name: 'core/paragraph', title: paragraphBlockType.title, frecency: 1, diff --git a/packages/block-editor/src/components/use-block-commands/index.js b/packages/block-editor/src/components/use-block-commands/index.js index c88ec4e5378926..829aec92c23c5f 100644 --- a/packages/block-editor/src/components/use-block-commands/index.js +++ b/packages/block-editor/src/components/use-block-commands/index.js @@ -6,6 +6,7 @@ import { hasBlockSupport, store as blocksStore, switchToBlockType, + getBlockTransformationResults, isTemplatePart, } from '@wordpress/blocks'; import { useSelect, useDispatch } from '@wordpress/data'; @@ -87,8 +88,14 @@ export const useTransformCommands = () => { } // Simple block tranformation based on the `Block Transforms` API. - function onBlockTransform( name ) { - const newBlocks = switchToBlockType( blocks, name ); + function onBlockTransform( { name, variation, transform } ) { + const newBlocks = getBlockTransformationResults( + blocks, + name, + transform, + variation + ); + replaceBlocks( clientIds, newBlocks ); selectForMultipleBlocks( newBlocks ); } @@ -110,14 +117,14 @@ export const useTransformCommands = () => { } const commands = possibleBlockTransformations.map( ( transformation ) => { - const { name, title, icon } = transformation; + const { id, name, variation, title, icon, transform } = transformation; return { - name: 'core/block-editor/transform-to-' + name.replace( '/', '-' ), + name: 'core/block-editor/transform-to-' + id.replace( '/', '-' ), // translators: %s: block title/name. label: sprintf( __( 'Transform to %s' ), title ), icon: , callback: ( { close } ) => { - onBlockTransform( name ); + onBlockTransform( { name, variation, transform } ); close(); }, }; diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 210cd26aeaa954..4daac911bb8874 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -6,7 +6,7 @@ import { getBlockTypes, getBlockVariations, hasBlockSupport, - getPossibleBlockTransformations, + getPossibleTransformationsForBlocks, switchToBlockType, store as blocksStore, } from '@wordpress/blocks'; @@ -2217,6 +2217,8 @@ export const getInserterItems = createRegistrySelector( ( select ) => * @property {string} name The type of block to create. * @property {string} title Title of the item, as it appears in the inserter. * @property {string} icon Dashicon for the item, as it appears in the inserter. + * @property {Object} transform The block transform config to use. + * @property {string|null} variation The specific varaition for the block, if applicable. * @property {boolean} isDisabled Whether or not the user should be prevented from inserting * this item. * @property {number} frecency Heuristic that combines frequency and recency. @@ -2240,19 +2242,56 @@ export const getBlockTransformItems = createSelector( ] ) ); - const possibleTransforms = getPossibleBlockTransformations( + const possibleTransforms = getPossibleTransformationsForBlocks( normalizedBlocks - ).reduce( ( accumulator, block ) => { - if ( itemsByName[ block?.name ] ) { - accumulator.push( itemsByName[ block.name ] ); + ).reduce( ( accumulator, { blockName, transform } ) => { + if ( itemsByName[ blockName ] ) { + const { title, icon, ...item } = itemsByName[ blockName ]; + + let id = blockName; + if ( transform.name ) { + id += `/${ transform.name }`; + } + + accumulator.push( { + ...item, + id, + // Use transform title/icon if set + title: transform.title ?? title, + icon: transform.icon ?? icon, + transform, + variation: null, + } ); + + // Add variations if it's the generic transform + if ( ! transform.name ) { + // Get switcher-scoped variations, add them as well + const transformVariations = getBlockVariations( + blockName, + 'switcher' + ); + for ( const variation of transformVariations ) { + if ( variation.isDefault ) { + continue; + } + + accumulator.push( { + ...item, + id: `${ item.id }/${ variation.name }`, + title: variation.title, + // Use block icon if variation has none + icon: variation.icon ?? icon, + transform, + // Flag the variation to use when selected + variation: variation.name, + } ); + } + } } return accumulator; }, [] ); - return orderBy( - possibleTransforms, - ( block ) => itemsByName[ block.name ].frecency, - 'desc' - ); + + return orderBy( possibleTransforms, ( item ) => item.frecency, 'desc' ); }, ( state, blocks, rootClientId ) => [ getBlockTypes(), diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 00aa085f667093..1bdaa15b7500f6 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -3546,6 +3546,13 @@ describe( 'selectors', () => { blocks: [ 'core/with-tranforms-b' ], transform: () => {}, }, + { + type: 'block', + name: 'alt-transform-a2b', + title: 'Transforms b Alternative', + blocks: [ 'core/with-tranforms-b' ], + transform: () => {}, + }, { type: 'block', blocks: [ 'core/with-tranforms-c' ], @@ -3562,6 +3569,20 @@ describe( 'selectors', () => { }, ], }, + variations: [ + { + name: 'variation-transform', + title: 'Transform Variation', + attributes: {}, + scope: [ 'transform' ], + }, + { + name: 'variation-switcher', + title: 'Switcher Variation', + attributes: {}, + scope: [ 'switcher' ], + }, + ], } ); registerBlockType( 'core/with-tranforms-b', { category: 'text', @@ -3618,27 +3639,35 @@ describe( 'selectors', () => { }; const blocks = [ { name: 'core/with-tranforms-a' } ]; const items = getBlockTransformItems( state, blocks ); - expect( items ).toHaveLength( 2 ); + expect( items ).toHaveLength( 3 ); const returnedProps = Object.keys( items[ 0 ] ); // Verify we have only the wanted props. - expect( returnedProps ).toHaveLength( 6 ); + expect( returnedProps ).toHaveLength( 8 ); expect( returnedProps ).toEqual( expect.arrayContaining( [ 'id', 'name', + 'variation', 'title', 'icon', 'frecency', 'isDisabled', + 'transform', ] ) ); expect( items ).toEqual( expect.arrayContaining( [ expect.objectContaining( { name: 'core/with-tranforms-b', + title: 'Tranforms b', } ), expect.objectContaining( { name: 'core/with-tranforms-c', + title: 'Tranforms c', + } ), + expect.objectContaining( { + name: 'core/with-tranforms-b', + title: 'Transforms b Alternative', } ), ] ) ); @@ -3659,7 +3688,7 @@ describe( 'selectors', () => { }; const block = { name: 'core/with-tranforms-a' }; const items = getBlockTransformItems( state, block ); - expect( items ).toHaveLength( 2 ); + expect( items ).toHaveLength( 3 ); } ); it( 'should return only eligible blocks for transformation - `allowedBlocks`', () => { const state = { @@ -3746,17 +3775,24 @@ describe( 'selectors', () => { }; const blocks = [ { name: 'core/with-tranforms-a' } ]; const items = getBlockTransformItems( state, blocks ); - expect( items ).toHaveLength( 2 ); + expect( items ).toHaveLength( 3 ); expect( items ).toEqual( expect.arrayContaining( [ expect.objectContaining( { name: 'core/with-tranforms-b', + title: 'Tranforms b', isDisabled: false, } ), expect.objectContaining( { name: 'core/with-tranforms-c', + title: 'Tranforms c', isDisabled: true, } ), + expect.objectContaining( { + name: 'core/with-tranforms-b', + title: 'Transforms b Alternative', + isDisabled: false, + } ), ] ) ); } ); @@ -3780,10 +3816,16 @@ describe( 'selectors', () => { }; const blocks = [ { name: 'core/with-tranforms-c' } ]; const items = getBlockTransformItems( state, blocks ); - expect( items ).toHaveLength( 1 ); + expect( items ).toHaveLength( 2 ); expect( items[ 0 ] ).toEqual( expect.objectContaining( { name: 'core/with-tranforms-a', + variation: null, + frecency: 2.5, + } ), + expect.objectContaining( { + name: 'core/with-tranforms-a', + variation: 'variation-switcher', frecency: 2.5, } ) ); diff --git a/packages/blocks/README.md b/packages/blocks/README.md index f4805e1c60b381..7242b2cad972a8 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -206,6 +206,21 @@ _Returns_ - `?*`: Block support value +### getBlockTransformationResults + +Get the results of a specific transformation of the new block type. + +_Parameters_ + +- _blocks_ `Array|Object`: Blocks array or block object. +- _name_ `string`: Block name. +- _transformation_ `Object`: The specific transformation to use. +- _variation_ `?string`: Optional variation of new block type to apply. + +_Returns_ + +- `?Array`: Array of blocks or null. + ### getBlockTransforms 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. @@ -291,6 +306,18 @@ _Returns_ - `Array`: Block types that the blocks argument can be transformed to. +### getPossibleTransformationsForBlocks + +Returns an array of transformations that the set of blocks received as argument can be transformed into. + +_Parameters_ + +- _blocks_ `Array`: Blocks array. + +_Returns_ + +- `Array`: Block transformations that the blocks argument can be used with. + ### getSaveContent Given a block type containing a save render implementation and attributes, returns the static markup to be saved. @@ -835,6 +862,7 @@ _Parameters_ - _blocks_ `Array|Object`: Blocks array or block object. - _name_ `string`: Block name. +- _variation_ `?string`: Optional variation of new block type. _Returns_ diff --git a/packages/blocks/src/api/factory.js b/packages/blocks/src/api/factory.js index 25bf64ca65dc90..4336d218edc065 100644 --- a/packages/blocks/src/api/factory.js +++ b/packages/blocks/src/api/factory.js @@ -14,6 +14,7 @@ import { createHooks, applyFilters } from '@wordpress/hooks'; import { getBlockType, getBlockTypes, + getBlockVariations, getGroupingBlockName, } from './registration'; import { @@ -281,6 +282,124 @@ const getBlockTypesForPossibleToTransforms = ( blocks ) => { return blockNames.map( getBlockType ); }; +/** + * Returns transformations that the 'blocks' are valid for, based on + * 'from' transforms on other blocks. + * + * @param {Array} blocks The blocks to transform from. + * + * @return {Array} Transformations that the blocks can be transformed into. + */ +const getPossibleFromTransformations = ( blocks ) => { + if ( ! blocks.length ) { + return []; + } + + const allBlockTypes = getBlockTypes(); + + const possibleFromTransformations = allBlockTypes + .map( ( blockType ) => { + const fromTransforms = getBlockTransforms( 'from', blockType.name ); + + // Get the first valid generic transform + const genericTransform = findTransform( + fromTransforms, + ( transform ) => { + return ( + ! transform.name && + isPossibleTransformForSource( + transform, + 'from', + blocks + ) + ); + } + ); + + // Get the valid named transformations + const namedTransformations = fromTransforms + .filter( ( transform ) => { + return ( + !! transform.name && + isPossibleTransformForSource( + transform, + 'from', + blocks + ) + ); + } ) + .map( ( transform ) => { + return { + blockName: blockType.name, + transform, + }; + } ); + + if ( genericTransform ) { + return [ + { + blockName: blockType.name, + transform: genericTransform, + }, + ...namedTransformations, + ]; + } + + return namedTransformations; + } ) + .flat(); + + return possibleFromTransformations; +}; + +/** + * Returns transformations that the 'blocks' are valid for, based on + * the source block's own 'to' transforms. + * + * @param {Array} blocks The blocks to transform from. + * + * @return {Array} Transformations that the source can be transformed into. + */ +const getPossibleToTransformations = ( 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 ) + ); + } ); + + // Compile the generic transformations (1 per block type) + // and the named transformations (as many as exist) + const genericTransformations = new Map(); + const namedTransformations = []; + for ( const transform of possibleTransforms ) { + const blockName = transform.blocks[ 0 ]; + if ( transform.name ) { + namedTransformations.push( { + blockName, + transform, + } ); + } else if ( ! genericTransformations.has( blockName ) ) { + genericTransformations.set( blockName, { + blockName, + transform, + } ); + } + } + + return [ ...genericTransformations.values(), ...namedTransformations ]; +}; + /** * Determines whether transform is a "block" type * and if so whether it is a "wildcard" transform @@ -334,6 +453,37 @@ export function getPossibleBlockTransformations( blocks ) { ]; } +/** + * Returns an array of transformations that the set of blocks received as argument + * can be transformed into. + * + * @param {Array} blocks Blocks array. + * + * @return {Array} Block transformations that the blocks argument can be used with. + */ +export function getPossibleTransformationsForBlocks( blocks ) { + if ( ! blocks.length ) { + return []; + } + + const fromTransformations = getPossibleFromTransformations( blocks ); + const toTransformations = getPossibleToTransformations( blocks ); + + // Get unique transformations by blockName + transformatio name + const uniqueTransformations = new Map( [ + ...fromTransformations.map( ( entry ) => { + const key = entry.blockName + '/' + entry.transform.name; + return [ key, entry ]; + } ), + ...toTransformations.map( ( entry ) => { + const key = entry.blockName + '/' + entry.transform.name; + return [ key, entry ]; + } ), + ] ); + + return [ ...uniqueTransformations.values() ]; +} + /** * Given an array of transforms, returns the highest-priority transform where * the predicate function returns a truthy value. A higher-priority transform @@ -452,48 +602,23 @@ function maybeCheckTransformIsMatch( transform, blocks ) { } /** - * Switch one or more blocks into one or more blocks of the new block type. + * Get the results of a specific transformation of the new block type. * - * @param {Array|Object} blocks Blocks array or block object. - * @param {string} name Block name. + * @param {Array|Object} blocks Blocks array or block object. + * @param {string} name Block name. + * @param {Object} transformation The specific transformation to use. + * @param {?string} variation Optional variation of new block type to apply. * * @return {?Array} Array of blocks or null. */ -export function switchToBlockType( blocks, name ) { +export function getBlockTransformationResults( + blocks, + name, + transformation, + variation +) { 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; @@ -552,6 +677,27 @@ export function switchToBlockType( blocks, name ) { return null; } + // If a variation was specified, apply that variation's attributes to each resulting block + if ( variation ) { + const variationConfig = getBlockVariations( name ).find( + ( config ) => config.name === variation + ); + + if ( variationConfig ) { + transformationResults = transformationResults.map( + ( { attributes, ...result } ) => { + return { + ...result, + attributes: { + ...attributes, + ...variationConfig.attributes, + }, + }; + } + ); + } + } + const ret = transformationResults.map( ( result, index, results ) => { /** * Filters an individual transform result from block transformation. @@ -575,6 +721,59 @@ export function switchToBlockType( blocks, name ) { return ret; } +/** + * 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. + * @param {?string} variation Optional variation of new block type. + * + * @return {?Array} Array of blocks or null. + */ +export function switchToBlockType( blocks, name, variation ) { + 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; + } + + return getBlockTransformationResults( + blocks, + name, + transformation, + variation + ); +} + /** * Create a block object from the example API. * diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index 0b38b8e29e68a0..a73fbb451d3c75 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -16,7 +16,9 @@ export { cloneBlock, __experimentalCloneSanitizedBlock, getPossibleBlockTransformations, + getPossibleTransformationsForBlocks, switchToBlockType, + getBlockTransformationResults, getBlockTransforms, findTransform, getBlockFromExample, diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index ef1b6bd20a4cdd..f08735d59af77c 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -55,7 +55,7 @@ import { unlock } from '../lock-unlock'; /** * Named block variation scopes. * - * @typedef {'block'|'inserter'|'transform'} WPBlockVariationScope + * @typedef {'block'|'inserter'|'transform'|'switcher'} WPBlockVariationScope */ /** diff --git a/platform-docs/docs/create-block/transforms.md b/platform-docs/docs/create-block/transforms.md index fd235b669cd720..eaa85468c022a7 100644 --- a/platform-docs/docs/create-block/transforms.md +++ b/platform-docs/docs/create-block/transforms.md @@ -38,6 +38,9 @@ A transformation of type `block` is an object that takes the following parameter - **isMatch** _(function, optional)_: a callback that receives the block attributes as the first argument and the block object as the second argument and should return a boolean. Returning `false` from this function will prevent the transform from being available and displayed as an option to the user. - **isMultiBlock** _(boolean, optional)_: whether the transformation can be applied when multiple blocks are selected. If `true`, the `transform` function's first parameter will be an array containing each selected block's attributes, and the second an array of each selected block's inner blocks. Returns `false` by default. - **priority** _(number, optional)_: controls the priority with which a transformation is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://codex.wordpress.org/Plugin_API#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set. +- **name** _(string, optional)_: a unique name for the transformation. If set, this transformation will be listed separately from the highest priority non-titled match. +- **title** _(string, optional)_: a custom title for the transformation, used in place of the block title. Only used if `name` is provided. +- **icon** _(string|Element, optional)_: a [Dashicon](https://developer.wordpress.org/resource/dashicons/) icon slug string, or an SVG WP element. Only used if `name` is provided. **Example: Let's declare a transform from our Gutenpride block to Heading block**