Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/experimental/editor-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' );
Expand Down
12 changes: 12 additions & 0 deletions lib/experiments-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
17 changes: 6 additions & 11 deletions packages/block-editor/src/autocompleters/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 )
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is working well now!

One thing that would also be good to fix is having theme patterns appear in the slash inserter; currently they don't. But that can be addressed separately!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, lets treat is as separate, I'm not sure if there's a reason they're omitted.

If we can include them I think it would help tidy up some of the Inserter code.

: createBlock(
name,
initialAttributes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -196,6 +197,7 @@ export const BlockSwitcher = ( { clientIds } ) => {
isReusable,
isTemplate,
isDisabled,
isSection,
} = useSelect(
( select ) => {
const {
Expand All @@ -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 );
Expand Down Expand Up @@ -250,6 +253,7 @@ export const BlockSwitcher = ( { clientIds } ) => {
_isSingleBlockSelected && isTemplatePart( _blocks[ 0 ] ),
hasContentOnlyLocking: _hasTemplateLock,
isDisabled: editingMode !== 'default',
isSection: isSectionBlock( clientIds[ 0 ] ),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to check for multi-selection? Maybe it should use the same condition as the template and synced pattern.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oooh! Maybe yes 😄

Screenshot 2025-09-17 at 1 21 58 pm

Maybe it should use the same condition as the template and synced pattern.

Which condition is that?

A bit related, but another thing I noticed while testing was that the inspector sidebar also shows default controls when several patterns are selected.

Screenshot 2025-09-17 at 1 12 18 pm

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition - _isSingleBlockSelected && isTemplatePart( _blocks[ 0 ] ).

Current logic will return true if the first block in the multi-selection is a section block. Not sure if that's the result we want.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah got it, thanks!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think #71708 should take care of this since it turns off block switching in a multi-selection if any block is a section.

};
},
[ clientIds ]
Expand Down Expand Up @@ -278,7 +282,10 @@ export const BlockSwitcher = ( { clientIds } ) => {
? blockTitle
: undefined;

const hideTransformsForSections =
window?.__experimentalContentOnlyPatternInsertion && isSection;
const hideDropdown =
hideTransformsForSections ||
isDisabled ||
( ! hasBlockStyles && ! canRemove ) ||
hasContentOnlyLocking;
Expand Down
16 changes: 11 additions & 5 deletions packages/block-editor/src/components/block-toolbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -189,7 +197,10 @@ function __experimentalBlockVariationTransforms( { blockClientId } ) {
} );
};

if ( ! variations?.length || isContentOnly ) {
const hideVariationsForSections =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this supposed to hide the transforms menu? It's still there and usable:

Screenshot 2025-09-10 at 3 43 51 pm

Copy link
Contributor Author

@talldan talldan Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It hides the variations on the block card for groups. Only remembered this one is still there after doing it 😄 .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed a commit that should address this.

window?.__experimentalContentOnlyPatternInsertion && isSection;

if ( ! variations?.length || isContentOnly || hideVariationsForSections ) {
return null;
}

Expand Down
31 changes: 10 additions & 21 deletions packages/block-editor/src/store/private-selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 ) => {
Expand Down Expand Up @@ -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' ) {
Expand Down
31 changes: 22 additions & 9 deletions packages/block-editor/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
21 changes: 13 additions & 8 deletions packages/block-editor/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
getInsertBlockTypeDependants,
getParsedPattern,
getGrammar,
mapUserPattern,
} from './utils';
import { orderBy } from '../utils/sorting';
import { STORE_NAME } from './constants';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2261,7 +2266,7 @@ export const getInserterItems = createRegistrySelector( ( select ) =>
{ core: [], noncore: [] }
);
const sortedBlockTypes = [ ...coreItems, ...nonCoreItems ];
return [ ...sortedBlockTypes, ...syncedPatternInserterItems ];
return [ ...sortedBlockTypes, ...patternInserterItems ];
},
( state, rootClientId ) => [
getBlockTypes(),
Expand Down
12 changes: 12 additions & 0 deletions packages/block-editor/src/store/test/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<!-- /wp:test-block-a -->',
frecency: 0,
Expand Down
Loading
Loading