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..af008a0c6156dd
--- /dev/null
+++ b/lib/test-block-patterns.php
@@ -0,0 +1,214 @@
+ __( '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' => '
+
+ ',
+ )
+);
+
+// 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
+
+
+
+
+
+
+
+
+
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 ef0d601c495c86..56bd8746bde0c8 100644
--- a/packages/block-editor/src/components/block-switcher/index.js
+++ b/packages/block-editor/src/components/block-switcher/index.js
@@ -31,20 +31,29 @@ 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';
+
+const nestedSingleBlocksToHandle = [ 'core/template-part' ];
export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => {
- const { replaceBlocks } = useDispatch( blockEditorStore );
+ const { replaceBlocks, replaceInnerBlocks } = useDispatch(
+ blockEditorStore
+ );
const blockInformation = useBlockDisplayInformation( blocks[ 0 ].clientId );
const {
possibleBlockTransformations,
hasBlockStyles,
icon,
blockTitle,
+ patterns,
+ replaceInnerBlocksMode,
} = useSelect(
( select ) => {
- const { getBlockRootClientId, getBlockTransformItems } = select(
- blockEditorStore
- );
+ const {
+ getBlockRootClientId,
+ getBlockTransformItems,
+ __experimentalGetPatternTransformItems,
+ } = select( blockEditorStore );
const { getBlockStyles, getBlockType } = select( blocksStore );
const rootClientId = getBlockRootClientId(
@@ -66,7 +75,10 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => {
? getBlockType( firstBlockName )?.icon
: stack;
}
-
+ const _patterns = __experimentalGetPatternTransformItems(
+ blocks,
+ rootClientId
+ );
return {
possibleBlockTransformations: getBlockTransformItems(
blocks,
@@ -75,6 +87,12 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => {
hasBlockStyles: !! styles?.length,
icon: _icon,
blockTitle: getBlockType( firstBlockName ).title,
+ patterns: _patterns,
+ // TODO Need more thought here. We have the same check for template
+ // part in the `__experimentalGetPatternTransformItems` selector.
+ replaceInnerBlocksMode:
+ _isSingleBlockSelected &&
+ nestedSingleBlocksToHandle.includes( firstBlockName ),
};
},
[ clientIds, blocks, blockInformation?.icon ]
@@ -83,9 +101,22 @@ 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 ) => {
+ // 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 );
+ }
+ };
const hasPossibleBlockTransformations = !! possibleBlockTransformations.length;
+ const hasPatternTransformation = !! patterns?.length;
if ( ! hasBlockStyles && ! hasPossibleBlockTransformations ) {
return (
@@ -114,6 +145,10 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => {
blocks.length
);
+ const showDropDown =
+ hasBlockStyles ||
+ hasPossibleBlockTransformations ||
+ hasPatternTransformation;
return (
@@ -147,9 +182,25 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => {
menuProps={ { orientation: 'both' } }
>
{ ( { onClose } ) =>
- ( hasBlockStyles ||
- hasPossibleBlockTransformations ) && (
+ showDropDown && (
+ { hasPatternTransformation && (
+
{
+ onPatternTransform(
+ transformedBlocks
+ );
+ onClose();
+ } }
+ replaceInnerBlocksMode={
+ replaceInnerBlocksMode
+ }
+ />
+ ) }
{ 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..97423148244dd0
--- /dev/null
+++ b/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js
@@ -0,0 +1,259 @@
+/**
+ * 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,
+ replaceInnerBlocksMode = false,
+} ) {
+ const [ showTransforms, setShowTransforms ] = useState( false );
+ // `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 ) {
+ _patterns = statePatterns.map( ( statePattern ) => ( {
+ ...statePattern,
+ transformedBlocks: statePattern.blocks.map( ( block ) =>
+ cloneBlock( block )
+ ),
+ } ) );
+ } else {
+ _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;
+ }, [ replaceInnerBlocksMode, statePatterns ] );
+
+ if ( ! patterns.length ) return null;
+
+ return (
+
+ { showTransforms && (
+
+ ) }
+
+
+ );
+}
+
+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 1a506d3d084b09..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;
}
}
@@ -149,6 +148,7 @@
.block-editor-block-switcher__preview {
width: 300px;
height: auto;
+ max-height: 500px;
padding: $grid-unit-20;
}
}
@@ -182,3 +182,41 @@
}
}
}
+
+.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 d3dcb13a583149..a35e5b035ad774 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';
@@ -1844,28 +1845,159 @@ 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 ) => {
- if ( ! scope && ! blockName ) return EMPTY_ARRAY;
- const patterns = state.settings.__experimentalBlockPatterns;
- return patterns.filter( ( pattern ) =>
- pattern.scope?.[ scope ]?.includes?.( blockName )
+ ( state, scope, blockNames, rootClientId = null ) => {
+ if ( ! ( scope && blockNames ) ) return EMPTY_ARRAY;
+ const patterns = __experimentalGetAllowedPatterns(
+ state,
+ rootClientId
);
+ 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 ) => [ 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.
+ * 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 {WPEditorTransformItem[]} Items that are eligible for a pattern transformation.
+ */
+// TODO tests
+export const __experimentalGetPatternTransformItems = createSelector(
+ ( state, blocks, rootClientId = null ) => {
+ if ( ! blocks ) return EMPTY_ARRAY;
+ // 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;
+ const nestedSingleBlocksToHandle = [ 'core/template-part' ];
+ if (
+ isSingleBlockSelected &&
+ nestedSingleBlocksToHandle.includes( blocks[ 0 ].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}). 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 ) =>
+ variation.isActive?.(
+ selectedBlock.attributes,
+ variation.attributes
+ )
+ );
+ if ( match ) {
+ blockName = `${ blockName }/${ match.name }`;
+ }
+ }
+ return __experimentalGetScopedBlockPatterns(
+ state,
+ 'transform',
+ blockName
+ );
+ }
+
+ // Create a Set of the selected block names that is used in patterns filtering.
+ 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.
+ // 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.includes( 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
+ // and try to find matches from the selected blocks.
+ // Now this happens in the consumer to avoid heavy operations
+ // in the selector.
+ return __experimentalGetScopedBlockPatterns(
+ state,
+ 'transform',
+ selectedBlockNames,
+ rootClientId
+ );
+ },
+ ( state, rootClientId ) => [
+ state.settings.__experimentalBlockPatterns,
+ state.blockListSettings[ rootClientId ],
+ state.blocks.byClientId,
+ state.settings.allowedBlockTypes,
+ state.settings.templateLock,
+ getBlockTypes(),
+ ]
);
/**
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' ] },
} )
);
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/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/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'
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;
+ },
+ []
+ );
+}