diff --git a/docs/reference-guides/data/data-core-blocks.md b/docs/reference-guides/data/data-core-blocks.md index 084c9c1d7a5fb..a25a521931e25 100644 --- a/docs/reference-guides/data/data-core-blocks.md +++ b/docs/reference-guides/data/data-core-blocks.md @@ -504,6 +504,54 @@ _Returns_ - `string?`: Name of the block for handling the grouping of blocks. +### getHookedBlocks + +Returns the hooked blocks for a given anchor block. + +Given an anchor block name, returns an object whose keys are relative positions, and whose values are arrays of block names that are hooked to the anchor block at that relative position. + +_Usage_ + +```js +import { store as blocksStore } from '@wordpress/blocks'; +import { useSelect } from '@wordpress/data'; + +const ExampleComponent = () => { + const hookedBlockNames = useSelect( + ( select ) => + select( blocksStore ).getHookedBlocks( 'core/navigation' ), + [] + ); + + return ( + + ); +}; +``` + +_Parameters_ + +- _state_ `Object`: Data state. +- _blockName_ `string`: Anchor block type name. + +_Returns_ + +- `Object`: Lists of hooked block names for each relative position. + ### getUnregisteredFallbackBlockName Returns the name of the block for handling unregistered blocks. diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 3475e2b5351c8..373611cd3bd8e 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -10,6 +10,7 @@ import { getBlockType, getBlockTypes, getBlockVariations, + getHookedBlocks, hasBlockSupport, getPossibleBlockTransformations, parse, @@ -1936,9 +1937,16 @@ const buildBlockTypeItem = blockType.name, 'inserter' ); + + const ignoredHookedBlocks = [ + ...new Set( Object.values( getHookedBlocks( id ) ).flat() ), + ]; + return { ...blockItemBase, - initialAttributes: {}, + initialAttributes: ignoredHookedBlocks.length + ? { metadata: { ignoredHookedBlocks } } + : {}, description: blockType.description, category: blockType.category, keywords: blockType.keywords, diff --git a/packages/blocks/README.md b/packages/blocks/README.md index 8e6fdc9d900db..eda3ac629bef8 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -234,6 +234,20 @@ _Returns_ - `?string`: Block name. +### getHookedBlocks + +Returns the hooked blocks for a given anchor block. + +Given an anchor block name, returns an object whose keys are relative positions, and whose values are arrays of block names that are hooked to the anchor block at that relative position. + +_Parameters_ + +- _name_ `string`: Anchor block name. + +_Returns_ + +- `Object`: Lists of hooked block names for each relative position. + ### getPhrasingContentSchema Undocumented declaration. diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index 2ddeb3a60f0ab..92738c6e16fbc 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -124,6 +124,7 @@ export { getBlockTypes, getBlockSupport, hasBlockSupport, + getHookedBlocks, getBlockVariations, isReusableBlock, isTemplatePart, diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 6633adf40050c..71e59949d51d1 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -550,6 +550,21 @@ export function hasBlockSupport( nameOrType, feature, defaultSupports ) { ); } +/** + * Returns the hooked blocks for a given anchor block. + * + * Given an anchor block name, returns an object whose keys are relative positions, + * and whose values are arrays of block names that are hooked to the anchor block + * at that relative position. + * + * @param {string} name Anchor block name. + * + * @return {Object} Lists of hooked block names for each relative position. + */ +export function getHookedBlocks( name ) { + return select( blocksStore ).getHookedBlocks( name ); +} + /** * Determines whether or not the given block is a reusable block. This is a * special block type that is used to point to a global block stored via the diff --git a/packages/blocks/src/api/templates.js b/packages/blocks/src/api/templates.js index bc76218892688..34e6954a9ff33 100644 --- a/packages/blocks/src/api/templates.js +++ b/packages/blocks/src/api/templates.js @@ -8,7 +8,7 @@ import { renderToString } from '@wordpress/element'; */ import { convertLegacyBlockNameAndAttributes } from './parser/convert-legacy-block'; import { createBlock } from './factory'; -import { getBlockType } from './registration'; +import { getBlockType, getHookedBlocks } from './registration'; /** * Checks whether a list of blocks matches a template by comparing the block names. @@ -115,6 +115,35 @@ export function synchronizeBlocksWithTemplate( blocks = [], template ) { normalizedAttributes ); + const ignoredHookedBlocks = [ + ...new Set( + Object.values( getHookedBlocks( blockName ) ).flat() + ), + ]; + + if ( ignoredHookedBlocks.length ) { + const { metadata = {}, ...otherAttributes } = blockAttributes; + const { + ignoredHookedBlocks: ignoredHookedBlocksFromTemplate = [], + ...otherMetadata + } = metadata; + + const newIgnoredHookedBlocks = [ + ...new Set( [ + ...ignoredHookedBlocks, + ...ignoredHookedBlocksFromTemplate, + ] ), + ]; + + blockAttributes = { + metadata: { + ignoredHookedBlocks: newIgnoredHookedBlocks, + ...otherMetadata, + }, + ...otherAttributes, + }; + } + // If a Block is undefined at this point, use the core/missing block as // a placeholder for a better user experience. if ( undefined === getBlockType( blockName ) ) { diff --git a/packages/blocks/src/api/test/templates.js b/packages/blocks/src/api/test/templates.js index 0a23505f0ac03..8ee031aedbeef 100644 --- a/packages/blocks/src/api/test/templates.js +++ b/packages/blocks/src/api/test/templates.js @@ -28,7 +28,11 @@ describe( 'templates', () => { beforeEach( () => { registerBlockType( 'core/test-block', { - attributes: {}, + attributes: { + metadata: { + type: 'object', + }, + }, save: noop, category: 'text', title: 'test block', @@ -132,6 +136,80 @@ describe( 'templates', () => { ] ); } ); + it( 'should set ignoredHookedBlocks metadata if a block has hooked blocks', () => { + registerBlockType( 'core/hooked-block', { + attributes: {}, + save: noop, + category: 'text', + title: 'hooked block', + blockHooks: { 'core/test-block': 'after' }, + } ); + + const template = [ + [ 'core/test-block' ], + [ 'core/test-block-2' ], + [ 'core/test-block-2' ], + ]; + const blockList = []; + + expect( + synchronizeBlocksWithTemplate( blockList, template ) + ).toMatchObject( [ + { + name: 'core/test-block', + attributes: { + metadata: { + ignoredHookedBlocks: [ 'core/hooked-block' ], + }, + }, + }, + { name: 'core/test-block-2' }, + { name: 'core/test-block-2' }, + ] ); + } ); + + it( 'retains previously set ignoredHookedBlocks metadata', () => { + registerBlockType( 'core/hooked-block', { + attributes: {}, + save: noop, + category: 'text', + title: 'hooked block', + blockHooks: { 'core/test-block': 'after' }, + } ); + + const template = [ + [ + 'core/test-block', + { + metadata: { + ignoredHookedBlocks: [ 'core/other-hooked-block' ], + }, + }, + ], + [ 'core/test-block-2' ], + [ 'core/test-block-2' ], + ]; + const blockList = []; + + expect( + synchronizeBlocksWithTemplate( blockList, template ) + ).toMatchObject( [ + { + name: 'core/test-block', + attributes: { + metadata: { + ignoredHookedBlocks: [ + 'core/hooked-block', + 'core/other-hooked-block', + ], + }, + }, + }, + { name: 'core/test-block-2' }, + { name: 'core/test-block-2' }, + ] ); + } ); + it( 'should create nested blocks', () => { const template = [ [ 'core/test-block', {}, [ [ 'core/test-block-2' ] ] ], diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js index b2b8ab8106f09..9eda135d0d699 100644 --- a/packages/blocks/src/store/selectors.js +++ b/packages/blocks/src/store/selectors.js @@ -106,6 +106,68 @@ export function getBlockType( state, name ) { return state.blockTypes[ name ]; } +/** + * Returns the hooked blocks for a given anchor block. + * + * Given an anchor block name, returns an object whose keys are relative positions, + * and whose values are arrays of block names that are hooked to the anchor block + * at that relative position. + * + * @param {Object} state Data state. + * @param {string} blockName Anchor block type name. + * + * @example + * ```js + * import { store as blocksStore } from '@wordpress/blocks'; + * import { useSelect } from '@wordpress/data'; + * + * const ExampleComponent = () => { + * const hookedBlockNames = useSelect( ( select ) => + * select( blocksStore ).getHookedBlocks( 'core/navigation' ), + * [] + * ); + * + * return ( + * + * ); + * }; + * ``` + * + * @return {Object} Lists of hooked block names for each relative position. + */ +export const getHookedBlocks = createSelector( + ( state, blockName ) => { + const hookedBlockTypes = getBlockTypes( state ).filter( + ( { blockHooks } ) => blockHooks && blockName in blockHooks + ); + + let hookedBlocks = {}; + for ( const blockType of hookedBlockTypes ) { + const relativePosition = blockType.blockHooks[ blockName ]; + hookedBlocks = { + ...hookedBlocks, + [ relativePosition ]: [ + ...( hookedBlocks[ relativePosition ] ?? [] ), + blockType.name, + ], + }; + } + return hookedBlocks; + }, + ( state ) => [ state.blockTypes ] +); + /** * Returns block styles by block name. * diff --git a/packages/blocks/src/store/test/selectors.js b/packages/blocks/src/store/test/selectors.js index 1fda11d72311a..0dcdde3c07bf1 100644 --- a/packages/blocks/src/store/test/selectors.js +++ b/packages/blocks/src/store/test/selectors.js @@ -12,6 +12,7 @@ import { getBlockVariations, getDefaultBlockVariation, getGroupingBlockName, + getHookedBlocks, isMatchingSearchTerm, getCategories, getActiveBlockVariation, @@ -228,6 +229,111 @@ describe( 'selectors', () => { } ); } ); + describe( 'getHookedBlocks', () => { + it( 'should return an empty object if state is empty', () => { + const state = { + blockTypes: {}, + }; + + expect( getHookedBlocks( state, 'anchor' ) ).toEqual( {} ); + } ); + + it( 'should return an empty object if the anchor block is not found', () => { + const state = { + blockTypes: { + anchor: { + name: 'anchor', + }, + hookedBlock: { + name: 'hookedBlock', + blockHooks: { + anchor: 'after', + }, + }, + }, + }; + + expect( getHookedBlocks( state, 'otherAnchor' ) ).toEqual( {} ); + } ); + + it( "should return the anchor block name even if the anchor block doesn't exist", () => { + const state = { + blockTypes: { + hookedBlock: { + name: 'hookedBlock', + blockHooks: { + anchor: 'after', + }, + }, + }, + }; + + expect( getHookedBlocks( state, 'anchor' ) ).toEqual( { + after: [ 'hookedBlock' ], + } ); + } ); + + it( 'should return an array with the hooked block names', () => { + const state = { + blockTypes: { + anchor: { + name: 'anchor', + }, + hookedBlock1: { + name: 'hookedBlock1', + blockHooks: { + anchor: 'after', + }, + }, + hookedBlock2: { + name: 'hookedBlock2', + blockHooks: { + anchor: 'before', + }, + }, + }, + }; + + expect( getHookedBlocks( state, 'anchor' ) ).toEqual( { + after: [ 'hookedBlock1' ], + before: [ 'hookedBlock2' ], + } ); + } ); + + it( 'should return an array with the hooked block names, even if multiple blocks are in the same relative position', () => { + const state = { + blockTypes: { + anchor: { + name: 'anchor', + }, + hookedBlock1: { + name: 'hookedBlock1', + blockHooks: { + anchor: 'after', + }, + }, + hookedBlock2: { + name: 'hookedBlock2', + blockHooks: { + anchor: 'before', + }, + }, + hookedBlock3: { + name: 'hookedBlock3', + blockHooks: { + anchor: 'after', + }, + }, + }, + }; + + expect( getHookedBlocks( state, 'anchor' ) ).toEqual( { + after: [ 'hookedBlock1', 'hookedBlock3' ], + before: [ 'hookedBlock2' ], + } ); + } ); + } ); + describe( 'Testing block variations selectors', () => { const blockName = 'block/name'; const createBlockVariationsState = ( variations ) => {