Skip to content

Commit

Permalink
Pattern overrides: use block binding editing API
Browse files Browse the repository at this point in the history
  • Loading branch information
ellatrix committed Apr 13, 2024
1 parent 6fa790c commit d4531d6
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 246 deletions.
271 changes: 26 additions & 245 deletions packages/block-library/src/block/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { useRegistry, useSelect, useDispatch } from '@wordpress/data';
import { useSelect, useDispatch } from '@wordpress/data';
import { useRef, useMemo, useEffect } from '@wordpress/element';
import { useEntityRecord, store as coreStore } from '@wordpress/core-data';
import {
useEntityRecord,
store as coreStore,
useEntityBlockEditor,
} from '@wordpress/core-data';
import {
Placeholder,
Spinner,
Expand All @@ -20,16 +24,14 @@ import {
useInnerBlocksProps,
RecursionProvider,
useHasRecursion,
InnerBlocks,
useBlockProps,
Warning,
privateApis as blockEditorPrivateApis,
store as blockEditorStore,
BlockControls,
} from '@wordpress/block-editor';
import { privateApis as patternsPrivateApis } from '@wordpress/patterns';
import { parse, cloneBlock, store as blocksStore } from '@wordpress/blocks';
import { RichTextData } from '@wordpress/rich-text';
import { store as blocksStore } from '@wordpress/blocks';

/**
* Internal dependencies
Expand All @@ -42,25 +44,6 @@ const { isOverridableBlock } = unlock( patternsPrivateApis );

const fullAlignments = [ 'full', 'wide', 'left', 'right' ];

function getLegacyIdMap( blocks, content, nameCount = {} ) {
let idToClientIdMap = {};
for ( const block of blocks ) {
if ( block?.innerBlocks?.length ) {
idToClientIdMap = {
...idToClientIdMap,
...getLegacyIdMap( block.innerBlocks, content, nameCount ),
};
}

const id = block.attributes.metadata?.id;
const clientId = block.clientId;
if ( id && content?.[ id ] ) {
idToClientIdMap[ clientId ] = id;
}
}
return idToClientIdMap;
}

const useInferredLayout = ( blocks, parentLayout ) => {
const initialInferredAlignmentRef = useRef();

Expand Down Expand Up @@ -97,110 +80,6 @@ function hasOverridableBlocks( blocks ) {
} );
}

function getOverridableAttributes( block ) {
return Object.entries( block.attributes.metadata.bindings )
.filter(
( [ , binding ] ) => binding.source === 'core/pattern-overrides'
)
.map( ( [ attributeKey ] ) => attributeKey );
}

function applyInitialContentValuesToInnerBlocks(
blocks,
content = {},
defaultValues,
legacyIdMap
) {
return blocks.map( ( block ) => {
const innerBlocks = applyInitialContentValuesToInnerBlocks(
block.innerBlocks,
content,
defaultValues,
legacyIdMap
);
const metadataName =
legacyIdMap?.[ block.clientId ] ?? block.attributes.metadata?.name;

if ( ! metadataName || ! isOverridableBlock( block ) ) {
return { ...block, innerBlocks };
}

const attributes = getOverridableAttributes( block );
const newAttributes = { ...block.attributes };
for ( const attributeKey of attributes ) {
defaultValues[ metadataName ] ??= {};
defaultValues[ metadataName ][ attributeKey ] =
block.attributes[ attributeKey ];

const contentValues = content[ metadataName ];
if ( contentValues?.[ attributeKey ] !== undefined ) {
newAttributes[ attributeKey ] = contentValues[ attributeKey ];
}
}
return {
...block,
attributes: newAttributes,
innerBlocks,
};
} );
}

function isAttributeEqual( attribute1, attribute2 ) {
if (
attribute1 instanceof RichTextData &&
attribute2 instanceof RichTextData
) {
return attribute1.toString() === attribute2.toString();
}
return attribute1 === attribute2;
}

function getContentValuesFromInnerBlocks( blocks, defaultValues, legacyIdMap ) {
/** @type {Record<string, { values: Record<string, unknown>}>} */
const content = {};
for ( const block of blocks ) {
if ( block.name === patternBlockName ) continue;
if ( block.innerBlocks.length ) {
Object.assign(
content,
getContentValuesFromInnerBlocks(
block.innerBlocks,
defaultValues,
legacyIdMap
)
);
}
const metadataName =
legacyIdMap?.[ block.clientId ] ?? block.attributes.metadata?.name;
if ( ! metadataName || ! isOverridableBlock( block ) ) {
continue;
}

const attributes = getOverridableAttributes( block );

for ( const attributeKey of attributes ) {
if (
! isAttributeEqual(
block.attributes[ attributeKey ],
defaultValues?.[ metadataName ]?.[ attributeKey ]
)
) {
content[ metadataName ] ??= {};
// TODO: We need a way to represent `undefined` in the serialized overrides.
// Also see: https://github.com/WordPress/gutenberg/pull/57249#discussion_r1452987871
content[ metadataName ][ attributeKey ] =
block.attributes[ attributeKey ] === undefined
? // TODO: We use an empty string to represent undefined for now until
// we support a richer format for overrides and the block binding API.
// Currently only the `linkTarget` attribute of `core/button` is affected.
''
: block.attributes[ attributeKey ];
}
}
}
return Object.keys( content ).length > 0 ? content : undefined;
}

function setBlockEditMode( setEditMode, blocks, mode ) {
blocks.forEach( ( block ) => {
const editMode =
Expand Down Expand Up @@ -253,50 +132,35 @@ function ReusableBlockEdit( {
clientId: patternClientId,
setAttributes,
} ) {
const registry = useRegistry();
const { record, editedRecord, hasResolved } = useEntityRecord(
const { record, hasResolved } = useEntityRecord(
'postType',
'wp_block',
ref
);
const [ blocks, onInput, onChange ] = useEntityBlockEditor(
'postType',
'wp_block',
{ id: ref }
);
const isMissing = hasResolved && ! record;

// The initial value of the `content` attribute.
const initialContent = useRef( content );

// The default content values from the original pattern for overridable attributes.
// Set by the `applyInitialContentValuesToInnerBlocks` function.
const defaultContent = useRef( {} );

const {
replaceInnerBlocks,
__unstableMarkNextChangeAsNotPersistent,
setBlockEditingMode,
} = useDispatch( blockEditorStore );
const { syncDerivedUpdates } = unlock( useDispatch( blockEditorStore ) );
const { setBlockEditingMode } = useDispatch( blockEditorStore );

const {
innerBlocks,
userCanEdit,
getBlockEditingMode,
onNavigateToEntityRecord,
editingMode,
hasPatternOverridesSource,
} = useSelect(
( select ) => {
const { canUser } = select( coreStore );
const {
getBlocks,
getSettings,
getBlockEditingMode: _getBlockEditingMode,
} = select( blockEditorStore );
const { getSettings, getBlockEditingMode: _getBlockEditingMode } =
select( blockEditorStore );
const { getBlockBindingsSource } = unlock( select( blocksStore ) );
const blocks = getBlocks( patternClientId );
const canEdit = canUser( 'update', 'blocks', ref );

// For editing link to the site editor if the theme and user permissions support it.
return {
innerBlocks: blocks,
userCanEdit: canEdit,
getBlockEditingMode: _getBlockEditingMode,
onNavigateToEntityRecord:
Expand All @@ -314,78 +178,25 @@ function ReusableBlockEdit( {
useEffect( () => {
setBlockEditMode(
setBlockEditingMode,
innerBlocks,
blocks,
// Disable editing if the pattern itself is disabled.
editingMode === 'disabled' || ! hasPatternOverridesSource
? 'disabled'
: undefined
);
}, [
editingMode,
innerBlocks,
blocks,
setBlockEditingMode,
hasPatternOverridesSource,
] );

const canOverrideBlocks = useMemo(
() => hasPatternOverridesSource && hasOverridableBlocks( innerBlocks ),
[ hasPatternOverridesSource, innerBlocks ]
() => hasPatternOverridesSource && hasOverridableBlocks( blocks ),
[ hasPatternOverridesSource, blocks ]
);

const initialBlocks = useMemo(
() =>
// Clone the blocks to generate new client IDs.
editedRecord.blocks?.map( ( block ) => cloneBlock( block ) ) ??
( editedRecord.content && typeof editedRecord.content !== 'function'
? parse( editedRecord.content )
: [] ),
[ editedRecord.blocks, editedRecord.content ]
);

const legacyIdMap = useRef( {} );

// Apply the initial overrides from the pattern block to the inner blocks.
useEffect( () => {
// Build a map of clientIds to the old nano id system to provide back compat.
legacyIdMap.current = getLegacyIdMap(
initialBlocks,
initialContent.current
);
defaultContent.current = {};
const originalEditingMode = getBlockEditingMode( patternClientId );
// Replace the contents of the blocks with the overrides.
registry.batch( () => {
setBlockEditingMode( patternClientId, 'default' );
syncDerivedUpdates( () => {
const blocks = hasPatternOverridesSource
? applyInitialContentValuesToInnerBlocks(
initialBlocks,
initialContent.current,
defaultContent.current,
legacyIdMap.current
)
: initialBlocks;

replaceInnerBlocks( patternClientId, blocks );
} );
setBlockEditingMode( patternClientId, originalEditingMode );
} );
}, [
hasPatternOverridesSource,
__unstableMarkNextChangeAsNotPersistent,
patternClientId,
initialBlocks,
replaceInnerBlocks,
registry,
getBlockEditingMode,
setBlockEditingMode,
syncDerivedUpdates,
] );

const { alignment, layout } = useInferredLayout(
innerBlocks,
parentLayout
);
const { alignment, layout } = useInferredLayout( blocks, parentLayout );
const layoutClasses = useLayoutClasses( { layout }, name );

const blockProps = useBlockProps( {
Expand All @@ -399,42 +210,12 @@ function ReusableBlockEdit( {
const innerBlocksProps = useInnerBlocksProps( blockProps, {
templateLock: 'all',
layout,
renderAppender: innerBlocks?.length
? undefined
: InnerBlocks.ButtonBlockAppender,
value: blocks,
onInput,
onChange,
renderAppender: blocks?.length ? undefined : blocks.ButtonBlockAppender,
} );

// Sync the `content` attribute from the updated blocks to the pattern block.
// `syncDerivedUpdates` is used here to avoid creating an additional undo level.
useEffect( () => {
if ( ! hasPatternOverridesSource ) {
return;
}
const { getBlocks } = registry.select( blockEditorStore );
let prevBlocks = getBlocks( patternClientId );
return registry.subscribe( () => {
const blocks = getBlocks( patternClientId );
if ( blocks !== prevBlocks ) {
prevBlocks = blocks;
syncDerivedUpdates( () => {
setAttributes( {
content: getContentValuesFromInnerBlocks(
blocks,
defaultContent.current,
legacyIdMap.current
),
} );
} );
}
}, blockEditorStore );
}, [
hasPatternOverridesSource,
syncDerivedUpdates,
patternClientId,
registry,
setAttributes,
] );

const handleEditOriginal = () => {
onNavigateToEntityRecord( {
postId: ref,
Expand All @@ -444,7 +225,7 @@ function ReusableBlockEdit( {

const resetContent = () => {
if ( content ) {
replaceInnerBlocks( patternClientId, initialBlocks );
setAttributes( { content: undefined } );
}
};

Expand Down
Loading

0 comments on commit d4531d6

Please sign in to comment.