diff --git a/packages/block-editor/src/components/block-breadcrumb/index.js b/packages/block-editor/src/components/block-breadcrumb/index.js index 419d4c729b17d2..9e4ab5391d8c44 100644 --- a/packages/block-editor/src/components/block-breadcrumb/index.js +++ b/packages/block-editor/src/components/block-breadcrumb/index.js @@ -11,6 +11,7 @@ import { chevronRightSmall, Icon } from '@wordpress/icons'; */ import BlockTitle from '../block-title'; import { store as blockEditorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; /** * Block breadcrumb component, displaying the hierarchy of the current block selection as a breadcrumb. @@ -22,11 +23,18 @@ import { store as blockEditorStore } from '../../store'; function BlockBreadcrumb( { rootLabelText } ) { const { selectBlock, clearSelectedBlock } = useDispatch( blockEditorStore ); const { clientId, parents, hasSelection } = useSelect( ( select ) => { - const { getSelectionStart, getSelectedBlockClientId, getBlockParents } = - select( blockEditorStore ); + const { + getSelectionStart, + getSelectedBlockClientId, + getBlockParents, + getBlockEditingMode, + } = unlock( select( blockEditorStore ) ); const selectedBlockClientId = getSelectedBlockClientId(); return { - parents: getBlockParents( selectedBlockClientId ), + parents: getBlockParents( selectedBlockClientId ).filter( + ( parentClientId ) => + getBlockEditingMode( parentClientId ) !== 'disabled' + ), clientId: selectedBlockClientId, hasSelection: !! getSelectionStart().clientId, }; diff --git a/packages/block-editor/src/components/block-editing-mode/index.js b/packages/block-editor/src/components/block-editing-mode/index.js new file mode 100644 index 00000000000000..0347a9b7378d0e --- /dev/null +++ b/packages/block-editor/src/components/block-editing-mode/index.js @@ -0,0 +1,71 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { useContext, useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; +import { BlockListBlockContext } from '../block-list/block-list-block-context'; + +/** + * @typedef {'disabled'|'contentOnly'|'default'} BlockEditingMode + */ + +/** + * Allows a block to restrict the user interface that is displayed for editing + * that block and its inner blocks. + * + * @example + * ```js + * function MyBlock( { attributes, setAttributes } ) { + * useBlockEditingMode( 'disabled' ); + * return
; + * } + * ``` + * + * `mode` can be one of three options: + * + * - `'disabled'`: Prevents editing the block entirely, i.e. it cannot be + * selected. + * - `'contentOnly'`: Hides all non-content UI, e.g. auxiliary controls in the + * toolbar, the block movers, block settings. + * - `'default'`: Allows editing the block as normal. + * + * The mode is inherited by all of the block's inner blocks, unless they have + * their own mode. + * + * If called outside of a block context, the mode is applied to all blocks. + * + * @param {?BlockEditingMode} mode The editing mode to apply. If undefined, the + * current editing mode is not changed. + * + * @return {BlockEditingMode} The current editing mode. + */ +export function useBlockEditingMode( mode ) { + const { clientId = '' } = useContext( BlockListBlockContext ) ?? {}; + const blockEditingMode = useSelect( + ( select ) => + unlock( select( blockEditorStore ) ).getBlockEditingMode( + clientId + ), + [ clientId ] + ); + const { setBlockEditingMode, unsetBlockEditingMode } = unlock( + useDispatch( blockEditorStore ) + ); + useEffect( () => { + if ( mode ) { + setBlockEditingMode( clientId, mode ); + } + return () => { + if ( mode ) { + unsetBlockEditingMode( clientId ); + } + }; + }, [ clientId, mode ] ); + return blockEditingMode; +} diff --git a/packages/block-editor/src/components/block-list/block-list-block-context.js b/packages/block-editor/src/components/block-list/block-list-block-context.js new file mode 100644 index 00000000000000..6fa09c6969ec59 --- /dev/null +++ b/packages/block-editor/src/components/block-list/block-list-block-context.js @@ -0,0 +1,6 @@ +/** + * WordPress dependencies + */ +import { createContext } from '@wordpress/element'; + +export const BlockListBlockContext = createContext( null ); diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index c385ea4cd6367f..aa1a40ef480579 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -6,19 +6,13 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { - createContext, - useMemo, - useCallback, - RawHTML, -} from '@wordpress/element'; +import { useMemo, useCallback, RawHTML } from '@wordpress/element'; import { getBlockType, getSaveContent, isUnmodifiedDefaultBlock, serializeRawBlock, switchToBlockType, - store as blocksStore, getDefaultBlockName, isUnmodifiedBlock, } from '@wordpress/blocks'; @@ -43,7 +37,8 @@ import BlockHtml from './block-html'; import { useBlockProps } from './use-block-props'; import { store as blockEditorStore } from '../../store'; import { useLayout } from './layout'; -export const BlockListBlockContext = createContext(); +import { unlock } from '../../lock-unlock'; +import { BlockListBlockContext } from './block-list-block-context'; /** * Merges wrapper props with special handling for classNames and styles. @@ -99,35 +94,23 @@ function BlockListBlock( { } ) { const { themeSupportsLayout, - hasContentLockedParent, - isContentBlock, - isContentLocking, isTemporarilyEditingAsBlocks, + blockEditingMode, } = useSelect( ( select ) => { const { getSettings, - __unstableGetContentLockingParent, - getTemplateLock, __unstableGetTemporarilyEditingAsBlocks, - } = select( blockEditorStore ); - const _hasContentLockedParent = - !! __unstableGetContentLockingParent( clientId ); + getBlockEditingMode, + } = unlock( select( blockEditorStore ) ); return { themeSupportsLayout: getSettings().supportsLayout, - isContentBlock: - select( blocksStore ).__experimentalHasContentRoleAttribute( - name - ), - hasContentLockedParent: _hasContentLockedParent, - isContentLocking: - getTemplateLock( clientId ) === 'contentOnly' && - ! _hasContentLockedParent, isTemporarilyEditingAsBlocks: __unstableGetTemporarilyEditingAsBlocks() === clientId, + blockEditingMode: getBlockEditingMode( clientId ), }; }, - [ name, clientId ] + [ clientId ] ); const { removeBlock } = useDispatch( blockEditorStore ); const onRemove = useCallback( () => removeBlock( clientId ), [ clientId ] ); @@ -160,7 +143,7 @@ function BlockListBlock( { const blockType = getBlockType( name ); - if ( hasContentLockedParent && ! isContentBlock ) { + if ( blockEditingMode === 'disabled' ) { wrapperProps = { ...wrapperProps, tabIndex: -1, @@ -234,10 +217,9 @@ function BlockListBlock( { clientId, className: classnames( { - 'is-content-locked': isContentLocking, + 'is-editing-disabled': blockEditingMode === 'disabled', 'is-content-locked-temporarily-editing-as-blocks': isTemporarilyEditingAsBlocks, - 'is-content-block': hasContentLockedParent && isContentBlock, }, 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 80056f35cf0d6e..dab07ced873e9f 100644 --- a/packages/block-editor/src/components/block-list/content.scss +++ b/packages/block-editor/src/components/block-list/content.scss @@ -161,13 +161,13 @@ padding: 0; } -.is-content-locked { - .block-editor-block-list__block { +.block-editor-block-list__layout, +.block-editor-block-list__block { + pointer-events: initial; + + &.is-editing-disabled { pointer-events: none; } - .is-content-block { - pointer-events: initial; - } } .block-editor-block-list__layout .block-editor-block-list__block { diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index ca6bb4355f52db..acc2bd7f510ede 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -21,7 +21,7 @@ import warning from '@wordpress/warning'; * Internal dependencies */ import useMovingAnimation from '../../use-moving-animation'; -import { BlockListBlockContext } from '../block'; +import { BlockListBlockContext } from '../block-list-block-context'; import { useFocusFirstElement } from './use-focus-first-element'; import { useIsHovered } from './use-is-hovered'; import { useBlockEditContext } from '../../block-edit/context'; 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 6e9365ad102242..5081951696d461 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 @@ -11,6 +11,7 @@ import { isRTL } from '@wordpress/i18n'; */ import { store as blockEditorStore } from '../../store'; import { InsertionPointOpenRef } from '../block-tools/insertion-point'; +import { unlock } from '../../lock-unlock'; export function useInBetweenInserter() { const openRef = useContext( InsertionPointOpenRef ); @@ -29,7 +30,8 @@ export function useInBetweenInserter() { getSelectedBlockClientIds, getTemplateLock, __unstableIsWithinBlockOverlay, - } = useSelect( blockEditorStore ); + getBlockEditingMode, + } = unlock( useSelect( blockEditorStore ) ); const { showInsertionPoint, hideInsertionPoint } = useDispatch( blockEditorStore ); @@ -74,8 +76,10 @@ export function useInBetweenInserter() { rootClientId = blockElement.getAttribute( 'data-block' ); } - // Don't set the insertion point if the template is locked. - if ( getTemplateLock( rootClientId ) ) { + if ( + getTemplateLock( rootClientId ) || + getBlockEditingMode( rootClientId ) === 'disabled' + ) { return; } diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index d9799849b13990..4dc53aae0afffc 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -32,6 +32,7 @@ import BlockEditVisuallyButton from '../block-edit-visually-button'; import { useShowMoversGestures } from './utils'; import { store as blockEditorStore } from '../../store'; import __unstableBlockNameContext from './block-name-context'; +import { unlock } from '../../lock-unlock'; const BlockToolbar = ( { hideDragHandle } ) => { const { @@ -42,7 +43,7 @@ const BlockToolbar = ( { hideDragHandle } ) => { isDistractionFree, isValid, isVisual, - isContentLocked, + blockEditingMode, } = useSelect( ( select ) => { const { getBlockName, @@ -51,8 +52,8 @@ const BlockToolbar = ( { hideDragHandle } ) => { isBlockValid, getBlockRootClientId, getSettings, - __unstableGetContentLockingParent, - } = select( blockEditorStore ); + getBlockEditingMode, + } = unlock( select( blockEditorStore ) ); const selectedBlockClientIds = getSelectedBlockClientIds(); const selectedBlockClientId = selectedBlockClientIds[ 0 ]; const blockRootClientId = getBlockRootClientId( selectedBlockClientId ); @@ -73,9 +74,7 @@ const BlockToolbar = ( { hideDragHandle } ) => { isVisual: selectedBlockClientIds.every( ( id ) => getBlockMode( id ) === 'visual' ), - isContentLocked: !! __unstableGetContentLockingParent( - selectedBlockClientId - ), + blockEditingMode: getBlockEditingMode( selectedBlockClientId ), }; }, [] ); @@ -125,12 +124,12 @@ const BlockToolbar = ( { hideDragHandle } ) => { return (
- { ! isMultiToolbar && isLargeViewport && ! isContentLocked && ( - - ) } + { ! isMultiToolbar && + isLargeViewport && + blockEditingMode === 'default' && }
{ ( shouldShowVisualToolbar || isMultiToolbar ) && - ! isContentLocked && ( + blockEditingMode === 'default' && ( { ! isMultiToolbar && ( @@ -175,7 +174,7 @@ const BlockToolbar = ( { hideDragHandle } ) => { ) } - { ! isContentLocked && ( + { blockEditingMode === 'default' && ( ) }
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 385b120c352d22..d9c06f0324701c 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 @@ -25,6 +25,7 @@ import NavigableToolbar from '../navigable-toolbar'; import BlockToolbar from '../block-toolbar'; import { store as blockEditorStore } from '../../store'; import BlockIcon from '../block-icon'; +import { unlock } from '../../lock-unlock'; function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { // When the toolbar is fixed it can be collapsed @@ -38,8 +39,8 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { getBlockName, getBlockParents, getSelectedBlockClientIds, - __unstableGetContentLockingParent, - } = select( blockEditorStore ); + getBlockEditingMode, + } = unlock( select( blockEditorStore ) ); const { getBlockType } = select( blocksStore ); const selectedBlockClientIds = getSelectedBlockClientIds(); const _selectedBlockClientId = selectedBlockClientIds[ 0 ]; @@ -62,9 +63,7 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { true ) && selectedBlockClientIds.length <= 1 && - ! __unstableGetContentLockingParent( - _selectedBlockClientId - ), + getBlockEditingMode( _selectedBlockClientId ) === 'default', }; }, [] ); diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index 004eb9061cf20e..dc863dd337c0c3 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -38,6 +38,7 @@ import { getBlockPositionDescription } from './utils'; import { store as blockEditorStore } from '../../store'; import useBlockDisplayInformation from '../use-block-display-information'; import { useBlockLock } from '../block-lock'; +import { unlock } from '../../lock-unlock'; function ListViewBlock( { block: { clientId }, @@ -59,31 +60,13 @@ function ListViewBlock( { const rowRef = useRef( null ); const [ isHovered, setIsHovered ] = useState( false ); - 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 ); @@ -97,15 +80,21 @@ function ListViewBlock( { ( select ) => select( blockEditorStore ).getBlockName( clientId ), [ clientId ] ); - - // When a block hides its toolbar it also hides the block settings menu, - // since that menu is part of the toolbar in the editor canvas. - // List View respects this by also hiding the block settings menu. - const showBlockActions = hasBlockSupport( - blockName, - '__experimentalToolbar', - true + const blockEditingMode = useSelect( + ( select ) => + unlock( select( blockEditorStore ) ).getBlockEditingMode( + clientId + ), + [ clientId ] ); + + const showBlockActions = + // When a block hides its toolbar it also hides the block settings menu, + // since that menu is part of the toolbar in the editor canvas. + // List View respects this by also hiding the block settings menu. + hasBlockSupport( blockName, '__experimentalToolbar', true ) && + // Don't show the settings menu if block is disabled or content only. + blockEditingMode === 'default'; const instanceId = useInstanceId( ListViewBlock ); const descriptionId = `list-view-block-select-button__${ instanceId }`; const blockPositionDescription = getBlockPositionDescription( @@ -205,7 +194,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, @@ -249,14 +238,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 } ) => (
@@ -273,7 +262,7 @@ function ListViewBlock( { currentlyEditingBlockInCanvas ? 0 : tabIndex } onFocus={ onFocus } - isExpanded={ canExpand ? isExpanded : undefined } + isExpanded={ canEdit ? isExpanded : undefined } selectedClientIds={ selectedClientIds } ariaLabel={ blockAriaLabel } ariaDescribedBy={ descriptionId } @@ -322,7 +311,7 @@ function ListViewBlock( { { showBlockActions && BlockSettingsMenu && ( { ( { ref, tabIndex, onFocus } ) => ( { + return tree.flatMap( ( { clientId, innerBlocks, ...rest } ) => { + if ( getBlockEditingMode( clientId ) === 'disabled' ) { + return removeDisabledBlocks( innerBlocks ); + } + return [ + { + clientId, + innerBlocks: removeDisabledBlocks( innerBlocks ), + ...rest, + }, + ]; + } ); + }; return { selectedClientIds: getSelectedBlockClientIds(), draggedClientIds: getDraggedBlockClientIds(), - clientIdsTree: blocks - ? blocks - : __unstableGetClientIdsTree( rootClientId ), + clientIdsTree: removeDisabledBlocks( + blocks ?? __unstableGetClientIdsTree( rootClientId ) + ), }; }, [ blocks, rootClientId ] diff --git a/packages/block-editor/src/components/use-block-drop-zone/index.js b/packages/block-editor/src/components/use-block-drop-zone/index.js index d0700bd8d05abb..75f1de2b03e594 100644 --- a/packages/block-editor/src/components/use-block-drop-zone/index.js +++ b/packages/block-editor/src/components/use-block-drop-zone/index.js @@ -19,6 +19,7 @@ import { isPointContainedByRect, } from '../../utils/math'; import { store as blockEditorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; /** @typedef {import('../../utils/math').WPPoint} WPPoint */ /** @typedef {import('../use-on-block-drop/types').WPDropOperation} WPDropOperation */ @@ -150,15 +151,13 @@ export default function useBlockDropZone( { const isDisabled = useSelect( ( select ) => { const { - getTemplateLock, __unstableIsWithinBlockOverlay, __unstableHasActiveBlockOverlayActive, - } = select( blockEditorStore ); - const templateLock = getTemplateLock( targetRootClientId ); + getBlockEditingMode, + } = unlock( select( blockEditorStore ) ); + const blockEditingMode = getBlockEditingMode( targetRootClientId ); return ( - [ 'all', 'contentOnly' ].some( - ( lock ) => lock === templateLock - ) || + blockEditingMode !== 'default' || __unstableHasActiveBlockOverlayActive( targetRootClientId ) || __unstableIsWithinBlockOverlay( targetRootClientId ) ); diff --git a/packages/block-editor/src/hooks/align.js b/packages/block-editor/src/hooks/align.js index 23e6809685377a..a417d11d900431 100644 --- a/packages/block-editor/src/hooks/align.js +++ b/packages/block-editor/src/hooks/align.js @@ -13,14 +13,13 @@ import { getBlockType, hasBlockSupport, } from '@wordpress/blocks'; -import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ import { BlockControls, BlockAlignmentControl } from '../components'; import useAvailableAlignments from '../components/block-alignment-control/use-available-alignments'; -import { store as blockEditorStore } from '../store'; +import { useBlockEditingMode } from '../components/block-editing-mode'; /** * An array which includes all possible valid alignments, @@ -133,15 +132,8 @@ export const withToolbarControls = createHigherOrderComponent( const validAlignments = useAvailableAlignments( blockAllowedAlignments ).map( ( { name } ) => name ); - const isContentLocked = useSelect( - ( select ) => { - return select( - blockEditorStore - ).__unstableGetContentLockingParent( props.clientId ); - }, - [ props.clientId ] - ); - if ( ! validAlignments.length || isContentLocked ) { + const blockEditingMode = useBlockEditingMode(); + if ( ! validAlignments.length || blockEditingMode !== 'default' ) { return blockEdit; } diff --git a/packages/block-editor/src/hooks/duotone.js b/packages/block-editor/src/hooks/duotone.js index c28310cc7b86b0..2ecd35c99ee8c4 100644 --- a/packages/block-editor/src/hooks/duotone.js +++ b/packages/block-editor/src/hooks/duotone.js @@ -16,7 +16,6 @@ import { import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { useMemo, useContext, createPortal } from '@wordpress/element'; -import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -36,8 +35,8 @@ import { import { getBlockCSSSelector } from '../components/global-styles/get-block-css-selector'; import { scopeSelector } from '../components/global-styles/utils'; import { useBlockSettings } from './utils'; -import { store as blockEditorStore } from '../store'; import { default as StylesFiltersPanel } from '../components/global-styles/filters-panel'; +import { useBlockEditingMode } from '../components/block-editing-mode'; const EMPTY_ARRAY = []; @@ -226,14 +225,7 @@ const withDuotoneControls = createHigherOrderComponent( 'filter.duotone' ); - const isContentLocked = useSelect( - ( select ) => { - return select( - blockEditorStore - ).__unstableGetContentLockingParent( props.clientId ); - }, - [ props.clientId ] - ); + const blockEditingMode = useBlockEditingMode(); // CAUTION: code added before this line will be executed // for all blocks, not just those that support duotone. Code added @@ -241,7 +233,7 @@ const withDuotoneControls = createHigherOrderComponent( // performance. return ( <> - { hasDuotoneSupport && ! isContentLocked && ( + { hasDuotoneSupport && blockEditingMode === 'default' && ( ) } diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index 9b11e991004b39..815b36e785a81d 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -29,6 +29,7 @@ import useSetting from '../components/use-setting'; import { LayoutStyle } from '../components/block-list/layout'; import BlockList from '../components/block-list'; import { getLayoutType, getLayoutTypes } from '../layouts'; +import { useBlockEditingMode } from '../components/block-editing-mode'; const layoutBlockSupportKey = '__experimentalLayout'; @@ -131,25 +132,16 @@ export function useLayoutStyles( blockAttributes = {}, blockName, selector ) { return css; } -function LayoutPanel( { - clientId, - setAttributes, - attributes, - name: blockName, -} ) { +function LayoutPanel( { setAttributes, attributes, name: blockName } ) { const { layout } = attributes; const defaultThemeLayout = useSetting( 'layout' ); - const { themeSupportsLayout, isContentLocked } = useSelect( - ( select ) => { - const { getSettings, __unstableGetContentLockingParent } = - select( blockEditorStore ); - return { - themeSupportsLayout: getSettings().supportsLayout, - isContentLocked: __unstableGetContentLockingParent( clientId ), - }; - }, - [ clientId ] - ); + const { themeSupportsLayout } = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + return { + themeSupportsLayout: getSettings().supportsLayout, + }; + }, [] ); + const blockEditingMode = useBlockEditingMode(); const layoutBlockSupport = getBlockSupport( blockName, @@ -270,7 +262,7 @@ function LayoutPanel( { ) } - { ! inherit && ! isContentLocked && layoutType && ( + { ! inherit && blockEditingMode === 'default' && layoutType && ( + ( state, clientId = '' ) => { + const explicitEditingMode = getExplcitBlockEditingMode( + state, + clientId + ); + const rootClientId = getBlockRootClientId( state, clientId ); + const templateLock = getTemplateLock( state, rootClientId ); + const name = getBlockName( state, clientId ); + const isContent = + select( blocksStore ).__experimentalHasContentRoleAttribute( + name + ); + if ( + explicitEditingMode === 'disabled' || + ( templateLock === 'contentOnly' && ! isContent ) + ) { + return 'disabled'; + } + if ( + explicitEditingMode === 'contentOnly' || + ( templateLock === 'contentOnly' && isContent ) + ) { + return 'contentOnly'; + } + return 'default'; + } +); + +const getExplcitBlockEditingMode = createSelector( + ( state, clientId = '' ) => { + while ( + ! state.blockEditingModes.has( clientId ) && + state.blocks.parents.has( clientId ) + ) { + clientId = state.blocks.parents.get( clientId ); + } + return state.blockEditingModes.get( clientId ) ?? 'default'; + }, + ( state ) => [ state.blockEditingModes, state.blocks.parents ] +); diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 4239cb1aba8489..b316bbeb5079e1 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1834,6 +1834,32 @@ export function temporarilyEditingAsBlocks( state = '', action ) { return state; } +/** + * Reducer returning a map of block client IDs to block editing modes. + * + * @param {Map} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Map} Updated state. + */ +export function blockEditingModes( state = new Map(), action ) { + switch ( action.type ) { + case 'SET_BLOCK_EDITING_MODE': + return new Map( state ).set( action.clientId, action.mode ); + case 'UNSET_BLOCK_EDITING_MODE': { + const newState = new Map( state ); + newState.delete( action.clientId ); + return newState; + } + case 'RESET_BLOCKS': { + return state.has( '' ) + ? new Map().set( '', state.get( '' ) ) + : state; + } + } + return state; +} + const combinedReducers = combineReducers( { blocks, isTyping, @@ -1856,6 +1882,7 @@ const combinedReducers = combineReducers( { lastBlockInserted, temporarilyEditingAsBlocks, blockVisibility, + blockEditingModes, } ); function withAutomaticChangeReset( reducer ) { diff --git a/packages/block-editor/src/store/test/private-actions.js b/packages/block-editor/src/store/test/private-actions.js index c4453547f6ce6a..fdfe993091fef7 100644 --- a/packages/block-editor/src/store/test/private-actions.js +++ b/packages/block-editor/src/store/test/private-actions.js @@ -1,7 +1,12 @@ /** * Internal dependencies */ -import { hideBlockInterface, showBlockInterface } from '../private-actions'; +import { + hideBlockInterface, + showBlockInterface, + setBlockEditingMode, + unsetBlockEditingMode, +} from '../private-actions'; describe( 'private actions', () => { describe( 'hideBlockInterface', () => { @@ -19,4 +24,30 @@ describe( 'private actions', () => { } ); } ); } ); + + describe( 'setBlockEditingMode', () => { + it( 'should return the SET_BLOCK_EDITING_MODE action', () => { + expect( + setBlockEditingMode( + '14501cc2-90a6-4f52-aa36-ab6e896135d1', + 'default' + ) + ).toEqual( { + type: 'SET_BLOCK_EDITING_MODE', + clientId: '14501cc2-90a6-4f52-aa36-ab6e896135d1', + mode: 'default', + } ); + } ); + } ); + + describe( 'unsetBlockEditingMode', () => { + it( 'should return the UNSET_BLOCK_EDITING_MODE action', () => { + expect( + unsetBlockEditingMode( '14501cc2-90a6-4f52-aa36-ab6e896135d1' ) + ).toEqual( { + type: 'UNSET_BLOCK_EDITING_MODE', + clientId: '14501cc2-90a6-4f52-aa36-ab6e896135d1', + } ); + } ); + } ); } ); diff --git a/packages/block-editor/src/store/test/private-selectors.js b/packages/block-editor/src/store/test/private-selectors.js index 2c287ceda0f88f..954c8c94c13799 100644 --- a/packages/block-editor/src/store/test/private-selectors.js +++ b/packages/block-editor/src/store/test/private-selectors.js @@ -1,7 +1,11 @@ /** * Internal dependencies */ -import { isBlockInterfaceHidden } from '../private-selectors'; +import { + isBlockInterfaceHidden, + getLastInsertedBlocksClientIds, + getBlockEditingMode, +} from '../private-selectors'; describe( 'private selectors', () => { describe( 'isBlockInterfaceHidden', () => { @@ -21,4 +25,186 @@ describe( 'private selectors', () => { expect( isBlockInterfaceHidden( state ) ).toBe( false ); } ); } ); + + describe( 'getLastInsertedBlocksClientIds', () => { + it( 'should return undefined if no blocks have been inserted', () => { + const state = { + lastBlockInserted: {}, + }; + + expect( getLastInsertedBlocksClientIds( state ) ).toEqual( + undefined + ); + } ); + + it( 'should return clientIds if blocks have been inserted', () => { + const state = { + lastBlockInserted: { + clientIds: [ '123456', '78910' ], + }, + }; + + expect( getLastInsertedBlocksClientIds( state ) ).toEqual( [ + '123456', + '78910', + ] ); + } ); + } ); + + describe( 'getBlockEditingMode', () => { + const baseState = { + settings: {}, + blocks: { + byClientId: new Map( [ + [ '6cf70164-9097-4460-bcbf-200560546988', {} ], // Header + [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', {} ], // Group + [ 'b26fc763-417d-4f01-b81c-2ec61e14a972', {} ], // | Post Title + [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', {} ], // | Post Content + [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', {} ], // | | Paragraph + [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', {} ], // | | Paragraph + ] ), + 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', + ], + ] ), + }, + blockListSettings: { + 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337': {}, + '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f': {}, + }, + blockEditingModes: new Map( [] ), + }; + + const __experimentalHasContentRoleAttribute = jest.fn( () => false ); + getBlockEditingMode.registry = { + select: jest.fn( () => ( { + __experimentalHasContentRoleAttribute, + } ) ), + }; + + it( 'should return default by default', () => { + expect( + getBlockEditingMode( + baseState, + 'b3247f75-fd94-4fef-97f9-5bfd162cc416' + ) + ).toBe( 'default' ); + } ); + + [ 'disabled', 'contentOnly' ].forEach( ( mode ) => { + it( `should return ${ mode } if explicitly set`, () => { + const state = { + ...baseState, + blockEditingModes: new Map( [ + [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', mode ], + ] ), + }; + expect( + getBlockEditingMode( + state, + 'b3247f75-fd94-4fef-97f9-5bfd162cc416' + ) + ).toBe( mode ); + } ); + + it( `should return ${ mode } if explicitly set on a parent`, () => { + const state = { + ...baseState, + blockEditingModes: new Map( [ + [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', mode ], + ] ), + }; + expect( + getBlockEditingMode( + state, + 'b3247f75-fd94-4fef-97f9-5bfd162cc416' + ) + ).toBe( mode ); + } ); + + it( `should return ${ mode } if overridden by a parent`, () => { + const state = { + ...baseState, + blockEditingModes: new Map( [ + [ '', mode ], + [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'default' ], + [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', mode ], + ] ), + }; + expect( + getBlockEditingMode( + state, + 'b3247f75-fd94-4fef-97f9-5bfd162cc416' + ) + ).toBe( mode ); + } ); + + it( `should return ${ mode } if explicitly set on root`, () => { + const state = { + ...baseState, + blockEditingModes: new Map( [ [ '', mode ] ] ), + }; + expect( + getBlockEditingMode( + state, + 'b3247f75-fd94-4fef-97f9-5bfd162cc416' + ) + ).toBe( mode ); + } ); + } ); + + 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', + }, + }, + }; + __experimentalHasContentRoleAttribute.mockReturnValueOnce( false ); + 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', + }, + }, + }; + __experimentalHasContentRoleAttribute.mockReturnValueOnce( true ); + expect( + getBlockEditingMode( + state, + 'b3247f75-fd94-4fef-97f9-5bfd162cc416' + ) + ).toBe( 'contentOnly' ); + } ); + } ); } ); diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js index 609cbb59c6e54b..67ed0ae69106d6 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, + blockEditingModes, } from '../reducer'; const noop = () => {}; @@ -3367,4 +3368,51 @@ describe( 'state', () => { expect( state ).toEqual( expectedState ); } ); } ); + + describe( 'blockEditingModes', () => { + it( 'should return an empty map by default', () => { + expect( blockEditingModes( undefined, {} ) ).toEqual( new Map() ); + } ); + + it( 'should set the editing mode for a block', () => { + const state = new Map(); + const newState = blockEditingModes( state, { + type: 'SET_BLOCK_EDITING_MODE', + clientId: '14501cc2-90a6-4f52-aa36-ab6e896135d1', + mode: 'default', + } ); + expect( newState ).toEqual( + new Map( [ + [ '14501cc2-90a6-4f52-aa36-ab6e896135d1', 'default' ], + ] ) + ); + } ); + + it( 'should clear the editing mode for a block', () => { + const state = new Map( [ + [ '14501cc2-90a6-4f52-aa36-ab6e896135d1', 'default' ], + ] ); + const newState = blockEditingModes( state, { + type: 'UNSET_BLOCK_EDITING_MODE', + clientId: '14501cc2-90a6-4f52-aa36-ab6e896135d1', + } ); + expect( newState ).toEqual( new Map() ); + } ); + + it( 'should clear editing modes when blocks are reset', () => { + const state = new Map( [ + [ '', 'disabled' ], + [ '14501cc2-90a6-4f52-aa36-ab6e896135d1', 'default' ], + ] ); + const newState = blockEditingModes( state, { + type: 'RESET_BLOCKS', + } ); + expect( newState ).toEqual( + new Map( [ + // Root mode should be maintained. + [ '', 'disabled' ], + ] ) + ); + } ); + } ); } ); diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index c4d8de316ea997..95de37062c09a0 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -17,6 +17,7 @@ import { useBlockProps, store as blockEditorStore, __experimentalUseBorderProps as useBorderProps, + privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { useEffect, useRef, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; @@ -27,6 +28,7 @@ import { store as noticesStore } from '@wordpress/notices'; * Internal dependencies */ import Image from './image'; +import { unlock } from '../private-apis'; /** * Module constants @@ -39,6 +41,8 @@ import { ALLOWED_MEDIA_TYPES, } from './constants'; +const { useBlockEditingMode } = unlock( blockEditorPrivateApis ); + export const pickRelevantMediaFiles = ( image, size ) => { const imageProps = Object.fromEntries( Object.entries( image ?? {} ).filter( ( [ key ] ) => @@ -124,20 +128,15 @@ export function ImageEdit( { }, [ caption ] ); const ref = useRef(); - const { imageDefaultSize, mediaUpload, isContentLocked } = useSelect( - ( select ) => { - const { getSettings, __unstableGetContentLockingParent } = - select( blockEditorStore ); - const settings = getSettings(); - return { - imageDefaultSize: settings.imageDefaultSize, - mediaUpload: settings.mediaUpload, - isContentLocked: - !! __unstableGetContentLockingParent( clientId ), - }; - }, - [] - ); + const { imageDefaultSize, mediaUpload } = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + const settings = getSettings(); + return { + imageDefaultSize: settings.imageDefaultSize, + mediaUpload: settings.mediaUpload, + }; + }, [] ); + const blockEditingMode = useBlockEditingMode(); const { createErrorNotice } = useDispatch( noticesStore ); function onUploadError( message ) { @@ -366,10 +365,10 @@ export function ImageEdit( { containerRef={ ref } context={ context } clientId={ clientId } - isContentLocked={ isContentLocked } + blockEditingMode={ blockEditingMode } /> ) } - { ! url && ! isContentLocked && ( + { ! url && blockEditingMode === 'default' && ( - { ! isContentLocked && ( + { hasNonContentControls && ( ) } - { ! isContentLocked && ( + { hasNonContentControls && ( { setShowCaption( ! showCaption ); diff --git a/packages/block-library/src/media-text/edit.js b/packages/block-library/src/media-text/edit.js index f2848a584d0d8a..dcf8eae4ecf5a9 100644 --- a/packages/block-library/src/media-text/edit.js +++ b/packages/block-library/src/media-text/edit.js @@ -18,6 +18,7 @@ import { __experimentalImageURLInputUI as ImageURLInputUI, __experimentalImageSizeControl as ImageSizeControl, store as blockEditorStore, + privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { PanelBody, @@ -43,6 +44,9 @@ import { LINK_DESTINATION_ATTACHMENT, TEMPLATE, } from './constants'; +import { unlock } from '../private-apis'; + +const { useBlockEditingMode } = unlock( blockEditorPrivateApis ); // this limits the resize to a safe zone to avoid making broken layouts const applyWidthConstraints = ( width ) => @@ -126,7 +130,7 @@ function attributesFromMedia( { }; } -function MediaTextEdit( { attributes, isSelected, setAttributes, clientId } ) { +function MediaTextEdit( { attributes, isSelected, setAttributes } ) { const { focalPoint, href, @@ -147,13 +151,10 @@ function MediaTextEdit( { attributes, isSelected, setAttributes, clientId } ) { } = attributes; const mediaSizeSlug = attributes.mediaSizeSlug || DEFAULT_MEDIA_SIZE_SLUG; - const { imageSizes, image, isContentLocked } = useSelect( + const { imageSizes, image } = useSelect( ( select ) => { - const { __unstableGetContentLockingParent, getSettings } = - select( blockEditorStore ); + const { getSettings } = select( blockEditorStore ); return { - isContentLocked: - !! __unstableGetContentLockingParent( clientId ), image: mediaId && isSelected ? select( coreStore ).getMedia( mediaId, { @@ -163,8 +164,7 @@ function MediaTextEdit( { attributes, isSelected, setAttributes, clientId } ) { imageSizes: getSettings()?.imageSizes, }; }, - - [ isSelected, mediaId, clientId ] + [ isSelected, mediaId ] ); const refMediaContainer = useRef(); @@ -319,11 +319,13 @@ function MediaTextEdit( { attributes, isSelected, setAttributes, clientId } ) { { template: TEMPLATE, allowedBlocks } ); + const blockEditingMode = useBlockEditingMode(); + return ( <> { mediaTextGeneralSettings } - { ! isContentLocked && ( + { blockEditingMode === 'default' && ( <> { mediaPosition !== 'right' &&
} diff --git a/packages/block-library/src/media-text/media-container.js b/packages/block-library/src/media-text/media-container.js index e5a6270bad8dc9..951c0013b76ebc 100644 --- a/packages/block-library/src/media-text/media-container.js +++ b/packages/block-library/src/media-text/media-container.js @@ -109,7 +109,7 @@ function MediaContainer( props, ref ) { mediaWidth, onSelectMedia, onWidthChange, - isContentLocked, + enableResize, } = props; const isTemporaryMedia = ! mediaId && isBlobURL( mediaUrl ); @@ -128,8 +128,8 @@ function MediaContainer( props, ref ) { commitWidthChange( parseInt( elt.style.width ) ); }; const enablePositions = { - right: ! isContentLocked && mediaPosition === 'left', - left: ! isContentLocked && mediaPosition === 'right', + right: enableResize && mediaPosition === 'left', + left: enableResize && mediaPosition === 'right', }; const backgroundStyles = diff --git a/packages/data/src/redux-store/index.js b/packages/data/src/redux-store/index.js index c4dc12643673b6..50a0ade0d551c9 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 ] );