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
- { ! 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 ]
);