diff --git a/docs/reference-guides/data/data-core-blocks.md b/docs/reference-guides/data/data-core-blocks.md index 084c9c1d7a5fbc..a25a521931e25a 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 3475e2b5351c80..373611cd3bd8e8 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 8e6fdc9d900dbb..eda3ac629bef87 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 2ddeb3a60f0abb..92738c6e16fbc3 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 6633adf40050c5..71e59949d51d17 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 bc76218892688a..34e6954a9ff33f 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 0a23505f0ac036..8ee031aedbeefc 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 b2b8ab8106f097..9eda135d0d6999 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 1fda11d72311a3..0dcdde3c07bf10 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 ) => {