diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index 2a4b64f3608874..e83a251ec7e671 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -144,8 +144,6 @@ export const getEnabledClientIdsTree = createRegistrySelector( ( select ) => state.derivedBlockEditingModes, state.derivedNavModeBlockEditingModes, state.blockEditingModes, - state.settings.templateLock, - state.blockListSettings, select( STORE_NAME ).__unstableGetEditorMode( state ), ] ) ); diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 5fc08ce7a37b89..ec82d5192dc6f7 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -2267,6 +2267,12 @@ function getDerivedBlockEditingModesForTree( syncedPatternClientIds.push( clientId ); } } ); + const contentOnlyTemplateLockedClientIds = Object.keys( + state.blockListSettings + ).filter( + ( clientId ) => + state.blockListSettings[ clientId ]?.templateLock === 'contentOnly' + ); traverseBlockTree( state, treeClientId, ( block ) => { const { clientId, name: blockName } = block; @@ -2457,6 +2463,23 @@ function getDerivedBlockEditingModesForTree( derivedBlockEditingModes.set( clientId, 'disabled' ); } } + + // `templateLock: 'contentOnly'` derived modes. + if ( contentOnlyTemplateLockedClientIds.length ) { + const hasContentOnlyTemplateLockedParent = + !! findParentInClientIdsList( + state, + clientId, + contentOnlyTemplateLockedClientIds + ); + if ( hasContentOnlyTemplateLockedParent ) { + if ( isContentBlock( blockName ) ) { + derivedBlockEditingModes.set( clientId, 'contentOnly' ); + } else { + derivedBlockEditingModes.set( clientId, 'disabled' ); + } + } + } } ); return derivedBlockEditingModes; @@ -2628,6 +2651,75 @@ export function withDerivedBlockEditingModes( reducer ) { } break; } + case 'UPDATE_BLOCK_LIST_SETTINGS': { + // Handle the addition and removal of contentOnly template locked blocks. + const addedBlocks = []; + const removedClientIds = []; + + const updates = + typeof action.clientId === 'string' + ? { [ action.clientId ]: action.settings } + : action.clientId; + + for ( const clientId in updates ) { + const isNewContentOnlyBlock = + state.blockListSettings[ clientId ]?.templateLock !== + 'contentOnly' && + nextState.blockListSettings[ clientId ] + ?.templateLock === 'contentOnly'; + + const wasContentOnlyBlock = + state.blockListSettings[ clientId ]?.templateLock === + 'contentOnly' && + nextState.blockListSettings[ clientId ] + ?.templateLock !== 'contentOnly'; + + if ( isNewContentOnlyBlock ) { + addedBlocks.push( + nextState.blocks.tree.get( clientId ) + ); + } else if ( wasContentOnlyBlock ) { + removedClientIds.push( clientId ); + } + } + + if ( ! addedBlocks.length && ! removedClientIds.length ) { + break; + } + + const nextDerivedBlockEditingModes = + getDerivedBlockEditingModesUpdates( { + prevState: state, + nextState, + addedBlocks, + removedClientIds, + isNavMode: false, + } ); + const nextDerivedNavModeBlockEditingModes = + getDerivedBlockEditingModesUpdates( { + prevState: state, + nextState, + addedBlocks, + removedClientIds, + isNavMode: true, + } ); + + if ( + nextDerivedBlockEditingModes || + nextDerivedNavModeBlockEditingModes + ) { + return { + ...nextState, + derivedBlockEditingModes: + nextDerivedBlockEditingModes ?? + state.derivedBlockEditingModes, + derivedNavModeBlockEditingModes: + nextDerivedNavModeBlockEditingModes ?? + state.derivedNavModeBlockEditingModes, + }; + } + break; + } case 'SET_BLOCK_EDITING_MODE': case 'UNSET_BLOCK_EDITING_MODE': case 'SET_HAS_CONTROLLED_INNER_BLOCKS': { diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 85ea65e5875d5c..1e18ca232a87fa 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -3076,62 +3076,36 @@ export function __unstableIsWithinBlockOverlay( state, clientId ) { * @return {BlockEditingMode} The block editing mode. One of `'disabled'`, * `'contentOnly'`, or `'default'`. */ -export const getBlockEditingMode = createRegistrySelector( - ( select ) => - ( state, clientId = '' ) => { - // Some selectors that call this provide `null` as the default - // rootClientId, but the default rootClientId is actually `''`. - if ( clientId === null ) { - clientId = ''; - } +export function getBlockEditingMode( state, clientId = '' ) { + // Some selectors that call this provide `null` as the default + // rootClientId, but the default rootClientId is actually `''`. + if ( clientId === null ) { + clientId = ''; + } - const isNavMode = isNavigationMode( state ); - - // If the editor is currently not in navigation mode, check if the clientId - // has an editing mode set in the regular derived map. - // There may be an editing mode set here for synced patterns or in zoomed out - // mode. - if ( - ! isNavMode && - state.derivedBlockEditingModes?.has( clientId ) - ) { - return state.derivedBlockEditingModes.get( clientId ); - } + const isNavMode = isNavigationMode( state ); - // If the editor *is* in navigation mode, the block editing mode states - // are stored in the derivedNavModeBlockEditingModes map. - if ( - isNavMode && - state.derivedNavModeBlockEditingModes?.has( clientId ) - ) { - return state.derivedNavModeBlockEditingModes.get( clientId ); - } + // If the editor is currently not in navigation mode, check if the clientId + // has an editing mode set in the regular derived map. + // There may be an editing mode set here for synced patterns or in zoomed out + // mode. + if ( ! isNavMode && state.derivedBlockEditingModes?.has( clientId ) ) { + return state.derivedBlockEditingModes.get( clientId ); + } - // In normal mode, consider that an explicitly set editing mode takes over. - const blockEditingMode = state.blockEditingModes.get( clientId ); - if ( blockEditingMode ) { - return blockEditingMode; - } + // If the editor *is* in navigation mode, the block editing mode states + // are stored in the derivedNavModeBlockEditingModes map. + if ( isNavMode && state.derivedNavModeBlockEditingModes?.has( clientId ) ) { + return state.derivedNavModeBlockEditingModes.get( clientId ); + } - // In normal mode, top level is default mode. - if ( clientId === '' ) { - return 'default'; - } + // In normal mode, consider that an explicitly set editing mode takes over. + if ( state.blockEditingModes.has( clientId ) ) { + return state.blockEditingModes.get( clientId ); + } - const rootClientId = getBlockRootClientId( state, clientId ); - const templateLock = getTemplateLock( state, rootClientId ); - // If the parent of the block is contentOnly locked, check whether it's a content block. - if ( templateLock === 'contentOnly' ) { - const name = getBlockName( state, clientId ); - const { hasContentRoleAttribute } = unlock( - select( blocksStore ) - ); - const isContent = hasContentRoleAttribute( name ); - return isContent ? 'contentOnly' : 'disabled'; - } - return 'default'; - } -); + return 'default'; +} /** * Indicates if a block is ungroupable. diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js index b0e67a5de53169..02d5962ef50efc 100644 --- a/packages/block-editor/src/store/test/reducer.js +++ b/packages/block-editor/src/store/test/reducer.js @@ -3575,6 +3575,7 @@ describe( 'state', () => { blocks, settings, zoomLevel, + blockListSettings, blockEditingModes, } ) ); @@ -3885,6 +3886,112 @@ describe( 'state', () => { } ); } ); + describe( 'contentOnly template locking', () => { + let initialState; + beforeAll( () => { + initialState = dispatchActions( + [ + { + type: 'RESET_BLOCKS', + blocks: [ + { + name: 'core/group', + clientId: 'group-1', + attributes: {}, + innerBlocks: [ + { + name: 'core/paragraph', + clientId: 'paragraph-1', + attributes: {}, + innerBlocks: [], + }, + { + name: 'core/group', + clientId: 'group-2', + attributes: {}, + innerBlocks: [ + { + name: 'core/paragraph', + clientId: 'paragraph-2', + attributes: {}, + innerBlocks: [], + }, + ], + }, + ], + }, + ], + }, + { + type: 'UPDATE_BLOCK_LIST_SETTINGS', + clientId: 'group-1', + settings: { + templateLock: 'contentOnly', + }, + }, + ], + testReducer, + initialState + ); + } ); + + it( 'returns the expected block editing modes for a parent block with contentOnly template locking', () => { + // Only the parent pattern and its own children that have bindings + // are in contentOnly mode. All other blocks are disabled. + expect( initialState.derivedBlockEditingModes ).toEqual( + new Map( + Object.entries( { + 'paragraph-1': 'contentOnly', + 'group-2': 'disabled', + 'paragraph-2': 'contentOnly', + } ) + ) + ); + } ); + + it( 'removes block editing modes when template locking is removed', () => { + const { derivedBlockEditingModes } = dispatchActions( + [ + { + type: 'UPDATE_BLOCK_LIST_SETTINGS', + clientId: 'group-1', + settings: { + templateLock: false, + }, + }, + ], + testReducer, + initialState + ); + + expect( derivedBlockEditingModes ).toEqual( new Map() ); + } ); + + it( 'allows explicitly set blockEditingModes to override the contentOnly template locking', () => { + const { derivedBlockEditingModes } = dispatchActions( + [ + { + type: 'SET_BLOCK_EDITING_MODE', + clientId: 'paragraph-2', + mode: 'disabled', + }, + ], + testReducer, + initialState + ); + + expect( derivedBlockEditingModes ).toEqual( + new Map( + Object.entries( { + 'paragraph-1': 'contentOnly', + 'group-2': 'disabled', + // Paragraph 2 already has an explicit mode, so isn't set as a derived mode. + } ) + ) + ); + } ); + } ); + describe( 'navigation mode', () => { let initialState; diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 587e1036e405e2..d5fedb4ea7c0b8 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -4380,92 +4380,9 @@ describe( '__unstableGetClientIdsTree', () => { describe( 'getBlockEditingMode', () => { const baseState = { - settings: {}, - blocks: { - byClientId: new Map( [ - [ - '6cf70164-9097-4460-bcbf-200560546988', - { name: 'core/template-part' }, - ], // Header - [ - 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', - { name: 'core/group' }, - ], // Group - [ - 'b26fc763-417d-4f01-b81c-2ec61e14a972', - { name: 'core/post-title' }, - ], // | Post Title - [ - '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', - { name: 'core/group' }, - ], // | Group - [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', { name: 'core/p' } ], // | | Paragraph - [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', { name: 'core/p' } ], // | | Paragraph - [ - '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', - { name: 'core/group' }, - ], // | | Group - ] ), - order: new Map( [ - [ - '', - [ - '6cf70164-9097-4460-bcbf-200560546988', - 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', - ], - ], - [ '6cf70164-9097-4460-bcbf-200560546988', [] ], - [ - 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', - [ - 'b26fc763-417d-4f01-b81c-2ec61e14a972', - '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', - ], - ], - [ 'b26fc763-417d-4f01-b81c-2ec61e14a972', [] ], - [ - '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', - [ - 'b3247f75-fd94-4fef-97f9-5bfd162cc416', - 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', - '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', - ], - ], - [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', [] ], - [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', [] ], - [ '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', [] ], - ] ), - parents: new Map( [ - [ '6cf70164-9097-4460-bcbf-200560546988', '' ], - [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', '' ], - [ - 'b26fc763-417d-4f01-b81c-2ec61e14a972', - 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', - ], - [ - '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', - 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', - ], - [ - 'b3247f75-fd94-4fef-97f9-5bfd162cc416', - '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', - ], - [ - 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', - '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', - ], - [ - '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', - '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', - ], - ] ), - }, - blockListSettings: { - 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337': {}, - '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f': {}, - }, blockEditingModes: new Map( [] ), derivedBlockEditingModes: new Map( [] ), + derivedNavModeBlockEditingModes: new Map( [] ), }; const hasContentRoleAttribute = jest.fn( () => false ); @@ -4514,115 +4431,59 @@ describe( 'getBlockEditingMode', () => { ).toBe( 'contentOnly' ); } ); - it( 'should return disabled if explicitly set on a parent', () => { - const state = { - ...baseState, - blockEditingModes: new Map( [ - [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'disabled' ], - ] ), - derivedBlockEditingModes: new Map( [ - [ 'b26fc763-417d-4f01-b81c-2ec61e14a972', 'disabled' ], - [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', 'disabled' ], - [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'disabled' ], - [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', 'disabled' ], - [ '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', 'disabled' ], - ] ), - }; - expect( - getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' ) - ).toBe( 'disabled' ); - } ); - - it( 'should return default if parent is set to contentOnly', () => { - const state = { - ...baseState, - blockEditingModes: new Map( [ - [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'contentOnly' ], - ] ), - }; - expect( - getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' ) - ).toBe( 'default' ); - } ); - - it( 'should return disabled if overridden by a parent', () => { - const state = { - ...baseState, - blockEditingModes: new Map( [ - [ '', 'disabled' ], - [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'default' ], - [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', 'disabled' ], - ] ), - derivedBlockEditingModes: new Map( [ - [ '6cf70164-9097-4460-bcbf-200560546988', 'disabled' ], - [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'disabled' ], - [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', 'disabled' ], - [ '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', 'disabled' ], - ] ), - }; - expect( - getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' ) - ).toBe( 'disabled' ); - } ); - - it( 'should return disabled if explicitly set on root', () => { - const state = { - ...baseState, - blockEditingModes: new Map( [ [ '', 'disabled' ] ] ), - derivedBlockEditingModes: new Map( [ - [ '6cf70164-9097-4460-bcbf-200560546988', 'disabled' ], - [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'disabled' ], - [ 'b26fc763-417d-4f01-b81c-2ec61e14a972', 'disabled' ], - [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', 'disabled' ], - [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'disabled' ], - [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', 'disabled' ], - [ '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', 'disabled' ], - ] ), - }; - expect( - getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' ) - ).toBe( 'disabled' ); - } ); - - it( 'should return default if root is contentOnly', () => { - const state = { - ...baseState, - blockEditingModes: new Map( [ [ '', 'contentOnly' ] ] ), - }; - expect( - getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' ) - ).toBe( 'default' ); - } ); + describe( 'derived block editing modes override standard block editing modes', () => { + it( 'should return default if explicitly set', () => { + const state = { + ...baseState, + blockEditingModes: new Map( [ + [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'contentOnly' ], + ] ), + derivedBlockEditingModes: new Map( [ + [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'default' ], + ] ), + }; + expect( + getBlockEditingMode( + state, + 'b3247f75-fd94-4fef-97f9-5bfd162cc416' + ) + ).toBe( 'default' ); + } ); - it( 'should return disabled if parent is locked and the block has no content role', () => { - const state = { - ...baseState, - blockListSettings: { - ...baseState.blockListSettings, - '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f': { - templateLock: 'contentOnly', - }, - }, - }; - hasContentRoleAttribute.mockReturnValueOnce( false ); - expect( - getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' ) - ).toBe( 'disabled' ); - } ); + it( 'should return disabled if explicitly set', () => { + const state = { + ...baseState, + blockEditingModes: new Map( [ + [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'contentOnly' ], + ] ), + derivedBlockEditingModes: new Map( [ + [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'disabled' ], + ] ), + }; + expect( + getBlockEditingMode( + state, + 'b3247f75-fd94-4fef-97f9-5bfd162cc416' + ) + ).toBe( 'disabled' ); + } ); - it( 'should return contentOnly if parent is locked and the block has a content role', () => { - const state = { - ...baseState, - blockListSettings: { - ...baseState.blockListSettings, - '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f': { - templateLock: 'contentOnly', - }, - }, - }; - hasContentRoleAttribute.mockReturnValueOnce( true ); - expect( - getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' ) - ).toBe( 'contentOnly' ); + it( 'should return contentOnly if explicitly set', () => { + const state = { + ...baseState, + blockEditingModes: new Map( [ + [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'default' ], + ] ), + derivedBlockEditingModes: new Map( [ + [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'contentOnly' ], + ] ), + }; + expect( + getBlockEditingMode( + state, + 'b3247f75-fd94-4fef-97f9-5bfd162cc416' + ) + ).toBe( 'contentOnly' ); + } ); } ); } );