diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index 7c7c8ff88176e5..31ababe1ab4357 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -37,6 +37,9 @@ function gutenberg_enable_experiments() { if ( $gutenberg_experiments && array_key_exists( 'gutenberg-editor-write-mode', $gutenberg_experiments ) ) { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEditorWriteMode = true', 'before' ); } + if ( $gutenberg_experiments && array_key_exists( 'gutenberg-content-only-pattern-insertion', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalContentOnlyPatternInsertion = true', 'before' ); + } } add_action( 'admin_init', 'gutenberg_enable_experiments' ); diff --git a/lib/experiments-page.php b/lib/experiments-page.php index a2032eb99abdc5..fe4ce4a2cc756f 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -199,6 +199,18 @@ function gutenberg_initialize_experiments_settings() { ) ); + add_settings_field( + 'gutenberg-content-only-pattern-insertion', + __( 'contentOnly: Make patterns contentOnly by default upon insertion', 'gutenberg' ), + 'gutenberg_display_experiment_field', + 'gutenberg-experiments', + 'gutenberg_experiments_section', + array( + 'label' => __( 'When patterns are inserted, default to a simplified content only mode for editing pattern content.', 'gutenberg' ), + 'id' => 'gutenberg-content-only-pattern-insertion', + ) + ); + register_setting( 'gutenberg-experiments', 'gutenberg-experiments' diff --git a/packages/block-editor/src/autocompleters/block.js b/packages/block-editor/src/autocompleters/block.js index eedd5e102db2ca..6cd8e67a547bd0 100644 --- a/packages/block-editor/src/autocompleters/block.js +++ b/packages/block-editor/src/autocompleters/block.js @@ -3,9 +3,9 @@ */ import { useSelect } from '@wordpress/data'; import { + cloneBlock, createBlock, createBlocksFromInnerBlocksTemplate, - parse, store as blocksStore, } from '@wordpress/blocks'; import { useMemo } from '@wordpress/element'; @@ -125,21 +125,16 @@ function createBlockCompleter() { return ! ( /\S/.test( before ) || /\S/.test( after ) ); }, getOptionCompletion( inserterItem ) { - const { - name, - initialAttributes, - innerBlocks, - syncStatus, - content, - } = inserterItem; + const { name, initialAttributes, innerBlocks, syncStatus, blocks } = + inserterItem; return { action: 'replace', value: syncStatus === 'unsynced' - ? parse( content, { - __unstableSkipMigrationLogs: true, - } ) + ? ( blocks ?? [] ).map( ( block ) => + cloneBlock( block ) + ) : createBlock( name, initialAttributes, diff --git a/packages/block-editor/src/components/block-switcher/index.js b/packages/block-editor/src/components/block-switcher/index.js index 981dd4a395f89a..d25fa14cbc744a 100644 --- a/packages/block-editor/src/components/block-switcher/index.js +++ b/packages/block-editor/src/components/block-switcher/index.js @@ -30,6 +30,7 @@ import { useBlockVariationTransforms } from './block-variation-transformations'; import BlockStylesMenu from './block-styles-menu'; import PatternTransformationsMenu from './pattern-transformations-menu'; import useBlockDisplayTitle from '../block-title/use-block-display-title'; +import { unlock } from '../../lock-unlock'; function BlockSwitcherDropdownMenuContents( { onClose, @@ -196,6 +197,7 @@ export const BlockSwitcher = ( { clientIds } ) => { isReusable, isTemplate, isDisabled, + isSection, } = useSelect( ( select ) => { const { @@ -204,7 +206,8 @@ export const BlockSwitcher = ( { clientIds } ) => { getBlockAttributes, canRemoveBlocks, getBlockEditingMode, - } = select( blockEditorStore ); + isSectionBlock, + } = unlock( select( blockEditorStore ) ); const { getBlockStyles, getBlockType, getActiveBlockVariation } = select( blocksStore ); const _blocks = getBlocksByClientId( clientIds ); @@ -250,6 +253,7 @@ export const BlockSwitcher = ( { clientIds } ) => { _isSingleBlockSelected && isTemplatePart( _blocks[ 0 ] ), hasContentOnlyLocking: _hasTemplateLock, isDisabled: editingMode !== 'default', + isSection: isSectionBlock( clientIds[ 0 ] ), }; }, [ clientIds ] @@ -278,7 +282,10 @@ export const BlockSwitcher = ( { clientIds } ) => { ? blockTitle : undefined; + const hideTransformsForSections = + window?.__experimentalContentOnlyPatternInsertion && isSection; const hideDropdown = + hideTransformsForSections || isDisabled || ( ! hasBlockStyles && ! canRemove ) || hasContentOnlyLocking; diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index 9545488b8d8af5..7ccbd792a4b0d5 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -128,6 +128,16 @@ export function PrivateBlockToolbar( { const _isZoomOut = isZoomOut(); + // The switch style button appears more prominently with the + // content only pattern experiment. + const _showSwitchSectionStyleButton = + window?.__experimentalContentOnlyPatternInsertion + ? _isZoomOut || isSectionBlock( selectedBlockClientId ) + : _isZoomOut || + ( isNavigationModeEnabled && + editingMode === 'contentOnly' && + isSectionBlock( selectedBlockClientId ) ); + return { blockClientId: selectedBlockClientId, blockClientIds: selectedBlockClientIds, @@ -153,11 +163,7 @@ export function PrivateBlockToolbar( { showSlots: ! _isZoomOut, showGroupButtons: ! _isZoomOut, showLockButtons: ! _isZoomOut, - showSwitchSectionStyleButton: - _isZoomOut || - ( isNavigationModeEnabled && - editingMode === 'contentOnly' && - isSectionBlock( selectedBlockClientId ) ), // Zoom out or Write Mode Section Blocks + showSwitchSectionStyleButton: _showSwitchSectionStyleButton, hasFixedToolbar: getSettings().hasFixedToolbar, isNavigationMode: isNavigationModeEnabled, }; diff --git a/packages/block-editor/src/components/block-variation-transforms/index.js b/packages/block-editor/src/components/block-variation-transforms/index.js index 42b6f626af5a10..8250535be5158a 100644 --- a/packages/block-editor/src/components/block-variation-transforms/index.js +++ b/packages/block-editor/src/components/block-variation-transforms/index.js @@ -139,32 +139,40 @@ function VariationsToggleGroupControl( { function __experimentalBlockVariationTransforms( { blockClientId } ) { const { updateBlockAttributes } = useDispatch( blockEditorStore ); - const { activeBlockVariation, variations, isContentOnly } = useSelect( - ( select ) => { - const { getActiveBlockVariation, getBlockVariations } = - select( blocksStore ); - - const { getBlockName, getBlockAttributes, getBlockEditingMode } = - select( blockEditorStore ); - - const name = blockClientId && getBlockName( blockClientId ); - - const { hasContentRoleAttribute } = unlock( select( blocksStore ) ); - const isContentBlock = hasContentRoleAttribute( name ); - - return { - activeBlockVariation: getActiveBlockVariation( - name, - getBlockAttributes( blockClientId ) - ), - variations: name && getBlockVariations( name, 'transform' ), - isContentOnly: - getBlockEditingMode( blockClientId ) === 'contentOnly' && - ! isContentBlock, - }; - }, - [ blockClientId ] - ); + const { activeBlockVariation, variations, isContentOnly, isSection } = + useSelect( + ( select ) => { + const { getActiveBlockVariation, getBlockVariations } = + select( blocksStore ); + + const { + getBlockName, + getBlockAttributes, + getBlockEditingMode, + isSectionBlock, + } = unlock( select( blockEditorStore ) ); + + const name = blockClientId && getBlockName( blockClientId ); + + const { hasContentRoleAttribute } = unlock( + select( blocksStore ) + ); + const isContentBlock = hasContentRoleAttribute( name ); + + return { + activeBlockVariation: getActiveBlockVariation( + name, + getBlockAttributes( blockClientId ) + ), + variations: name && getBlockVariations( name, 'transform' ), + isContentOnly: + getBlockEditingMode( blockClientId ) === + 'contentOnly' && ! isContentBlock, + isSection: isSectionBlock( blockClientId ), + }; + }, + [ blockClientId ] + ); const selectedValue = activeBlockVariation?.name; @@ -189,7 +197,10 @@ function __experimentalBlockVariationTransforms( { blockClientId } ) { } ); }; - if ( ! variations?.length || isContentOnly ) { + const hideVariationsForSections = + window?.__experimentalContentOnlyPatternInsertion && isSection; + + if ( ! variations?.length || isContentOnly || hideVariationsForSections ) { return null; } diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index e83a251ec7e671..f26f96a5081679 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -18,14 +18,15 @@ import { getClientIdsWithDescendants, isNavigationMode, getBlockRootClientId, + getBlockAttributes, } from './selectors'; import { checkAllowListRecursive, getAllPatternsDependants, getInsertBlockTypeDependants, getGrammar, + mapUserPattern, } from './utils'; -import { INSERTER_PATTERN_TYPES } from '../components/inserter/block-patterns-tab/utils'; import { STORE_NAME } from './constants'; import { unlock } from '../lock-unlock'; import { @@ -348,26 +349,6 @@ export const hasAllowedPatterns = createRegistrySelector( ( select ) => ) ); -function mapUserPattern( - userPattern, - __experimentalUserPatternCategories = [] -) { - return { - name: `core/block/${ userPattern.id }`, - id: userPattern.id, - type: INSERTER_PATTERN_TYPES.user, - title: userPattern.title.raw, - categories: userPattern.wp_pattern_category?.map( ( catId ) => { - const category = __experimentalUserPatternCategories.find( - ( { id } ) => id === catId - ); - return category ? category.slug : catId; - } ), - content: userPattern.content.raw, - syncStatus: userPattern.wp_pattern_sync_status, - }; -} - export const getPatternBySlug = createRegistrySelector( ( select ) => createSelector( ( state, patternName ) => { @@ -537,6 +518,14 @@ export function isSectionBlock( state, clientId ) { return true; } + const attributes = getBlockAttributes( state, clientId ); + if ( + attributes?.metadata?.patternName && + !! window?.__experimentalContentOnlyPatternInsertion + ) { + return true; + } + // Template parts become sections in navigation mode. const _isNavigationMode = isNavigationMode( state ); if ( _isNavigationMode && blockName === 'core/template-part' ) { diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index ec82d5192dc6f7..2abbd8547b46f3 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -2273,6 +2273,20 @@ function getDerivedBlockEditingModesForTree( ( clientId ) => state.blockListSettings[ clientId ]?.templateLock === 'contentOnly' ); + // Use array.from for better back compat. Older versions of the iterator returned + // from `keys()` didn't have the `filter` method. + const unsyncedPatternClientIds = + !! window?.__experimentalContentOnlyPatternInsertion + ? Array.from( state.blocks.attributes.keys() ).filter( + ( clientId ) => + state.blocks.attributes.get( clientId )?.metadata + ?.patternName + ) + : []; + const contentOnlyParents = [ + ...contentOnlyTemplateLockedClientIds, + ...unsyncedPatternClientIds, + ]; traverseBlockTree( state, treeClientId, ( block ) => { const { clientId, name: blockName } = block; @@ -2464,15 +2478,14 @@ function getDerivedBlockEditingModesForTree( } } - // `templateLock: 'contentOnly'` derived modes. - if ( contentOnlyTemplateLockedClientIds.length ) { - const hasContentOnlyTemplateLockedParent = - !! findParentInClientIdsList( - state, - clientId, - contentOnlyTemplateLockedClientIds - ); - if ( hasContentOnlyTemplateLockedParent ) { + // Handle `templateLock=contentOnly` blocks and unsynced patterns. + if ( contentOnlyParents.length ) { + const hasContentOnlyParent = !! findParentInClientIdsList( + state, + clientId, + contentOnlyParents + ); + if ( hasContentOnlyParent ) { if ( isContentBlock( blockName ) ) { derivedBlockEditingModes.set( clientId, 'contentOnly' ); } else { diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 1e18ca232a87fa..a5e8298058d1d7 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -30,6 +30,7 @@ import { getInsertBlockTypeDependants, getParsedPattern, getGrammar, + mapUserPattern, } from './utils'; import { orderBy } from '../utils/sorting'; import { STORE_NAME } from './constants'; @@ -2155,27 +2156,31 @@ export const getInserterItems = createRegistrySelector( ( select ) => foreground: 'var(--wp-block-synced-color)', } : symbol; - const id = `core/block/${ reusableBlock.id }`; - const { time, count = 0 } = getInsertUsage( state, id ) || {}; + const userPattern = mapUserPattern( reusableBlock ); + const { time, count = 0 } = + getInsertUsage( state, userPattern.name ) || {}; const frecency = calculateFrecency( time, count ); return { - id, + id: userPattern.name, name: 'core/block', initialAttributes: { ref: reusableBlock.id }, - title: reusableBlock.title?.raw, + title: userPattern.title, icon, category: 'reusable', keywords: [ 'reusable' ], isDisabled: false, utility: 1, // Deprecated. frecency, - content: reusableBlock.content?.raw, - syncStatus: reusableBlock.wp_pattern_sync_status, + content: userPattern.content, + get blocks() { + return getParsedPattern( userPattern ).blocks; + }, + syncStatus: userPattern.syncStatus, }; }; - const syncedPatternInserterItems = canInsertBlockTypeUnmemoized( + const patternInserterItems = canInsertBlockTypeUnmemoized( state, 'core/block', rootClientId @@ -2261,7 +2266,7 @@ export const getInserterItems = createRegistrySelector( ( select ) => { core: [], noncore: [] } ); const sortedBlockTypes = [ ...coreItems, ...nonCoreItems ]; - return [ ...sortedBlockTypes, ...syncedPatternInserterItems ]; + return [ ...sortedBlockTypes, ...patternInserterItems ]; }, ( state, rootClientId ) => [ getBlockTypes(), diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index d5fedb4ea7c0b8..ca3aee370530f5 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -3402,6 +3402,18 @@ describe( 'selectors', () => { ( item ) => item.id === 'core/block/1' ); expect( reusableBlockItem ).toEqual( { + blocks: [ + expect.objectContaining( { + attributes: { + metadata: expect.objectContaining( { + name: 'Reusable Block 1', + patternName: 'core/block/1', + } ), + }, + isValid: true, + innerBlocks: [], + } ), + ], category: 'reusable', content: '', frecency: 0, diff --git a/packages/block-editor/src/store/utils.js b/packages/block-editor/src/store/utils.js index bdf8e3675edff1..0f41dd3a998c2d 100644 --- a/packages/block-editor/src/store/utils.js +++ b/packages/block-editor/src/store/utils.js @@ -11,11 +11,32 @@ import { selectBlockPatternsKey } from './private-keys'; import { unlock } from '../lock-unlock'; import { STORE_NAME } from './constants'; import { getSectionRootClientId } from './private-selectors'; +import { INSERTER_PATTERN_TYPES } from '../components/inserter/block-patterns-tab/utils'; export const isFiltered = Symbol( 'isFiltered' ); const parsedPatternCache = new WeakMap(); const grammarMapCache = new WeakMap(); +export function mapUserPattern( + userPattern, + __experimentalUserPatternCategories = [] +) { + return { + name: `core/block/${ userPattern.id }`, + id: userPattern.id, + type: INSERTER_PATTERN_TYPES.user, + title: userPattern.title?.raw, + categories: userPattern.wp_pattern_category?.map( ( catId ) => { + const category = __experimentalUserPatternCategories.find( + ( { id } ) => id === catId + ); + return category ? category.slug : catId; + } ), + content: userPattern.content?.raw, + syncStatus: userPattern.wp_pattern_sync_status, + }; +} + function parsePattern( pattern ) { const blocks = parse( pattern.content, { __unstableSkipMigrationLogs: true,