From 78cf5674980969dedd3eded85c794f3520ea8816 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Fri, 2 Apr 2021 12:47:39 +0300 Subject: [PATCH 01/13] spin off from #29890 for simple blocks without InnerBlocks --- lib/load.php | 1 + lib/test-block-patterns.php | 204 ++++++++++++++ .../src/components/block-switcher/index.js | 47 +++- .../pattern-transformations-menu.js | 248 ++++++++++++++++++ .../src/components/block-switcher/style.scss | 39 ++- packages/block-editor/src/store/selectors.js | 64 +++++ packages/block-library/src/heading/block.json | 3 +- .../block-library/src/paragraph/block.json | 3 +- packages/blocks/src/api/index.js | 1 + packages/blocks/src/api/utils.js | 20 ++ 10 files changed, 618 insertions(+), 12 deletions(-) create mode 100644 lib/test-block-patterns.php create mode 100644 packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js diff --git a/lib/load.php b/lib/load.php index 2deda54b6646b..90a19ee8c89ae 100644 --- a/lib/load.php +++ b/lib/load.php @@ -107,6 +107,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/blocks.php'; require __DIR__ . '/block-patterns.php'; +require __DIR__ . '/test-block-patterns.php'; require __DIR__ . '/client-assets.php'; require __DIR__ . '/demo.php'; require __DIR__ . '/widgets.php'; diff --git a/lib/test-block-patterns.php b/lib/test-block-patterns.php new file mode 100644 index 0000000000000..162d2e60af419 --- /dev/null +++ b/lib/test-block-patterns.php @@ -0,0 +1,204 @@ + __( 'Paragraph version 1', 'gutenberg' ), + 'scope' => array( + 'inserter' => false, + 'transform' => array( 'core/paragraph' ), + ), + 'content' => ' +

Hello my paragraph!

+ ', + ) +); +register_block_pattern( + 'paragraph/v2', + array( + 'title' => __( 'Paragraph version 2', 'gutenberg' ), + 'scope' => array( + 'inserter' => false, + 'transform' => array( 'core/paragraph' ), + ), + 'content' => ' +

Hello my paragraph!

+ ', + ) +); + +// Multi block transform patterns. +register_block_pattern( + 'multi/v2', + array( + 'title' => __( 'Multi blocks v2 - deep nesting', 'gutenberg' ), + 'scope' => array( + 'inserter' => false, + 'transform' => array( 'core/paragraph', 'core/heading' ), + ), + 'content' => ' +
+ +

2.Which treats of the first sally the ingenious Don Quixote made from home

+ + +
+ +

These preliminaries settled, he did not care to put off any longer the execution of his design, urged on to it by the thought of all the world was losing by his delay, seeing what wrongs he intended to right, grievances to redress, injustices to repair, abuses to remove, and duties to discharge.

+ +
+ + +

Pattern Heading

+ +
+ ', + ) +); +register_block_pattern( + 'multi/v1', + array( + 'title' => __( 'Multi blocks v1', 'gutenberg' ), + 'scope' => array( + 'inserter' => false, + 'transform' => array( 'core/paragraph', 'core/heading' ), + ), + 'content' => ' +
+

2.Which treats of the first sally the ingenious Don Quixote made from home

+ + +

These preliminaries settled, he did not care to put off any longer the execution of his design, urged on to it by the thought of all the world was losing by his delay, seeing what wrongs he intended to right, grievances to redress, injustices to repair, abuses to remove, and duties to discharge.

+
+ ', + ) +); + +// Template Parts Patterns. +// Headers. +register_block_pattern( + 'header/v1', + array( + 'title' => __( 'Header v1', 'gutenberg' ), + 'scope' => array( + 'inserter' => false, + 'transform' => array( 'core/template-part/header' ), + ), + 'content' => ' + + + + +
+ +
+ + +
+ +
+ ', + ) +); +register_block_pattern( + 'header/v2', + array( + 'title' => __( 'Header v2', 'gutenberg' ), + 'scope' => array( + 'inserter' => false, + 'transform' => array( 'core/template-part/header' ), + ), + 'content' => ' + +

This is the Header

+ + +
+
+ + +
+ +
+
+ ', + ) +); +register_block_pattern( + 'footer/v1', + array( + 'title' => __( 'Footer v1', 'gutenberg' ), + 'scope' => array( + 'inserter' => false, + 'transform' => array( 'core/template-part/footer' ), + ), + 'content' => ' + +

This is a Footer

+ + +
+
+

example@example.com
T. +00 (0)1 22 33 44 55

+
+ + +
+

2, Rue Losuis-Boilly
Paris, France

+
+ + +
+ +
+
+', + ) +); + + +// Tests with InnerBlocks like Buttons. +register_block_pattern( + 'buttons/rigas', + array( + 'title' => __( 'Buttons v1', 'gutenberg' ), + 'scope' => array( + 'inserter' => false, + 'transform' => array( 'core/buttons' ), + ), + 'content' => ' +
+
+
+ + + + +
+
+ + +
+
+ + + + +
+
+
+ ', + ) +); diff --git a/packages/block-editor/src/components/block-switcher/index.js b/packages/block-editor/src/components/block-switcher/index.js index ef0d601c495c8..99e5d657cc210 100644 --- a/packages/block-editor/src/components/block-switcher/index.js +++ b/packages/block-editor/src/components/block-switcher/index.js @@ -31,6 +31,7 @@ import BlockIcon from '../block-icon'; import BlockTitle from '../block-title'; import BlockTransformationsMenu from './block-transformations-menu'; import BlockStylesMenu from './block-styles-menu'; +import PatternTransformationsMenu from './pattern-transformations-menu'; export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { const { replaceBlocks } = useDispatch( blockEditorStore ); @@ -40,12 +41,14 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { hasBlockStyles, icon, blockTitle, + patterns, } = useSelect( ( select ) => { - const { getBlockRootClientId, getBlockTransformItems } = select( - blockEditorStore - ); - + const { + getBlockRootClientId, + getBlockTransformItems, + __experimentalGetPatternTransformItems, + } = select( blockEditorStore ); const { getBlockStyles, getBlockType } = select( blocksStore ); const rootClientId = getBlockRootClientId( castArray( clientIds )[ 0 ] @@ -66,7 +69,10 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { ? getBlockType( firstBlockName )?.icon : stack; } - + const _patterns = __experimentalGetPatternTransformItems( + blocks, + rootClientId + ); return { possibleBlockTransformations: getBlockTransformItems( blocks, @@ -75,6 +81,7 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { hasBlockStyles: !! styles?.length, icon: _icon, blockTitle: getBlockType( firstBlockName ).title, + patterns: _patterns, }; }, [ clientIds, blocks, blockInformation?.icon ] @@ -83,9 +90,14 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { const isReusable = blocks.length === 1 && isReusableBlock( blocks[ 0 ] ); const isTemplate = blocks.length === 1 && isTemplatePart( blocks[ 0 ] ); - const onTransform = ( name ) => + // Simple block tranformation based on the `Block Transforms` API. + const onBlockTransform = ( name ) => replaceBlocks( clientIds, switchToBlockType( blocks, name ) ); + // Pattern transformation through the `Patterns` API. + const onPatternTransform = ( transformedBlocks ) => + replaceBlocks( clientIds, transformedBlocks ); const hasPossibleBlockTransformations = !! possibleBlockTransformations.length; + const hasPatternTransformation = !! patterns?.length; if ( ! hasBlockStyles && ! hasPossibleBlockTransformations ) { return ( @@ -114,6 +126,10 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { blocks.length ); + const showDropDown = + hasBlockStyles || + hasPossibleBlockTransformations || + hasPatternTransformation; return ( @@ -147,9 +163,22 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { menuProps={ { orientation: 'both' } } > { ( { onClose } ) => - ( hasBlockStyles || - hasPossibleBlockTransformations ) && ( + showDropDown && (
+ { hasPatternTransformation && ( + { + onPatternTransform( + transformedBlocks + ); + onClose(); + } } + /> + ) } { hasPossibleBlockTransformations && ( { } blocks={ blocks } onSelect={ ( name ) => { - onTransform( name ); + onBlockTransform( name ); onClose(); } } /> diff --git a/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js b/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js new file mode 100644 index 0000000000000..35ca1f7bb3857 --- /dev/null +++ b/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js @@ -0,0 +1,248 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState, useMemo } from '@wordpress/element'; +import { useInstanceId } from '@wordpress/compose'; +import { chevronRight } from '@wordpress/icons'; +import { + cloneBlock, + __experimentalGetBlockAttributesNamesByRole as getBlockAttributesNamesByRole, +} from '@wordpress/blocks'; +import { + MenuGroup, + MenuItem, + Popover, + VisuallyHidden, + __unstableComposite as Composite, + __unstableUseCompositeState as useCompositeState, + __unstableCompositeItem as CompositeItem, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import BlockPreview from '../block-preview'; + +/** + * Find a selected block match in a pattern and return it. + * We return a reference to the block object to mutate it. + * We have first cloned the pattern blocks in a new property + * `transformedBlocks` and we mutate this. + * + * @param {WPBlock} parsedBlock The pattern's parsed block to try to find a match. + * @param {string} selectedBlockName The current selected block's name. + * @param {Set} transformedBlocks A set holding the previously matched blocks. + * + * @return {WPBlock|boolean} The matched block if found or `false`. + */ +// TODO tests +function findMatchingBlockInPattern( + parsedBlock, + selectedBlockName, + transformedBlocks +) { + const { clientId, name, innerBlocks = [] } = parsedBlock; + // Check if parsedBlock has been transformed already. + // This is needed because we loop the selected blocks + // and for example we may have selected two paragraphs and + // the patterns could have more `paragraphs`. + if ( transformedBlocks.has( clientId ) ) return false; + if ( name === selectedBlockName ) { + // We have found a matched block type, so + // add it to the transformed blocks Set and return it. + transformedBlocks.add( clientId ); + return parsedBlock; + } + // Recurse through the inner blocks of a parsed block and + // try to find a matching block. + for ( const innerBlock of innerBlocks ) { + const match = findMatchingBlockInPattern( + innerBlock, + selectedBlockName, + transformedBlocks + ); + if ( match ) return match; + } +} + +function PatternTransformationsMenu( { + blocks, + patterns: statePatterns, + onSelect, +} ) { + const [ showTransforms, setShowTransforms ] = useState( false ); + const patterns = useMemo( () => { + const _patterns = statePatterns.reduce( + ( accumulator, statePattern ) => { + // Clone the parsed pattern's block in `transformedBlocks` + // to mutate this prop. + const pattern = { + ...statePattern, + transformedBlocks: statePattern.blocks.map( ( block ) => + cloneBlock( block ) + ), + }; + const { transformedBlocks: patternBlocks } = pattern; + const transformedBlocksSet = new Set(); + blocks.forEach( ( block ) => { + // Recurse through every pattern block + // to find matches with each selected block, + // and transform these blocks (we mutate patternBlocks). + patternBlocks.forEach( ( patternBlock ) => { + const match = findMatchingBlockInPattern( + patternBlock, + block.name, + transformedBlocksSet + ); + if ( ! match ) return; + // Found a match, so find and retain block attributes + // with `content` role. Everything else comes from the + // pattern's block. If no `content` attributes found, + // update the match with all the selected block's attributes. + const contentAttributes = getBlockAttributesNamesByRole( + block.name, + 'content' + ); + let retainedBlockAttributes = block.attributes; + if ( contentAttributes?.length ) { + retainedBlockAttributes = contentAttributes.reduce( + ( _accumulator, attribute ) => { + if ( block.attributes[ attribute ] ) + _accumulator[ attribute ] = + block.attributes[ attribute ]; + return _accumulator; + }, + {} + ); + } + match.attributes = { + ...match.attributes, + ...retainedBlockAttributes, + }; + // When we have a match with inner blocks keep only the + // blocks from the selected block and skip the inner blocks + // from the pattern. + match.innerBlocks = block.innerBlocks; + } ); + } ); + // If we haven't matched all the selected blocks, don't add + // the pattern to the transformation list. + if ( blocks.length !== transformedBlocksSet.size ) { + return accumulator; + } + // TODO Maybe prioritize first matches with fewer tries to find a match? + accumulator.push( pattern ); + return accumulator; + }, + [] + ); + return _patterns; + }, [ statePatterns ] ); + + if ( ! patterns.length ) return null; + + return ( + + { showTransforms && ( + + ) } + { + event.preventDefault(); + setShowTransforms( ! showTransforms ); + } } + icon={ chevronRight } + > + { __( 'Patterns' ) } + + + ); +} + +function PreviewPatternsPopover( { patterns, onSelect } ) { + return ( +
+
+ +
+
+ { __( 'Preview' ) } +
+ +
+
+
+
+ ); +} + +function BlockPatternsList( { patterns, onSelect } ) { + const composite = useCompositeState(); + return ( + + { patterns.map( ( pattern ) => ( + + ) ) } + + ); +} + +// TODO: This needs to be consolidated to probably be reused across: Patterns in Placeholder, Inserter and here. +function BlockPattern( { pattern, onSelect, composite } ) { + const baseClassName = + 'block-editor-block-switcher__preview-patterns-container'; + const descriptionId = useInstanceId( + BlockPattern, + `${ baseClassName }-list__item-description` + ); + return ( +
+ onSelect( pattern.transformedBlocks ) } + > + +
+ { pattern.title } +
+
+ { !! pattern.description && ( + + { pattern.description } + + ) } +
+ ); +} + +export default PatternTransformationsMenu; diff --git a/packages/block-editor/src/components/block-switcher/style.scss b/packages/block-editor/src/components/block-switcher/style.scss index 1a506d3d084b0..ba74c190b361d 100644 --- a/packages/block-editor/src/components/block-switcher/style.scss +++ b/packages/block-editor/src/components/block-switcher/style.scss @@ -93,7 +93,6 @@ padding: 0; .components-menu-group { - padding: $grid-unit-20; margin: 0; } } @@ -149,6 +148,7 @@ .block-editor-block-switcher__preview { width: 300px; height: auto; + max-height: 500px; padding: $grid-unit-20; } } @@ -182,3 +182,40 @@ } } } + +.block-editor-block-switcher__preview-patterns-container { + .block-editor-block-switcher__preview-patterns-container-list__list-item { + margin-top: $grid-unit-20; + + .block-editor-block-preview__container { + cursor: pointer; + } + + .block-editor-block-switcher__preview-patterns-container-list__item { + height: 100%; + border-radius: $radius-block-ui; + transition: all 0.05s ease-in-out; + position: relative; + border: $border-width solid transparent; + + &:hover, + &:focus { + box-shadow: inset 0 0 0 1px $white, 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + + // Windows High Contrast mode will show this outline, but not the box-shadow. + outline: 2px solid transparent; + } + + &:hover { + box-shadow: inset 0 0 0 1px $white, 0 0 0 var(--wp-admin-border-width-focus) $gray-900; + } + + .block-editor-block-switcher__preview-patterns-container-list__item-title { + padding: $grid-unit-05; + font-size: 12px; + text-align: center; + cursor: pointer; + } + } + } +} diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 3af1ae3ba200b..ce1526eb95fc7 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1874,6 +1874,70 @@ export const __experimentalGetPatternsByBlockTypes = createSelector( ] ); +/** + * Determines the items that appear in the available pattern transforms list. + * There is special handling in two cases: + * 1. For some blocks (`blocksToSkip`) when multiple blocks are selected, + * don't show any transforms, as it doesn't make sense to try to be too smart. + * 2. There are some blocks (`nestedSingleBlocksToHandle`) that makes sense to + * replace everything when they are the only block selected. + * + * For the rest blocks we return a first set of possible eligible block patterns, + * by checking the `scope` Patterns API. We still have to recurse through block + * pattern's blocks and try to find matches from the selected blocks. Now this + * happens in the consumer to avoid heavy operations in the selector. + * + * @param {Object} state Editor state. + * @param {Object[]} blocks The selected blocks. + * @param {?string} rootClientId Optional root client ID of block list. + * + * @return {WPBlockPattern[]} Items that are eligible for a pattern transformation. + */ +// TODO tests +export const __experimentalGetPatternTransformItems = createSelector( + ( state, blocks, rootClientId = null ) => { + if ( ! blocks ) return EMPTY_ARRAY; + /** + * For now we only hanlde blocks without InnerBlocks and take into account + * the `role` property of block's attributes for the transformation. + * Noting that blocks have been retrieved through `getBlock`, that doen't + * return the child inner blocks of an inner block controller, so we still + * need to check for this case too. + */ + if ( + blocks.some( + ( { clientId, innerBlocks } ) => + innerBlocks.length || + areInnerBlocksControlled( state, clientId ) + ) + ) { + return EMPTY_ARRAY; + } + + // Create a Set of the selected block names that is used in patterns filtering. + const selectedBlockNames = Array.from( + new Set( blocks.map( ( { name } ) => name ) ) + ); + /** + * Here we will return first set of possible eligible block patterns, + * by checking the `scope` property. We still have to recurse through + * block pattern's blocks and try to find matches from the selected blocks. + * Now this happens in the consumer to avoid heavy operations in the selector. + */ + return __experimentalGetPatternsByBlockTypes( + state, + selectedBlockNames, + rootClientId + ); + }, + ( state, rootClientId ) => [ + ...__experimentalGetPatternsByBlockTypes.getDependants( + state, + rootClientId + ), + ] +); + /** * Returns the Block List settings of a block, if any exist. * diff --git a/packages/block-library/src/heading/block.json b/packages/block-library/src/heading/block.json index 6cd496431f799..b54ad9860ade1 100644 --- a/packages/block-library/src/heading/block.json +++ b/packages/block-library/src/heading/block.json @@ -10,7 +10,8 @@ "type": "string", "source": "html", "selector": "h1,h2,h3,h4,h5,h6", - "default": "" + "default": "", + "role": "content" }, "level": { "type": "number", diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json index 709eda64eee58..2f95db4ac1c85 100644 --- a/packages/block-library/src/paragraph/block.json +++ b/packages/block-library/src/paragraph/block.json @@ -10,7 +10,8 @@ "type": "string", "source": "html", "selector": "p", - "default": "" + "default": "", + "role": "content" }, "dropCap": { "type": "boolean", diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index 625377c0df3c0..5fdd8aad97bdf 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -141,6 +141,7 @@ export { getBlockLabel as __experimentalGetBlockLabel, getAccessibleBlockLabel as __experimentalGetAccessibleBlockLabel, __experimentalSanitizeBlockAttributes, + __experimentalGetBlockAttributesNamesByRole, } from './utils'; // Templates are, in a general sense, a basic collection of block nodes with any diff --git a/packages/blocks/src/api/utils.js b/packages/blocks/src/api/utils.js index c82515c4e5cd2..5048dc9e276e9 100644 --- a/packages/blocks/src/api/utils.js +++ b/packages/blocks/src/api/utils.js @@ -275,3 +275,23 @@ export function __experimentalSanitizeBlockAttributes( name, attributes ) { {} ); } + +/** + * I created this wrapper to hide the complexity for the consumer.. + * + * @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. + */ +// TODO jsdoc +// TODO tests +export function __experimentalGetBlockAttributesNamesByRole( name, role ) { + const attributes = getBlockType( name )?.attributes; + if ( ! attributes ) return; + const attributesNames = Object.keys( attributes ); + if ( ! role ) return attributesNames; + return attributesNames.filter( + ( attributeName ) => attributes[ attributeName ]?.role === role + ); +} From dace9f31b88342641fa1923aa798c33742591dab Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Fri, 2 Apr 2021 14:01:01 +0300 Subject: [PATCH 02/13] __experimentalGetBlockAttributesNamesByRole tests --- packages/blocks/src/api/test/utils.js | 90 +++++++++++++++++++++++++++ packages/blocks/src/api/utils.js | 6 +- 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/packages/blocks/src/api/test/utils.js b/packages/blocks/src/api/test/utils.js index ea2aa6c214185..6425e70055d24 100644 --- a/packages/blocks/src/api/test/utils.js +++ b/packages/blocks/src/api/test/utils.js @@ -18,6 +18,7 @@ import { getAccessibleBlockLabel, getBlockLabel, __experimentalSanitizeBlockAttributes, + __experimentalGetBlockAttributesNamesByRole, } from '../utils'; describe( 'block helpers', () => { @@ -310,3 +311,92 @@ describe( 'sanitizeBlockAttributes', () => { } ); } ); } ); + +describe( '__experimentalGetBlockAttributesNamesByRole', () => { + beforeAll( () => { + registerBlockType( 'core/test-block-1', { + attributes: { + align: { + type: 'string', + }, + content: { + type: 'boolean', + role: 'content', + }, + level: { + type: 'number', + role: 'content', + }, + color: { + type: 'string', + role: 'other', + }, + }, + save: noop, + category: 'text', + title: 'test block 1', + } ); + registerBlockType( 'core/test-block-2', { + attributes: { + align: { type: 'string' }, + content: { type: 'boolean' }, + color: { type: 'string' }, + }, + save: noop, + category: 'text', + title: 'test block 2', + } ); + registerBlockType( 'core/test-block-3', { + save: noop, + category: 'text', + title: 'test block 3', + } ); + } ); + afterAll( () => { + [ + 'core/test-block-1', + 'core/test-block-2', + 'core/test-block-3', + ].forEach( unregisterBlockType ); + } ); + it( 'should return empty array if block has no attributes', () => { + expect( + __experimentalGetBlockAttributesNamesByRole( 'core/test-block-3' ) + ).toEqual( [] ); + } ); + it( 'should return all attribute names if no role is provided', () => { + const res = __experimentalGetBlockAttributesNamesByRole( + 'core/test-block-1' + ); + expect( res ).toEqual( + expect.arrayContaining( [ 'align', 'content', 'level', 'color' ] ) + ); + } ); + it( 'should return proper results with existing attributes and provided role', () => { + expect( + __experimentalGetBlockAttributesNamesByRole( + 'core/test-block-1', + 'content' + ) + ).toEqual( expect.arrayContaining( [ 'content', 'level' ] ) ); + expect( + __experimentalGetBlockAttributesNamesByRole( + 'core/test-block-1', + 'other' + ) + ).toEqual( [ 'color' ] ); + expect( + __experimentalGetBlockAttributesNamesByRole( + 'core/test-block-1', + 'not-exists' + ) + ).toEqual( [] ); + // A block with no `role` in any attributes. + expect( + __experimentalGetBlockAttributesNamesByRole( + 'core/test-block-2', + 'content' + ) + ).toEqual( [] ); + } ); +} ); diff --git a/packages/blocks/src/api/utils.js b/packages/blocks/src/api/utils.js index 5048dc9e276e9..f2bd2a76a4583 100644 --- a/packages/blocks/src/api/utils.js +++ b/packages/blocks/src/api/utils.js @@ -277,18 +277,16 @@ export function __experimentalSanitizeBlockAttributes( name, attributes ) { } /** - * I created this wrapper to hide the complexity for the consumer.. + * 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. */ -// TODO jsdoc -// TODO tests export function __experimentalGetBlockAttributesNamesByRole( name, role ) { const attributes = getBlockType( name )?.attributes; - if ( ! attributes ) return; + if ( ! attributes ) return []; const attributesNames = Object.keys( attributes ); if ( ! role ) return attributesNames; return attributesNames.filter( From 3bc3284024cb6376d5220506969745b6783a011e Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Wed, 7 Apr 2021 11:41:19 +0300 Subject: [PATCH 03/13] update patterns --- lib/test-block-patterns.php | 156 +++--------------------------------- 1 file changed, 12 insertions(+), 144 deletions(-) diff --git a/lib/test-block-patterns.php b/lib/test-block-patterns.php index 162d2e60af419..ee57b303a12cf 100644 --- a/lib/test-block-patterns.php +++ b/lib/test-block-patterns.php @@ -12,12 +12,9 @@ register_block_pattern( 'paragraph/v1', array( - 'title' => __( 'Paragraph version 1', 'gutenberg' ), - 'scope' => array( - 'inserter' => false, - 'transform' => array( 'core/paragraph' ), - ), - 'content' => ' + 'title' => __( 'Paragraph version 1', 'gutenberg' ), + 'blockTypes' => array( 'core/paragraph' ), + 'content' => '

Hello my paragraph!

', ) @@ -25,12 +22,9 @@ register_block_pattern( 'paragraph/v2', array( - 'title' => __( 'Paragraph version 2', 'gutenberg' ), - 'scope' => array( - 'inserter' => false, - 'transform' => array( 'core/paragraph' ), - ), - 'content' => ' + 'title' => __( 'Paragraph version 2', 'gutenberg' ), + 'blockTypes' => array( 'core/paragraph' ), + 'content' => '

Hello my paragraph!

', ) @@ -40,12 +34,9 @@ register_block_pattern( 'multi/v2', array( - 'title' => __( 'Multi blocks v2 - deep nesting', 'gutenberg' ), - 'scope' => array( - 'inserter' => false, - 'transform' => array( 'core/paragraph', 'core/heading' ), - ), - 'content' => ' + 'title' => __( 'Multi blocks v2 - deep nesting', 'gutenberg' ), + 'blockTypes' => array( 'core/paragraph', 'core/heading' ), + 'content' => '

2.Which treats of the first sally the ingenious Don Quixote made from home

@@ -67,12 +58,9 @@ register_block_pattern( 'multi/v1', array( - 'title' => __( 'Multi blocks v1', 'gutenberg' ), - 'scope' => array( - 'inserter' => false, - 'transform' => array( 'core/paragraph', 'core/heading' ), - ), - 'content' => ' + 'title' => __( 'Multi blocks v1', 'gutenberg' ), + 'blockTypes' => array( 'core/paragraph', 'core/heading' ), + 'content' => '

2.Which treats of the first sally the ingenious Don Quixote made from home

@@ -82,123 +70,3 @@ ', ) ); - -// Template Parts Patterns. -// Headers. -register_block_pattern( - 'header/v1', - array( - 'title' => __( 'Header v1', 'gutenberg' ), - 'scope' => array( - 'inserter' => false, - 'transform' => array( 'core/template-part/header' ), - ), - 'content' => ' - - - - -
- -
- - -
- -
- ', - ) -); -register_block_pattern( - 'header/v2', - array( - 'title' => __( 'Header v2', 'gutenberg' ), - 'scope' => array( - 'inserter' => false, - 'transform' => array( 'core/template-part/header' ), - ), - 'content' => ' - -

This is the Header

- - -
-
- - -
- -
-
- ', - ) -); -register_block_pattern( - 'footer/v1', - array( - 'title' => __( 'Footer v1', 'gutenberg' ), - 'scope' => array( - 'inserter' => false, - 'transform' => array( 'core/template-part/footer' ), - ), - 'content' => ' - -

This is a Footer

- - -
-
-

example@example.com
T. +00 (0)1 22 33 44 55

-
- - -
-

2, Rue Losuis-Boilly
Paris, France

-
- - -
- -
-
-', - ) -); - - -// Tests with InnerBlocks like Buttons. -register_block_pattern( - 'buttons/rigas', - array( - 'title' => __( 'Buttons v1', 'gutenberg' ), - 'scope' => array( - 'inserter' => false, - 'transform' => array( 'core/buttons' ), - ), - 'content' => ' -
-
-
- - - - -
-
- - -
-
- - - - -
-
-
- ', - ) -); From 163b9836a18f4b39ef2a8717357fc7a3fc3a847c Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Wed, 7 Apr 2021 19:31:34 +0300 Subject: [PATCH 04/13] useTransformedPatterns and refactoring part 1 --- .../pattern-transformations-menu.js | 122 +----------------- .../components/block-switcher/test/utils.js | 71 ++++++++++ .../use-transformed-patterns.js | 93 +++++++++++++ .../src/components/block-switcher/utils.js | 62 +++++++++ packages/block-editor/src/store/selectors.js | 2 +- 5 files changed, 233 insertions(+), 117 deletions(-) create mode 100644 packages/block-editor/src/components/block-switcher/test/utils.js create mode 100644 packages/block-editor/src/components/block-switcher/use-transformed-patterns.js create mode 100644 packages/block-editor/src/components/block-switcher/utils.js diff --git a/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js b/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js index 35ca1f7bb3857..88a5e6db2e88b 100644 --- a/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js +++ b/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js @@ -2,13 +2,10 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { useState, useMemo } from '@wordpress/element'; +import { useState } from '@wordpress/element'; import { useInstanceId } from '@wordpress/compose'; import { chevronRight } from '@wordpress/icons'; -import { - cloneBlock, - __experimentalGetBlockAttributesNamesByRole as getBlockAttributesNamesByRole, -} from '@wordpress/blocks'; + import { MenuGroup, MenuItem, @@ -23,48 +20,7 @@ import { * Internal dependencies */ import BlockPreview from '../block-preview'; - -/** - * Find a selected block match in a pattern and return it. - * We return a reference to the block object to mutate it. - * We have first cloned the pattern blocks in a new property - * `transformedBlocks` and we mutate this. - * - * @param {WPBlock} parsedBlock The pattern's parsed block to try to find a match. - * @param {string} selectedBlockName The current selected block's name. - * @param {Set} transformedBlocks A set holding the previously matched blocks. - * - * @return {WPBlock|boolean} The matched block if found or `false`. - */ -// TODO tests -function findMatchingBlockInPattern( - parsedBlock, - selectedBlockName, - transformedBlocks -) { - const { clientId, name, innerBlocks = [] } = parsedBlock; - // Check if parsedBlock has been transformed already. - // This is needed because we loop the selected blocks - // and for example we may have selected two paragraphs and - // the patterns could have more `paragraphs`. - if ( transformedBlocks.has( clientId ) ) return false; - if ( name === selectedBlockName ) { - // We have found a matched block type, so - // add it to the transformed blocks Set and return it. - transformedBlocks.add( clientId ); - return parsedBlock; - } - // Recurse through the inner blocks of a parsed block and - // try to find a matching block. - for ( const innerBlock of innerBlocks ) { - const match = findMatchingBlockInPattern( - innerBlock, - selectedBlockName, - transformedBlocks - ); - if ( match ) return match; - } -} +import useTransformedPatterns from './use-transformed-patterns'; function PatternTransformationsMenu( { blocks, @@ -72,74 +28,7 @@ function PatternTransformationsMenu( { onSelect, } ) { const [ showTransforms, setShowTransforms ] = useState( false ); - const patterns = useMemo( () => { - const _patterns = statePatterns.reduce( - ( accumulator, statePattern ) => { - // Clone the parsed pattern's block in `transformedBlocks` - // to mutate this prop. - const pattern = { - ...statePattern, - transformedBlocks: statePattern.blocks.map( ( block ) => - cloneBlock( block ) - ), - }; - const { transformedBlocks: patternBlocks } = pattern; - const transformedBlocksSet = new Set(); - blocks.forEach( ( block ) => { - // Recurse through every pattern block - // to find matches with each selected block, - // and transform these blocks (we mutate patternBlocks). - patternBlocks.forEach( ( patternBlock ) => { - const match = findMatchingBlockInPattern( - patternBlock, - block.name, - transformedBlocksSet - ); - if ( ! match ) return; - // Found a match, so find and retain block attributes - // with `content` role. Everything else comes from the - // pattern's block. If no `content` attributes found, - // update the match with all the selected block's attributes. - const contentAttributes = getBlockAttributesNamesByRole( - block.name, - 'content' - ); - let retainedBlockAttributes = block.attributes; - if ( contentAttributes?.length ) { - retainedBlockAttributes = contentAttributes.reduce( - ( _accumulator, attribute ) => { - if ( block.attributes[ attribute ] ) - _accumulator[ attribute ] = - block.attributes[ attribute ]; - return _accumulator; - }, - {} - ); - } - match.attributes = { - ...match.attributes, - ...retainedBlockAttributes, - }; - // When we have a match with inner blocks keep only the - // blocks from the selected block and skip the inner blocks - // from the pattern. - match.innerBlocks = block.innerBlocks; - } ); - } ); - // If we haven't matched all the selected blocks, don't add - // the pattern to the transformation list. - if ( blocks.length !== transformedBlocksSet.size ) { - return accumulator; - } - // TODO Maybe prioritize first matches with fewer tries to find a match? - accumulator.push( pattern ); - return accumulator; - }, - [] - ); - return _patterns; - }, [ statePatterns ] ); - + const patterns = useTransformedPatterns( statePatterns, blocks ); if ( ! patterns.length ) return null; return ( @@ -209,6 +98,7 @@ function BlockPatternsList( { patterns, onSelect } ) { // TODO: This needs to be consolidated to probably be reused across: Patterns in Placeholder, Inserter and here. function BlockPattern( { pattern, onSelect, composite } ) { + // TODO check pattern/preview width... const baseClassName = 'block-editor-block-switcher__preview-patterns-container'; const descriptionId = useInstanceId( @@ -230,7 +120,7 @@ function BlockPattern( { pattern, onSelect, composite } ) { >
{ pattern.title } diff --git a/packages/block-editor/src/components/block-switcher/test/utils.js b/packages/block-editor/src/components/block-switcher/test/utils.js new file mode 100644 index 0000000000000..8c574ad2f22fa --- /dev/null +++ b/packages/block-editor/src/components/block-switcher/test/utils.js @@ -0,0 +1,71 @@ +/** + * WordPress dependencies + */ +import { unregisterBlockType, registerBlockType } from '@wordpress/blocks'; +/** + * Internal dependencies + */ +import { + // getMatchingBlockInPattern, + getBlockRetainingAttributes, +} from '../utils'; +describe( 'BlockSwitcher - utils', () => { + describe( 'getBlockRetainingAttributes', () => { + beforeAll( () => { + registerBlockType( 'core/test-block-1', { + attributes: { + align: { + type: 'string', + }, + content: { + type: 'boolean', + role: 'content', + }, + level: { + type: 'number', + role: 'content', + }, + color: { + type: 'string', + role: 'other', + }, + }, + save() {}, + category: 'text', + title: 'test block 1', + } ); + registerBlockType( 'core/test-block-2', { + attributes: { + align: { type: 'string' }, + content: { type: 'boolean' }, + color: { type: 'string' }, + }, + save() {}, + category: 'text', + title: 'test block 2', + } ); + } ); + afterAll( () => { + [ 'core/test-block-1', 'core/test-block-2' ].forEach( + unregisterBlockType + ); + } ); + it( 'should return passed attributes if no `role:content` attributes were found', () => { + const attributes = { align: 'right' }; + const res = getBlockRetainingAttributes( + 'core/test-block-2', + attributes + ); + expect( res ).toEqual( attributes ); + } ); + it( 'should return only the `role:content` attributes that exist in passed attributes', () => { + const attributes = { align: 'right', level: 2 }; + const res = getBlockRetainingAttributes( + 'core/test-block-1', + attributes + ); + expect( res ).toEqual( { level: 2 } ); + } ); + } ); + describe( 'findMatchingBlockInPattern', () => {} ); +} ); diff --git a/packages/block-editor/src/components/block-switcher/use-transformed-patterns.js b/packages/block-editor/src/components/block-switcher/use-transformed-patterns.js new file mode 100644 index 0000000000000..710e6881c075e --- /dev/null +++ b/packages/block-editor/src/components/block-switcher/use-transformed-patterns.js @@ -0,0 +1,93 @@ +/** + * WordPress dependencies + */ +import { useMemo } from '@wordpress/element'; +import { cloneBlock } from '@wordpress/blocks'; +/** + * Internal dependencies + */ +import { + getMatchingBlockInPattern, + getBlockRetainingAttributes, +} from './utils'; + +/** + * + * @param {WPBlock[]} selectedBlocks The selected blocks. + * @param {WPBlock[]} patternBlocks The pattern's blocks. + * @return {WPBlock[]|void} The transformed pattern's blocks or undefined if not all selected blocks have been matched. + */ +// TODO jsdoc +// TODO tests +const getPatternTransformedBlocks = ( selectedBlocks, patternBlocks ) => { + // const eligiblePattern = getEligiblePattern() + // Clone Pattern's blocks in `transformedBlocks` prop, to mutate them. + const _patternBlocks = patternBlocks.map( ( block ) => + cloneBlock( block ) + ); + const consumedBlocks = new Set(); + for ( const selectedBlock of selectedBlocks ) { + let isMatch = false; + for ( const patternBlock of _patternBlocks ) { + const match = getMatchingBlockInPattern( + patternBlock, + selectedBlock.name, + consumedBlocks + ); + if ( ! match ) continue; + isMatch = true; + consumedBlocks.add( match.clientId ); + // We update (mutate) the matching pattern block. + transformMatchingBlock( match, selectedBlock ); + break; + } + // Bail eary if a selected block has not been matched. + if ( ! isMatch ) return; + } + return _patternBlocks; +}; +// here goes the mutation... :) +const transformMatchingBlock = ( match, selectedBlock ) => { + // Get the block attributes to retain through the transformation. + const retainedBlockAttributes = getBlockRetainingAttributes( + selectedBlock.name, + selectedBlock.attributes + ); + match.attributes = { + ...match.attributes, + ...retainedBlockAttributes, + }; +}; + +/** + * Custom hook that accepts patterns from state and the selected + * blocks and tries to match these with the pattern's blocks. + * If all selected blocks are matched with a Pattern's block, + * we transform them by retaining block's attributes with `role:content`. + * The transformed pattern's blocks are set to a new pattern + * property `transformedBlocks`. + * + * @param {WPBlockPattern[]} patterns Patterns from state. + * @param {WPBlock[]} selectedBlocks The currently selected blocks. + * @return {WPBlockPattern[]} Returns the eligible matched patterns with all the selected blocks. + */ +const useTransformedPatterns = ( patterns, selectedBlocks ) => { + return useMemo( () => { + const _patterns = patterns.reduce( ( accumulator, _pattern ) => { + const transformedBlocks = getPatternTransformedBlocks( + selectedBlocks, + _pattern.blocks + ); + if ( transformedBlocks ) { + accumulator.push( { + ..._pattern, + transformedBlocks, + } ); + } + return accumulator; + }, [] ); + return _patterns; + }, [ patterns, selectedBlocks ] ); +}; + +export default useTransformedPatterns; diff --git a/packages/block-editor/src/components/block-switcher/utils.js b/packages/block-editor/src/components/block-switcher/utils.js new file mode 100644 index 0000000000000..bdc717ffce0be --- /dev/null +++ b/packages/block-editor/src/components/block-switcher/utils.js @@ -0,0 +1,62 @@ +/** + * WordPress dependencies + */ +import { __experimentalGetBlockAttributesNamesByRole as getBlockAttributesNamesByRole } from '@wordpress/blocks'; +/** + * Find a selected block match in a pattern and return it. + * We return a reference to the block object to mutate it. + * We have first cloned the pattern blocks in a new property + * `transformedBlocks` and we mutate this. + * + * @param {WPBlock} block The pattern's block to try to find a match. + * @param {string} selectedBlockName The current selected block's name. + * @param {Set} consumedBlocks A set holding the previously matched blocks. + * + * @return {WPBlock?} The matched block if found or `false`. + */ +// TODO tests +export const getMatchingBlockInPattern = ( + block, + selectedBlockName, + consumedBlocks +) => { + const { clientId, name, innerBlocks = [] } = block; + /** + * Check if block has been transformed already. + * This is needed because we loop the selected blocks + * and for example we may have selected two paragraphs and + * the patterns could have more `paragraphs`. + */ + if ( consumedBlocks.has( clientId ) ) return; + if ( name === selectedBlockName ) return block; + // Try to find a matching block from InnerBlocks. + for ( const innerBlock of innerBlocks ) { + const match = getMatchingBlockInPattern( + innerBlock, + selectedBlockName, + consumedBlocks + ); + if ( match ) return match; + } +}; + +/** + * Find and return the block attributes to retain through + * the transformation, based on Block Type's `role:content` + * attributes. If no `role:content` attributes exist, + * return selected block's attributes. + * + * @param {string} name Block type's namespaced name. + * @param {Object} attributes Selected block's attributes. + * @return {Object} The block's attributes to retain. + */ +export const getBlockRetainingAttributes = ( name, attributes ) => { + const contentAttributes = getBlockAttributesNamesByRole( name, 'content' ); + if ( ! contentAttributes?.length ) return attributes; + + return contentAttributes.reduce( ( _accumulator, attribute ) => { + if ( attributes[ attribute ] ) + _accumulator[ attribute ] = attributes[ attribute ]; + return _accumulator; + }, {} ); +}; diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index ce1526eb95fc7..5a55a6151f0ff 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1920,7 +1920,7 @@ export const __experimentalGetPatternTransformItems = createSelector( ); /** * Here we will return first set of possible eligible block patterns, - * by checking the `scope` property. We still have to recurse through + * by checking the `blockTypes` property. We still have to recurse through * block pattern's blocks and try to find matches from the selected blocks. * Now this happens in the consumer to avoid heavy operations in the selector. */ From 4fd4cce3f45ad045dcde85c680521708bfa7f264 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Thu, 8 Apr 2021 11:40:40 +0300 Subject: [PATCH 05/13] getMatchingBlockByName tests + and other jsdoc --- .../pattern-transformations-menu.js | 1 - .../components/block-switcher/test/utils.js | 112 ++++++++++++++++-- .../use-transformed-patterns.js | 53 +++++---- .../src/components/block-switcher/utils.js | 35 +++--- 4 files changed, 151 insertions(+), 50 deletions(-) diff --git a/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js b/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js index 88a5e6db2e88b..765787adb216d 100644 --- a/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js +++ b/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js @@ -96,7 +96,6 @@ function BlockPatternsList( { patterns, onSelect } ) { ); } -// TODO: This needs to be consolidated to probably be reused across: Patterns in Placeholder, Inserter and here. function BlockPattern( { pattern, onSelect, composite } ) { // TODO check pattern/preview width... const baseClassName = diff --git a/packages/block-editor/src/components/block-switcher/test/utils.js b/packages/block-editor/src/components/block-switcher/test/utils.js index 8c574ad2f22fa..eafe5e8a4d937 100644 --- a/packages/block-editor/src/components/block-switcher/test/utils.js +++ b/packages/block-editor/src/components/block-switcher/test/utils.js @@ -2,15 +2,14 @@ * WordPress dependencies */ import { unregisterBlockType, registerBlockType } from '@wordpress/blocks'; + /** * Internal dependencies */ -import { - // getMatchingBlockInPattern, - getBlockRetainingAttributes, -} from '../utils'; +import { getMatchingBlockByName, getRetainedBlockAttributes } from '../utils'; + describe( 'BlockSwitcher - utils', () => { - describe( 'getBlockRetainingAttributes', () => { + describe( 'getRetainedBlockAttributes', () => { beforeAll( () => { registerBlockType( 'core/test-block-1', { attributes: { @@ -52,7 +51,7 @@ describe( 'BlockSwitcher - utils', () => { } ); it( 'should return passed attributes if no `role:content` attributes were found', () => { const attributes = { align: 'right' }; - const res = getBlockRetainingAttributes( + const res = getRetainedBlockAttributes( 'core/test-block-2', attributes ); @@ -60,12 +59,109 @@ describe( 'BlockSwitcher - utils', () => { } ); it( 'should return only the `role:content` attributes that exist in passed attributes', () => { const attributes = { align: 'right', level: 2 }; - const res = getBlockRetainingAttributes( + const res = getRetainedBlockAttributes( 'core/test-block-1', attributes ); expect( res ).toEqual( { level: 2 } ); } ); } ); - describe( 'findMatchingBlockInPattern', () => {} ); + describe( 'getMatchingBlockByName', () => { + it( 'should return nothing if no match is found', () => { + const block = { + clientId: 'client-1', + name: 'test-1', + innerBlocks: [ + { + clientId: 'client-1-1', + name: 'test-1-1', + innerBlocks: [], + }, + ], + }; + const res = getMatchingBlockByName( block, 'not-a-match' ); + expect( res ).toBeUndefined(); + } ); + it( 'should return nothing if provided block has already been consumed', () => { + const block = { + clientId: 'client-1', + name: 'test-1', + innerBlocks: [ + { + clientId: 'client-1-1', + name: 'test-1-1', + innerBlocks: [], + }, + { + clientId: 'client-1-2', + name: 'test-1-2', + innerBlocks: [], + }, + ], + }; + const res = getMatchingBlockByName( + block, + 'test-1-2', + new Set( [ 'client-1-2' ] ) + ); + expect( res ).toBeUndefined(); + } ); + describe( 'should return the matched block', () => { + it( 'if top level block', () => { + const block = { + clientId: 'client-1', + name: 'test-1', + innerBlocks: [], + }; + const res = getMatchingBlockByName( + block, + 'test-1', + new Set( [ 'client-1-2' ] ) + ); + expect( res ).toEqual( + expect.objectContaining( { + clientId: 'client-1', + name: 'test-1', + innerBlocks: [], + } ) + ); + } ); + it( 'if nested block', () => { + const block = { + clientId: 'client-1', + name: 'test-1', + innerBlocks: [ + { + clientId: 'client-1-1', + name: 'test-1-1', + innerBlocks: [], + }, + { + clientId: 'client-1-2', + name: 'test-1-2', + innerBlocks: [ + { + clientId: 'client-1-2-1', + name: 'test-1-2-1', + innerBlocks: [], + }, + ], + }, + ], + }; + const res = getMatchingBlockByName( + block, + 'test-1-2-1', + new Set( [ 'someId' ] ) + ); + expect( res ).toEqual( + expect.objectContaining( { + clientId: 'client-1-2-1', + name: 'test-1-2-1', + innerBlocks: [], + } ) + ); + } ); + } ); + } ); } ); diff --git a/packages/block-editor/src/components/block-switcher/use-transformed-patterns.js b/packages/block-editor/src/components/block-switcher/use-transformed-patterns.js index 710e6881c075e..82b86f9a41d70 100644 --- a/packages/block-editor/src/components/block-switcher/use-transformed-patterns.js +++ b/packages/block-editor/src/components/block-switcher/use-transformed-patterns.js @@ -6,30 +6,52 @@ import { cloneBlock } from '@wordpress/blocks'; /** * Internal dependencies */ -import { - getMatchingBlockInPattern, - getBlockRetainingAttributes, -} from './utils'; +import { getMatchingBlockByName, getRetainedBlockAttributes } from './utils'; + +// here goes the mutation... :) +// TODO jsdoc +// TODO tests +const transformMatchingBlock = ( match, selectedBlock ) => { + // Get the block attributes to retain through the transformation. + const retainedBlockAttributes = getRetainedBlockAttributes( + selectedBlock.name, + selectedBlock.attributes + ); + match.attributes = { + ...match.attributes, + ...retainedBlockAttributes, + }; +}; /** + * By providing the selected blocks and pattern's blocks + * find the matched blocks, transform them return them. + * If not all selected blocks are matched, return nothing. * * @param {WPBlock[]} selectedBlocks The selected blocks. * @param {WPBlock[]} patternBlocks The pattern's blocks. * @return {WPBlock[]|void} The transformed pattern's blocks or undefined if not all selected blocks have been matched. */ -// TODO jsdoc // TODO tests -const getPatternTransformedBlocks = ( selectedBlocks, patternBlocks ) => { - // const eligiblePattern = getEligiblePattern() - // Clone Pattern's blocks in `transformedBlocks` prop, to mutate them. +export const getPatternTransformedBlocks = ( + selectedBlocks, + patternBlocks +) => { + // Clone Pattern's blocks to produce new clientIds and be able to mutate the matches. const _patternBlocks = patternBlocks.map( ( block ) => cloneBlock( block ) ); + /** + * Keep track of the consumed pattern blocks. + * This is needed because we loop the selected blocks + * and for example we may have selected two paragraphs and + * the pattern's blocks could have more `paragraphs`. + */ const consumedBlocks = new Set(); for ( const selectedBlock of selectedBlocks ) { let isMatch = false; for ( const patternBlock of _patternBlocks ) { - const match = getMatchingBlockInPattern( + const match = getMatchingBlockByName( patternBlock, selectedBlock.name, consumedBlocks @@ -39,6 +61,7 @@ const getPatternTransformedBlocks = ( selectedBlocks, patternBlocks ) => { consumedBlocks.add( match.clientId ); // We update (mutate) the matching pattern block. transformMatchingBlock( match, selectedBlock ); + // No need to loop through other pattern's blocks. break; } // Bail eary if a selected block has not been matched. @@ -46,18 +69,6 @@ const getPatternTransformedBlocks = ( selectedBlocks, patternBlocks ) => { } return _patternBlocks; }; -// here goes the mutation... :) -const transformMatchingBlock = ( match, selectedBlock ) => { - // Get the block attributes to retain through the transformation. - const retainedBlockAttributes = getBlockRetainingAttributes( - selectedBlock.name, - selectedBlock.attributes - ); - match.attributes = { - ...match.attributes, - ...retainedBlockAttributes, - }; -}; /** * Custom hook that accepts patterns from state and the selected diff --git a/packages/block-editor/src/components/block-switcher/utils.js b/packages/block-editor/src/components/block-switcher/utils.js index bdc717ffce0be..2004f1d65172b 100644 --- a/packages/block-editor/src/components/block-switcher/utils.js +++ b/packages/block-editor/src/components/block-switcher/utils.js @@ -2,36 +2,31 @@ * WordPress dependencies */ import { __experimentalGetBlockAttributesNamesByRole as getBlockAttributesNamesByRole } from '@wordpress/blocks'; + /** - * Find a selected block match in a pattern and return it. - * We return a reference to the block object to mutate it. - * We have first cloned the pattern blocks in a new property - * `transformedBlocks` and we mutate this. + * Try to find a matching block by a block's name in a provided + * block. We recurse through InnerBlocks and return the reference + * of the matched block (it could be an InnerBlock). + * If no match is found return nothing. * - * @param {WPBlock} block The pattern's block to try to find a match. - * @param {string} selectedBlockName The current selected block's name. - * @param {Set} consumedBlocks A set holding the previously matched blocks. + * @param {WPBlock} block The block to try to find a match. + * @param {string} selectedBlockName The block's name to use for matching condition. + * @param {Set} consumedBlocks A set holding the previously matched/consumed blocks. * - * @return {WPBlock?} The matched block if found or `false`. + * @return {WPBlock?} The matched block if found or nothing(`undefined`). */ -// TODO tests -export const getMatchingBlockInPattern = ( +export const getMatchingBlockByName = ( block, selectedBlockName, - consumedBlocks + consumedBlocks = new Set() ) => { const { clientId, name, innerBlocks = [] } = block; - /** - * Check if block has been transformed already. - * This is needed because we loop the selected blocks - * and for example we may have selected two paragraphs and - * the patterns could have more `paragraphs`. - */ + // Check if block has been consumed already. if ( consumedBlocks.has( clientId ) ) return; if ( name === selectedBlockName ) return block; - // Try to find a matching block from InnerBlocks. + // Try to find a matching block from InnerBlocks recursively. for ( const innerBlock of innerBlocks ) { - const match = getMatchingBlockInPattern( + const match = getMatchingBlockByName( innerBlock, selectedBlockName, consumedBlocks @@ -50,7 +45,7 @@ export const getMatchingBlockInPattern = ( * @param {Object} attributes Selected block's attributes. * @return {Object} The block's attributes to retain. */ -export const getBlockRetainingAttributes = ( name, attributes ) => { +export const getRetainedBlockAttributes = ( name, attributes ) => { const contentAttributes = getBlockAttributesNamesByRole( name, 'content' ); if ( ! contentAttributes?.length ) return attributes; From 18b4ec6b924f8bfb9bac2c3bfc19173c2f2347ac Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Thu, 8 Apr 2021 13:10:31 +0300 Subject: [PATCH 06/13] getPatternTransformedBlocks + transformMatchingBlock tests --- .../test/use-transformed.patterns.js | 336 ++++++++++++++++++ .../use-transformed-patterns.js | 52 +-- 2 files changed, 366 insertions(+), 22 deletions(-) create mode 100644 packages/block-editor/src/components/block-switcher/test/use-transformed.patterns.js diff --git a/packages/block-editor/src/components/block-switcher/test/use-transformed.patterns.js b/packages/block-editor/src/components/block-switcher/test/use-transformed.patterns.js new file mode 100644 index 0000000000000..4d63c76317479 --- /dev/null +++ b/packages/block-editor/src/components/block-switcher/test/use-transformed.patterns.js @@ -0,0 +1,336 @@ +/** + * WordPress dependencies + */ +import { unregisterBlockType, registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { + transformMatchingBlock, + getPatternTransformedBlocks, +} from '../use-transformed-patterns'; + +describe( 'use-transformed-patterns', () => { + beforeAll( () => { + registerBlockType( 'core/test-block-1', { + attributes: { + align: { + type: 'string', + }, + content: { + type: 'boolean', + role: 'content', + }, + level: { + type: 'number', + role: 'content', + }, + color: { + type: 'string', + role: 'other', + }, + }, + save() {}, + category: 'text', + title: 'test block 1', + } ); + registerBlockType( 'core/test-block-2', { + attributes: { + align: { type: 'string' }, + content: { type: 'boolean' }, + color: { type: 'string' }, + }, + save() {}, + category: 'text', + title: 'test block 2', + } ); + } ); + afterAll( () => { + [ 'core/test-block-1', 'core/test-block-2' ].forEach( + unregisterBlockType + ); + } ); + describe( 'transformMatchingBlock', () => { + it( 'should properly update the matching block - No retained block attributes', () => { + const match = { + clientId: 'block-2', + name: 'core/test-block-2', + attributes: { align: 'center' }, + }; + const selectedBlock = { + clientId: 'selected-block-2', + name: 'core/test-block-2', + attributes: { align: 'right', content: 'hi' }, + }; + transformMatchingBlock( match, selectedBlock ); + expect( match ).toEqual( + expect.objectContaining( { + clientId: 'block-2', + name: 'core/test-block-2', + attributes: expect.objectContaining( { + align: 'right', + content: 'hi', + } ), + } ) + ); + } ); + it( 'should properly update the matching block - WITH retained block attributes', () => { + const match = { + clientId: 'block-1', + name: 'core/test-block-1', + attributes: { + align: 'center', + content: 'from match', + level: 3, + color: 'red', + }, + }; + const selectedBlock = { + clientId: 'selected-block-1', + name: 'core/test-block-1', + attributes: { + align: 'left', + content: 'from selected block', + level: 1, + color: 'green', + }, + }; + transformMatchingBlock( match, selectedBlock ); + expect( match ).toEqual( + expect.objectContaining( { + clientId: 'block-1', + name: 'core/test-block-1', + attributes: expect.objectContaining( { + align: 'center', + content: 'from selected block', + level: 1, + color: 'red', + } ), + } ) + ); + } ); + } ); + describe( 'getPatternTransformedBlocks', () => { + const patternBlocks = [ + { + clientId: 'client-1', + name: 'core/test-block-1', + attributes: { content: 'top level block 1', color: 'red' }, + innerBlocks: [ + { + clientId: 'client-1-1', + name: 'core/test-block-2', + innerBlocks: [], + }, + { + clientId: 'client-1-2', + name: 'core/test-block-2', + innerBlocks: [ + { + clientId: 'client-1-2-1', + name: 'core/test-block-1', + attributes: { + content: 'nested block 1', + level: 6, + color: 'yellow', + }, + innerBlocks: [], + }, + ], + }, + ], + }, + { + clientId: 'client-2', + name: 'core/test-block-2', + innerBlocks: [ + { + clientId: 'client-1-1', + name: 'core/test-block-2', + innerBlocks: [], + }, + { + clientId: 'client-1-2', + name: 'nested block', + innerBlocks: [ + { + clientId: 'client-1-2-1', + name: 'core/test-block-1', + attributes: { + content: 'nested block 1', + level: 6, + color: 'yellow', + }, + innerBlocks: [], + }, + ], + }, + ], + }, + { + clientId: 'client-3', + name: 'core/test-block-1', + attributes: { content: 'top level block 3', color: 'purple' }, + innerBlocks: [], + }, + ]; + describe( 'return nothing', () => { + it( 'when no match is found', () => { + const selectedBlocks = [ + { + clientId: 'selected-1', + name: 'selected-1', + innerBlocks: [], + }, + ]; + const res = getPatternTransformedBlocks( + selectedBlocks, + patternBlocks + ); + expect( res ).toBeUndefined(); + } ); + it( 'when not ALL blocks are matched', () => { + const selectedBlocks = [ + { + clientId: 'selected-1', + name: 'core/test-block-1', + attributes: { + content: 'from selected', + color: 'green', + }, + innerBlocks: [], + }, + { + clientId: 'selected-2', + name: 'not in pattern', + innerBlocks: [], + }, + ]; + const res = getPatternTransformedBlocks( + selectedBlocks, + patternBlocks + ); + expect( res ).toBeUndefined(); + } ); + } ); + describe( 'return properly transformed pattern blocks', () => { + it( 'when single block is selected', () => { + const selectedBlocks = [ + { + clientId: 'selected-1', + name: 'core/test-block-1', + attributes: { + content: 'from selected', + color: 'green', + }, + innerBlocks: [], + }, + ]; + const res = getPatternTransformedBlocks( + selectedBlocks, + patternBlocks + ); + expect( res ).toHaveLength( 3 ); + expect( res ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + name: 'core/test-block-1', + attributes: expect.objectContaining( { + content: 'from selected', + color: 'red', + } ), + } ), + expect.objectContaining( { + name: 'core/test-block-2', + } ), + expect.objectContaining( { + name: 'core/test-block-1', + attributes: { + content: 'top level block 3', + color: 'purple', + }, + } ), + ] ) + ); + } ); + it( 'when multiple selected blocks', () => { + /** + * The matching is performed recursively searching depth first, + * so top level blocks' InnerBlocks are search before trying + * the next top level pattern's block. + */ + const selectedBlocks = [ + { + clientId: 'selected-1', + name: 'core/test-block-1', + attributes: { + content: 'from selected 1', + color: 'green', + }, + innerBlocks: [], + }, + { + clientId: 'selected-2', + name: 'core/test-block-1', + attributes: { + content: 'from selected 2', + level: 1, + }, + innerBlocks: [], + }, + { + clientId: 'selected-3', + name: 'core/test-block-1', + attributes: { + content: 'from selected 3', + color: 'white', + }, + innerBlocks: [], + }, + ]; + const res = getPatternTransformedBlocks( + selectedBlocks, + patternBlocks + ); + expect( res ).toHaveLength( 3 ); + expect( res ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + name: 'core/test-block-1', + attributes: expect.objectContaining( { + content: 'from selected 1', + color: 'red', + } ), + } ), + expect.objectContaining( { + name: 'core/test-block-2', + innerBlocks: expect.arrayContaining( [ + expect.objectContaining( { + name: 'nested block', + innerBlocks: [ + expect.objectContaining( { + name: 'core/test-block-1', + attributes: { + content: 'from selected 2', + level: 1, + color: 'yellow', + }, + } ), + ], + } ), + ] ), + } ), + expect.objectContaining( { + name: 'core/test-block-1', + attributes: { + content: 'from selected 3', + color: 'purple', + }, + } ), + ] ) + ); + } ); + } ); + } ); +} ); diff --git a/packages/block-editor/src/components/block-switcher/use-transformed-patterns.js b/packages/block-editor/src/components/block-switcher/use-transformed-patterns.js index 82b86f9a41d70..e1d9e390ec2ca 100644 --- a/packages/block-editor/src/components/block-switcher/use-transformed-patterns.js +++ b/packages/block-editor/src/components/block-switcher/use-transformed-patterns.js @@ -3,15 +3,22 @@ */ import { useMemo } from '@wordpress/element'; import { cloneBlock } from '@wordpress/blocks'; + /** * Internal dependencies */ import { getMatchingBlockByName, getRetainedBlockAttributes } from './utils'; -// here goes the mutation... :) -// TODO jsdoc -// TODO tests -const transformMatchingBlock = ( match, selectedBlock ) => { +/** + * Mutate the matched block's attributes by getting + * which block type's attributes to retain and prioritize + * them in the merging of the attributes. + * + * @param {WPBlock} match The matched block. + * @param {WPBlock} selectedBlock The selected block. + * @return {void} + */ +export const transformMatchingBlock = ( match, selectedBlock ) => { // Get the block attributes to retain through the transformation. const retainedBlockAttributes = getRetainedBlockAttributes( selectedBlock.name, @@ -25,14 +32,13 @@ const transformMatchingBlock = ( match, selectedBlock ) => { /** * By providing the selected blocks and pattern's blocks - * find the matched blocks, transform them return them. + * find the matching blocks, transform them and return them. * If not all selected blocks are matched, return nothing. * * @param {WPBlock[]} selectedBlocks The selected blocks. * @param {WPBlock[]} patternBlocks The pattern's blocks. * @return {WPBlock[]|void} The transformed pattern's blocks or undefined if not all selected blocks have been matched. */ -// TODO tests export const getPatternTransformedBlocks = ( selectedBlocks, patternBlocks @@ -82,23 +88,25 @@ export const getPatternTransformedBlocks = ( * @param {WPBlock[]} selectedBlocks The currently selected blocks. * @return {WPBlockPattern[]} Returns the eligible matched patterns with all the selected blocks. */ +// TODO tests const useTransformedPatterns = ( patterns, selectedBlocks ) => { - return useMemo( () => { - const _patterns = patterns.reduce( ( accumulator, _pattern ) => { - const transformedBlocks = getPatternTransformedBlocks( - selectedBlocks, - _pattern.blocks - ); - if ( transformedBlocks ) { - accumulator.push( { - ..._pattern, - transformedBlocks, - } ); - } - return accumulator; - }, [] ); - return _patterns; - }, [ patterns, selectedBlocks ] ); + return useMemo( + () => + patterns.reduce( ( accumulator, _pattern ) => { + const transformedBlocks = getPatternTransformedBlocks( + selectedBlocks, + _pattern.blocks + ); + if ( transformedBlocks ) { + accumulator.push( { + ..._pattern, + transformedBlocks, + } ); + } + return accumulator; + }, [] ), + [ patterns, selectedBlocks ] + ); }; export default useTransformedPatterns; From 6b7fd2f660bb2e4a6c20d326a442ad5959fab9af Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Thu, 8 Apr 2021 16:02:40 +0300 Subject: [PATCH 07/13] add role:content to more blocks + a couple test patterns --- lib/test-block-patterns.php | 32 +++++++++++++++++++ packages/block-library/src/list/block.json | 6 ++-- .../block-library/src/preformatted/block.json | 3 +- .../block-library/src/pullquote/block.json | 6 ++-- packages/block-library/src/quote/block.json | 6 ++-- packages/block-library/src/search/block.json | 9 ++++-- packages/block-library/src/verse/block.json | 3 +- 7 files changed, 54 insertions(+), 11 deletions(-) diff --git a/lib/test-block-patterns.php b/lib/test-block-patterns.php index ee57b303a12cf..eb7e1451bd6e4 100644 --- a/lib/test-block-patterns.php +++ b/lib/test-block-patterns.php @@ -70,3 +70,35 @@ ', ) ); + +register_block_pattern( + 'multi/v3', + array( + 'title' => __( 'Multi blocks v3', 'gutenberg' ), + 'blockTypes' => array( 'core/list', 'core/paragraph' ), + 'content' => ' +
  • pattern list item 1
  • pattern list item 1
+ + +

These preliminaries settled, he did not care to put off any longer the execution of his design, urged on to it by the thought of all the world was losing by his delay, seeing what wrongs he intended to right, grievances to redress, injustices to repair, abuses to remove, and duties to discharge.

+ ', + ) +); + +register_block_pattern( + 'Search', + array( + 'title' => __( 'Search v1', 'gutenberg' ), + 'blockTypes' => array( 'core/search' ), + 'content' => ' +
+ +

Search something

+ + +
+ +
+ ', + ) +); diff --git a/packages/block-library/src/list/block.json b/packages/block-library/src/list/block.json index 002f0bc82bc7b..42c49db06fadc 100644 --- a/packages/block-library/src/list/block.json +++ b/packages/block-library/src/list/block.json @@ -5,7 +5,8 @@ "attributes": { "ordered": { "type": "boolean", - "default": false + "default": false, + "role": "content" }, "values": { "type": "string", @@ -13,7 +14,8 @@ "selector": "ol,ul", "multiline": "li", "__unstableMultilineWrapperTags": [ "ol", "ul" ], - "default": "" + "default": "", + "role": "content" }, "type": { "type": "string" diff --git a/packages/block-library/src/preformatted/block.json b/packages/block-library/src/preformatted/block.json index 75ebf7ecb6661..22de3918fa389 100644 --- a/packages/block-library/src/preformatted/block.json +++ b/packages/block-library/src/preformatted/block.json @@ -8,7 +8,8 @@ "source": "html", "selector": "pre", "default": "", - "__unstablePreserveWhiteSpace": true + "__unstablePreserveWhiteSpace": true, + "role": "content" } }, "supports": { diff --git a/packages/block-library/src/pullquote/block.json b/packages/block-library/src/pullquote/block.json index 8413ba8473483..45f6e0765335a 100644 --- a/packages/block-library/src/pullquote/block.json +++ b/packages/block-library/src/pullquote/block.json @@ -7,13 +7,15 @@ "type": "string", "source": "html", "selector": "blockquote", - "multiline": "p" + "multiline": "p", + "role": "content" }, "citation": { "type": "string", "source": "html", "selector": "cite", - "default": "" + "default": "", + "role": "content" }, "mainColor": { "type": "string" diff --git a/packages/block-library/src/quote/block.json b/packages/block-library/src/quote/block.json index bba83461367fa..87bb110babdf9 100644 --- a/packages/block-library/src/quote/block.json +++ b/packages/block-library/src/quote/block.json @@ -8,13 +8,15 @@ "source": "html", "selector": "blockquote", "multiline": "p", - "default": "" + "default": "", + "role": "content" }, "citation": { "type": "string", "source": "html", "selector": "cite", - "default": "" + "default": "", + "role": "content" }, "align": { "type": "string" diff --git a/packages/block-library/src/search/block.json b/packages/block-library/src/search/block.json index d6399a0857397..2c84ee2b27329 100644 --- a/packages/block-library/src/search/block.json +++ b/packages/block-library/src/search/block.json @@ -4,7 +4,8 @@ "category": "widgets", "attributes": { "label": { - "type": "string" + "type": "string", + "role": "content" }, "showLabel": { "type": "boolean", @@ -12,7 +13,8 @@ }, "placeholder": { "type": "string", - "default": "" + "default": "", + "role": "content" }, "width": { "type": "number" @@ -21,7 +23,8 @@ "type": "string" }, "buttonText": { - "type": "string" + "type": "string", + "role": "content" }, "buttonPosition": { "type": "string", diff --git a/packages/block-library/src/verse/block.json b/packages/block-library/src/verse/block.json index 5ca413b2e1376..6c5af54cd5b87 100644 --- a/packages/block-library/src/verse/block.json +++ b/packages/block-library/src/verse/block.json @@ -8,7 +8,8 @@ "source": "html", "selector": "pre", "default": "", - "__unstablePreserveWhiteSpace": true + "__unstablePreserveWhiteSpace": true, + "role": "content" }, "textAlign": { "type": "string" From 081e257c334c8fb069084149a7fbfd02aa91c5c7 Mon Sep 17 00:00:00 2001 From: James Koster Date: Thu, 8 Apr 2021 15:55:09 +0100 Subject: [PATCH 08/13] pattern list padding --- packages/block-editor/src/components/block-switcher/style.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/block-editor/src/components/block-switcher/style.scss b/packages/block-editor/src/components/block-switcher/style.scss index ba74c190b361d..1900b6a1b3c9a 100644 --- a/packages/block-editor/src/components/block-switcher/style.scss +++ b/packages/block-editor/src/components/block-switcher/style.scss @@ -184,6 +184,8 @@ } .block-editor-block-switcher__preview-patterns-container { + padding-bottom: $grid-unit-20; + .block-editor-block-switcher__preview-patterns-container-list__list-item { margin-top: $grid-unit-20; From edd5588ad71ebf8c545d48049aa0633d1b87983b Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Tue, 13 Apr 2021 17:21:15 +0300 Subject: [PATCH 09/13] rename role to __experimentalRole --- .../src/components/block-switcher/index.js | 9 ++++----- packages/block-library/src/heading/block.json | 2 +- packages/block-library/src/list/block.json | 4 ++-- packages/block-library/src/paragraph/block.json | 2 +- packages/block-library/src/preformatted/block.json | 2 +- packages/block-library/src/pullquote/block.json | 4 ++-- packages/block-library/src/quote/block.json | 4 ++-- packages/block-library/src/search/block.json | 6 +++--- packages/block-library/src/verse/block.json | 2 +- packages/blocks/src/api/test/utils.js | 13 ++++++------- packages/blocks/src/api/utils.js | 3 ++- 11 files changed, 25 insertions(+), 26 deletions(-) diff --git a/packages/block-editor/src/components/block-switcher/index.js b/packages/block-editor/src/components/block-switcher/index.js index 99e5d657cc210..16f720aabb439 100644 --- a/packages/block-editor/src/components/block-switcher/index.js +++ b/packages/block-editor/src/components/block-switcher/index.js @@ -69,10 +69,6 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { ? getBlockType( firstBlockName )?.icon : stack; } - const _patterns = __experimentalGetPatternTransformItems( - blocks, - rootClientId - ); return { possibleBlockTransformations: getBlockTransformItems( blocks, @@ -81,7 +77,10 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { hasBlockStyles: !! styles?.length, icon: _icon, blockTitle: getBlockType( firstBlockName ).title, - patterns: _patterns, + patterns: __experimentalGetPatternTransformItems( + blocks, + rootClientId + ), }; }, [ clientIds, blocks, blockInformation?.icon ] diff --git a/packages/block-library/src/heading/block.json b/packages/block-library/src/heading/block.json index b54ad9860ade1..8d7e0fdd5c194 100644 --- a/packages/block-library/src/heading/block.json +++ b/packages/block-library/src/heading/block.json @@ -11,7 +11,7 @@ "source": "html", "selector": "h1,h2,h3,h4,h5,h6", "default": "", - "role": "content" + "__experimentalRole": "content" }, "level": { "type": "number", diff --git a/packages/block-library/src/list/block.json b/packages/block-library/src/list/block.json index 42c49db06fadc..509da21bef437 100644 --- a/packages/block-library/src/list/block.json +++ b/packages/block-library/src/list/block.json @@ -6,7 +6,7 @@ "ordered": { "type": "boolean", "default": false, - "role": "content" + "__experimentalRole": "content" }, "values": { "type": "string", @@ -15,7 +15,7 @@ "multiline": "li", "__unstableMultilineWrapperTags": [ "ol", "ul" ], "default": "", - "role": "content" + "__experimentalRole": "content" }, "type": { "type": "string" diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json index 2f95db4ac1c85..f7dee53633483 100644 --- a/packages/block-library/src/paragraph/block.json +++ b/packages/block-library/src/paragraph/block.json @@ -11,7 +11,7 @@ "source": "html", "selector": "p", "default": "", - "role": "content" + "__experimentalRole": "content" }, "dropCap": { "type": "boolean", diff --git a/packages/block-library/src/preformatted/block.json b/packages/block-library/src/preformatted/block.json index 22de3918fa389..56a325bf8f07a 100644 --- a/packages/block-library/src/preformatted/block.json +++ b/packages/block-library/src/preformatted/block.json @@ -9,7 +9,7 @@ "selector": "pre", "default": "", "__unstablePreserveWhiteSpace": true, - "role": "content" + "__experimentalRole": "content" } }, "supports": { diff --git a/packages/block-library/src/pullquote/block.json b/packages/block-library/src/pullquote/block.json index 45f6e0765335a..9b64a2c253bc7 100644 --- a/packages/block-library/src/pullquote/block.json +++ b/packages/block-library/src/pullquote/block.json @@ -8,14 +8,14 @@ "source": "html", "selector": "blockquote", "multiline": "p", - "role": "content" + "__experimentalRole": "content" }, "citation": { "type": "string", "source": "html", "selector": "cite", "default": "", - "role": "content" + "__experimentalRole": "content" }, "mainColor": { "type": "string" diff --git a/packages/block-library/src/quote/block.json b/packages/block-library/src/quote/block.json index 87bb110babdf9..0f026e96f6abf 100644 --- a/packages/block-library/src/quote/block.json +++ b/packages/block-library/src/quote/block.json @@ -9,14 +9,14 @@ "selector": "blockquote", "multiline": "p", "default": "", - "role": "content" + "__experimentalRole": "content" }, "citation": { "type": "string", "source": "html", "selector": "cite", "default": "", - "role": "content" + "__experimentalRole": "content" }, "align": { "type": "string" diff --git a/packages/block-library/src/search/block.json b/packages/block-library/src/search/block.json index 2c84ee2b27329..96e0f7a923c19 100644 --- a/packages/block-library/src/search/block.json +++ b/packages/block-library/src/search/block.json @@ -5,7 +5,7 @@ "attributes": { "label": { "type": "string", - "role": "content" + "__experimentalRole": "content" }, "showLabel": { "type": "boolean", @@ -14,7 +14,7 @@ "placeholder": { "type": "string", "default": "", - "role": "content" + "__experimentalRole": "content" }, "width": { "type": "number" @@ -24,7 +24,7 @@ }, "buttonText": { "type": "string", - "role": "content" + "__experimentalRole": "content" }, "buttonPosition": { "type": "string", diff --git a/packages/block-library/src/verse/block.json b/packages/block-library/src/verse/block.json index 6c5af54cd5b87..490d6a6ea30b4 100644 --- a/packages/block-library/src/verse/block.json +++ b/packages/block-library/src/verse/block.json @@ -9,7 +9,7 @@ "selector": "pre", "default": "", "__unstablePreserveWhiteSpace": true, - "role": "content" + "__experimentalRole": "content" }, "textAlign": { "type": "string" diff --git a/packages/blocks/src/api/test/utils.js b/packages/blocks/src/api/test/utils.js index 6425e70055d24..3ffc1bf5944bc 100644 --- a/packages/blocks/src/api/test/utils.js +++ b/packages/blocks/src/api/test/utils.js @@ -321,15 +321,15 @@ describe( '__experimentalGetBlockAttributesNamesByRole', () => { }, content: { type: 'boolean', - role: 'content', + __experimentalRole: 'content', }, level: { type: 'number', - role: 'content', + __experimentalRole: 'content', }, color: { type: 'string', - role: 'other', + __experimentalRole: 'other', }, }, save: noop, @@ -365,10 +365,9 @@ describe( '__experimentalGetBlockAttributesNamesByRole', () => { ).toEqual( [] ); } ); it( 'should return all attribute names if no role is provided', () => { - const res = __experimentalGetBlockAttributesNamesByRole( - 'core/test-block-1' - ); - expect( res ).toEqual( + expect( + __experimentalGetBlockAttributesNamesByRole( 'core/test-block-1' ) + ).toEqual( expect.arrayContaining( [ 'align', 'content', 'level', 'color' ] ) ); } ); diff --git a/packages/blocks/src/api/utils.js b/packages/blocks/src/api/utils.js index f2bd2a76a4583..c14f5d003dd25 100644 --- a/packages/blocks/src/api/utils.js +++ b/packages/blocks/src/api/utils.js @@ -290,6 +290,7 @@ export function __experimentalGetBlockAttributesNamesByRole( name, role ) { const attributesNames = Object.keys( attributes ); if ( ! role ) return attributesNames; return attributesNames.filter( - ( attributeName ) => attributes[ attributeName ]?.role === role + ( attributeName ) => + attributes[ attributeName ]?.__experimentalRole === role ); } From 5b505ec3f227b38249d5021b6405bb8d732d4a75 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Tue, 13 Apr 2021 17:22:53 +0300 Subject: [PATCH 10/13] Update docs suggestion Co-authored-by: Miguel Fonseca --- packages/block-editor/src/store/selectors.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 5a55a6151f0ff..2641d68534a21 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1898,11 +1898,11 @@ export const __experimentalGetPatternTransformItems = createSelector( ( state, blocks, rootClientId = null ) => { if ( ! blocks ) return EMPTY_ARRAY; /** - * For now we only hanlde blocks without InnerBlocks and take into account - * the `role` property of block's attributes for the transformation. - * Noting that blocks have been retrieved through `getBlock`, that doen't - * return the child inner blocks of an inner block controller, so we still - * need to check for this case too. + * For now we only handle blocks without InnerBlocks and take into account + * the `role` property of blocks' attributes for the transformation. Note + * that the blocks have been retrieved through `getBlock`, which doesn't + * return the inner blocks of an inner block controller, so we still need + * to check for this case too. */ if ( blocks.some( From ed23a77bf803c92676adbcfbfd80da814701e8a2 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Tue, 13 Apr 2021 17:51:52 +0300 Subject: [PATCH 11/13] fix tests + docs --- .../test/use-transformed.patterns.js | 6 +++--- .../components/block-switcher/test/utils.js | 6 +++--- .../use-transformed-patterns.js | 6 +++++- packages/block-editor/src/store/selectors.js | 20 +++++++++---------- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/block-editor/src/components/block-switcher/test/use-transformed.patterns.js b/packages/block-editor/src/components/block-switcher/test/use-transformed.patterns.js index 4d63c76317479..05ce545667d46 100644 --- a/packages/block-editor/src/components/block-switcher/test/use-transformed.patterns.js +++ b/packages/block-editor/src/components/block-switcher/test/use-transformed.patterns.js @@ -20,15 +20,15 @@ describe( 'use-transformed-patterns', () => { }, content: { type: 'boolean', - role: 'content', + __experimentalRole: 'content', }, level: { type: 'number', - role: 'content', + __experimentalRole: 'content', }, color: { type: 'string', - role: 'other', + __experimentalRole: 'other', }, }, save() {}, diff --git a/packages/block-editor/src/components/block-switcher/test/utils.js b/packages/block-editor/src/components/block-switcher/test/utils.js index eafe5e8a4d937..38009601e1646 100644 --- a/packages/block-editor/src/components/block-switcher/test/utils.js +++ b/packages/block-editor/src/components/block-switcher/test/utils.js @@ -18,15 +18,15 @@ describe( 'BlockSwitcher - utils', () => { }, content: { type: 'boolean', - role: 'content', + __experimentalRole: 'content', }, level: { type: 'number', - role: 'content', + __experimentalRole: 'content', }, color: { type: 'string', - role: 'other', + __experimentalRole: 'other', }, }, save() {}, diff --git a/packages/block-editor/src/components/block-switcher/use-transformed-patterns.js b/packages/block-editor/src/components/block-switcher/use-transformed-patterns.js index e1d9e390ec2ca..7f8c956ea6988 100644 --- a/packages/block-editor/src/components/block-switcher/use-transformed-patterns.js +++ b/packages/block-editor/src/components/block-switcher/use-transformed-patterns.js @@ -76,6 +76,10 @@ export const getPatternTransformedBlocks = ( return _patternBlocks; }; +/** + * @typedef {WPBlockPattern & {transformedBlocks: WPBlock[]}} TransformedBlockPattern + */ + /** * Custom hook that accepts patterns from state and the selected * blocks and tries to match these with the pattern's blocks. @@ -86,7 +90,7 @@ export const getPatternTransformedBlocks = ( * * @param {WPBlockPattern[]} patterns Patterns from state. * @param {WPBlock[]} selectedBlocks The currently selected blocks. - * @return {WPBlockPattern[]} Returns the eligible matched patterns with all the selected blocks. + * @return {TransformedBlockPattern[]} Returns the eligible matched patterns with all the selected blocks. */ // TODO tests const useTransformedPatterns = ( patterns, selectedBlocks ) => { diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 2641d68534a21..ffac34cea81fb 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1876,16 +1876,14 @@ export const __experimentalGetPatternsByBlockTypes = createSelector( /** * Determines the items that appear in the available pattern transforms list. - * There is special handling in two cases: - * 1. For some blocks (`blocksToSkip`) when multiple blocks are selected, - * don't show any transforms, as it doesn't make sense to try to be too smart. - * 2. There are some blocks (`nestedSingleBlocksToHandle`) that makes sense to - * replace everything when they are the only block selected. * - * For the rest blocks we return a first set of possible eligible block patterns, - * by checking the `scope` Patterns API. We still have to recurse through block - * pattern's blocks and try to find matches from the selected blocks. Now this - * happens in the consumer to avoid heavy operations in the selector. + * For now we only handle blocks without InnerBlocks and take into account + * the `__experimentalRole` property of blocks' attributes for the transformation. + * + * We return the first set of possible eligible block patterns, + * by checking the `blockTypes` property. We still have to recurse through + * block pattern's blocks and try to find matches from the selected blocks. + * Now this happens in the consumer to avoid heavy operations in the selector. * * @param {Object} state Editor state. * @param {Object[]} blocks The selected blocks. @@ -1899,8 +1897,8 @@ export const __experimentalGetPatternTransformItems = createSelector( if ( ! blocks ) return EMPTY_ARRAY; /** * For now we only handle blocks without InnerBlocks and take into account - * the `role` property of blocks' attributes for the transformation. Note - * that the blocks have been retrieved through `getBlock`, which doesn't + * the `__experimentalRole` property of blocks' attributes for the transformation. + * Note that the blocks have been retrieved through `getBlock`, which doesn't * return the inner blocks of an inner block controller, so we still need * to check for this case too. */ From 6854db72e79576b82902353a2273378986fb25f7 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Tue, 13 Apr 2021 18:31:24 +0300 Subject: [PATCH 12/13] change patterns --- lib/block-patterns.php | 26 +++++ lib/load.php | 1 - lib/test-block-patterns.php | 104 ------------------ .../pattern-transformations-menu.js | 2 +- 4 files changed, 27 insertions(+), 106 deletions(-) delete mode 100644 lib/test-block-patterns.php diff --git a/lib/block-patterns.php b/lib/block-patterns.php index b35f07a955ed3..c905e90a1ecc6 100644 --- a/lib/block-patterns.php +++ b/lib/block-patterns.php @@ -132,3 +132,29 @@ ', ) ); + +// Initial block patterns to be used in block transformations with patterns. +register_block_pattern( + 'paragraph/large-with-background-color', + array( + 'title' => __( 'Large Paragraph with background color', 'gutenberg' ), + 'blockTypes' => array( 'core/paragraph' ), + 'viewportWidth' => 500, + 'content' => ' + + ', + ) +); +register_block_pattern( + 'social-links/shared-background-color', + array( + 'title' => __( 'Social links with a shared background color', 'gutenberg' ), + 'blockTypes' => array( 'core/social-links' ), + 'viewportWidth' => 500, + 'content' => ' + + ', + ) +); diff --git a/lib/load.php b/lib/load.php index 90a19ee8c89ae..2deda54b6646b 100644 --- a/lib/load.php +++ b/lib/load.php @@ -107,7 +107,6 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/blocks.php'; require __DIR__ . '/block-patterns.php'; -require __DIR__ . '/test-block-patterns.php'; require __DIR__ . '/client-assets.php'; require __DIR__ . '/demo.php'; require __DIR__ . '/widgets.php'; diff --git a/lib/test-block-patterns.php b/lib/test-block-patterns.php deleted file mode 100644 index eb7e1451bd6e4..0000000000000 --- a/lib/test-block-patterns.php +++ /dev/null @@ -1,104 +0,0 @@ - __( 'Paragraph version 1', 'gutenberg' ), - 'blockTypes' => array( 'core/paragraph' ), - 'content' => ' -

Hello my paragraph!

- ', - ) -); -register_block_pattern( - 'paragraph/v2', - array( - 'title' => __( 'Paragraph version 2', 'gutenberg' ), - 'blockTypes' => array( 'core/paragraph' ), - 'content' => ' -

Hello my paragraph!

- ', - ) -); - -// Multi block transform patterns. -register_block_pattern( - 'multi/v2', - array( - 'title' => __( 'Multi blocks v2 - deep nesting', 'gutenberg' ), - 'blockTypes' => array( 'core/paragraph', 'core/heading' ), - 'content' => ' -
- -

2.Which treats of the first sally the ingenious Don Quixote made from home

- - -
- -

These preliminaries settled, he did not care to put off any longer the execution of his design, urged on to it by the thought of all the world was losing by his delay, seeing what wrongs he intended to right, grievances to redress, injustices to repair, abuses to remove, and duties to discharge.

- -
- - -

Pattern Heading

- -
- ', - ) -); -register_block_pattern( - 'multi/v1', - array( - 'title' => __( 'Multi blocks v1', 'gutenberg' ), - 'blockTypes' => array( 'core/paragraph', 'core/heading' ), - 'content' => ' -
-

2.Which treats of the first sally the ingenious Don Quixote made from home

- - -

These preliminaries settled, he did not care to put off any longer the execution of his design, urged on to it by the thought of all the world was losing by his delay, seeing what wrongs he intended to right, grievances to redress, injustices to repair, abuses to remove, and duties to discharge.

-
- ', - ) -); - -register_block_pattern( - 'multi/v3', - array( - 'title' => __( 'Multi blocks v3', 'gutenberg' ), - 'blockTypes' => array( 'core/list', 'core/paragraph' ), - 'content' => ' -
  • pattern list item 1
  • pattern list item 1
- - -

These preliminaries settled, he did not care to put off any longer the execution of his design, urged on to it by the thought of all the world was losing by his delay, seeing what wrongs he intended to right, grievances to redress, injustices to repair, abuses to remove, and duties to discharge.

- ', - ) -); - -register_block_pattern( - 'Search', - array( - 'title' => __( 'Search v1', 'gutenberg' ), - 'blockTypes' => array( 'core/search' ), - 'content' => ' -
- -

Search something

- - -
- -
- ', - ) -); diff --git a/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js b/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js index 765787adb216d..c3516f7e3ec79 100644 --- a/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js +++ b/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js @@ -119,7 +119,7 @@ function BlockPattern( { pattern, onSelect, composite } ) { >
{ pattern.title } From a4a217d250cc89d800282c171475b6a7b7b52bd8 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Tue, 13 Apr 2021 18:38:50 +0300 Subject: [PATCH 13/13] update social pattern --- lib/block-patterns.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/block-patterns.php b/lib/block-patterns.php index c905e90a1ecc6..d578cade9bf04 100644 --- a/lib/block-patterns.php +++ b/lib/block-patterns.php @@ -141,8 +141,8 @@ 'blockTypes' => array( 'core/paragraph' ), 'viewportWidth' => 500, 'content' => ' - - ', + + ', ) ); register_block_pattern( @@ -151,10 +151,10 @@ 'title' => __( 'Social links with a shared background color', 'gutenberg' ), 'blockTypes' => array( 'core/social-links' ), 'viewportWidth' => 500, - 'content' => ' - - ', + 'content' => ' + + ', ) );