From d4531d64347bc0834d361df3496b1a9bf56f36a0 Mon Sep 17 00:00:00 2001 From: Ella Date: Sat, 13 Apr 2024 21:07:32 +0300 Subject: [PATCH] Pattern overrides: use block binding editing API --- packages/block-library/src/block/edit.js | 271 ++---------------- .../editor/src/bindings/pattern-overrides.js | 45 ++- 2 files changed, 70 insertions(+), 246 deletions(-) diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 0d465e2d0fb61d..2c9b7f0e8cd5b8 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -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, @@ -20,7 +24,6 @@ import { useInnerBlocksProps, RecursionProvider, useHasRecursion, - InnerBlocks, useBlockProps, Warning, privateApis as blockEditorPrivateApis, @@ -28,8 +31,7 @@ import { 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 @@ -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(); @@ -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}>} */ - 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 = @@ -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: @@ -314,7 +178,7 @@ function ReusableBlockEdit( { useEffect( () => { setBlockEditMode( setBlockEditingMode, - innerBlocks, + blocks, // Disable editing if the pattern itself is disabled. editingMode === 'disabled' || ! hasPatternOverridesSource ? 'disabled' @@ -322,70 +186,17 @@ function ReusableBlockEdit( { ); }, [ 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( { @@ -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, @@ -444,7 +225,7 @@ function ReusableBlockEdit( { const resetContent = () => { if ( content ) { - replaceInnerBlocks( patternClientId, initialBlocks ); + setAttributes( { content: undefined } ); } }; diff --git a/packages/editor/src/bindings/pattern-overrides.js b/packages/editor/src/bindings/pattern-overrides.js index 5f7f475a649a3e..972b2ad08cc99f 100644 --- a/packages/editor/src/bindings/pattern-overrides.js +++ b/packages/editor/src/bindings/pattern-overrides.js @@ -2,10 +2,53 @@ * WordPress dependencies */ import { _x } from '@wordpress/i18n'; +import { store as blockEditorStore } from '@wordpress/block-editor'; + +const CONTENT = 'content'; export default { name: 'core/pattern-overrides', label: _x( 'Pattern Overrides', 'block bindings source' ), - useSource: null, + getValue( { registry, clientId, attributeName } ) { + const { getBlockAttributes, getBlockParents, getBlockName } = + registry.select( blockEditorStore ); + const currentBlockAttributes = getBlockAttributes( clientId ); + const parents = getBlockParents( clientId, true ); + const patternClientId = parents.find( + ( id ) => getBlockName( id ) === 'core/block' + ); + + return getBlockAttributes( patternClientId )?.[ CONTENT ]?.[ + currentBlockAttributes?.metadata?.name + ]?.[ attributeName ]; + }, + setValue( { registry, clientId, attributeName, value } ) { + const { getBlockAttributes, getBlockParents, getBlockName } = + registry.select( blockEditorStore ); + const currentBlockAttributes = getBlockAttributes( clientId ); + const parents = getBlockParents( clientId, true ); + const patternClientId = parents.find( + ( id ) => getBlockName( id ) === 'core/block' + ); + const blockName = currentBlockAttributes?.metadata?.name; + + if ( ! patternClientId ) { + return; + } + + const currentBindingValue = + getBlockAttributes( patternClientId )?.[ CONTENT ]; + registry + .dispatch( blockEditorStore ) + .updateBlockAttributes( patternClientId, { + [ CONTENT ]: { + ...currentBindingValue, + [ blockName ]: { + ...currentBindingValue?.[ blockName ], + [ attributeName ]: value, + }, + }, + } ); + }, lockAttributesEditing: false, };