diff --git a/package-lock.json b/package-lock.json index f239212bd376c0..05773bdc375ddd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18666,7 +18666,7 @@ "app-root-dir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/app-root-dir/-/app-root-dir-1.0.2.tgz", - "integrity": "sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g==", + "integrity": "sha1-OBh+wt6nV3//Az/8sSFyaS/24Rg=", "dev": true }, "app-root-path": { @@ -26845,7 +26845,7 @@ "babel-plugin-add-react-displayname": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/babel-plugin-add-react-displayname/-/babel-plugin-add-react-displayname-0.0.5.tgz", - "integrity": "sha512-LY3+Y0XVDYcShHHorshrDbt4KFWL4bSeniCtl4SYZbask+Syngk1uMPCeN9+nSiZo6zX5s0RTq/J9Pnaaf/KHw==", + "integrity": "sha1-M51M3be2X9YtHfnbn+BN4TQSK9U=", "dev": true }, "babel-plugin-apply-mdx-type-prop": { @@ -27268,7 +27268,7 @@ "batch-processor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/batch-processor/-/batch-processor-1.0.0.tgz", - "integrity": "sha512-xoLQD8gmmR32MeuBHgH0Tzd5PuSZx71ZsbhVxOCRbgktZEPe4SQy7s9Z50uPp0F/f7iw2XmkHN2xkgbMfckMDA==", + "integrity": "sha1-dclcMrdI4IUNEMKxaPa9vpiRrOg=", "dev": true }, "bcrypt-pbkdf": { @@ -30558,7 +30558,7 @@ "css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", "dev": true }, "cssesc": { @@ -36679,7 +36679,7 @@ "has-glob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-glob/-/has-glob-1.0.0.tgz", - "integrity": "sha512-D+8A457fBShSEI3tFCj65PAbT++5sKiFtdCdOam0gnfBgw9D277OERk+HM9qYJXmdVLZ/znez10SqHN0BBQ50g==", + "integrity": "sha1-mqqe7b/7G6OZCnsAEPtnjuAIEgc=", "dev": true, "requires": { "is-glob": "^3.0.0" @@ -36688,7 +36688,7 @@ "is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", "dev": true, "requires": { "is-extglob": "^2.1.0" @@ -38502,7 +38502,7 @@ "is-window": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-window/-/is-window-1.0.2.tgz", - "integrity": "sha512-uj00kdXyZb9t9RcAUAwMZAnkBUwdYGhYlt7djMXhfyhUCzwNba50tIiBKR7q0l7tdoBtFVw/3JmLY6fI3rmZmg==", + "integrity": "sha1-LIlspT25feRdPDMTOmXYyfVjSA0=", "dev": true }, "is-windows": { @@ -41879,7 +41879,7 @@ "js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", - "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", + "integrity": "sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8=", "dev": true }, "js-tokens": { @@ -43407,7 +43407,7 @@ "lz-string": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", - "integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==", + "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", "dev": true }, "macos-release": { @@ -46738,7 +46738,7 @@ "num2fraction": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", "dev": true }, "number-is-nan": { @@ -47817,7 +47817,7 @@ "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", "dev": true }, "p-event": { @@ -49156,7 +49156,7 @@ "pretty-hrtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", "dev": true }, "prismjs": { @@ -51388,7 +51388,7 @@ "relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", "dev": true }, "remark": { diff --git a/packages/block-editor/src/components/block-card/index.js b/packages/block-editor/src/components/block-card/index.js index 21832629b9557f..f1ae52859eb6d1 100644 --- a/packages/block-editor/src/components/block-card/index.js +++ b/packages/block-editor/src/components/block-card/index.js @@ -8,7 +8,7 @@ import deprecated from '@wordpress/deprecated'; */ import BlockIcon from '../block-icon'; -function BlockCard( { title, icon, description, blockType } ) { +function BlockCard( { title, icon, description, blockType, backButton } ) { if ( blockType ) { deprecated( '`blockType` property in `BlockCard component`', { since: '5.7', @@ -18,7 +18,7 @@ function BlockCard( { title, icon, description, blockType } ) { } return (
- + { backButton ? backButton : }

{ title }

diff --git a/packages/block-editor/src/components/block-edit/edit.js b/packages/block-editor/src/components/block-edit/edit.js index b302b7b1dc44a3..f9154067e0729a 100644 --- a/packages/block-editor/src/components/block-edit/edit.js +++ b/packages/block-editor/src/components/block-edit/edit.js @@ -59,7 +59,11 @@ export const Edit = ( props ) => { const generatedClassName = hasBlockSupport( blockType, 'className', true ) ? getBlockDefaultClassName( name ) : null; - const className = classnames( generatedClassName, attributes.className ); + const className = classnames( + generatedClassName, + attributes.className, + props.className + ); return ( diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index 92c1fc479b620c..56b0fb0dd5062e 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -11,8 +11,13 @@ import { import { PanelBody, __experimentalUseSlot as useSlot, + FlexItem, + __experimentalHStack as HStack, + __experimentalVStack as VStack, + Button, } from '@wordpress/components'; -import { useSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { useMemo, useCallback } from '@wordpress/element'; /** * Internal dependencies @@ -29,19 +34,116 @@ import DefaultStylePicker from '../default-style-picker'; import BlockVariationTransforms from '../block-variation-transforms'; import useBlockDisplayInformation from '../use-block-display-information'; import { store as blockEditorStore } from '../../store'; +import BlockIcon from '../block-icon'; + +function useContentBlocks( blockTypes, block ) { + const contenBlocksObjectAux = useMemo( () => { + return blockTypes.reduce( ( result, blockType ) => { + if ( + Object.entries( blockType.attributes ).some( + ( [ , { __experimentalRole } ] ) => + __experimentalRole === 'content' + ) + ) { + result[ blockType.name ] = true; + } + return result; + }, {} ); + }, [ blockTypes ] ); + const isContentBlock = useCallback( + ( blockName ) => { + return !! contenBlocksObjectAux[ blockName ]; + }, + [ blockTypes ] + ); + 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( { 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( topLevelLockedBlock ); + const contentBlocks = useContentBlocks( blockTypes, block ); + return ( +
+ + + +

+ { __( 'Content' ) } +

+ { contentBlocks.map( ( contentBlock ) => ( + + ) ) } +
+
+ ); +} const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => { const { count, - hasBlockStyles, selectedBlockName, selectedBlockClientId, blockType, + topLevelLockedBlock, } = useSelect( ( select ) => { const { getSelectedBlockClientId, getSelectedBlockCount, getBlockName, + __unstableGetContentLockingParent, + getTemplateLock, } = select( blockEditorStore ); const { getBlockStyles } = select( blocksStore ); @@ -59,6 +161,12 @@ const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => { selectedBlockName: _selectedBlockName, blockType: _blockType, hasBlockStyles: blockStyles && blockStyles.length > 0, + topLevelLockedBlock: + getTemplateLock( _selectedBlockClientId ) === 'noContent' + ? _selectedBlockClientId + : __unstableGetContentLockingParent( + _selectedBlockClientId + ), }; }, [] ); @@ -109,24 +217,34 @@ const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => { } return null; } + if ( topLevelLockedBlock ) { + return ( + + ); + } return ( ); }; -const BlockInspectorSingleBlock = ( { - clientId, - blockName, - hasBlockStyles, -} ) => { +const BlockInspectorSingleBlock = ( { clientId, blockName, backButton } ) => { + const hasBlockStyles = useSelect( + ( select ) => { + const { getBlockStyles } = select( blocksStore ); + const blockStyles = getBlockStyles( blockName ); + return blockStyles && blockStyles.length > 0; + }, + [ blockName ] + ); const blockInformation = useBlockDisplayInformation( clientId ); return (
- + { hasBlockStyles && (
diff --git a/packages/block-editor/src/components/block-inspector/style.scss b/packages/block-editor/src/components/block-inspector/style.scss index 393121ce4d8bd4..cb864b73ebef3d 100644 --- a/packages/block-editor/src/components/block-inspector/style.scss +++ b/packages/block-editor/src/components/block-inspector/style.scss @@ -34,3 +34,17 @@ padding: ($grid-unit-20 * 2) $grid-unit-20; text-align: center; } + + +.block-editor-block-inspector__block-buttons-container { + border-top: $border-width solid $gray-200; + padding: $grid-unit-20; +} + +.block-editor-block-inspector__block-type-type { + font-weight: 500; + &.block-editor-block-inspector__block-type-type { + line-height: $button-size-small; + margin: 0 0 $grid-unit-05; + } +} diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index fc668c174d2272..a554069d97bb65 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -18,6 +18,7 @@ import { isUnmodifiedDefaultBlock, serializeRawBlock, switchToBlockType, + store as blocksStore, } from '@wordpress/blocks'; import { withFilters } from '@wordpress/components'; import { @@ -93,10 +94,38 @@ function BlockListBlock( { onMerge, toggleSelection, } ) { - const themeSupportsLayout = useSelect( ( select ) => { - const { getSettings } = select( blockEditorStore ); - return getSettings().supportsLayout; - }, [] ); + const { + themeSupportsLayout, + hasContentLockedParent, + isContentBlock, + isContentLocking, + isTemporarilyEditingAsBlocks, + } = useSelect( + ( select ) => { + const { + getSettings, + __unstableGetContentLockingParent, + getTemplateLock, + __unstableGetTemporarilyEditingAsBlocks, + } = select( blockEditorStore ); + const _hasContentLockedParent = + !! __unstableGetContentLockingParent( clientId ); + return { + themeSupportsLayout: getSettings().supportsLayout, + isContentBlock: + select( blocksStore ).__experimentalHasContentRoleAttribute( + name + ), + hasContentLockedParent: _hasContentLockedParent, + isContentLocking: + getTemplateLock( clientId ) === 'noContent' && + ! _hasContentLockedParent, + isTemporarilyEditingAsBlocks: + __unstableGetTemporarilyEditingAsBlocks() === clientId, + }; + }, + [ name, clientId ] + ); const { removeBlock } = useDispatch( blockEditorStore ); const onRemove = useCallback( () => removeBlock( clientId ), [ clientId ] ); @@ -122,6 +151,12 @@ function BlockListBlock( { const blockType = getBlockType( name ); + if ( hasContentLockedParent && ! isContentBlock ) { + wrapperProps = { + ...wrapperProps, + tabIndex: -1, + }; + } // Determine whether the block has props to apply to the wrapper. if ( blockType?.getEditWrapperProps ) { wrapperProps = mergeWrapperProps( @@ -188,13 +223,20 @@ function BlockListBlock( { const value = { clientId, - className: - dataAlign && themeSupportsLayout - ? classnames( className, `align${ dataAlign }` ) - : className, + className: classnames( + { + 'is-content-locked': isContentLocking, + 'is-content-locked-temporarily-editing-as-blocks': + isTemporarilyEditingAsBlocks, + 'is-content-block': hasContentLockedParent && isContentBlock, + }, + dataAlign && themeSupportsLayout && `align${ dataAlign }`, + className + ), wrapperProps: restWrapperProps, isAligned, }; + const memoizedValue = useMemo( () => value, Object.values( value ) ); return ( diff --git a/packages/block-editor/src/components/block-list/style.scss b/packages/block-editor/src/components/block-list/style.scss index 860b468e708421..cf6f2324a58ae4 100644 --- a/packages/block-editor/src/components/block-list/style.scss +++ b/packages/block-editor/src/components/block-list/style.scss @@ -123,6 +123,15 @@ padding: 0; } +.is-content-locked { + .block-editor-block-list__block { + pointer-events: none; + } + .is-content-block { + pointer-events: initial; + } +} + .block-editor-block-list__layout .block-editor-block-list__block { position: relative; @@ -331,6 +340,14 @@ } } +.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 { + &, + & .block-editor-block-list__block { + opacity: 1; + } +} + .wp-block[data-align="left"] > *, .wp-block[data-align="right"] > *, .wp-block.alignleft, 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 24a25d41d8b36b..7733c197286dbb 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 @@ -144,11 +144,11 @@ export function useBlockProps( } return { + tabIndex: 0, ...wrapperProps, ...props, ref: mergedRefs, id: `block-${ clientId }${ htmlSuffix }`, - tabIndex: 0, role: 'document', 'aria-label': blockLabel, 'data-block': clientId, diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index 93a2265f61fb47..07d4e861f60947 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -37,6 +37,7 @@ const BlockToolbar = ( { hideDragHandle } ) => { hasReducedUI, isValid, isVisual, + isContentLocked, } = useSelect( ( select ) => { const { getBlockName, @@ -45,6 +46,7 @@ const BlockToolbar = ( { hideDragHandle } ) => { isBlockValid, getBlockRootClientId, getSettings, + __unstableGetContentLockingParent, } = select( blockEditorStore ); const selectedBlockClientIds = getSelectedBlockClientIds(); const selectedBlockClientId = selectedBlockClientIds[ 0 ]; @@ -66,6 +68,9 @@ const BlockToolbar = ( { hideDragHandle } ) => { isVisual: selectedBlockClientIds.every( ( id ) => getBlockMode( id ) === 'visual' ), + isContentLocked: !! __unstableGetContentLockingParent( + selectedBlockClientId + ), }; }, [] ); @@ -112,24 +117,27 @@ const BlockToolbar = ( { hideDragHandle } ) => { return (
- { ! isMultiToolbar && ! displayHeaderToolbar && ( - - ) } + { ! isMultiToolbar && + ! displayHeaderToolbar && + ! isContentLocked && }
- { ( shouldShowVisualToolbar || isMultiToolbar ) && ( - - - { ! isMultiToolbar && ( - + + { ! isMultiToolbar && ( + + ) } + - ) } - - - ) } + + ) }
{ shouldShowVisualToolbar && isMultiToolbar && ( @@ -161,7 +169,9 @@ const BlockToolbar = ( { hideDragHandle } ) => { ) } - + { ! isContentLocked && ( + + ) }
); }; 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 bd7a3547e13748..ba31043ada2dd8 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 @@ -20,8 +20,12 @@ import { store as blockEditorStore } from '../../store'; function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { const { blockType, hasParents, showParentSelector } = useSelect( ( select ) => { - const { getBlockName, getBlockParents, getSelectedBlockClientIds } = - select( blockEditorStore ); + const { + getBlockName, + getBlockParents, + getSelectedBlockClientIds, + __unstableGetContentLockingParent, + } = select( blockEditorStore ); const { getBlockType } = select( blocksStore ); const selectedBlockClientIds = getSelectedBlockClientIds(); const selectedBlockClientId = selectedBlockClientIds[ 0 ]; @@ -42,7 +46,10 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { '__experimentalParentSelector', true ) && - selectedBlockClientIds.length <= 1, + selectedBlockClientIds.length <= 1 && + ! __unstableGetContentLockingParent( + selectedBlockClientId + ), }; }, [] diff --git a/packages/block-editor/src/components/inner-blocks/README.md b/packages/block-editor/src/components/inner-blocks/README.md index 029d6cf1dad949..d564819acf0ed2 100644 --- a/packages/block-editor/src/components/inner-blocks/README.md +++ b/packages/block-editor/src/components/inner-blocks/README.md @@ -125,6 +125,7 @@ Template locking of `InnerBlocks` is similar to [Custom Post Type templates lock Template locking allows locking the `InnerBlocks` area for the current template. _Options:_ +- `noContent` — prevents all operations. Additionally, the block types that don't have content are hidden from the list view and can't gain focus within the block list. - `'all'` — prevents all operations. It is not possible to insert new blocks. Move existing blocks or delete them. - `'insert'` — prevents inserting or removing blocks, but allows moving existing ones. - `false` — prevents locking from being applied to an `InnerBlocks` area even if a parent block contains locking. ( Boolean ) diff --git a/packages/block-editor/src/components/inner-blocks/use-inner-block-template-sync.js b/packages/block-editor/src/components/inner-blocks/use-inner-block-template-sync.js index 47efbc41b4a61c..8323f8f7ce4ffc 100644 --- a/packages/block-editor/src/components/inner-blocks/use-inner-block-template-sync.js +++ b/packages/block-editor/src/components/inner-blocks/use-inner-block-template-sync.js @@ -19,8 +19,8 @@ import { store as blockEditorStore } from '../../store'; * This hook makes sure that a block's inner blocks stay in sync with the given * block "template". The template is a block hierarchy to which inner blocks must * conform. If the blocks get "out of sync" with the template and the template - * is meant to be locked (e.g. templateLock = "all"), then we replace the inner - * blocks with the correct value after synchronizing it with the template. + * is meant to be locked (e.g. templateLock = "all" or templateLock = "noContent"), + * then we replace the inner blocks with the correct value after synchronizing it with the template. * * @param {string} clientId The block client ID. * @param {Object} template The template to match. @@ -51,9 +51,13 @@ export default function useInnerBlockTemplateSync( // Maintain a reference to the previous value so we can do a deep equality check. const existingTemplate = useRef( null ); useLayoutEffect( () => { - // Only synchronize innerBlocks with template if innerBlocks are empty or - // a locking all exists directly on the block. - if ( innerBlocks.length === 0 || templateLock === 'all' ) { + // Only synchronize innerBlocks with template if innerBlocks are empty + // or a locking "all" or "noContent" exists directly on the block. + if ( + innerBlocks.length === 0 || + templateLock === 'all' || + templateLock === 'noContent' + ) { const hasTemplateChanged = ! isEqual( template, existingTemplate.current diff --git a/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js b/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js index d0bf71d1e72d22..c8e2e3443ae15d 100644 --- a/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js +++ b/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js @@ -69,7 +69,9 @@ export default function useNestedSettingsUpdate( const newSettings = { allowedBlocks: _allowedBlocks, templateLock: - templateLock === undefined ? parentLock : templateLock, + templateLock === undefined || parentLock === 'noContent' + ? parentLock + : templateLock, }; // These values are not defined for RN, so only include them if they diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index f8645a1071ff10..ef066bbc61aacd 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -67,8 +67,15 @@ function ListViewBlock( { const { toggleBlockHighlight } = useDispatch( blockEditorStore ); const blockInformation = useBlockDisplayInformation( clientId ); - const blockName = useSelect( - ( select ) => select( blockEditorStore ).getBlockName( clientId ), + const { isContentLocked, blockName } = useSelect( + ( select ) => { + const { getBlockName, getTemplateLock } = + select( blockEditorStore ); + return { + blockName: getBlockName( clientId ), + isContentLocked: getTemplateLock( clientId ) === 'noContent', + }; + }, [ clientId ] ); @@ -210,7 +217,7 @@ function ListViewBlock( { path={ path } id={ `list-view-block-${ clientId }` } data-block={ clientId } - isExpanded={ isExpanded } + isExpanded={ isContentLocked ? undefined : isExpanded } aria-selected={ !! isSelected } > { ( { ref, tabIndex, onFocus } ) => ( diff --git a/packages/block-editor/src/components/list-view/branch.js b/packages/block-editor/src/components/list-view/branch.js index 16f0e6488431c2..f486d3631834ba 100644 --- a/packages/block-editor/src/components/list-view/branch.js +++ b/packages/block-editor/src/components/list-view/branch.js @@ -2,14 +2,18 @@ * WordPress dependencies */ import { memo } from '@wordpress/element'; -import { AsyncModeProvider } from '@wordpress/data'; +import { AsyncModeProvider, useSelect } from '@wordpress/data'; +/** + * Internal dependencies + */ /** * Internal dependencies */ import ListViewBlock from './block'; import { useListViewContext } from './context'; import { isClientIdSelected } from './utils'; +import { store as blockEditorStore } from '../../store'; /** * Given a block, returns the total number of blocks in that subtree. This is used to help determine @@ -86,10 +90,26 @@ function ListViewBranch( props ) { listPosition = 0, fixedListWindow, isExpanded, + parentId, } = props; + const isContentLocked = useSelect( + ( select ) => { + return !! ( + parentId && + select( blockEditorStore ).getTemplateLock( parentId ) === + 'noContent' + ); + }, + [ parentId ] + ); + const { expandedState, draggedClientIds } = useListViewContext(); + if ( isContentLocked ) { + return null; + } + const filteredBlocks = blocks.filter( Boolean ); const blockCount = filteredBlocks.length; let nextPosition = listPosition; @@ -161,6 +181,7 @@ function ListViewBranch( props ) { ) } { hasNestedBlocks && shouldExpand && ! isDragged && ( { const { getGlobalBlockCount, getClientIdsOfDescendants } = diff --git a/packages/block-editor/src/components/list-view/use-block-selection.js b/packages/block-editor/src/components/list-view/use-block-selection.js index 417fb436212319..59aaaeacb01d40 100644 --- a/packages/block-editor/src/components/list-view/use-block-selection.js +++ b/packages/block-editor/src/components/list-view/use-block-selection.js @@ -32,7 +32,6 @@ export default function useBlockSelection() { const updateBlockSelection = useCallback( async ( event, clientId, destinationClientId ) => { if ( ! event?.shiftKey ) { - await clearSelectedBlock(); selectBlock( clientId ); return; } 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 427dfcbb56ad51..ab88e1596b508f 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 @@ -92,10 +92,13 @@ export default function useBlockDropZone( { } = {} ) { const [ targetBlockIndex, setTargetBlockIndex ] = useState( null ); - const isLockedAll = useSelect( + const isLocked = useSelect( ( select ) => { const { getTemplateLock } = select( blockEditorStore ); - return getTemplateLock( targetRootClientId ) === 'all'; + const templateLock = getTemplateLock( targetRootClientId ); + return [ 'all', 'noContent' ].some( + ( lock ) => lock === templateLock + ); }, [ targetRootClientId ] ); @@ -127,7 +130,7 @@ export default function useBlockDropZone( { ); return useDropZone( { - isDisabled: isLockedAll, + isDisabled: isLocked, onDrop: onBlockDrop, onDragOver( event ) { // `currentTarget` is only available while the event is being diff --git a/packages/block-editor/src/hooks/content-lock-ui.js b/packages/block-editor/src/hooks/content-lock-ui.js new file mode 100644 index 00000000000000..98098cf583361c --- /dev/null +++ b/packages/block-editor/src/hooks/content-lock-ui.js @@ -0,0 +1,161 @@ +/** + * WordPress dependencies + */ +import { ToolbarButton } from '@wordpress/components'; +import { createHigherOrderComponent } from '@wordpress/compose'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { addFilter } from '@wordpress/hooks'; +import { __ } from '@wordpress/i18n'; +import { useEffect, useRef, useCallback } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../store'; +import { BlockControls } from '../components'; +/** + * External dependencies + */ +import classnames from 'classnames'; + +function StopEditingAsBlocksOnOutsideSelect( { + clientId, + stopEditingAsBlock, +} ) { + const isBlockOrDescendantSelected = useSelect( + ( select ) => { + const { isBlockSelected, hasSelectedInnerBlock } = + select( blockEditorStore ); + return ( + isBlockSelected( clientId ) || + hasSelectedInnerBlock( clientId, true ) + ); + }, + [ clientId ] + ); + useEffect( () => { + if ( ! isBlockOrDescendantSelected ) { + stopEditingAsBlock(); + } + }, [ isBlockOrDescendantSelected ] ); + return null; +} + +export const withBlockControls = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + const { getBlockListSettings, getSettings } = + useSelect( blockEditorStore ); + const focusModeToRevert = useRef(); + const { templateLock, isLockedByParent, isEditingAsBlocks } = useSelect( + ( select ) => { + const { + __unstableGetContentLockingParent, + getTemplateLock, + __unstableGetTemporarilyEditingAsBlocks, + } = select( blockEditorStore ); + return { + templateLock: getTemplateLock( props.clientId ), + isLockedByParent: !! __unstableGetContentLockingParent( + props.clientId + ), + isEditingAsBlocks: + __unstableGetTemporarilyEditingAsBlocks() === + props.clientId, + }; + }, + [ props.clientId ] + ); + + const { + updateSettings, + updateBlockListSettings, + __unstableSetTemporarilyEditingAsBlocks, + } = useDispatch( blockEditorStore ); + const isContentLocked = + ! isLockedByParent && templateLock === 'noContent'; + const { + __unstableMarkNextChangeAsNotPersistent, + updateBlockAttributes, + } = useDispatch( blockEditorStore ); + + const stopEditingAsBlock = useCallback( () => { + __unstableMarkNextChangeAsNotPersistent(); + updateBlockAttributes( props.clientId, { + templateLock: 'noContent', + } ); + updateBlockListSettings( props.clientId, { + ...getBlockListSettings( props.clientId ), + templateLock: 'noContent', + } ); + updateSettings( { focusMode: focusModeToRevert.current } ); + __unstableSetTemporarilyEditingAsBlocks(); + }, [ + props.clientId, + focusModeToRevert, + updateSettings, + updateBlockListSettings, + getBlockListSettings, + __unstableMarkNextChangeAsNotPersistent, + updateBlockAttributes, + __unstableSetTemporarilyEditingAsBlocks, + ] ); + + if ( ! isContentLocked && ! isEditingAsBlocks ) { + return ; + } + + return ( + <> + { isEditingAsBlocks && ! isContentLocked && ( + + ) } + + { + if ( isEditingAsBlocks && ! isContentLocked ) { + stopEditingAsBlock(); + } else { + __unstableMarkNextChangeAsNotPersistent(); + updateBlockAttributes( props.clientId, { + templateLock: undefined, + } ); + updateBlockListSettings( props.clientId, { + ...getBlockListSettings( props.clientId ), + templateLock: false, + } ); + focusModeToRevert.current = + getSettings().focusMode; + updateSettings( { focusMode: true } ); + __unstableSetTemporarilyEditingAsBlocks( + props.clientId + ); + } + } } + > + { isEditingAsBlocks && ! isContentLocked + ? __( 'Done' ) + : __( 'Modify' ) } + + + + + ); + }, + 'withToolbarControls' +); + +addFilter( + 'editor.BlockEdit', + 'core/style/with-block-controls', + withBlockControls +); diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 272e79e78dbdd3..5a07d864beb620 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -15,6 +15,7 @@ import './duotone'; import './font-size'; import './border'; import './layout'; +import './content-lock-ui'; export { useCustomSides } from './dimensions'; export { getBorderClassesAndStyles, useBorderProps } from './use-border-props'; diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index b292bf6437c388..a1c5bea96cdae8 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -1673,3 +1673,17 @@ export function setBlockVisibility( updates ) { updates, }; } + +/** + * Action that sets whether a block is being temporaritly edited as blocks. + * + * @param {?string} temporarilyEditingAsBlocks The block's clientId being temporaritly edited as blocks. + */ +export function __unstableSetTemporarilyEditingAsBlocks( + temporarilyEditingAsBlocks +) { + return { + type: 'SET_TEMPORARILY_EDITING_AS_BLOCKS', + temporarilyEditingAsBlocks, + }; +} diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 534a69c67f1382..73055bdef71996 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1778,6 +1778,21 @@ export function lastBlockInserted( state = {}, action ) { return state; } +/** + * Reducer returning the block that is eding temporarily edited as blocks. + * + * @param {Object} 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; + } + return state; +} + export default combineReducers( { blocks, isTyping, @@ -1798,4 +1813,5 @@ export default combineReducers( { automaticChangeStatus, highlightedBlock, lastBlockInserted, + temporarilyEditingAsBlocks, } ); diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 030977812dcd4d..9f986bcf228ced 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2679,3 +2679,22 @@ export const __unstableGetVisibleBlocks = createSelector( }, ( state ) => [ state.blocks.visibility ] ); + +export const __unstableGetContentLockingParent = createSelector( + ( state, clientId ) => { + let current = clientId; + let result; + while ( !! state.blocks.parents[ current ] ) { + current = state.blocks.parents[ current ]; + if ( getTemplateLock( state, current ) === 'noContent' ) { + result = current; + } + } + return result; + }, + ( state ) => [ state.blocks.parents, state.blockListSettings ] +); + +export function __unstableGetTemporarilyEditingAsBlocks( state ) { + return state.temporarilyEditingAsBlocks; +} diff --git a/packages/block-library/src/column/block.json b/packages/block-library/src/column/block.json index 0601abddd79be7..59472b4c124046 100644 --- a/packages/block-library/src/column/block.json +++ b/packages/block-library/src/column/block.json @@ -19,7 +19,7 @@ }, "templateLock": { "type": [ "string", "boolean" ], - "enum": [ "all", "insert", false ] + "enum": [ "all", "insert", "noContent", false ] } }, "supports": { diff --git a/packages/block-library/src/cover/block.json b/packages/block-library/src/cover/block.json index 12c717370c426d..a7b495cfc3f8f6 100644 --- a/packages/block-library/src/cover/block.json +++ b/packages/block-library/src/cover/block.json @@ -73,7 +73,7 @@ }, "templateLock": { "type": [ "string", "boolean" ], - "enum": [ "all", "insert", false ] + "enum": [ "all", "insert", "noContent", false ] } }, "usesContext": [ "postId", "postType" ], diff --git a/packages/block-library/src/group/block.json b/packages/block-library/src/group/block.json index cfc07d59d15b4a..4e0f15c90d6e96 100644 --- a/packages/block-library/src/group/block.json +++ b/packages/block-library/src/group/block.json @@ -14,7 +14,7 @@ }, "templateLock": { "type": [ "string", "boolean" ], - "enum": [ "all", "insert", false ] + "enum": [ "all", "insert", "noContent", false ] }, "layout": { "type": "object", diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js index 04c87a439fd593..74237b5d5f210c 100644 --- a/packages/blocks/src/store/selectors.js +++ b/packages/blocks/src/store/selectors.js @@ -790,3 +790,13 @@ export const hasChildBlocksWithInserterSupport = ( state, blockName ) => { return hasBlockSupport( state, childBlockName, 'inserter', true ); } ); }; + +export const __experimentalHasContentRoleAttribute = createSelector( + ( state, blockTypeName ) => { + const blockType = getBlockType( state, blockTypeName ); + return Object.entries( blockType.attributes ).some( + ( [ , { __experimentalRole } ] ) => __experimentalRole === 'content' + ); + }, + ( state, blockTypeName ) => [ state.blockTypes[ blockTypeName ].attributes ] +);