From d283507200c920457b6cd738066a9a2b4ad83ada Mon Sep 17 00:00:00 2001 From: Kerry Liu Date: Tue, 2 Mar 2021 14:33:26 -0800 Subject: [PATCH] Add __experimentalDefaultBlock setting to block list settings to allow insertion of elements before and after a block, when the default block is not supported in the parent container. --- .../data/data-core-block-editor.md | 3 +- .../src/components/block-actions/index.js | 7 +- .../src/components/inner-blocks/index.js | 4 +- .../use-nested-settings-update.js | 8 +- packages/block-editor/src/store/actions.js | 28 ++- packages/block-editor/src/store/selectors.js | 30 +++- .../block-editor/src/store/test/actions.js | 18 ++ .../block-editor/src/store/test/selectors.js | 167 ++++++++++++++++++ packages/block-library/src/buttons/edit.js | 2 + packages/block-library/src/navigation/edit.js | 6 + 10 files changed, 260 insertions(+), 13 deletions(-) diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 2eaa32887a4440..6807e78f20f282 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -188,7 +188,8 @@ _Returns_ ### getBlockHierarchyRootClientId -Given a block client ID, returns the root of the hierarchy from which the block is nested, return the block itself for root level blocks. +Given a block client ID, returns the root of the hierarchy from which the block is nested, return the block itself +for root level blocks. _Parameters_ diff --git a/packages/block-editor/src/components/block-actions/index.js b/packages/block-editor/src/components/block-actions/index.js index f7873ef3c48e45..2d9afddd1d2568 100644 --- a/packages/block-editor/src/components/block-actions/index.js +++ b/packages/block-editor/src/components/block-actions/index.js @@ -30,8 +30,9 @@ export default function BlockActions( { getBlocksByClientId, canRemoveBlocks, getTemplateLock, + __experimentalGetDefaultBlockForAllowedBlocks, } = useSelect( ( select ) => select( blockEditorStore ), [] ); - const { getDefaultBlockName, getGroupingBlockName } = useSelect( + const { getGroupingBlockName } = useSelect( ( select ) => select( blocksStore ), [] ); @@ -46,11 +47,11 @@ export default function BlockActions( { ); } ); - const canInsertDefaultBlock = canInsertBlockType( - getDefaultBlockName(), + const defaultBlock = __experimentalGetDefaultBlockForAllowedBlocks( rootClientId ); + const canInsertDefaultBlock = !! defaultBlock; const canRemove = canRemoveBlocks( clientIds, rootClientId ); const { diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index dcf07e7391fc8b..0d80a6e31ff97d 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -52,6 +52,7 @@ function UncontrolledInnerBlocks( props ) { orientation, placeholder, __experimentalLayout, + __experimentalDefaultBlock, } = props; useNestedSettingsUpdate( @@ -60,7 +61,8 @@ function UncontrolledInnerBlocks( props ) { templateLock, captureToolbars, orientation, - __experimentalLayout + __experimentalLayout, + __experimentalDefaultBlock ); useInnerBlockTemplateSync( diff --git a/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js b/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js index 8a276be0a69e4f..6bb96406b6ca38 100644 --- a/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js +++ b/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js @@ -29,6 +29,10 @@ import { getLayoutType } from '../../layouts'; * @param {string} orientation The direction in which the block * should face. * @param {Object} layout The layout object for the block container. + * + * @param {[]} __experimentalDefaultBlock The default block: [ blockName, { blockAttributes } ]. + * Used to insert blocks of this type, before/after blocks + * in inner blocks. */ export default function useNestedSettingsUpdate( clientId, @@ -36,7 +40,8 @@ export default function useNestedSettingsUpdate( templateLock, captureToolbars, orientation, - layout + layout, + __experimentalDefaultBlock ) { const { updateBlockListSettings } = useDispatch( blockEditorStore ); @@ -64,6 +69,7 @@ export default function useNestedSettingsUpdate( useLayoutEffect( () => { const newSettings = { allowedBlocks: _allowedBlocks, + __experimentalDefaultBlock, templateLock: templateLock === undefined ? parentLock : templateLock, }; diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 56bc0f0e0a7acb..f5729b7dfd5166 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -12,7 +12,6 @@ import { createBlock, doBlocksMatchTemplate, getBlockType, - getDefaultBlockName, hasBlockSupport, switchToBlockType, synchronizeBlocksWithTemplate, @@ -1131,16 +1130,33 @@ export function selectionChange( * * @return {Object} Action object */ -export function insertDefaultBlock( attributes, rootClientId, index ) { +export function* insertDefaultBlock( attributes = {}, rootClientId, index ) { + // See if we specified a default for allowed blocks + const defaultBlock = yield controls.select( + blockEditorStoreName, + '__experimentalGetDefaultBlockForAllowedBlocks', + rootClientId + ); + // Abort if there is no default block type (if it has been unregistered). - const defaultBlockName = getDefaultBlockName(); - if ( ! defaultBlockName ) { + if ( ! defaultBlock ) { return; } - const block = createBlock( defaultBlockName, attributes ); + const [ defaultBlockName, defaultBlockAttributes ] = defaultBlock; + + // prefer the non-empty block attributes + let blockAttributes = attributes; + if ( + Object.keys( attributes ).length === 0 && + Object.keys( defaultBlockAttributes ).length > 0 + ) { + blockAttributes = defaultBlockAttributes; + } + + const block = createBlock( defaultBlockName, blockAttributes ); - return insertBlock( block, index, rootClientId ); + return yield insertBlock( block, index, rootClientId ); } /** diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 8e9f3792b0e0d5..63d7a4e23e81de 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -24,6 +24,7 @@ import createSelector from 'rememo'; import { getBlockType, getBlockTypes, + getDefaultBlockName, hasBlockSupport, getPossibleBlockTransformations, parse, @@ -499,7 +500,8 @@ export const getBlockParentsByBlockName = createSelector( ); /** - * Given a block client ID, returns the root of the hierarchy from which the block is nested, return the block itself for root level blocks. + * Given a block client ID, returns the root of the hierarchy from which the block is nested, return the block itself + * for root level blocks. * * @param {Object} state Editor state. * @param {string} clientId Block from which to find root client ID. @@ -1991,6 +1993,32 @@ export function getBlockListSettings( state, clientId ) { return state.blockListSettings[ clientId ]; } +/** + * Returns the default block name for a list of allowed blocks, if any exist. + * + * @param {Object} state Editor state. + * @param {?string} clientId Block client ID. + * + * @return {?[]} default block, [ blockName, { blockAttributes } ]. + */ +export function __experimentalGetDefaultBlockForAllowedBlocks( + state, + clientId +) { + const settings = getBlockListSettings( state, clientId ); + + const [ + blockName, + blockAttributes = {}, + ] = settings?.__experimentalDefaultBlock ?? [ getDefaultBlockName() ]; + + if ( ! canInsertBlockType( state, blockName, clientId ) ) { + return; + } + + return [ blockName, blockAttributes ]; +} + /** * Returns the editor settings. * diff --git a/packages/block-editor/src/store/test/actions.js b/packages/block-editor/src/store/test/actions.js index 0f983baf7949c9..c21de2e32a4608 100644 --- a/packages/block-editor/src/store/test/actions.js +++ b/packages/block-editor/src/store/test/actions.js @@ -31,6 +31,7 @@ const { hideInsertionPoint, insertBlock, insertBlocks, + insertDefaultBlock, mergeBlocks, moveBlocksToPosition, multiSelect, @@ -459,6 +460,23 @@ describe( 'actions', () => { } ); } ); + describe( 'insertDefaultBlock', () => { + it( 'should check for allowed default block', () => { + const insertDefaultBlockGenerator = insertDefaultBlock( + {}, + 'testclientid', + 0 + ); + expect( insertDefaultBlockGenerator.next().value ).toEqual( + controls.select( + blockEditorStoreName, + '__experimentalGetDefaultBlockForAllowedBlocks', + 'testclientid' + ) + ); + } ); + } ); + describe( 'insertBlocks', () => { it( 'should apply default styles to blocks if blocks do not contain a style', () => { const ribsBlock = { diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index e6f514f8faab9d..eb97978037ca1b 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -7,6 +7,7 @@ import { filter } from 'lodash'; * WordPress dependencies */ import { + setDefaultBlockName, registerBlockType, unregisterBlockType, setFreeformContentHandlerName, @@ -77,6 +78,7 @@ const { __unstableGetClientIdsTree, __experimentalGetPatternTransformItems, wasBlockJustInserted, + __experimentalGetDefaultBlockForAllowedBlocks, } = selectors; describe( 'selectors', () => { @@ -124,6 +126,14 @@ describe( 'selectors', () => { parent: [ 'core/test-block-b' ], } ); + registerBlockType( 'core/test-block-default', { + save: ( props ) => props.attributes.text, + category: 'text', + title: 'Test Block Default', + icon: 'test', + keywords: [ 'testing' ], + } ); + registerBlockType( 'core/test-freeform', { save: ( props ) => { props.attributes.content }, category: 'text', @@ -146,6 +156,7 @@ describe( 'selectors', () => { } ); setFreeformContentHandlerName( 'core/test-freeform' ); + setDefaultBlockName( 'core/test-block-default' ); cachedSelectors.forEach( ( { clear } ) => clear() ); } ); @@ -156,7 +167,9 @@ describe( 'selectors', () => { unregisterBlockType( 'core/test-block-b' ); unregisterBlockType( 'core/test-block-c' ); unregisterBlockType( 'core/test-freeform' ); + unregisterBlockType( 'core/test-block-default' ); unregisterBlockType( 'core/post-content-child' ); + setDefaultBlockName( null ); setFreeformContentHandlerName( undefined ); } ); @@ -2617,6 +2630,7 @@ describe( 'selectors', () => { expect( firstBlockFirstCall.map( ( item ) => item.id ) ).toEqual( [ 'core/test-block-a', 'core/test-block-b', + 'core/test-block-default', 'core/test-freeform', 'core/block/1', 'core/block/2', @@ -2631,6 +2645,7 @@ describe( 'selectors', () => { expect( secondBlockFirstCall.map( ( item ) => item.id ) ).toEqual( [ 'core/test-block-a', 'core/test-block-b', + 'core/test-block-default', 'core/test-freeform', 'core/block/1', 'core/block/2', @@ -3031,6 +3046,158 @@ describe( 'selectors', () => { } ); } ); + describe( '__experimentalGetDefaultBlockForAllowedBlocks', () => { + it( 'should return the default block for allowed blocks', () => { + const state = { + blocks: { + byClientId: { + testClientIdA: { + name: 'core/test-block-a', + }, + }, + attributes: { + testClientIdA: {}, + }, + }, + settings: {}, + blockListSettings: { + testClientIdA: { + allowedBlocks: [ + 'core/test-block-b', + 'core/test-block-c', + ], + __experimentalDefaultBlock: [ 'core/test-block-c' ], + }, + }, + }; + expect( + __experimentalGetDefaultBlockForAllowedBlocks( + state, + 'testClientIdA' + ) + ).toEqual( [ 'core/test-block-c', {} ] ); + } ); + it( 'should return the default block and block attributes for allowed blocks', () => { + const state = { + blocks: { + byClientId: { + testClientIdA: { + name: 'core/test-block-a', + }, + }, + attributes: { + testClientIdA: {}, + }, + }, + settings: {}, + blockListSettings: { + testClientIdA: { + allowedBlocks: [ + 'core/test-block-b', + 'core/test-block-c', + ], + __experimentalDefaultBlock: [ + 'core/test-block-c', + { foo: 'foo', bar: 'bar' }, + ], + }, + }, + }; + expect( + __experimentalGetDefaultBlockForAllowedBlocks( + state, + 'testClientIdA' + ) + ).toEqual( [ 'core/test-block-c', { foo: 'foo', bar: 'bar' } ] ); + } ); + it( 'should return the editor default block when block list default is not specified', () => { + const state = { + blocks: { + byClientId: { + testClientIdA: { + name: 'core/test-block-a', + }, + }, + attributes: { + testClientIdA: {}, + }, + }, + settings: {}, + blockListSettings: { + testClientIdA: { + allowedBlocks: [ + 'core/test-block-b', + 'core/test-block-c', + 'core/test-block-default', + ], + }, + }, + }; + expect( + __experimentalGetDefaultBlockForAllowedBlocks( + state, + 'testClientIdA' + ) + ).toEqual( [ 'core/test-block-default', {} ] ); + } ); + it( 'should return undefined when default is not specified and editor default is not supported', () => { + const state = { + blocks: { + byClientId: { + testClientIdA: { + name: 'core/test-block-a', + }, + }, + attributes: { + testClientIdA: {}, + }, + }, + settings: {}, + blockListSettings: { + testClientIdA: { + allowedBlocks: [ + 'core/test-block-b', + 'core/test-block-c', + ], + }, + }, + }; + expect( + __experimentalGetDefaultBlockForAllowedBlocks( + state, + 'testClientIdB' + ) + ).toEqual( undefined ); + } ); + it( 'should return undefined when default block is not in allowedBlocks', () => { + const state = { + blocks: { + byClientId: { + testClientIdA: { + name: 'core/test-block-a', + }, + }, + attributes: { + testClientIdA: {}, + }, + }, + settings: {}, + blockListSettings: { + testClientIdA: { + allowedBlocks: [ 'core/test-block-b' ], + __experimentalDefaultBlock: 'core/test-block-c', + }, + }, + }; + expect( + __experimentalGetDefaultBlockForAllowedBlocks( + state, + 'testClientIdA' + ) + ).toEqual( undefined ); + } ); + } ); + describe( '__experimentalGetBlockListSettingsForBlocks', () => { it( 'should return the settings for a set of blocks', () => { const state = { diff --git a/packages/block-library/src/buttons/edit.js b/packages/block-library/src/buttons/edit.js index a22d8a7296068d..d71eff4ed16d5f 100644 --- a/packages/block-library/src/buttons/edit.js +++ b/packages/block-library/src/buttons/edit.js @@ -21,6 +21,7 @@ import { useSelect } from '@wordpress/data'; import { name as buttonBlockName } from '../button'; const ALLOWED_BLOCKS = [ buttonBlockName ]; +const DEFAULT_BLOCK = [ buttonBlockName ]; const LAYOUT = { type: 'default', alignments: [], @@ -52,6 +53,7 @@ function ButtonsEdit( { const innerBlocksProps = useInnerBlocksProps( blockProps, { allowedBlocks: ALLOWED_BLOCKS, + __experimentalDefaultBlock: DEFAULT_BLOCK, template: [ [ buttonBlockName, diff --git a/packages/block-library/src/navigation/edit.js b/packages/block-library/src/navigation/edit.js index b9e9fd8e53184e..228d5f7426270d 100644 --- a/packages/block-library/src/navigation/edit.js +++ b/packages/block-library/src/navigation/edit.js @@ -50,6 +50,11 @@ const ALLOWED_BLOCKS = [ 'core/navigation-submenu', ]; +const DEFAULT_BLOCK = [ + 'core/navigation-link', + { type: 'page', kind: 'post-type' }, +]; + const LAYOUT = { type: 'default', alignments: [], @@ -166,6 +171,7 @@ function Navigation( { allowedBlocks: ALLOWED_BLOCKS, orientation: attributes.orientation, renderAppender: CustomAppender || appender, + __experimentalDefaultBlock: DEFAULT_BLOCK, // Ensure block toolbar is not too far removed from item // being edited when in vertical mode.