From 4fc3d10c438352c1aef6fe6eccd3fb8fbb6b6955 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Wed, 26 Apr 2023 13:23:21 +1000 Subject: [PATCH 1/3] Add ability to set templateLock = 'contentOnly' in editor settings --- .../data/data-core-block-editor.md | 32 ++ .../src/components/block-inspector/index.js | 140 ++------ .../src/components/block-inspector/style.scss | 5 - .../components/block-list-appender/index.js | 4 +- .../src/components/block-list/block.js | 42 +-- .../src/components/block-list/content.scss | 5 +- .../src/components/block-list/index.js | 12 +- .../block-list/use-in-between-inserter.js | 4 +- .../src/components/block-toolbar/index.js | 6 +- .../block-tools/block-contextual-toolbar.js | 6 +- .../components/content-blocks-list/index.js | 77 ++++ .../default-block-appender/index.js | 4 +- .../src/components/list-view/block.js | 36 +- .../src/components/list-view/branch.js | 46 +-- packages/block-editor/src/hooks/align.js | 9 +- .../block-editor/src/hooks/content-lock-ui.js | 23 +- packages/block-editor/src/hooks/duotone.js | 9 +- packages/block-editor/src/hooks/layout.js | 4 +- packages/block-editor/src/private-apis.js | 2 + packages/block-editor/src/store/actions.js | 27 +- packages/block-editor/src/store/defaults.js | 2 + .../block-editor/src/store/private-actions.js | 14 + .../src/store/private-selectors.js | 130 +++++++ packages/block-editor/src/store/reducer.js | 14 +- packages/block-editor/src/store/selectors.js | 165 ++++++--- .../src/store/test/private-actions.js | 19 +- .../src/store/test/private-selectors.js | 335 +++++++++++++++++- .../block-editor/src/store/test/reducer.js | 16 + .../block-editor/src/store/test/selectors.js | 150 ++++++++ packages/data/src/redux-store/index.js | 14 +- 30 files changed, 1047 insertions(+), 305 deletions(-) create mode 100644 packages/block-editor/src/components/content-blocks-list/index.js diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index a3d6fa25e97c8..41c9771765c10 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -1000,6 +1000,23 @@ _Returns_ - `boolean`: Whether the caret is within formatted text. +### isContentLockedBlock + +Returns whether or not the given block is _content locked_. + +A block is _content locked_ if it is nested within a block that has a `templateLock` attribute set to `'contentOnly'` (a _content locking_ block), or if the editor has a `templateLock` of `'contentOnly'`. + +If the block is nested within a content block type (see `settings.contentBlockTypes`) then it is not _content locked_. + +_Parameters_ + +- _state_ `Object`: Global application state. +- _clientId_ `string`: The client ID of the block to check. + +_Returns_ + +- `boolean`: Whether or not the block is content locked. + ### isDraggingBlocks Returns true if the user is dragging blocks, or false otherwise. @@ -1025,6 +1042,21 @@ _Returns_ - `boolean`: Whether block is first in multi-selection. +### isInsertionLocked + +Determines if the editor or a given container is locked and does not allow block insertion. + +Only the `templateLock` settings of the editor or container block are checked. For more rigorous checking that checks the `allowedBlockTypes` attribute, use `canInsertBlockType()`. + +_Parameters_ + +- _state_ `Object`: Editor state. +- _rootClientId_ `?string`: Container block's client ID, or `null` to check the editor. + +_Returns_ + +- `boolean`: Whether block insertion is locked. + ### isLastBlockChangePersistent Returns true if the most recent block change is be considered persistent, or false otherwise. A persistent change is one committed by BlockEditorProvider via its `onChange` callback, in addition to `onInput`. diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index 199f4f4628b4b..e3af8b176d532 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -8,16 +8,8 @@ import { hasBlockSupport, store as blocksStore, } from '@wordpress/blocks'; -import { - FlexItem, - PanelBody, - __experimentalHStack as HStack, - __experimentalVStack as VStack, - Button, - __unstableMotion as motion, -} from '@wordpress/components'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { useMemo, useCallback } from '@wordpress/element'; +import { PanelBody, __unstableMotion as motion } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -28,7 +20,6 @@ import MultiSelectionInspector from '../multi-selection-inspector'; import BlockVariationTransforms from '../block-variation-transforms'; import useBlockDisplayInformation from '../use-block-display-information'; import { store as blockEditorStore } from '../../store'; -import BlockIcon from '../block-icon'; import BlockStyles from '../block-styles'; import DefaultStylePicker from '../default-style-picker'; import { default as InspectorControls } from '../inspector-controls'; @@ -38,102 +29,34 @@ import AdvancedControls from '../inspector-controls-tabs/advanced-controls-panel import PositionControls from '../inspector-controls-tabs/position-controls-panel'; import useBlockInspectorAnimationSettings from './useBlockInspectorAnimationSettings'; import BlockInfo from '../block-info-slot-fill'; +import ContentBlocksList from '../content-blocks-list'; +import { unlock } from '../../lock-unlock'; -function useContentBlocks( blockTypes, block ) { - const contentBlocksObjectAux = useMemo( () => { - return blockTypes.reduce( ( result, blockType ) => { - if ( - blockType.name !== 'core/list-item' && - Object.entries( blockType.attributes ).some( - ( [ , { __experimentalRole } ] ) => - __experimentalRole === 'content' - ) - ) { - result[ blockType.name ] = true; - } - return result; - }, {} ); - }, [ blockTypes ] ); - const isContentBlock = useCallback( - ( blockName ) => { - return !! contentBlocksObjectAux[ blockName ]; - }, - [ contentBlocksObjectAux ] - ); - return useMemo( () => { - return getContentBlocks( [ block ], isContentBlock ); - }, [ block, isContentBlock ] ); -} - -function getContentBlocks( blocks, isContentBlock ) { - const result = []; - for ( const block of blocks ) { - if ( isContentBlock( block.name ) ) { - result.push( block ); - } - result.push( ...getContentBlocks( block.innerBlocks, isContentBlock ) ); - } - return result; -} - -function BlockNavigationButton( { blockTypes, block, selectedBlock } ) { - const { selectBlock } = useDispatch( blockEditorStore ); - const blockType = blockTypes.find( ( { name } ) => name === block.name ); - const isSelected = - selectedBlock && selectedBlock.clientId === block.clientId; - return ( - +function BlockInspectorLockedBlocks( { contentLockingBlock } ) { + const selectedBlock = useSelect( + ( select ) => select( blockEditorStore ).getSelectedBlockClientId(), + [] ); -} - -function BlockInspectorLockedBlocks( { topLevelLockedBlock } ) { - const { blockTypes, block, selectedBlock } = useSelect( - ( select ) => { - return { - blockTypes: select( blocksStore ).getBlockTypes(), - block: select( blockEditorStore ).getBlock( - topLevelLockedBlock - ), - selectedBlock: select( blockEditorStore ).getSelectedBlock(), - }; - }, - [ topLevelLockedBlock ] + const blockInformation = useBlockDisplayInformation( + contentLockingBlock ?? selectedBlock ); - const blockInformation = useBlockDisplayInformation( topLevelLockedBlock ); - const contentBlocks = useContentBlocks( blockTypes, block ); return (
- + { contentLockingBlock && ( + + ) } - -

- { __( 'Content' ) } -

- { contentBlocks.map( ( contentBlock ) => ( - - ) ) } -
+ { contentLockingBlock && ( + + + + ) }
); } @@ -144,15 +67,16 @@ const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => { selectedBlockName, selectedBlockClientId, blockType, - topLevelLockedBlock, + isContentLocked, + contentLockingBlock, } = useSelect( ( select ) => { const { getSelectedBlockClientId, getSelectedBlockCount, getBlockName, - __unstableGetContentLockingParent, - getTemplateLock, - } = select( blockEditorStore ); + isContentLockedBlock, + getContentLockingBlock, + } = unlock( select( blockEditorStore ) ); const _selectedBlockClientId = getSelectedBlockClientId(); const _selectedBlockName = @@ -165,11 +89,10 @@ const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => { selectedBlockClientId: _selectedBlockClientId, selectedBlockName: _selectedBlockName, blockType: _blockType, - topLevelLockedBlock: - __unstableGetContentLockingParent( _selectedBlockClientId ) || - ( getTemplateLock( _selectedBlockClientId ) === 'contentOnly' - ? _selectedBlockClientId - : undefined ), + isContentLocked: isContentLockedBlock( _selectedBlockClientId ), + contentLockingBlock: getContentLockingBlock( + _selectedBlockClientId + ), }; }, [] ); @@ -241,10 +164,11 @@ const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => { } return null; } - if ( topLevelLockedBlock ) { + + if ( isContentLocked ) { return ( ); } diff --git a/packages/block-editor/src/components/block-inspector/style.scss b/packages/block-editor/src/components/block-inspector/style.scss index 07701f2e4b91d..5cf4a8b382f99 100644 --- a/packages/block-editor/src/components/block-inspector/style.scss +++ b/packages/block-editor/src/components/block-inspector/style.scss @@ -55,8 +55,3 @@ .block-editor-block-inspector__tab-item { flex: 1 1 0px; } - -.block-editor-block-inspector__block-buttons-container { - border-top: $border-width solid $gray-200; - padding: $grid-unit-20; -} diff --git a/packages/block-editor/src/components/block-list-appender/index.js b/packages/block-editor/src/components/block-list-appender/index.js index fc1e624e9a15d..917aa9c06b831 100644 --- a/packages/block-editor/src/components/block-list-appender/index.js +++ b/packages/block-editor/src/components/block-list-appender/index.js @@ -43,16 +43,16 @@ function useAppender( rootClientId, CustomAppender ) { const { hideInserter, isParentSelected } = useSelect( ( select ) => { const { - getTemplateLock, getSelectedBlockClientId, __unstableGetEditorMode, + isInsertionLocked, } = select( blockEditorStore ); const selectedBlockClientId = getSelectedBlockClientId(); return { hideInserter: - !! getTemplateLock( rootClientId ) || + isInsertionLocked( rootClientId ) || __unstableGetEditorMode() === 'zoom-out', isParentSelected: rootClientId === selectedBlockClientId || diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 3980dd7b2aead..d548da7be4af9 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -18,7 +18,6 @@ import { isUnmodifiedDefaultBlock, serializeRawBlock, switchToBlockType, - store as blocksStore, getDefaultBlockName, isUnmodifiedBlock, } from '@wordpress/blocks'; @@ -43,6 +42,8 @@ import BlockHtml from './block-html'; import { useBlockProps } from './use-block-props'; import { store as blockEditorStore } from '../../store'; import { useLayout } from './layout'; +import { unlock } from '../../lock-unlock'; + export const BlockListBlockContext = createContext(); /** @@ -99,32 +100,26 @@ function BlockListBlock( { } ) { const { themeSupportsLayout, - hasContentLockedParent, - isContentBlock, + isContentLocked, + isContent, isContentLocking, - isTemporarilyEditingAsBlocks, + isTemporarilyUnlocked, } = useSelect( ( select ) => { const { getSettings, - __unstableGetContentLockingParent, - getTemplateLock, - __unstableGetTemporarilyEditingAsBlocks, - } = select( blockEditorStore ); - const _hasContentLockedParent = - !! __unstableGetContentLockingParent( clientId ); + getTemporarilyUnlockedBlock, + isContentBlock, + isContentLockedBlock, + isContentLockingBlock, + } = unlock( select( blockEditorStore ) ); return { themeSupportsLayout: getSettings().supportsLayout, - isContentBlock: - select( blocksStore ).__experimentalHasContentRoleAttribute( - name - ), - hasContentLockedParent: _hasContentLockedParent, - isContentLocking: - getTemplateLock( clientId ) === 'contentOnly' && - ! _hasContentLockedParent, - isTemporarilyEditingAsBlocks: - __unstableGetTemporarilyEditingAsBlocks() === clientId, + isContent: isContentBlock( clientId ), + isContentLocked: isContentLockedBlock( clientId ), + isContentLocking: isContentLockingBlock( clientId ), + isTemporarilyUnlocked: + getTemporarilyUnlockedBlock() === clientId, }; }, [ name, clientId ] @@ -160,7 +155,7 @@ function BlockListBlock( { const blockType = getBlockType( name ); - if ( hasContentLockedParent && ! isContentBlock ) { + if ( isContentLocked && ! isContent ) { wrapperProps = { ...wrapperProps, tabIndex: -1, @@ -235,9 +230,8 @@ function BlockListBlock( { className: classnames( { 'is-content-locked': isContentLocking, - 'is-content-locked-temporarily-editing-as-blocks': - isTemporarilyEditingAsBlocks, - 'is-content-block': hasContentLockedParent && isContentBlock, + 'is-content-temporarily-unlocked': isTemporarilyUnlocked, + 'is-content-block': isContentLocked && isContent, }, dataAlign && themeSupportsLayout && `align${ dataAlign }`, className diff --git a/packages/block-editor/src/components/block-list/content.scss b/packages/block-editor/src/components/block-list/content.scss index 268301e598d5a..3e968fa353dd9 100644 --- a/packages/block-editor/src/components/block-list/content.scss +++ b/packages/block-editor/src/components/block-list/content.scss @@ -166,7 +166,8 @@ .block-editor-block-list__block { pointer-events: none; } - .is-content-block { + .is-content-block, + .is-content-block .block-editor-block-list__block { pointer-events: initial; } } @@ -323,7 +324,7 @@ } .is-focus-mode .block-editor-block-list__block.is-content-locked.has-child-selected, -.is-focus-mode .block-editor-block-list__block.is-content-locked-temporarily-editing-as-blocks.has-child-selected { +.is-focus-mode .block-editor-block-list__block.is-content-temporarily-unlocked.has-child-selected { &, & .block-editor-block-list__block { opacity: 1; diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js index 2193cedcf1c10..28c15999b4a88 100644 --- a/packages/block-editor/src/components/block-list/index.js +++ b/packages/block-editor/src/components/block-list/index.js @@ -49,19 +49,18 @@ const pendingBlockVisibilityUpdatesPerRegistry = new WeakMap(); function Root( { className, ...settings } ) { const [ element, setElement ] = useState(); const isLargeViewport = useViewportMatch( 'medium' ); - const { isOutlineMode, isFocusMode, editorMode } = useSelect( - ( select ) => { - const { getSettings, __unstableGetEditorMode } = + const { isOutlineMode, isFocusMode, editorMode, isContentLocked } = + useSelect( ( select ) => { + const { getSettings, __unstableGetEditorMode, getTemplateLock } = select( blockEditorStore ); const { outlineMode, focusMode } = getSettings(); return { isOutlineMode: outlineMode, isFocusMode: focusMode, editorMode: __unstableGetEditorMode(), + isContentLocked: getTemplateLock() === 'contentOnly', }; - }, - [] - ); + }, [] ); const registry = useRegistry(); const { setBlockVisibility } = useDispatch( blockEditorStore ); @@ -111,6 +110,7 @@ function Root( { className, ...settings } ) { 'is-outline-mode': isOutlineMode, 'is-focus-mode': isFocusMode && isLargeViewport, 'is-navigate-mode': editorMode === 'navigation', + 'is-content-locked': isContentLocked, } ), }, settings diff --git a/packages/block-editor/src/components/block-list/use-in-between-inserter.js b/packages/block-editor/src/components/block-list/use-in-between-inserter.js index 6e9365ad10224..8bc8c18cd4eef 100644 --- a/packages/block-editor/src/components/block-list/use-in-between-inserter.js +++ b/packages/block-editor/src/components/block-list/use-in-between-inserter.js @@ -27,7 +27,7 @@ export function useInBetweenInserter() { isBlockInsertionPointVisible, isMultiSelecting, getSelectedBlockClientIds, - getTemplateLock, + isInsertionLocked, __unstableIsWithinBlockOverlay, } = useSelect( blockEditorStore ); const { showInsertionPoint, hideInsertionPoint } = @@ -75,7 +75,7 @@ export function useInBetweenInserter() { } // Don't set the insertion point if the template is locked. - if ( getTemplateLock( rootClientId ) ) { + if ( isInsertionLocked( rootClientId ) ) { return; } diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index d9799849b1399..a05ec28e45f0f 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -51,7 +51,7 @@ const BlockToolbar = ( { hideDragHandle } ) => { isBlockValid, getBlockRootClientId, getSettings, - __unstableGetContentLockingParent, + isContentLockedBlock, } = select( blockEditorStore ); const selectedBlockClientIds = getSelectedBlockClientIds(); const selectedBlockClientId = selectedBlockClientIds[ 0 ]; @@ -73,9 +73,7 @@ const BlockToolbar = ( { hideDragHandle } ) => { isVisual: selectedBlockClientIds.every( ( id ) => getBlockMode( id ) === 'visual' ), - isContentLocked: !! __unstableGetContentLockingParent( - selectedBlockClientId - ), + isContentLocked: isContentLockedBlock( selectedBlockClientId ), }; }, [] ); diff --git a/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js b/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js index c8eee6b48cbd4..3fe013d4b7993 100644 --- a/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js +++ b/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js @@ -79,7 +79,7 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { getBlockName, getBlockParents, getSelectedBlockClientIds, - __unstableGetContentLockingParent, + isContentLockedBlock, } = select( blockEditorStore ); const { getBlockType } = select( blocksStore ); const selectedBlockClientIds = getSelectedBlockClientIds(); @@ -103,9 +103,7 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { true ) && selectedBlockClientIds.length <= 1 && - ! __unstableGetContentLockingParent( - _selectedBlockClientId - ), + ! isContentLockedBlock( _selectedBlockClientId ), }; }, [] ); diff --git a/packages/block-editor/src/components/content-blocks-list/index.js b/packages/block-editor/src/components/content-blocks-list/index.js new file mode 100644 index 0000000000000..2c167e61e5f3a --- /dev/null +++ b/packages/block-editor/src/components/content-blocks-list/index.js @@ -0,0 +1,77 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { + Button, + __experimentalVStack as VStack, + __experimentalHStack as HStack, + FlexItem, +} from '@wordpress/components'; +import { getBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; +import BlockIcon from '../block-icon'; +import { unlock } from '../../lock-unlock'; + +export default function ContentBlocksList( { rootClientId } ) { + const contentBlocks = useSelect( + ( select ) => { + const { + getSelectedBlockClientId, + getContentClientIdsTree, + getBlockName, + } = unlock( select( blockEditorStore ) ); + const selectedBlockClientId = getSelectedBlockClientId(); + return getContentClientIdsTree( rootClientId ) + .map( ( block ) => ( { + ...block, + blockName: getBlockName( block.clientId ), + isSelected: blockHasDescendant( + block, + selectedBlockClientId + ), + } ) ) + .filter( ( { blockName } ) => blockName !== 'core/list-item' ); + }, + [ rootClientId ] + ); + + const { selectBlock } = useDispatch( blockEditorStore ); + + if ( ! contentBlocks.length ) { + return null; + } + + return ( + + { contentBlocks.map( ( { clientId, blockName, isSelected } ) => { + const blockType = getBlockType( blockName ); + return ( + + ); + } ) } + + ); +} + +function blockHasDescendant( block, clientId ) { + return ( + block.clientId === clientId || + block.innerBlocks.some( ( child ) => + blockHasDescendant( child, clientId ) + ) + ); +} diff --git a/packages/block-editor/src/components/default-block-appender/index.js b/packages/block-editor/src/components/default-block-appender/index.js index 07ce2609d571f..1d7254e20887c 100644 --- a/packages/block-editor/src/components/default-block-appender/index.js +++ b/packages/block-editor/src/components/default-block-appender/index.js @@ -79,7 +79,7 @@ export function DefaultBlockAppender( { export default compose( withSelect( ( select, ownProps ) => { - const { getBlockCount, getSettings, getTemplateLock } = + const { getBlockCount, getSettings, isInsertionLocked } = select( blockEditorStore ); const isEmpty = ! getBlockCount( ownProps.rootClientId ); @@ -87,7 +87,7 @@ export default compose( return { showPrompt: isEmpty, - isLocked: !! getTemplateLock( ownProps.rootClientId ), + isLocked: isInsertionLocked( ownProps.rootClientId ), placeholder: bodyPlaceholder, }; } ), diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index 43a135db7e5e8..2ca6ce67f4af8 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -60,31 +60,13 @@ function ListViewBlock( { const [ isHovered, setIsHovered ] = useState( false ); const { clientId } = block; - const { isLocked, isContentLocked, canEdit } = useBlockLock( clientId ); - const forceSelectionContentLock = useSelect( - ( select ) => { - if ( isSelected ) { - return false; - } - if ( ! isContentLocked ) { - return false; - } - return select( blockEditorStore ).hasSelectedInnerBlock( - clientId, - true - ); - }, - [ isContentLocked, clientId, isSelected ] - ); + const { isLocked, canEdit } = useBlockLock( clientId ); - const canExpand = isContentLocked ? false : canEdit; const isFirstSelectedBlock = - forceSelectionContentLock || - ( isSelected && selectedClientIds[ 0 ] === clientId ); + isSelected && selectedClientIds[ 0 ] === clientId; const isLastSelectedBlock = - forceSelectionContentLock || - ( isSelected && - selectedClientIds[ selectedClientIds.length - 1 ] === clientId ); + isSelected && + selectedClientIds[ selectedClientIds.length - 1 ] === clientId; const { toggleBlockHighlight } = useDispatch( blockEditorStore ); @@ -200,7 +182,7 @@ function ListViewBlock( { } const classes = classnames( { - 'is-selected': isSelected || forceSelectionContentLock, + 'is-selected': isSelected, 'is-first-selected': isFirstSelectedBlock, 'is-last-selected': isLastSelectedBlock, 'is-branch-selected': isBranchSelected, @@ -244,14 +226,14 @@ function ListViewBlock( { path={ path } id={ `list-view-${ listViewInstanceId }-block-${ clientId }` } data-block={ clientId } - data-expanded={ canExpand ? isExpanded : undefined } + data-expanded={ canEdit ? isExpanded : undefined } ref={ rowRef } > { ( { ref, tabIndex, onFocus } ) => (
@@ -268,7 +250,7 @@ function ListViewBlock( { currentlyEditingBlockInCanvas ? 0 : tabIndex } onFocus={ onFocus } - isExpanded={ canExpand ? isExpanded : undefined } + isExpanded={ canEdit ? isExpanded : undefined } selectedClientIds={ selectedClientIds } ariaLabel={ blockAriaLabel } ariaDescribedBy={ descriptionId } @@ -317,7 +299,7 @@ function ListViewBlock( { { showBlockActions && BlockSettingsMenu && ( { ( { ref, tabIndex, onFocus } ) => ( { - if ( ! parentId ) { - return true; + const { + getTemplateLock, + isContentLockingBlock, + getContentClientIdsTree, + canEditBlock, + } = unlock( select( blockEditorStore ) ); + + const isContentLocking = parentId + ? isContentLockingBlock( parentId ) + : getTemplateLock() === 'contentOnly'; + + if ( isContentLocking ) { + return getContentClientIdsTree( parentId ); } - const isContentLocked = - select( blockEditorStore ).getTemplateLock( parentId ) === - 'contentOnly'; - const canEdit = select( blockEditorStore ).canEditBlock( parentId ); + if ( ! parentId || canEditBlock( parentId ) ) { + return blocks.filter( Boolean ); + } - return isContentLocked ? false : canEdit; + return []; }, - [ parentId ] + [ parentId, blocks ] ); - const { expandedState, draggedClientIds } = useListViewContext(); + const parentBlockInformation = useBlockDisplayInformation( parentId ); + const syncedBranch = isSyncedBranch || !! parentBlockInformation?.isSynced; - if ( ! canParentExpand ) { - return null; - } + const { expandedState, draggedClientIds } = useListViewContext(); // Only show the appender at the first level. const showAppender = showAppenderProp && level === 1; - const filteredBlocks = blocks.filter( Boolean ); - const blockCount = filteredBlocks.length; + const blockCount = branchBlocks.length; // The appender means an extra row in List View, so add 1 to the row count. const rowCount = showAppender ? blockCount + 1 : blockCount; let nextPosition = listPosition; return ( <> - { filteredBlocks.map( ( block, index ) => { + { branchBlocks.map( ( block, index ) => { const { clientId, innerBlocks } = block; if ( index > 0 ) { nextPosition += countBlocks( - filteredBlocks[ index - 1 ], + branchBlocks[ index - 1 ], expandedState, draggedClientIds, isExpanded diff --git a/packages/block-editor/src/hooks/align.js b/packages/block-editor/src/hooks/align.js index 8d5c7f850e89b..28f004d779821 100644 --- a/packages/block-editor/src/hooks/align.js +++ b/packages/block-editor/src/hooks/align.js @@ -134,11 +134,10 @@ export const withToolbarControls = createHigherOrderComponent( blockAllowedAlignments ).map( ( { name } ) => name ); const isContentLocked = useSelect( - ( select ) => { - return select( - blockEditorStore - ).__unstableGetContentLockingParent( props.clientId ); - }, + ( select ) => + select( blockEditorStore ).isContentLockedBlock( + props.clientId + ), [ props.clientId ] ); if ( ! validAlignments.length || isContentLocked ) { diff --git a/packages/block-editor/src/hooks/content-lock-ui.js b/packages/block-editor/src/hooks/content-lock-ui.js index 568da7974925a..712eb74dc9bb5 100644 --- a/packages/block-editor/src/hooks/content-lock-ui.js +++ b/packages/block-editor/src/hooks/content-lock-ui.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ @@ -13,10 +18,7 @@ import { useEffect, useRef, useCallback } from '@wordpress/element'; */ import { store as blockEditorStore } from '../store'; import { BlockControls, BlockSettingsMenuControls } from '../components'; -/** - * External dependencies - */ -import classnames from 'classnames'; +import { unlock } from '../lock-unlock'; function StopEditingAsBlocksOnOutsideSelect( { clientId, @@ -49,18 +51,15 @@ export const withBlockControls = createHigherOrderComponent( const { templateLock, isLockedByParent, isEditingAsBlocks } = useSelect( ( select ) => { const { - __unstableGetContentLockingParent, + isContentLockedBlock, getTemplateLock, - __unstableGetTemporarilyEditingAsBlocks, - } = select( blockEditorStore ); + getTemporarilyUnlockedBlock, + } = unlock( select( blockEditorStore ) ); return { templateLock: getTemplateLock( props.clientId ), - isLockedByParent: !! __unstableGetContentLockingParent( - props.clientId - ), + isLockedByParent: isContentLockedBlock( props.clientId ), isEditingAsBlocks: - __unstableGetTemporarilyEditingAsBlocks() === - props.clientId, + getTemporarilyUnlockedBlock() === props.clientId, }; }, [ props.clientId ] diff --git a/packages/block-editor/src/hooks/duotone.js b/packages/block-editor/src/hooks/duotone.js index c308cfb58a5b4..e3ebf0d704138 100644 --- a/packages/block-editor/src/hooks/duotone.js +++ b/packages/block-editor/src/hooks/duotone.js @@ -226,11 +226,10 @@ const withDuotoneControls = createHigherOrderComponent( ); const isContentLocked = useSelect( - ( select ) => { - return select( - blockEditorStore - ).__unstableGetContentLockingParent( props.clientId ); - }, + ( select ) => + select( blockEditorStore ).isContentLockedBlock( + props.clientId + ), [ props.clientId ] ); diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index b05068ffbbce5..efec9216612ef 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -141,11 +141,11 @@ function LayoutPanel( { const defaultThemeLayout = useSetting( 'layout' ); const { themeSupportsLayout, isContentLocked } = useSelect( ( select ) => { - const { getSettings, __unstableGetContentLockingParent } = + const { getSettings, isContentLockedBlock } = select( blockEditorStore ); return { themeSupportsLayout: getSettings().supportsLayout, - isContentLocked: __unstableGetContentLockingParent( clientId ), + isContentLocked: isContentLockedBlock( clientId ), }; }, [ clientId ] diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index 0b7e8b2511aa6..ee6de4937fa2f 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -12,6 +12,7 @@ import { PrivateListView } from './components/list-view'; import BlockInfo from './components/block-info-slot-fill'; import { useShouldContextualToolbarShow } from './utils/use-should-contextual-toolbar-show'; import { cleanEmptyObject } from './hooks/utils'; +import ContentBlocksList from './components/content-blocks-list'; /** * Private @wordpress/block-editor APIs. @@ -28,4 +29,5 @@ lock( privateApis, { BlockInfo, useShouldContextualToolbarShow, cleanEmptyObject, + ContentBlocksList, } ); diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 27ff57e74919c..e84b645defe1f 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -25,7 +25,10 @@ import { retrieveSelectedAttribute, START_OF_SELECTED_AREA, } from '../utils/selection'; -import { __experimentalUpdateSettings } from './private-actions'; +import { + __experimentalUpdateSettings, + setTemporarilyUnlockedBlock, +} from './private-actions'; /** @typedef {import('../components/use-on-block-drop/types').WPDropOperation} WPDropOperation */ @@ -1630,7 +1633,7 @@ export const insertBeforeBlock = return; } const rootClientId = select.getBlockRootClientId( clientId ); - const isLocked = select.getTemplateLock( rootClientId ); + const isLocked = select.isInsertionLocked( rootClientId ); if ( isLocked ) { return; } @@ -1655,7 +1658,7 @@ export const insertAfterBlock = return; } const rootClientId = select.getBlockRootClientId( clientId ); - const isLocked = select.getTemplateLock( rootClientId ); + const isLocked = select.isInsertionLocked( rootClientId ); if ( isLocked ) { return; } @@ -1724,20 +1727,12 @@ export function setBlockVisibility( updates ) { }; } -/** - * Action that sets whether a block is being temporaritly edited as blocks. - * - * DO-NOT-USE in production. - * This action is created for internal/experimental only usage and may be - * removed anytime without any warning, causing breakage on any plugin or theme invoking it. - * - * @param {?string} temporarilyEditingAsBlocks The block's clientId being temporaritly edited as blocks. - */ export function __unstableSetTemporarilyEditingAsBlocks( temporarilyEditingAsBlocks ) { - return { - type: 'SET_TEMPORARILY_EDITING_AS_BLOCKS', - temporarilyEditingAsBlocks, - }; + deprecated( '__unstableSetTemporarilyEditingAsBlocks', { + since: '6.3', + version: '6.4', + } ); + return setTemporarilyUnlockedBlock( temporarilyEditingAsBlocks ); } diff --git a/packages/block-editor/src/store/defaults.js b/packages/block-editor/src/store/defaults.js index 7c33350d12dd0..7b7684306e70b 100644 --- a/packages/block-editor/src/store/defaults.js +++ b/packages/block-editor/src/store/defaults.js @@ -269,4 +269,6 @@ export const SETTINGS_DEFAULTS = { ], __unstableResolvedAssets: { styles: [], scripts: [] }, + + contentBlockTypes: null, }; diff --git a/packages/block-editor/src/store/private-actions.js b/packages/block-editor/src/store/private-actions.js index 2d33ea82cb9b6..fa85e5c684d7b 100644 --- a/packages/block-editor/src/store/private-actions.js +++ b/packages/block-editor/src/store/private-actions.js @@ -66,3 +66,17 @@ export function showBlockInterface() { type: 'SHOW_BLOCK_INTERFACE', }; } + +/** + * Marks the given block as temporarily unlocked. + * + * @param {string} clientId The client ID of the block to unlock. + * + * @return {Object} Action object. + */ +export function setTemporarilyUnlockedBlock( clientId ) { + return { + type: 'SET_TEMPORARILY_UNLOCKED_BLOCK', + clientId, + }; +} diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index 60712e6b8eb6e..cdfb42b2a0462 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -1,3 +1,25 @@ +/** + * External dependencies + */ +import createSelector from 'rememo'; + +/** + * WordPress dependencies + */ +import { createRegistrySelector } from '@wordpress/data'; +import { store as blocksStore } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { + getBlockParents, + getTemplateLock, + getBlockName, + __unstableGetClientIdWithClientIdsTree, + getBlockOrder, +} from './selectors'; + /** * Returns true if the block interface is hidden, or false otherwise. * @@ -18,3 +40,111 @@ export function isBlockInterfaceHidden( state ) { export function getLastInsertedBlocksClientIds( state ) { return state?.lastBlockInserted?.clientIds; } + +/** + * Returns whether or not the given block is a _content locking_ block. + * + * A block is _content locking_ if it is the top-most block that has a + * `templateLock` attribute set to `'contentOnly'`. + * + * @param {Object} state Global application state. + * @param {string} clientId The client ID of the block to check. + * + * @return {boolean} Whether or not the block is a _content locking_ block. + */ +export const isContentLockingBlock = ( state, clientId ) => + getContentLockingBlock( state, clientId ) === clientId; + +/** + * Returns the client ID of the _content locking_ block that contains the given + * block, or `undefined` if the block is not nested within a _content locking_ + * block. + * + * A block is _content locking_ if it is the top-most block that has a + * `templateLock` attribute set to `'contentOnly'`. + * + * @param {Object} state Global application state. + * @param {string} clientId The client ID of the block to check. + * + * @return {string|undefined} The client ID of the _content locking_ block that + * contains the given block, if it exists. + */ +export const getContentLockingBlock = createSelector( + ( state, clientId ) => { + if ( getTemplateLock( state ) === 'contentOnly' ) { + return; + } + + return [ ...getBlockParents( state, clientId ), clientId ].find( + ( candidateClientId ) => + getTemplateLock( state, candidateClientId ) === 'contentOnly' + ); + }, + ( state ) => [ + state.settings.templateLock, + state.blocks.parents, + state.blockListSettings, + ] +); + +/** + * Returns whether or not the given block is a _content block_. + * + * A block is _content block_ if its block type is in + * `settings.contentBlockTypes` or if it has an attribute with the `'content'` + * role. + * + * @param {Object} state Global application state. + * @param {string} clientId The client ID of the block to check. + * + * @return {boolean} Whether or not the block is a _content block_. + */ +export const isContentBlock = createRegistrySelector( + ( select ) => ( state, clientId ) => { + const blockName = getBlockName( state, clientId ); + return state.settings.contentBlockTypes + ? state.settings.contentBlockTypes.includes( blockName ) + : select( blocksStore ).__experimentalHasContentRoleAttribute( + blockName + ); + } +); + +/** + * Returns all of the _content blocks_. If a `rootClientId` is provided, only + * the _content blocks_ that are descendants of that block are returned. + * + * The resultant array is a tree of block objects containing only the `clientId` + * and `innerBlocks` properties. + * + * @see isContentBlock + * + * @param {Object} state Global application state. + * @param {string} rootClientId Optional root client ID of block list. + * + * @return {Object[]} Array of block obejcts containing `clientId` and `innerBlocks`. + */ +export const getContentClientIdsTree = createSelector( + ( state, rootClientId = null ) => { + return getBlockOrder( state, rootClientId ).flatMap( ( clientId ) => + isContentBlock( state, clientId ) + ? [ __unstableGetClientIdWithClientIdsTree( state, clientId ) ] + : getContentClientIdsTree( state, clientId ) + ); + }, + ( state ) => [ state.blocks.order ] +); + +/** + * Returns the client ID of the block that is temporarily unlocked, or null if + * no block is temporarily unlocked. + * + * Used to allow the user to temporarily edit a _content locked_ block. + * + * @param {Object} state Global application state. + * + * @return {string|null} Client ID of the temporarily unlocked block, or null. + */ +export function getTemporarilyUnlockedBlock( state ) { + return state.temporarilyUnlockedBlock; +} diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 4239cb1aba848..5437d8938e78d 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1820,16 +1820,16 @@ export function lastBlockInserted( state = {}, action ) { } /** - * Reducer returning the block that is eding temporarily edited as blocks. + * Reducer returning the block client ID that is temporarily unlocked. * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. + * @param {string|null} state Current state. + * @param {Object} action Dispatched action. * * @return {Object} Updated state. */ -export function temporarilyEditingAsBlocks( state = '', action ) { - if ( action.type === 'SET_TEMPORARILY_EDITING_AS_BLOCKS' ) { - return action.temporarilyEditingAsBlocks; +export function temporarilyUnlockedBlock( state = null, action ) { + if ( action.type === 'SET_TEMPORARILY_UNLOCKED_BLOCK' ) { + return action.clientId; } return state; } @@ -1854,7 +1854,7 @@ const combinedReducers = combineReducers( { hasBlockMovingClientId, highlightedBlock, lastBlockInserted, - temporarilyEditingAsBlocks, + temporarilyUnlockedBlock, blockVisibility, } ); diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index e61e6063a33e6..49b5ec8bb9e97 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -27,6 +27,11 @@ import deprecated from '@wordpress/deprecated'; */ import { mapRichTextSettings } from './utils'; import { orderBy } from '../utils/sorting'; +import { + getContentLockingBlock, + getTemporarilyUnlockedBlock, + isContentBlock, +} from './private-selectors'; /** * A block selection object. @@ -1535,8 +1540,7 @@ const canInsertBlockTypeUnmemoized = ( return false; } - const isLocked = !! getTemplateLock( state, rootClientId ); - if ( isLocked ) { + if ( isInsertionLocked( state, rootClientId ) ) { return false; } @@ -1653,6 +1657,46 @@ export function canInsertBlocks( state, clientIds, rootClientId = null ) { ); } +/** + * Determines if the editor or a given container is locked and does not allow + * block insertion. + * + * Only the `templateLock` settings of the editor or container block are + * checked. For more rigorous checking that checks the `allowedBlockTypes` + * attribute, use `canInsertBlockType()`. + * + * @param {Object} state Editor state. + * @param {?string} rootClientId Container block's client ID, or `null` to check + * the editor. + * + * @return {boolean} Whether block insertion is locked. + */ +export const isInsertionLocked = createSelector( + ( state, rootClientId = null ) => { + const templateLock = getTemplateLock( state, rootClientId ); + if ( rootClientId && templateLock === 'contentOnly' ) { + // Lock insertion into a non content block. + const isWithinContentBlock = + isContentBlock( state, rootClientId ) || + getBlockParents( state, rootClientId ).some( + ( parentClientId ) => + isContentBlock( state, parentClientId ) + ); + return ! isWithinContentBlock; + } + // Otherwise lock insertion when there is a lock. + return !! templateLock; + }, + ( state, rootClientId ) => + rootClientId + ? [ + state.blockListSettings[ rootClientId ], + state.blocks.parents, + state.settings.contentBlockTypes, + ] + : [ state.settings.templateLock ] +); + /** * Determines if the given block is allowed to be deleted. * @@ -1670,15 +1714,27 @@ export function canRemoveBlock( state, clientId, rootClientId = null ) { return true; } + // If the block has a lock defined on it, we use that. const { lock } = attributes; - const parentIsLocked = !! getTemplateLock( state, rootClientId ); - // If we don't have a lock on the blockType level, we defer to the parent templateLock. - if ( lock === undefined || lock?.remove === undefined ) { - return ! parentIsLocked; + if ( lock !== undefined && lock?.remove !== undefined ) { + // When remove is true, it means we cannot remove it. + return ! lock?.remove; + } + + const templateLock = getTemplateLock( state, rootClientId ); + if ( templateLock === 'contentOnly' ) { + // Permit removal when inside a content block. + const isWithinContentBlock = getBlockParents( state, clientId ).some( + ( parentClientId ) => isContentBlock( state, parentClientId ) + ); + return isWithinContentBlock; + } else if ( templateLock ) { + // Prevent removal when template lock exists. + return false; } - // When remove is true, it means we cannot remove it. - return ! lock?.remove; + // Permit removing by default. + return true; } /** @@ -2767,40 +2823,6 @@ export const __unstableGetVisibleBlocks = createSelector( ( state ) => [ state.blockVisibility ] ); -/** - * DO-NOT-USE in production. - * This selector is created for internal/experimental only usage and may be - * removed anytime without any warning, causing breakage on any plugin or theme invoking it. - */ -export const __unstableGetContentLockingParent = createSelector( - ( state, clientId ) => { - let current = clientId; - let result; - while ( state.blocks.parents.has( current ) ) { - current = state.blocks.parents.get( current ); - if ( - current && - getTemplateLock( state, current ) === 'contentOnly' - ) { - result = current; - } - } - return result; - }, - ( state ) => [ state.blocks.parents, state.blockListSettings ] -); - -/** - * DO-NOT-USE in production. - * This selector is created for internal/experimental only usage and may be - * removed anytime without any warning, causing breakage on any plugin or theme invoking it. - * - * @param {Object} state Global application state. - */ -export function __unstableGetTemporarilyEditingAsBlocks( state ) { - return state.temporarilyEditingAsBlocks; -} - export function __unstableHasActiveBlockOverlayActive( state, clientId ) { // If the block editing is locked, the block overlay is always active. if ( ! canEditBlock( state, clientId ) ) { @@ -2841,6 +2863,65 @@ export function __unstableHasActiveBlockOverlayActive( state, clientId ) { ); } +export function __unstableGetContentLockingParent( state, clientId ) { + deprecated( '__unstableGetContentLockingParent', { + since: '6.3', + version: '6.4', + alternative: 'isContentLockedBlock', + } ); + return getContentLockingBlock( state, clientId ); +} + +export function __unstableGetTemporarilyEditingAsBlocks( state ) { + deprecated( '__unstableGetTemporarilyEditingAsBlocks', { + since: '6.3', + version: '6.4', + } ); + return getTemporarilyUnlockedBlock( state ); +} + +/** + * Returns whether or not the given block is _content locked_. + * + * A block is _content locked_ if it is nested within a block that has a + * `templateLock` attribute set to `'contentOnly'` (a _content locking_ block), + * or if the editor has a `templateLock` of `'contentOnly'`. + * + * If the block is nested within a content block type (see + * `settings.contentBlockTypes`) then it is not _content locked_. + * + * @param {Object} state Global application state. + * @param {string} clientId The client ID of the block to check. + * + * @return {boolean} Whether or not the block is content locked. + */ +export const isContentLockedBlock = createSelector( + ( state, clientId ) => { + const isWithinContentBlock = getBlockParents( state, clientId ).some( + ( parentClientId ) => isContentBlock( state, parentClientId ) + ); + if ( isWithinContentBlock ) { + return false; + } + + if ( getTemplateLock( state ) === 'contentOnly' ) { + return true; + } + + const isWithinContentLockingBlock = !! getContentLockingBlock( + state, + clientId + ); + return isWithinContentLockingBlock; + }, + ( state ) => [ + state.blocks.parents, + state.settings.contentBlockTypes, + state.settings.templateLock, + state.blockListSettings, + ] +); + export function __unstableIsWithinBlockOverlay( state, clientId ) { let parent = state.blocks.parents.get( clientId ); while ( !! parent ) { diff --git a/packages/block-editor/src/store/test/private-actions.js b/packages/block-editor/src/store/test/private-actions.js index c4453547f6ce6..b8b37be35e1be 100644 --- a/packages/block-editor/src/store/test/private-actions.js +++ b/packages/block-editor/src/store/test/private-actions.js @@ -1,7 +1,11 @@ /** * Internal dependencies */ -import { hideBlockInterface, showBlockInterface } from '../private-actions'; +import { + hideBlockInterface, + showBlockInterface, + setTemporarilyUnlockedBlock, +} from '../private-actions'; describe( 'private actions', () => { describe( 'hideBlockInterface', () => { @@ -19,4 +23,17 @@ describe( 'private actions', () => { } ); } ); } ); + + describe( 'setTemporarilyUnlockedBlock', () => { + it( 'should return the SET_TEMPORARILY_UNLOCKED_BLOCK action', () => { + expect( + setTemporarilyUnlockedBlock( + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' + ) + ).toEqual( { + type: 'SET_TEMPORARILY_UNLOCKED_BLOCK', + clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + } ); + } ); + } ); } ); diff --git a/packages/block-editor/src/store/test/private-selectors.js b/packages/block-editor/src/store/test/private-selectors.js index c5df265f75db3..d64f24f55d903 100644 --- a/packages/block-editor/src/store/test/private-selectors.js +++ b/packages/block-editor/src/store/test/private-selectors.js @@ -4,6 +4,11 @@ import { isBlockInterfaceHidden, getLastInsertedBlocksClientIds, + isContentLockingBlock, + getContentLockingBlock, + isContentBlock, + getContentClientIdsTree, + getTemporarilyUnlockedBlock, } from '../private-selectors'; describe( 'private selectors', () => { @@ -31,9 +36,7 @@ describe( 'private selectors', () => { lastBlockInserted: {}, }; - expect( getLastInsertedBlocksClientIds( state ) ).toEqual( - undefined - ); + expect( getLastInsertedBlocksClientIds( state ) ).toBeUndefined(); } ); it( 'should return clientIds if blocks have been inserted', () => { @@ -49,4 +52,330 @@ describe( 'private selectors', () => { ] ); } ); } ); + + describe( 'content locking selectors', () => { + const __experimentalHasContentRoleAttribute = jest.fn( () => false ); + isContentBlock.registry = { + select: jest.fn( () => ( { + __experimentalHasContentRoleAttribute, + } ) ), + }; + + const baseState = { + settings: {}, + blocks: { + byClientId: new Map( + Object.entries( { + '6926a815-c923-4daa-bc3f-7da2133b388d': { + clientId: '6926a815-c923-4daa-bc3f-7da2133b388d', + name: 'core/group', + }, + '9f88f941-9984-419f-8ae7-e427c5b57513': { + clientId: '9f88f941-9984-419f-8ae7-e427c5b57513', + name: 'core/post-content', + }, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { + clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + name: 'core/paragraph', + }, + } ) + ), + attributes: new Map( + Object.entries( { + '6926a815-c923-4daa-bc3f-7da2133b388d': {}, + '9f88f941-9984-419f-8ae7-e427c5b57513': {}, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': {}, + } ) + ), + order: new Map( + Object.entries( { + '': [ '6926a815-c923-4daa-bc3f-7da2133b388d' ], + '6926a815-c923-4daa-bc3f-7da2133b388d': [ + '9f88f941-9984-419f-8ae7-e427c5b57513', + ], + '9f88f941-9984-419f-8ae7-e427c5b57513': [ + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + ], + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': [], + } ) + ), + parents: new Map( + Object.entries( { + '6926a815-c923-4daa-bc3f-7da2133b388d': '', + '9f88f941-9984-419f-8ae7-e427c5b57513': + '6926a815-c923-4daa-bc3f-7da2133b388d', + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': + '9f88f941-9984-419f-8ae7-e427c5b57513', + } ) + ), + }, + blockListSettings: { + '6926a815-c923-4daa-bc3f-7da2133b388d': {}, + '9f88f941-9984-419f-8ae7-e427c5b57513': {}, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': {}, + }, + }; + + describe( 'isContentLockingBlock', () => { + it( 'should return true if the block is the content locking block', () => { + const state = { + ...baseState, + blockListSettings: { + '6926a815-c923-4daa-bc3f-7da2133b388d': { + templateLock: 'contentOnly', + }, + '9f88f941-9984-419f-8ae7-e427c5b57513': { + templateLock: 'contentOnly', + }, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { + templateLock: 'contentOnly', + }, + }, + }; + expect( + isContentLockingBlock( + state, + '6926a815-c923-4daa-bc3f-7da2133b388d' + ) + ).toBe( true ); + } ); + + it( 'should return false if the block is not the content locking block', () => { + const state = { + ...baseState, + blockListSettings: { + '6926a815-c923-4daa-bc3f-7da2133b388d': { + templateLock: 'contentOnly', + }, + '9f88f941-9984-419f-8ae7-e427c5b57513': { + templateLock: 'contentOnly', + }, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { + templateLock: 'contentOnly', + }, + }, + }; + expect( + isContentLockingBlock( + state, + 'a0d1cb17-2c08-4e7a-91be-007ba7ddc3a1' + ) + ).toBe( false ); + } ); + } ); + + describe( 'getContentLockingBlock', () => { + it( 'should return undefined if there is no content locking block', () => { + const state = { + ...baseState, + }; + expect( + getContentLockingBlock( + state, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' + ) + ).toBeUndefined(); + } ); + + it( 'should return the topmost content locking block', () => { + const state = { + ...baseState, + blockListSettings: { + '6926a815-c923-4daa-bc3f-7da2133b388d': { + templateLock: 'contentOnly', + }, + '9f88f941-9984-419f-8ae7-e427c5b57513': { + templateLock: 'contentOnly', + }, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { + templateLock: 'contentOnly', + }, + }, + }; + expect( + getContentLockingBlock( + state, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' + ) + ).toBe( '6926a815-c923-4daa-bc3f-7da2133b388d' ); + } ); + + it( 'should return the given block if it is the sole content locking block', () => { + const state = { + ...baseState, + blockListSettings: { + '6926a815-c923-4daa-bc3f-7da2133b388d': {}, + '9f88f941-9984-419f-8ae7-e427c5b57513': {}, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { + templateLock: 'contentOnly', + }, + }, + }; + expect( + getContentLockingBlock( + state, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' + ) + ).toBe( 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ); + } ); + + it( 'should return undefined if editor is content locked', () => { + const state = { + ...baseState, + settings: { + templateLock: 'contentOnly', + }, + blockListSettings: { + '6926a815-c923-4daa-bc3f-7da2133b388d': { + templateLock: 'contentOnly', + }, + '9f88f941-9984-419f-8ae7-e427c5b57513': { + templateLock: 'contentOnly', + }, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { + templateLock: 'contentOnly', + }, + }, + }; + expect( + getContentLockingBlock( + state, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' + ) + ).toBeUndefined(); + } ); + } ); + + describe( 'isContentBlock', () => { + it( 'should return false by default', () => { + const state = { + ...baseState, + }; + expect( + isContentBlock( + state, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' + ) + ).toBe( false ); + } ); + + it( 'should return true if the block type is in settings.contentBlockTypes', () => { + const state = { + ...baseState, + settings: { + contentBlockTypes: [ 'core/paragraph' ], + }, + }; + expect( + isContentBlock( + state, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' + ) + ).toBe( true ); + } ); + + it( 'should return true if the block has a content attribute', () => { + __experimentalHasContentRoleAttribute.mockReturnValue( true ); + const state = { + ...baseState, + }; + expect( + isContentBlock( + state, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' + ) + ).toBe( true ); + __experimentalHasContentRoleAttribute.mockReturnValue( false ); + } ); + } ); + + describe( 'getContentClientIdsTree', () => { + it( 'should return an empty array if there are no content blocks', () => { + const state = { + ...baseState, + }; + expect( getContentClientIdsTree( state ) ).toEqual( [] ); + getContentClientIdsTree.clear(); + } ); + + it( 'should return all content blocks', () => { + const state = { + ...baseState, + settings: { + contentBlockTypes: [ 'core/paragraph' ], + }, + }; + expect( getContentClientIdsTree( state ) ).toEqual( [ + { + clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + innerBlocks: [], + }, + ] ); + getContentClientIdsTree.clear(); + } ); + + it( 'should return all children of content blocks', () => { + const state = { + ...baseState, + settings: { + contentBlockTypes: [ 'core/post-content' ], + }, + }; + expect( getContentClientIdsTree( state ) ).toEqual( [ + { + clientId: '9f88f941-9984-419f-8ae7-e427c5b57513', + innerBlocks: [ + { + clientId: + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + innerBlocks: [], + }, + ], + }, + ] ); + getContentClientIdsTree.clear(); + } ); + + it( 'should return content blocks nested within a given root', () => { + const state = { + ...baseState, + settings: { + contentBlockTypes: [ + 'core/post-content', + 'core/paragraph', + ], + }, + }; + expect( + getContentClientIdsTree( + state, + '9f88f941-9984-419f-8ae7-e427c5b57513' + ) + ).toEqual( [ + { + clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + innerBlocks: [], + }, + ] ); + getContentClientIdsTree.clear(); + } ); + } ); + + describe( 'getTemporarilyUnlockedBlock', () => { + it( 'should return undefined if there are no temporarily unlocked blocks', () => { + const state = {}; + expect( getTemporarilyUnlockedBlock( state ) ).toBeUndefined(); + } ); + + it( 'should return the clientId of the temporarily unlocked block', () => { + const state = { + temporarilyUnlockedBlock: + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + }; + expect( getTemporarilyUnlockedBlock( state ) ).toBe( + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' + ); + } ); + } ); + } ); } ); diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js index 609cbb59c6e54..3d715830edd9f 100644 --- a/packages/block-editor/src/store/test/reducer.js +++ b/packages/block-editor/src/store/test/reducer.js @@ -32,6 +32,7 @@ import { blockListSettings, lastBlockAttributesChange, lastBlockInserted, + temporarilyUnlockedBlock, } from '../reducer'; const noop = () => {}; @@ -3367,4 +3368,19 @@ describe( 'state', () => { expect( state ).toEqual( expectedState ); } ); } ); + + describe( 'temporarilyUnlockedBlock', () => { + it( 'defaults to null', () => { + const state = temporarilyUnlockedBlock( undefined, {} ); + expect( state ).toBeNull(); + } ); + + it( 'is set when SET_TEMPORARILY_UNLOCKED_BLOCK is dispatched', () => { + const state = temporarilyUnlockedBlock( null, { + type: 'SET_TEMPORARILY_UNLOCKED_BLOCK', + clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + } ); + expect( state ).toBe( 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' ); + } ); + } ); } ); diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 60d90d80b9d41..6a14eb77a93f0 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -12,6 +12,7 @@ import { RawHTML } from '@wordpress/element'; * Internal dependencies */ import * as selectors from '../selectors'; +import { isContentBlock } from '../private-selectors'; const { getBlockName, @@ -72,6 +73,7 @@ const { __experimentalGetPatternTransformItems, wasBlockJustInserted, __experimentalGetGlobalBlocksByName, + isContentLockedBlock, } = selectors; describe( 'selectors', () => { @@ -4687,3 +4689,151 @@ describe( '__unstableGetClientIdsTree', () => { ] ); } ); } ); + +describe( 'isContentLockedBlock', () => { + isContentBlock.registry = { + select: jest.fn( () => ( { + __experimentalHasContentRoleAttribute: jest.fn( () => false ), + } ) ), + }; + + const baseState = { + settings: {}, + blocks: { + byClientId: new Map( + Object.entries( { + '6926a815-c923-4daa-bc3f-7da2133b388d': { + clientId: '6926a815-c923-4daa-bc3f-7da2133b388d', + name: 'core/group', + }, + '9f88f941-9984-419f-8ae7-e427c5b57513': { + clientId: '9f88f941-9984-419f-8ae7-e427c5b57513', + name: 'core/post-content', + }, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { + clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + name: 'core/paragraph', + }, + } ) + ), + attributes: new Map( + Object.entries( { + '6926a815-c923-4daa-bc3f-7da2133b388d': {}, + '9f88f941-9984-419f-8ae7-e427c5b57513': {}, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': {}, + } ) + ), + order: new Map( + Object.entries( { + '': [ '6926a815-c923-4daa-bc3f-7da2133b388d' ], + '6926a815-c923-4daa-bc3f-7da2133b388d': [ + '9f88f941-9984-419f-8ae7-e427c5b57513', + ], + '9f88f941-9984-419f-8ae7-e427c5b57513': [ + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + ], + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': [], + } ) + ), + parents: new Map( + Object.entries( { + '6926a815-c923-4daa-bc3f-7da2133b388d': '', + '9f88f941-9984-419f-8ae7-e427c5b57513': + '6926a815-c923-4daa-bc3f-7da2133b388d', + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': + '9f88f941-9984-419f-8ae7-e427c5b57513', + } ) + ), + }, + blockListSettings: { + '6926a815-c923-4daa-bc3f-7da2133b388d': {}, + '9f88f941-9984-419f-8ae7-e427c5b57513': {}, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': {}, + }, + }; + + it( 'should return false by default', () => { + const state = { + ...baseState, + }; + expect( + isContentLockedBlock( + state, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' + ) + ).toBe( false ); + } ); + + it( 'should return true if editor is content locked', () => { + const state = { + ...baseState, + settings: { + templateLock: 'contentOnly', + }, + }; + expect( + isContentLockedBlock( + state, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' + ) + ).toBe( true ); + } ); + + it( 'should return true if block is content locked', () => { + const state = { + ...baseState, + blockListSettings: { + ...baseState.blockListSettings, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { + templateLock: 'contentOnly', + }, + }, + }; + expect( + isContentLockedBlock( + state, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' + ) + ).toBe( true ); + } ); + + it( 'should return true if block is nested within a content locked block', () => { + const state = { + ...baseState, + blockListSettings: { + ...baseState.blockListSettings, + '6926a815-c923-4daa-bc3f-7da2133b388d': { + templateLock: 'contentOnly', + }, + }, + }; + expect( + isContentLockedBlock( + state, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' + ) + ).toBe( true ); + } ); + + it( 'should return false if block is nested within a content block', () => { + const state = { + ...baseState, + settings: { + ...baseState.settings, + contentBlockTypes: [ 'core/post-content' ], + }, + blockListSettings: { + ...baseState.blockListSettings, + '6926a815-c923-4daa-bc3f-7da2133b388d': { + templateLock: 'contentOnly', + }, + }, + }; + expect( + isContentLockedBlock( + state, + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1' + ) + ).toBe( false ); + } ); +} ); diff --git a/packages/data/src/redux-store/index.js b/packages/data/src/redux-store/index.js index c4dc12643673b..50a0ade0d551c 100644 --- a/packages/data/src/redux-store/index.js +++ b/packages/data/src/redux-store/index.js @@ -221,12 +221,14 @@ export default function createReduxStore( key, options ) { get: ( target, prop ) => { return ( mapSelectors( - mapValues( - privateSelectors, - ( selector ) => - ( state, ...args ) => - selector( state.root, ...args ) - ), + mapValues( privateSelectors, ( selector ) => { + if ( selector.isRegistrySelector ) { + selector.registry = registry; + } + + return ( state, ...args ) => + selector( state.root, ...args ); + } ), store )[ prop ] || selectors[ prop ] ); From d79a3ae37d1280305ea1a3a6443c1339456a9581 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Wed, 26 Apr 2023 16:57:23 +1000 Subject: [PATCH 2/3] Remove usage of deprecated selector --- packages/block-library/src/image/edit.js | 5 ++--- packages/block-library/src/media-text/edit.js | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 19e8196dfc7a5..9effc496749cd 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -127,14 +127,13 @@ export function ImageEdit( { const ref = useRef(); const { imageDefaultSize, mediaUpload, isContentLocked } = useSelect( ( select ) => { - const { getSettings, __unstableGetContentLockingParent } = + const { getSettings, isContentLockedBlock } = select( blockEditorStore ); const settings = getSettings(); return { imageDefaultSize: settings.imageDefaultSize, mediaUpload: settings.mediaUpload, - isContentLocked: - !! __unstableGetContentLockingParent( clientId ), + isContentLocked: isContentLockedBlock( clientId ), }; }, [] diff --git a/packages/block-library/src/media-text/edit.js b/packages/block-library/src/media-text/edit.js index f2848a584d0d8..1e39528c3e771 100644 --- a/packages/block-library/src/media-text/edit.js +++ b/packages/block-library/src/media-text/edit.js @@ -149,11 +149,10 @@ function MediaTextEdit( { attributes, isSelected, setAttributes, clientId } ) { const { imageSizes, image, isContentLocked } = useSelect( ( select ) => { - const { __unstableGetContentLockingParent, getSettings } = + const { isContentLockedBlock, getSettings } = select( blockEditorStore ); return { - isContentLocked: - !! __unstableGetContentLockingParent( clientId ), + isContentLocked: isContentLockedBlock( clientId ), image: mediaId && isSelected ? select( coreStore ).getMedia( mediaId, { From 76b011815d4b877d0862ee6107691d10e12bfc39 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Wed, 26 Apr 2023 16:59:42 +1000 Subject: [PATCH 3/3] Add missing useSelect dep --- packages/block-library/src/image/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 9effc496749cd..5f5d8760c0aa5 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -136,7 +136,7 @@ export function ImageEdit( { isContentLocked: isContentLockedBlock( clientId ), }; }, - [] + [ clientId ] ); const { createErrorNotice } = useDispatch( noticesStore );