From 7a250ca5fa54e9a8dea3fef23c03544d63c97612 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Tue, 16 Mar 2021 13:54:34 +0200 Subject: [PATCH 01/14] poc - part 1 --- lib/load.php | 1 + lib/test-block-patterns.php | 179 +++++++++++++ .../src/components/block-switcher/index.js | 59 ++++- .../pattern-transformations-menu.js | 239 ++++++++++++++++++ .../preview-patterns-popover.js | 31 +++ .../src/components/block-switcher/style.scss | 7 + packages/block-editor/src/store/selectors.js | 109 +++++++- 7 files changed, 616 insertions(+), 9 deletions(-) create mode 100644 lib/test-block-patterns.php create mode 100644 packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js create mode 100644 packages/block-editor/src/components/block-switcher/preview-patterns-popover.js diff --git a/lib/load.php b/lib/load.php index cf4b89c9d9fa3e..7c924221bd46d7 100644 --- a/lib/load.php +++ b/lib/load.php @@ -105,6 +105,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 00000000000000..6976c8f4445bba --- /dev/null +++ b/lib/test-block-patterns.php @@ -0,0 +1,179 @@ + __( '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

+
+ + + +
+ +
+
+', + ) +); diff --git a/packages/block-editor/src/components/block-switcher/index.js b/packages/block-editor/src/components/block-switcher/index.js index ef0d601c495c86..32d42d04267984 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,11 +41,15 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { hasBlockStyles, icon, blockTitle, + patterns, + replaceMode, } = useSelect( ( select ) => { - const { getBlockRootClientId, getBlockTransformItems } = select( - blockEditorStore - ); + const { + getBlockRootClientId, + getBlockTransformItems, + __experimentalGetPatternTransformItems, + } = select( blockEditorStore ); const { getBlockStyles, getBlockType } = select( blocksStore ); const rootClientId = getBlockRootClientId( @@ -66,7 +71,10 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { ? getBlockType( firstBlockName )?.icon : stack; } - + const _patterns = __experimentalGetPatternTransformItems( + blocks, + rootClientId + ); return { possibleBlockTransformations: getBlockTransformItems( blocks, @@ -75,6 +83,11 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { hasBlockStyles: !! styles?.length, icon: _icon, blockTitle: getBlockType( firstBlockName ).title, + patterns: _patterns, + // Need more thought here. + replaceMode: + _isSingleBlockSelected && + firstBlockName === 'core/template-part', }; }, [ clientIds, blocks, blockInformation?.icon ] @@ -83,9 +96,21 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { const isReusable = blocks.length === 1 && isReusableBlock( blocks[ 0 ] ); const isTemplate = blocks.length === 1 && isTemplatePart( blocks[ 0 ] ); - const onTransform = ( name ) => + const onBlockTransform = ( name ) => replaceBlocks( clientIds, switchToBlockType( blocks, name ) ); + // TODO comments (for the above too). + const onPatternTransform = ( transformedBlocks ) => { + // If on replaceMode (currently single Template Part block selected) + // we probably have to create a new Template part. + // This is not implemented yet!! + if ( ! replaceMode ) { + replaceBlocks( clientIds, transformedBlocks ); + } else { + // Handle Template Parts change/creation flow. + } + }; const hasPossibleBlockTransformations = !! possibleBlockTransformations.length; + const hasPatternTransformation = !! patterns?.length; if ( ! hasBlockStyles && ! hasPossibleBlockTransformations ) { return ( @@ -114,6 +139,10 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { blocks.length ); + const showDropDown = + hasBlockStyles || + hasPossibleBlockTransformations || + hasPatternTransformation; return ( @@ -147,9 +176,23 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { menuProps={ { orientation: 'both' } } > { ( { onClose } ) => - ( hasBlockStyles || - hasPossibleBlockTransformations ) && ( + showDropDown && (
+ { hasPatternTransformation && ( + { + onPatternTransform( + transformedBlocks + ); + onClose(); + } } + replaceMode={ replaceMode } + /> + ) } { 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 00000000000000..7bdbf88538d9dc --- /dev/null +++ b/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js @@ -0,0 +1,239 @@ +/** + * External dependencies + */ +import { cloneDeep } from 'lodash'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState, useMemo } from '@wordpress/element'; +import { useInstanceId } from '@wordpress/compose'; +import { chevronRight } from '@wordpress/icons'; +import { cloneBlock } 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 deep cloned the pattern from state. + * + * @param {*} parsedBlock + * @param {*} selectedBlockName + * @param {*} transformedBlocks + */ +// TODO jsdoc +// 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, + replaceMode = false, +} ) { + const [ showTransforms, setShowTransforms ] = useState( false ); + // Replace mode is if we want to replace all contents of selected block + // and not try to transform the selected blocks. This mode is set when a + // single block is selected and currently is a Template Part. + const patterns = useMemo( () => { + let _patterns; + if ( replaceMode ) { + _patterns = statePatterns.map( ( statePattern ) => ( { + ...statePattern, + // TODO check `cloneBlock` better and why was producing wrong results. + transformedBlocks: cloneDeep( statePattern.contentBlocks ), // statePattern.contentBlocks.map( cloneBlock ), + } ) ); + } else { + _patterns = statePatterns.reduce( ( accumulator, statePattern ) => { + // Clone deep the parsed pattern's block in `transformedBlocks` + // to mutate this prop. + const pattern = { + ...statePattern, + transformedBlocks: statePattern.contentBlocks.map( + cloneBlock + ), + }; + 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 (mutate). + patternBlocks.forEach( ( patternBlock ) => { + const match = findMatchingBlockInPattern( + patternBlock, + block.name, + transformedBlocksSet + ); + if ( ! match ) return; + // Found a match so update it with the selected block's attributes. + match.attributes = { + ...match.attributes, + ...block.attributes, + }; + // TODO check innerBlocks handling :) - not sure yet. + // match.innerBlocks = [ + // ...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; + } + // Maybe prioritize first matches with fewer tries to find a match? + accumulator.push( pattern ); + return accumulator; + }, [] ); + } + return _patterns; + }, [ replaceMode, 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 ) => ( + + ) ) } + + ); +} + +// 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'; + // TODO check viewportWidth. From pattern? From resizeObserver to have current width + // and manipulate later?? + const descriptionId = useInstanceId( + BlockPattern, + `${ baseClassName }-list__item-description` + ); + return ( +
+ onSelect( pattern.transformedBlocks ) } + > + + + { !! pattern.description && ( + + { pattern.description } + + ) } +
+ ); +} + +export default PatternTransformationsMenu; diff --git a/packages/block-editor/src/components/block-switcher/preview-patterns-popover.js b/packages/block-editor/src/components/block-switcher/preview-patterns-popover.js new file mode 100644 index 00000000000000..5ec6b18e16417a --- /dev/null +++ b/packages/block-editor/src/components/block-switcher/preview-patterns-popover.js @@ -0,0 +1,31 @@ +// /** +// * WordPress dependencies +// */ +// import { __ } from '@wordpress/i18n'; +// import { Popover } from '@wordpress/components'; + +// /** +// * Internal dependencies +// */ +// import BlockPreview from '../block-preview'; + +// export default function PreviewPatternsPopover( { blocks } ) { +// return ( +//
+//
+// +//
+//
+// { __( 'Preview' ) } +//
+// +//
+//
+//
+//
+// ); +// } diff --git a/packages/block-editor/src/components/block-switcher/style.scss b/packages/block-editor/src/components/block-switcher/style.scss index 1a506d3d084b09..5d8cf18e7bebad 100644 --- a/packages/block-editor/src/components/block-switcher/style.scss +++ b/packages/block-editor/src/components/block-switcher/style.scss @@ -182,3 +182,10 @@ } } } + +.block-editor-block-switcher__preview-patterns-container { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: $grid-unit-10; + padding: 1px; +} diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index d3dcb13a583149..ae7dc010904917 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -27,6 +27,7 @@ import { hasBlockSupport, getPossibleBlockTransformations, parse, + getBlockVariations, } from '@wordpress/blocks'; import { SVG, Rect, G, Path } from '@wordpress/components'; import { Platform } from '@wordpress/element'; @@ -1860,7 +1861,7 @@ export const __experimentalGetAllowedPatterns = createSelector( export const __experimentalGetScopedBlockPatterns = createSelector( ( state, scope, blockName ) => { if ( ! scope && ! blockName ) return EMPTY_ARRAY; - const patterns = state.settings.__experimentalBlockPatterns; + const patterns = __experimentalGetAllowedPatterns( state ); return patterns.filter( ( pattern ) => pattern.scope?.[ scope ]?.includes?.( blockName ) ); @@ -1868,6 +1869,112 @@ export const __experimentalGetScopedBlockPatterns = createSelector( ( state ) => [ state.settings.__experimentalBlockPatterns ] ); +/** + * Determines the items that appear in the available pattern transforms list. + * + * @param {Object} state Editor state. + * @param {Object[]} blocks The selected blocks. + * @param {?string} rootClientId Optional root client ID of block list. + * + * @return {WPEditorTransformItem[]} Items that are eligible for a pattern transformation. + */ +// TODO jsdoc +// TODO tests +export const __experimentalGetPatternTransformItems = createSelector( + ( state, blocks, rootClientId = null ) => { + if ( ! blocks ) return EMPTY_ARRAY; + // 1. fetch scoped ('transform') + // 2. check if pattern is allowed based on rootClientId + // 3. prioritize them (?) - not implemented yet. + // - Most matching blocks + + // If a selected block is nested (with InnerBlocks like Group or Columns) + // return nothing, as it doesn't make sense to try to be too smart. + // In a Group/Column a user could have anything inside as content + // so don't show any transforms. + // In these blocks it only makes sense to show `block` scoped patterns, + // during insertion that no content exists. + + // An exception could be Template Parts that IMO makes sense to replace everything + // and maybe try to be a bit smart about it with some InnerBlocks transforms (?). + const isSingleBlockSelected = blocks.length === 1; + const nestedSinglesToHandle = [ 'core/template-part' ]; + if ( + isSingleBlockSelected && + nestedSinglesToHandle.includes( blocks[ 0 ].name ) + ) { + // Try to find an active block variation for `template-parts`, + // to show patterns with `scope` `core/template-part/{variation name}. + const [ selectedBlock ] = blocks; + let { name: blockName } = selectedBlock; + const variations = getBlockVariations( blockName ); + if ( variations?.length ) { + const match = variations.find( ( variation ) => + variation.isActive?.( + selectedBlock.attributes, + variation.attributes + ) + ); + if ( match ) { + blockName = `${ blockName }/${ match.name }`; + } + } + // TODO Need to consolidate `__experimentalGetScopedBlockPatterns` with + // `__experimentalGetAllowedPatterns` that return the pattern's blocks parsed. + return __experimentalGetScopedBlockPatterns( + state, + 'transform', + blockName + ); + } + const selectedBlockNames = blocks.reduce( ( accumulator, block ) => { + accumulator.add( block.name ); + return accumulator; + }, new Set() ); + // TODO maybe skip blocks with InnerBlocks and have a whiteList and maybe a filter + // for that list?? We could check if the block has InnerBlocks. + // We have to be carefull with BlockSwitcher that uses the selector that + // doesn't fill the `controlled` InnerBlocks like Template Parts. + const blocksToSkip = [ 'core/group', 'core/columns' ]; + + // If we have a selected block that we don't want to handle (`blocksToSkip`) + // return nothing. + if ( + blocksToSkip.some( ( blockToSkip ) => + selectedBlockNames.has( blockToSkip ) + ) + ) { + return EMPTY_ARRAY; + } + + // Fetched patterns from `__experimentalGetAllowedPatterns` + // have parsed blocks in `contentBlocks` prop. + const allowedPatterns = __experimentalGetAllowedPatterns( + state, + rootClientId + ); + // Here we will return first set of possible eligible + // block patterns, by checking the `scope` prop. + // 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 allowedPatterns.filter( ( pattern ) => + pattern.scope?.transform?.some?.( ( blockName ) => + selectedBlockNames.has( blockName ) + ) + ); + }, + // TODO check deps + ( state, rootClientId ) => [ + state.blockListSettings[ rootClientId ], + state.blocks.byClientId, + state.settings.allowedBlockTypes, + state.settings.templateLock, + getBlockTypes(), + ] +); + /** * Returns the Block List settings of a block, if any exist. * From 7047be3c2fe3b9ad3d1a661107133faeeba74eac Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Tue, 16 Mar 2021 14:14:30 +0200 Subject: [PATCH 02/14] clone deep --- .../block-switcher/pattern-transformations-menu.js | 5 +---- 1 file changed, 1 insertion(+), 4 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 7bdbf88538d9dc..9fb7731d9f4328 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 @@ -10,7 +10,6 @@ import { __ } from '@wordpress/i18n'; import { useState, useMemo } from '@wordpress/element'; import { useInstanceId } from '@wordpress/compose'; import { chevronRight } from '@wordpress/icons'; -import { cloneBlock } from '@wordpress/blocks'; import { MenuGroup, MenuItem, @@ -90,9 +89,7 @@ function PatternTransformationsMenu( { // to mutate this prop. const pattern = { ...statePattern, - transformedBlocks: statePattern.contentBlocks.map( - cloneBlock - ), + transformedBlocks: cloneDeep( statePattern.contentBlocks ), }; const { transformedBlocks: patternBlocks } = pattern; const transformedBlocksSet = new Set(); From 8143345ba8464e10d624c583b045a0127af7030a Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Tue, 16 Mar 2021 14:49:08 +0200 Subject: [PATCH 03/14] remove extra file --- .../preview-patterns-popover.js | 31 ------------------- 1 file changed, 31 deletions(-) delete mode 100644 packages/block-editor/src/components/block-switcher/preview-patterns-popover.js diff --git a/packages/block-editor/src/components/block-switcher/preview-patterns-popover.js b/packages/block-editor/src/components/block-switcher/preview-patterns-popover.js deleted file mode 100644 index 5ec6b18e16417a..00000000000000 --- a/packages/block-editor/src/components/block-switcher/preview-patterns-popover.js +++ /dev/null @@ -1,31 +0,0 @@ -// /** -// * WordPress dependencies -// */ -// import { __ } from '@wordpress/i18n'; -// import { Popover } from '@wordpress/components'; - -// /** -// * Internal dependencies -// */ -// import BlockPreview from '../block-preview'; - -// export default function PreviewPatternsPopover( { blocks } ) { -// return ( -//
-//
-// -//
-//
-// { __( 'Preview' ) } -//
-// -//
-//
-//
-//
-// ); -// } From cbe3e771f51a3df17e2027303acf1ef0ef421990 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Thu, 18 Mar 2021 14:46:34 +0200 Subject: [PATCH 04/14] Tidy up and handle Template Part replacement and proper cloning of blocks --- lib/test-block-patterns.php | 35 ++++++++ .../src/components/block-switcher/index.js | 19 ++-- .../pattern-transformations-menu.js | 31 ++++--- packages/block-editor/src/store/selectors.js | 88 +++++++++---------- 4 files changed, 104 insertions(+), 69 deletions(-) diff --git a/lib/test-block-patterns.php b/lib/test-block-patterns.php index 6976c8f4445bba..af008a0c6156dd 100644 --- a/lib/test-block-patterns.php +++ b/lib/test-block-patterns.php @@ -177,3 +177,38 @@ ', ) ); + + +// 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 32d42d04267984..2153b7878b5658 100644 --- a/packages/block-editor/src/components/block-switcher/index.js +++ b/packages/block-editor/src/components/block-switcher/index.js @@ -34,7 +34,9 @@ import BlockStylesMenu from './block-styles-menu'; import PatternTransformationsMenu from './pattern-transformations-menu'; export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { - const { replaceBlocks } = useDispatch( blockEditorStore ); + const { replaceBlocks, replaceInnerBlocks } = useDispatch( + blockEditorStore + ); const blockInformation = useBlockDisplayInformation( blocks[ 0 ].clientId ); const { possibleBlockTransformations, @@ -84,7 +86,8 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { icon: _icon, blockTitle: getBlockType( firstBlockName ).title, patterns: _patterns, - // Need more thought here. + // TODO Need more thought here. We have the same check for template + // part in the `__experimentalGetPatternTransformItems` selector. replaceMode: _isSingleBlockSelected && firstBlockName === 'core/template-part', @@ -96,17 +99,17 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { const isReusable = blocks.length === 1 && isReusableBlock( blocks[ 0 ] ); const isTemplate = blocks.length === 1 && isTemplatePart( blocks[ 0 ] ); + // Simple block tranformation based on the `Block Transforms` API. const onBlockTransform = ( name ) => replaceBlocks( clientIds, switchToBlockType( blocks, name ) ); - // TODO comments (for the above too). + // Pattern transformation through the `Patterns` API. const onPatternTransform = ( transformedBlocks ) => { // If on replaceMode (currently single Template Part block selected) - // we probably have to create a new Template part. - // This is not implemented yet!! - if ( ! replaceMode ) { - replaceBlocks( clientIds, transformedBlocks ); + // we replace the InnerBlocks of the selected block. That means + if ( replaceMode ) { + replaceInnerBlocks( clientIds[ 0 ], transformedBlocks ); } else { - // Handle Template Parts change/creation flow. + replaceBlocks( clientIds, transformedBlocks ); } }; const hasPossibleBlockTransformations = !! possibleBlockTransformations.length; 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 9fb7731d9f4328..92d4dd1623e13b 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 @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { cloneDeep } from 'lodash'; - /** * WordPress dependencies */ @@ -10,6 +5,7 @@ import { __ } from '@wordpress/i18n'; import { useState, useMemo } from '@wordpress/element'; import { useInstanceId } from '@wordpress/compose'; import { chevronRight } from '@wordpress/icons'; +import { cloneBlock } from '@wordpress/blocks'; import { MenuGroup, MenuItem, @@ -80,23 +76,26 @@ function PatternTransformationsMenu( { if ( replaceMode ) { _patterns = statePatterns.map( ( statePattern ) => ( { ...statePattern, - // TODO check `cloneBlock` better and why was producing wrong results. - transformedBlocks: cloneDeep( statePattern.contentBlocks ), // statePattern.contentBlocks.map( cloneBlock ), + transformedBlocks: statePattern.contentBlocks.map( ( block ) => + cloneBlock( block ) + ), } ) ); } else { _patterns = statePatterns.reduce( ( accumulator, statePattern ) => { - // Clone deep the parsed pattern's block in `transformedBlocks` + // Clone the parsed pattern's block in `transformedBlocks` // to mutate this prop. const pattern = { ...statePattern, - transformedBlocks: cloneDeep( statePattern.contentBlocks ), + transformedBlocks: statePattern.contentBlocks.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 (mutate). + // and transform these blocks (we mutate patternBlocks). patternBlocks.forEach( ( patternBlock ) => { const match = findMatchingBlockInPattern( patternBlock, @@ -109,11 +108,10 @@ function PatternTransformationsMenu( { ...match.attributes, ...block.attributes, }; - // TODO check innerBlocks handling :) - not sure yet. - // match.innerBlocks = [ - // ...match.innerBlocks, - // ...block.innerBlocks, - // ]; + // 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 @@ -121,7 +119,7 @@ function PatternTransformationsMenu( { if ( blocks.length !== transformedBlocksSet.size ) { return accumulator; } - // Maybe prioritize first matches with fewer tries to find a match? + // TODO Maybe prioritize first matches with fewer tries to find a match? accumulator.push( pattern ); return accumulator; }, [] ); @@ -130,6 +128,7 @@ function PatternTransformationsMenu( { }, [ replaceMode, statePatterns ] ); if ( ! patterns.length ) return null; + return ( { showTransforms && ( diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index ae7dc010904917..60c4d2a7c18893 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1855,22 +1855,31 @@ export const __experimentalGetAllowedPatterns = createSelector( * @param {Object} state Editor state. * @param {string} scope Block pattern scope. * @param {string} blockName Block's name. + * @param {?string} rootClientId Optional target root client ID. * * @return {Array} The list of matched block patterns based on provided scope and block name. */ export const __experimentalGetScopedBlockPatterns = createSelector( - ( state, scope, blockName ) => { + ( state, scope, blockName, rootClientId ) => { if ( ! scope && ! blockName ) return EMPTY_ARRAY; - const patterns = __experimentalGetAllowedPatterns( state ); + const patterns = __experimentalGetAllowedPatterns( + state, + rootClientId + ); return patterns.filter( ( pattern ) => pattern.scope?.[ scope ]?.includes?.( blockName ) ); }, + // TODO check deps. ( state ) => [ state.settings.__experimentalBlockPatterns ] ); /** * Determines the items that appear in the available pattern transforms list. + * 1. fetch scoped ('transform') + * 2. check if pattern is allowed based on rootClientId + * 3. prioritize them (?) - not implemented yet. + * - Most matching blocks * * @param {Object} state Editor state. * @param {Object[]} blocks The selected blocks. @@ -1883,30 +1892,41 @@ export const __experimentalGetScopedBlockPatterns = createSelector( export const __experimentalGetPatternTransformItems = createSelector( ( state, blocks, rootClientId = null ) => { if ( ! blocks ) return EMPTY_ARRAY; - // 1. fetch scoped ('transform') - // 2. check if pattern is allowed based on rootClientId - // 3. prioritize them (?) - not implemented yet. - // - Most matching blocks + // Create a Set of the selected block names that is used in patterns filtering. + const selectedBlockNames = blocks.reduce( ( accumulator, block ) => { + accumulator.add( block.name ); + return accumulator; + }, new Set() ); // If a selected block is nested (with InnerBlocks like Group or Columns) - // return nothing, as it doesn't make sense to try to be too smart. - // In a Group/Column a user could have anything inside as content - // so don't show any transforms. - // In these blocks it only makes sense to show `block` scoped patterns, - // during insertion that no content exists. - - // An exception could be Template Parts that IMO makes sense to replace everything - // and maybe try to be a bit smart about it with some InnerBlocks transforms (?). + // return an EMPTY_ARRAY, as it doesn't make sense to try to be too smart. + // In such blocks a user could have anything inside as content so don't show + // any transforms. In these blocks it only makes sense to show `block` scoped patterns + // during insertion, where no content exists yet. + const blocksToSkip = [ 'core/group', 'core/columns' ]; + if ( + blocksToSkip.some( ( blockName ) => + selectedBlockNames.has( blockName ) + ) + ) { + return EMPTY_ARRAY; + } + + // An exception to the above is when we have a single `Template Part` + // selected, that makes sense to replace everything. + // TODO maybe try to be a bit smart about it with some InnerBlocks transforms (?). const isSingleBlockSelected = blocks.length === 1; - const nestedSinglesToHandle = [ 'core/template-part' ]; + // TODO do we need to handle other blocks like this?? + const nestedSingleBlocksToHandle = [ 'core/template-part' ]; if ( isSingleBlockSelected && - nestedSinglesToHandle.includes( blocks[ 0 ].name ) + nestedSingleBlocksToHandle.includes( blocks[ 0 ].name ) ) { - // Try to find an active block variation for `template-parts`, - // to show patterns with `scope` `core/template-part/{variation name}. const [ selectedBlock ] = blocks; let { name: blockName } = selectedBlock; + // Try to find an active block variation for `Template Part`, + // to show patterns with `scope` including the block variation + // name (ex. `core/template-part/{variation name}). const variations = getBlockVariations( blockName ); if ( variations?.length ) { const match = variations.find( ( variation ) => @@ -1919,46 +1939,23 @@ export const __experimentalGetPatternTransformItems = createSelector( blockName = `${ blockName }/${ match.name }`; } } - // TODO Need to consolidate `__experimentalGetScopedBlockPatterns` with - // `__experimentalGetAllowedPatterns` that return the pattern's blocks parsed. return __experimentalGetScopedBlockPatterns( state, 'transform', blockName ); } - const selectedBlockNames = blocks.reduce( ( accumulator, block ) => { - accumulator.add( block.name ); - return accumulator; - }, new Set() ); - // TODO maybe skip blocks with InnerBlocks and have a whiteList and maybe a filter - // for that list?? We could check if the block has InnerBlocks. - // We have to be carefull with BlockSwitcher that uses the selector that - // doesn't fill the `controlled` InnerBlocks like Template Parts. - const blocksToSkip = [ 'core/group', 'core/columns' ]; - - // If we have a selected block that we don't want to handle (`blocksToSkip`) - // return nothing. - if ( - blocksToSkip.some( ( blockToSkip ) => - selectedBlockNames.has( blockToSkip ) - ) - ) { - return EMPTY_ARRAY; - } - // Fetched patterns from `__experimentalGetAllowedPatterns` - // have parsed blocks in `contentBlocks` prop. - const allowedPatterns = __experimentalGetAllowedPatterns( - state, - rootClientId - ); // Here we will return first set of possible eligible // block patterns, by checking the `scope` prop. // 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. + const allowedPatterns = __experimentalGetAllowedPatterns( + state, + rootClientId + ); return allowedPatterns.filter( ( pattern ) => pattern.scope?.transform?.some?.( ( blockName ) => selectedBlockNames.has( blockName ) @@ -1966,6 +1963,7 @@ export const __experimentalGetPatternTransformItems = createSelector( ); }, // TODO check deps + // we need this updated when a template-part area is changed. ( state, rootClientId ) => [ state.blockListSettings[ rootClientId ], state.blocks.byClientId, From cbb12cd58bd5f395422e3907ce09d3e2a5f5fa17 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Thu, 18 Mar 2021 15:18:47 +0200 Subject: [PATCH 05/14] minor comment change --- packages/block-editor/src/components/block-switcher/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/block-switcher/index.js b/packages/block-editor/src/components/block-switcher/index.js index 2153b7878b5658..2d09de266b978f 100644 --- a/packages/block-editor/src/components/block-switcher/index.js +++ b/packages/block-editor/src/components/block-switcher/index.js @@ -105,7 +105,8 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { // Pattern transformation through the `Patterns` API. const onPatternTransform = ( transformedBlocks ) => { // If on replaceMode (currently single Template Part block selected) - // we replace the InnerBlocks of the selected block. That means + // we replace the InnerBlocks of the selected block. + // TODO probably rename this variable. if ( replaceMode ) { replaceInnerBlocks( clientIds[ 0 ], transformedBlocks ); } else { From 04438f7fe43eca11b1a69b25eadff375f99e0f2e Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Mon, 22 Mar 2021 13:02:31 +0200 Subject: [PATCH 06/14] jsdoc and small refactorings --- .../src/components/block-switcher/index.js | 20 +++-- .../pattern-transformations-menu.js | 6 +- packages/block-editor/src/store/selectors.js | 83 +++++++++++-------- 3 files changed, 64 insertions(+), 45 deletions(-) diff --git a/packages/block-editor/src/components/block-switcher/index.js b/packages/block-editor/src/components/block-switcher/index.js index 2d09de266b978f..56bd8746bde0c8 100644 --- a/packages/block-editor/src/components/block-switcher/index.js +++ b/packages/block-editor/src/components/block-switcher/index.js @@ -33,6 +33,8 @@ import BlockTransformationsMenu from './block-transformations-menu'; import BlockStylesMenu from './block-styles-menu'; import PatternTransformationsMenu from './pattern-transformations-menu'; +const nestedSingleBlocksToHandle = [ 'core/template-part' ]; + export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { const { replaceBlocks, replaceInnerBlocks } = useDispatch( blockEditorStore @@ -44,7 +46,7 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { icon, blockTitle, patterns, - replaceMode, + replaceInnerBlocksMode, } = useSelect( ( select ) => { const { @@ -88,9 +90,9 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { patterns: _patterns, // TODO Need more thought here. We have the same check for template // part in the `__experimentalGetPatternTransformItems` selector. - replaceMode: + replaceInnerBlocksMode: _isSingleBlockSelected && - firstBlockName === 'core/template-part', + nestedSingleBlocksToHandle.includes( firstBlockName ), }; }, [ clientIds, blocks, blockInformation?.icon ] @@ -104,10 +106,10 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { replaceBlocks( clientIds, switchToBlockType( blocks, name ) ); // Pattern transformation through the `Patterns` API. const onPatternTransform = ( transformedBlocks ) => { - // If on replaceMode (currently single Template Part block selected) - // we replace the InnerBlocks of the selected block. - // TODO probably rename this variable. - if ( replaceMode ) { + // If on replaceInnerBlocksMode we replace the InnerBlocks of + // the selected block (currently single block selected and one + // of `nestedSingleBlocksToHandle`). + if ( replaceInnerBlocksMode ) { replaceInnerBlocks( clientIds[ 0 ], transformedBlocks ); } else { replaceBlocks( clientIds, transformedBlocks ); @@ -194,7 +196,9 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { ); onClose(); } } - replaceMode={ replaceMode } + replaceInnerBlocksMode={ + replaceInnerBlocksMode + } /> ) } { hasPossibleBlockTransformations && ( 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 92d4dd1623e13b..f1ce26e69de06b 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 @@ -65,7 +65,7 @@ function PatternTransformationsMenu( { blocks, patterns: statePatterns, onSelect, - replaceMode = false, + replaceInnerBlocksMode = false, } ) { const [ showTransforms, setShowTransforms ] = useState( false ); // Replace mode is if we want to replace all contents of selected block @@ -73,7 +73,7 @@ function PatternTransformationsMenu( { // single block is selected and currently is a Template Part. const patterns = useMemo( () => { let _patterns; - if ( replaceMode ) { + if ( replaceInnerBlocksMode ) { _patterns = statePatterns.map( ( statePattern ) => ( { ...statePattern, transformedBlocks: statePattern.contentBlocks.map( ( block ) => @@ -125,7 +125,7 @@ function PatternTransformationsMenu( { }, [] ); } return _patterns; - }, [ replaceMode, statePatterns ] ); + }, [ replaceInnerBlocksMode, statePatterns ] ); if ( ! patterns.length ) return null; diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 60c4d2a7c18893..f1dd9928a8bbb0 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1870,16 +1870,27 @@ export const __experimentalGetScopedBlockPatterns = createSelector( pattern.scope?.[ scope ]?.includes?.( blockName ) ); }, - // TODO check deps. - ( state ) => [ state.settings.__experimentalBlockPatterns ] + ( state, rootClientId ) => [ + state.settings.__experimentalBlockPatterns, + state.settings.allowedBlockTypes, + state.settings.templateLock, + state.blockListSettings[ rootClientId ], + state.blocks.byClientId[ rootClientId ], + ] ); /** * Determines the items that appear in the available pattern transforms list. - * 1. fetch scoped ('transform') - * 2. check if pattern is allowed based on rootClientId - * 3. prioritize them (?) - not implemented yet. - * - Most matching blocks + * 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. @@ -1887,36 +1898,13 @@ export const __experimentalGetScopedBlockPatterns = createSelector( * * @return {WPEditorTransformItem[]} Items that are eligible for a pattern transformation. */ -// TODO jsdoc // TODO tests export const __experimentalGetPatternTransformItems = createSelector( ( state, blocks, rootClientId = null ) => { if ( ! blocks ) return EMPTY_ARRAY; - // Create a Set of the selected block names that is used in patterns filtering. - const selectedBlockNames = blocks.reduce( ( accumulator, block ) => { - accumulator.add( block.name ); - return accumulator; - }, new Set() ); - - // If a selected block is nested (with InnerBlocks like Group or Columns) - // return an EMPTY_ARRAY, as it doesn't make sense to try to be too smart. - // In such blocks a user could have anything inside as content so don't show - // any transforms. In these blocks it only makes sense to show `block` scoped patterns - // during insertion, where no content exists yet. - const blocksToSkip = [ 'core/group', 'core/columns' ]; - if ( - blocksToSkip.some( ( blockName ) => - selectedBlockNames.has( blockName ) - ) - ) { - return EMPTY_ARRAY; - } - - // An exception to the above is when we have a single `Template Part` - // selected, that makes sense to replace everything. - // TODO maybe try to be a bit smart about it with some InnerBlocks transforms (?). + // There are some blocks like `Template Part` that makes sense + // to replace everything when they are the only block selected. const isSingleBlockSelected = blocks.length === 1; - // TODO do we need to handle other blocks like this?? const nestedSingleBlocksToHandle = [ 'core/template-part' ]; if ( isSingleBlockSelected && @@ -1926,7 +1914,9 @@ export const __experimentalGetPatternTransformItems = createSelector( let { name: blockName } = selectedBlock; // Try to find an active block variation for `Template Part`, // to show patterns with `scope` including the block variation - // name (ex. `core/template-part/{variation name}). + // name (ex. `core/template-part/{variation name}). If no active + // block variation is found, we search for patterns for the base + // block. const variations = getBlockVariations( blockName ); if ( variations?.length ) { const match = variations.find( ( variation ) => @@ -1946,6 +1936,31 @@ export const __experimentalGetPatternTransformItems = createSelector( ); } + // Create a Set of the selected block names that is used in patterns filtering. + const selectedBlockNames = blocks.reduce( ( accumulator, block ) => { + accumulator.add( block.name ); + return accumulator; + }, new Set() ); + + // If a selected block is nested (with InnerBlocks like Group or Columns) + // return an EMPTY_ARRAY, as it doesn't make sense to try to be too smart. + // In such blocks a user could have anything inside as content, so don't show + // any transforms. In these blocks it only makes sense to show `block` scoped + // patterns during insertion, where no content exists yet. + const blocksToSkip = [ + 'core/group', + 'core/columns', + 'core/navigation', + 'core/template-part', + ]; + if ( + blocksToSkip.some( ( blockName ) => + selectedBlockNames.has( blockName ) + ) + ) { + return EMPTY_ARRAY; + } + // Here we will return first set of possible eligible // block patterns, by checking the `scope` prop. // We still have to recurse through block pattern's blocks @@ -1962,9 +1977,9 @@ export const __experimentalGetPatternTransformItems = createSelector( ) ); }, - // TODO check deps - // we need this updated when a template-part area is changed. + // TODO we need this updated when a template-part area is changed. ( state, rootClientId ) => [ + state.settings.__experimentalBlockPatterns, state.blockListSettings[ rootClientId ], state.blocks.byClientId, state.settings.allowedBlockTypes, From 7194d037965b3c55d076e2bf8cba5fb82a4f8569 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Mon, 22 Mar 2021 13:52:35 +0200 Subject: [PATCH 07/14] focus and style pattern transforms --- .../pattern-transformations-menu.js | 23 +++++++++---------- .../src/components/block-switcher/style.scss | 19 +++++++++++++++ packages/block-editor/src/store/selectors.js | 1 - 3 files changed, 30 insertions(+), 13 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 f1ce26e69de06b..3bacc8826b9b22 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 @@ -24,13 +24,15 @@ 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 deep cloned the pattern from state. + * We have first cloned the pattern blocks in a new property + * `transformedBlocks` and we mutate this. * - * @param {*} parsedBlock - * @param {*} selectedBlockName - * @param {*} transformedBlocks + * @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 jsdoc // TODO tests function findMatchingBlockInPattern( parsedBlock, @@ -68,9 +70,9 @@ function PatternTransformationsMenu( { replaceInnerBlocksMode = false, } ) { const [ showTransforms, setShowTransforms ] = useState( false ); - // Replace mode is if we want to replace all contents of selected block - // and not try to transform the selected blocks. This mode is set when a - // single block is selected and currently is a Template Part. + // `replaceInnerBlocksMode` is if we want to replace all contents of selected + // block and not try to transform the selected blocks. This mode is set when + // a single block is selected and currently is a Template Part. const patterns = useMemo( () => { let _patterns; if ( replaceInnerBlocksMode ) { @@ -157,7 +159,6 @@ function PreviewPatternsPopover( { patterns, onSelect } ) {
@@ -195,12 +196,10 @@ function BlockPatternsList( { patterns, onSelect } ) { ); } -// This needs to be consolidated to probably be reused across: Patterns in Placeholder, Inserter and here. +// 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'; - // TODO check viewportWidth. From pattern? From resizeObserver to have current width - // and manipulate later?? const descriptionId = useInstanceId( BlockPattern, `${ baseClassName }-list__item-description` diff --git a/packages/block-editor/src/components/block-switcher/style.scss b/packages/block-editor/src/components/block-switcher/style.scss index 5d8cf18e7bebad..5363b699e246b5 100644 --- a/packages/block-editor/src/components/block-switcher/style.scss +++ b/packages/block-editor/src/components/block-switcher/style.scss @@ -188,4 +188,23 @@ grid-template-columns: 1fr 1fr; grid-gap: $grid-unit-10; padding: 1px; + + .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 { + border: $border-width solid var(--wp-admin-theme-color); + } + + &: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; + } + } } diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index f1dd9928a8bbb0..2474671027671b 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1977,7 +1977,6 @@ export const __experimentalGetPatternTransformItems = createSelector( ) ); }, - // TODO we need this updated when a template-part area is changed. ( state, rootClientId ) => [ state.settings.__experimentalBlockPatterns, state.blockListSettings[ rootClientId ], From dd1660dc0b447a1e07cce38d0e34649dc3692255 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Mon, 22 Mar 2021 15:03:03 +0200 Subject: [PATCH 08/14] fix and add __experimentalGetScopedBlockPatterns tests --- packages/block-editor/src/store/selectors.js | 4 +- .../block-editor/src/store/test/selectors.js | 44 ++++++++++++++++++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 2474671027671b..d0d688aa9136ec 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1860,8 +1860,8 @@ export const __experimentalGetAllowedPatterns = createSelector( * @return {Array} The list of matched block patterns based on provided scope and block name. */ export const __experimentalGetScopedBlockPatterns = createSelector( - ( state, scope, blockName, rootClientId ) => { - if ( ! scope && ! blockName ) return EMPTY_ARRAY; + ( state, scope, blockName, rootClientId = null ) => { + if ( ! ( scope && blockName ) ) return EMPTY_ARRAY; const patterns = __experimentalGetAllowedPatterns( state, rootClientId diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 167c830f510366..ad39f37bf1bd88 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -3409,18 +3409,37 @@ describe( 'selectors', () => { } ); describe( '__experimentalGetScopedBlockPatterns', () => { const state = { - blocks: {}, + blocks: { + byClientId: { + block1: { name: 'core/test-block-a' }, + }, + }, + blockListSettings: { + block1: { + allowedBlocks: [ 'core/test-block-b' ], + }, + }, settings: { __experimentalBlockPatterns: [ { name: 'pattern-a', title: 'pattern a', scope: { block: [ 'test/block-a' ] }, + content: + '', }, { name: 'pattern-b', title: 'pattern b', scope: { block: [ 'test/block-b' ] }, + content: + '', + }, + { + title: 'pattern c', + scope: { block: [ 'test/block-a' ] }, + content: + '', }, ], }, @@ -3447,10 +3466,31 @@ describe( 'selectors', () => { 'block', 'test/block-a' ); + expect( patterns ).toHaveLength( 2 ); + expect( patterns ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + title: 'pattern a', + scope: { block: [ 'test/block-a' ] }, + } ), + expect.objectContaining( { + title: 'pattern c', + scope: { block: [ 'test/block-a' ] }, + } ), + ] ) + ); + } ); + it( 'should return proper result with matched patterns and allowed blocks from rootClientId', () => { + const patterns = __experimentalGetScopedBlockPatterns( + state, + 'block', + 'test/block-a', + 'block1' + ); expect( patterns ).toHaveLength( 1 ); expect( patterns[ 0 ] ).toEqual( expect.objectContaining( { - title: 'pattern a', + title: 'pattern c', scope: { block: [ 'test/block-a' ] }, } ) ); From fb9502d65968cb9929147ea06dece2567e71f11d Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Tue, 23 Mar 2021 10:12:11 +0200 Subject: [PATCH 09/14] single column patterns and description --- .../pattern-transformations-menu.js | 3 ++ .../src/components/block-switcher/style.scss | 48 +++++++++++-------- 2 files changed, 31 insertions(+), 20 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 3bacc8826b9b22..1b58ab4ee8caa0 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 @@ -221,6 +221,9 @@ function BlockPattern( { pattern, onSelect, composite } ) { blocks={ pattern.transformedBlocks } viewportWidth={ 500 } /> +
+ { pattern.title } +
{ !! pattern.description && ( diff --git a/packages/block-editor/src/components/block-switcher/style.scss b/packages/block-editor/src/components/block-switcher/style.scss index 5363b699e246b5..af55c44d2509d4 100644 --- a/packages/block-editor/src/components/block-switcher/style.scss +++ b/packages/block-editor/src/components/block-switcher/style.scss @@ -149,6 +149,7 @@ .block-editor-block-switcher__preview { width: 300px; height: auto; + max-height: 500px; padding: $grid-unit-20; } } @@ -184,27 +185,34 @@ } .block-editor-block-switcher__preview-patterns-container { - display: grid; - grid-template-columns: 1fr 1fr; - grid-gap: $grid-unit-10; - padding: 1px; - - .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 { - border: $border-width solid var(--wp-admin-theme-color); + .block-editor-block-switcher__preview-patterns-container-list__list-item { + margin-top: $grid-unit-20; + .block-editor-block-preview__container { + cursor: pointer; } - - &: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; + .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 { + border: $border-width solid var(--wp-admin-theme-color); + } + &: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; + } + + .block-editor-block-switcher__preview-patterns-container-list__item-title { + padding: $grid-unit-05; + font-size: 12px; + text-align: center; + cursor: pointer; + } } } + } From 7eb49a86872faa4bf5cb1d9d0c98bc9299d79940 Mon Sep 17 00:00:00 2001 From: jasmussen Date: Tue, 23 Mar 2021 09:56:04 +0100 Subject: [PATCH 10/14] Tweak paddings and hover. --- .../src/components/block-switcher/style.scss | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/block-switcher/style.scss b/packages/block-editor/src/components/block-switcher/style.scss index af55c44d2509d4..26f468c8458931 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; } } @@ -187,18 +186,19 @@ .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 { - border: $border-width solid var(--wp-admin-theme-color); - } + + &:hover, &:focus { box-shadow: inset 0 0 0 1px $white, 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); @@ -206,6 +206,10 @@ 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; From 3814f5bcc08a814385090fe8295c14ec50fb21be Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Wed, 24 Mar 2021 11:27:26 +0200 Subject: [PATCH 11/14] retainAttributes api in block's transforms --- .../pattern-transformations-menu.js | 25 ++++++++++++++++--- .../block-library/src/heading/transforms.js | 1 + .../block-library/src/paragraph/transforms.js | 1 + 3 files changed, 24 insertions(+), 3 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 1b58ab4ee8caa0..1bea3740d79ee7 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 @@ -5,7 +5,7 @@ import { __ } from '@wordpress/i18n'; import { useState, useMemo } from '@wordpress/element'; import { useInstanceId } from '@wordpress/compose'; import { chevronRight } from '@wordpress/icons'; -import { cloneBlock } from '@wordpress/blocks'; +import { cloneBlock, getBlockType } from '@wordpress/blocks'; import { MenuGroup, MenuItem, @@ -105,10 +105,29 @@ function PatternTransformationsMenu( { transformedBlocksSet ); if ( ! match ) return; - // Found a match so update it with the selected block's attributes. + // Found a match, so get the `retainAttributes` from + // block type's transforms to retain these and keep + // everything else from the pattern's block. + // If `retainAttributes` are not set, update the match + // with all the selected block's attributes. + const blockType = getBlockType( block.name ); + const retainAttributes = + blockType.transforms?.retainAttributes; + let retainedBlockAttributes = block.attributes; + if ( retainAttributes?.length ) { + retainedBlockAttributes = retainAttributes.reduce( + ( _accumulator, attribute ) => { + if ( block.attributes[ attribute ] ) + _accumulator[ attribute ] = + block.attributes[ attribute ]; + return _accumulator; + }, + {} + ); + } match.attributes = { ...match.attributes, - ...block.attributes, + ...retainedBlockAttributes, }; // When we have a match with inner blocks keep only the // blocks from the selected block and skip the inner blocks diff --git a/packages/block-library/src/heading/transforms.js b/packages/block-library/src/heading/transforms.js index 8e6768c0d19d8a..ef0e1dddbce83a 100644 --- a/packages/block-library/src/heading/transforms.js +++ b/packages/block-library/src/heading/transforms.js @@ -10,6 +10,7 @@ import { getLevelFromHeadingNodeName } from './shared'; import { name } from './block.json'; const transforms = { + retainAttributes: [ 'content' ], from: [ { type: 'block', diff --git a/packages/block-library/src/paragraph/transforms.js b/packages/block-library/src/paragraph/transforms.js index 5179a7f05c22fa..3ee12658bda9c4 100644 --- a/packages/block-library/src/paragraph/transforms.js +++ b/packages/block-library/src/paragraph/transforms.js @@ -9,6 +9,7 @@ import { createBlock, getBlockAttributes } from '@wordpress/blocks'; import { name } from './block.json'; const transforms = { + retainAttributes: [ 'content' ], from: [ { type: 'raw', From 21e8e75a24abe1e20a79010c8e4a1d2533c68dc3 Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Wed, 24 Mar 2021 12:03:16 +0200 Subject: [PATCH 12/14] use `to` transforms for patterns --- .../block-switcher/pattern-transformations-menu.js | 10 ++++++---- packages/block-library/src/heading/transforms.js | 2 +- packages/block-library/src/paragraph/transforms.js | 2 +- 3 files changed, 8 insertions(+), 6 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 1bea3740d79ee7..f2579cc69f3cc5 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 @@ -5,7 +5,7 @@ import { __ } from '@wordpress/i18n'; import { useState, useMemo } from '@wordpress/element'; import { useInstanceId } from '@wordpress/compose'; import { chevronRight } from '@wordpress/icons'; -import { cloneBlock, getBlockType } from '@wordpress/blocks'; +import { cloneBlock, getBlockTransforms } from '@wordpress/blocks'; import { MenuGroup, MenuItem, @@ -110,9 +110,11 @@ function PatternTransformationsMenu( { // everything else from the pattern's block. // If `retainAttributes` are not set, update the match // with all the selected block's attributes. - const blockType = getBlockType( block.name ); - const retainAttributes = - blockType.transforms?.retainAttributes; + const retainAttributes = getBlockTransforms( + 'to', + block.name + ).find( ( { type } ) => type === 'pattern' ) + ?.retainAttributes; let retainedBlockAttributes = block.attributes; if ( retainAttributes?.length ) { retainedBlockAttributes = retainAttributes.reduce( diff --git a/packages/block-library/src/heading/transforms.js b/packages/block-library/src/heading/transforms.js index ef0e1dddbce83a..17fcb2719e6055 100644 --- a/packages/block-library/src/heading/transforms.js +++ b/packages/block-library/src/heading/transforms.js @@ -10,7 +10,6 @@ import { getLevelFromHeadingNodeName } from './shared'; import { name } from './block.json'; const transforms = { - retainAttributes: [ 'content' ], from: [ { type: 'block', @@ -92,6 +91,7 @@ const transforms = { } ) ), }, + { type: 'pattern', retainAttributes: [ 'content' ] }, ], }; diff --git a/packages/block-library/src/paragraph/transforms.js b/packages/block-library/src/paragraph/transforms.js index 3ee12658bda9c4..e59b01ad14e3b9 100644 --- a/packages/block-library/src/paragraph/transforms.js +++ b/packages/block-library/src/paragraph/transforms.js @@ -9,7 +9,6 @@ import { createBlock, getBlockAttributes } from '@wordpress/blocks'; import { name } from './block.json'; const transforms = { - retainAttributes: [ 'content' ], from: [ { type: 'raw', @@ -38,6 +37,7 @@ const transforms = { }, }, ], + to: [ { type: 'pattern', retainAttributes: [ 'content' ] } ], }; export default transforms; From 6f177bde2876bbbb9243e64f60aebffaeb6c8aea Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Wed, 24 Mar 2021 13:35:03 +0200 Subject: [PATCH 13/14] try with `role: content` in block attributes --- .../pattern-transformations-menu.js | 27 ++++++++++--------- packages/block-library/src/heading/block.json | 3 ++- .../block-library/src/heading/transforms.js | 1 - .../block-library/src/paragraph/block.json | 3 ++- .../block-library/src/paragraph/transforms.js | 1 - packages/blocks/src/api/index.js | 1 + packages/blocks/src/api/utils.js | 21 +++++++++++++++ 7 files changed, 40 insertions(+), 17 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 f2579cc69f3cc5..f7f5a7c745ff17 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 @@ -5,7 +5,10 @@ import { __ } from '@wordpress/i18n'; import { useState, useMemo } from '@wordpress/element'; import { useInstanceId } from '@wordpress/compose'; import { chevronRight } from '@wordpress/icons'; -import { cloneBlock, getBlockTransforms } from '@wordpress/blocks'; +import { + cloneBlock, + __experimentalGetBlockAttributesNamesByRole as getBlockAttributesNamesByRole, +} from '@wordpress/blocks'; import { MenuGroup, MenuItem, @@ -105,19 +108,17 @@ function PatternTransformationsMenu( { transformedBlocksSet ); if ( ! match ) return; - // Found a match, so get the `retainAttributes` from - // block type's transforms to retain these and keep - // everything else from the pattern's block. - // If `retainAttributes` are not set, update the match - // with all the selected block's attributes. - const retainAttributes = getBlockTransforms( - 'to', - block.name - ).find( ( { type } ) => type === 'pattern' ) - ?.retainAttributes; + // 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 ( retainAttributes?.length ) { - retainedBlockAttributes = retainAttributes.reduce( + if ( contentAttributes?.length ) { + retainedBlockAttributes = contentAttributes.reduce( ( _accumulator, attribute ) => { if ( block.attributes[ attribute ] ) _accumulator[ attribute ] = diff --git a/packages/block-library/src/heading/block.json b/packages/block-library/src/heading/block.json index 6cd496431f799d..b54ad9860ade11 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/heading/transforms.js b/packages/block-library/src/heading/transforms.js index 17fcb2719e6055..8e6768c0d19d8a 100644 --- a/packages/block-library/src/heading/transforms.js +++ b/packages/block-library/src/heading/transforms.js @@ -91,7 +91,6 @@ const transforms = { } ) ), }, - { type: 'pattern', retainAttributes: [ 'content' ] }, ], }; diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json index a29e22c01d5af3..f8c60a50df3901 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/block-library/src/paragraph/transforms.js b/packages/block-library/src/paragraph/transforms.js index e59b01ad14e3b9..5179a7f05c22fa 100644 --- a/packages/block-library/src/paragraph/transforms.js +++ b/packages/block-library/src/paragraph/transforms.js @@ -37,7 +37,6 @@ const transforms = { }, }, ], - to: [ { type: 'pattern', retainAttributes: [ 'content' ] } ], }; export default transforms; diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index 625377c0df3c0f..5fdd8aad97bdf6 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 c82515c4e5cd2a..98ddcf819bfc8c 100644 --- a/packages/blocks/src/api/utils.js +++ b/packages/blocks/src/api/utils.js @@ -275,3 +275,24 @@ export function __experimentalSanitizeBlockAttributes( name, attributes ) { {} ); } + +/** + * I created this wrapper to hide the complexity for the consumer.. + * + * @param {*} name + * @param {*} role + */ +// TODO jsdoc +// TODO tests +export function __experimentalGetBlockAttributesNamesByRole( name, role ) { + const attributes = getBlockType( name )?.attributes; + if ( ! attributes ) return; + if ( ! role ) return Object.keys( attributes ); + return Object.entries( attributes ).reduce( + ( accumulator, [ attributeName, schema ] ) => { + if ( schema?.role === role ) accumulator.push( attributeName ); + return accumulator; + }, + [] + ); +} From 8bc5a9f125729ddbecacbc4fa069b341a4596a5d Mon Sep 17 00:00:00 2001 From: ntsekouras Date: Fri, 26 Mar 2021 20:05:26 +0200 Subject: [PATCH 14/14] use only __experimentalGetScopedBlockPatterns for fetching --- .../pattern-transformations-menu.js | 6 +- packages/block-editor/src/store/selectors.js | 59 +++++++++++-------- .../src/query/edit/block-setup/layout-step.js | 7 +-- 3 files changed, 42 insertions(+), 30 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 f7f5a7c745ff17..97423148244dd0 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 @@ -81,7 +81,7 @@ function PatternTransformationsMenu( { if ( replaceInnerBlocksMode ) { _patterns = statePatterns.map( ( statePattern ) => ( { ...statePattern, - transformedBlocks: statePattern.contentBlocks.map( ( block ) => + transformedBlocks: statePattern.blocks.map( ( block ) => cloneBlock( block ) ), } ) ); @@ -91,8 +91,8 @@ function PatternTransformationsMenu( { // to mutate this prop. const pattern = { ...statePattern, - transformedBlocks: statePattern.contentBlocks.map( - ( block ) => cloneBlock( block ) + transformedBlocks: statePattern.blocks.map( ( block ) => + cloneBlock( block ) ), }; const { transformedBlocks: patternBlocks } = pattern; diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index d0d688aa9136ec..a35e5b035ad774 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1845,30 +1845,44 @@ export const __experimentalGetAllowedPatterns = createSelector( /** * Returns the list of patterns based on specific `scope` and - * a block's name. - * `inserter` scope should be handled differently, probably in - * combination with `__experimentalGetAllowedPatterns`. - * For now `__experimentalGetScopedBlockPatterns` handles properly - * all other scopes. - * Since both APIs are experimental we should revisit this. + * a block's name or array of block names to find matching pattens. + * Internally is used `__experimentalGetAllowedPatterns` to have a + * single entry point for getting allowed patterns based on the `rootClientId`. + * TODO Since both APIs are experimental we should probably revisit this. * * @param {Object} state Editor state. * @param {string} scope Block pattern scope. - * @param {string} blockName Block's name. + * @param {string|string[]} blockNames Block's name or array of block names to find matching pattens. * @param {?string} rootClientId Optional target root client ID. * * @return {Array} The list of matched block patterns based on provided scope and block name. */ +// TODO add tests export const __experimentalGetScopedBlockPatterns = createSelector( - ( state, scope, blockName, rootClientId = null ) => { - if ( ! ( scope && blockName ) ) return EMPTY_ARRAY; + ( state, scope, blockNames, rootClientId = null ) => { + if ( ! ( scope && blockNames ) ) return EMPTY_ARRAY; const patterns = __experimentalGetAllowedPatterns( state, rootClientId ); - return patterns.filter( ( pattern ) => - pattern.scope?.[ scope ]?.includes?.( blockName ) - ); + const normalizedBlockNames = Array.isArray( blockNames ) + ? blockNames + : [ blockNames ]; + return patterns.reduce( ( accumulator, pattern ) => { + const match = pattern?.scope?.[ scope ]?.some?.( ( blockName ) => + normalizedBlockNames.includes( blockName ) + ); + if ( ! match ) return accumulator; + // If no `rootClientId` is provided, __experimentalGetAllowedPatterns are not + // filled with the `blocks` property that are the parsed blocks. In that case + // parse them. + accumulator.push( + rootClientId + ? pattern + : __experimentalGetParsedPattern( state, pattern.name ) + ); + return accumulator; + }, [] ); }, ( state, rootClientId ) => [ state.settings.__experimentalBlockPatterns, @@ -1937,10 +1951,12 @@ export const __experimentalGetPatternTransformItems = createSelector( } // Create a Set of the selected block names that is used in patterns filtering. - const selectedBlockNames = blocks.reduce( ( accumulator, block ) => { - accumulator.add( block.name ); - return accumulator; - }, new Set() ); + const selectedBlockNames = Array.from( + blocks.reduce( ( accumulator, block ) => { + accumulator.add( block.name ); + return accumulator; + }, new Set() ) + ); // If a selected block is nested (with InnerBlocks like Group or Columns) // return an EMPTY_ARRAY, as it doesn't make sense to try to be too smart. @@ -1955,7 +1971,7 @@ export const __experimentalGetPatternTransformItems = createSelector( ]; if ( blocksToSkip.some( ( blockName ) => - selectedBlockNames.has( blockName ) + selectedBlockNames.includes( blockName ) ) ) { return EMPTY_ARRAY; @@ -1967,15 +1983,12 @@ export const __experimentalGetPatternTransformItems = createSelector( // and try to find matches from the selected blocks. // Now this happens in the consumer to avoid heavy operations // in the selector. - const allowedPatterns = __experimentalGetAllowedPatterns( + return __experimentalGetScopedBlockPatterns( state, + 'transform', + selectedBlockNames, rootClientId ); - return allowedPatterns.filter( ( pattern ) => - pattern.scope?.transform?.some?.( ( blockName ) => - selectedBlockNames.has( blockName ) - ) - ); }, ( state, rootClientId ) => [ state.settings.__experimentalBlockPatterns, diff --git a/packages/block-library/src/query/edit/block-setup/layout-step.js b/packages/block-library/src/query/edit/block-setup/layout-step.js index a2a206dbb0dde8..01fc8db08d1363 100644 --- a/packages/block-library/src/query/edit/block-setup/layout-step.js +++ b/packages/block-library/src/query/edit/block-setup/layout-step.js @@ -2,8 +2,8 @@ * WordPress dependencies */ import { useSelect } from '@wordpress/data'; -import { useState, useMemo } from '@wordpress/element'; -import { parse, store as blocksStore } from '@wordpress/blocks'; +import { useState } from '@wordpress/element'; +import { store as blocksStore } from '@wordpress/blocks'; import { useInstanceId } from '@wordpress/compose'; import { BlockPreview, @@ -126,8 +126,7 @@ const LayoutSetupStep = ( { }; function BlockPattern( { pattern, onSelect, composite } ) { - const { content, viewportWidth } = pattern; - const blocks = useMemo( () => parse( content ), [ content ] ); + const { viewportWidth, blocks } = pattern; const descriptionId = useInstanceId( BlockPattern, 'block-setup-block-layout-list__item-description'