diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 0fac97b3372369..955da5a92d147e 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -43,7 +43,7 @@ Create and save content to reuse across your site. Update the pattern, and the c - **Name:** core/block - **Category:** reusable - **Supports:** ~~customClassName~~, ~~html~~, ~~inserter~~ -- **Attributes:** dynamicContent, patternId, ref +- **Attributes:** dynamicContent, ref ## Button diff --git a/lib/block-supports/pattern.php b/lib/block-supports/pattern.php index 174aa5949c48d7..069eb5e84c6ced 100644 --- a/lib/block-supports/pattern.php +++ b/lib/block-supports/pattern.php @@ -8,7 +8,7 @@ $gutenberg_experiments = get_option( 'gutenberg-experiments' ); if ( $gutenberg_experiments && array_key_exists( 'gutenberg-patterns', $gutenberg_experiments ) ) { /** - * Adds `patternId` and `dynamicContent` items to the block's `usesContext` + * Adds `dynamicContent` items to the block's `usesContext` * configuration. * * @param WP_Block_Type $block_type Block type. @@ -21,10 +21,6 @@ function gutenberg_register_pattern_support( $block_type ) { $block_type->uses_context = array(); } - if ( ! in_array( 'patternId', $block_type->uses_context, true ) ) { - $block_type->uses_context[] = 'patternId'; - } - if ( ! in_array( 'dynamicContent', $block_type->uses_context, true ) ) { $block_type->uses_context[] = 'dynamicContent'; } diff --git a/packages/block-editor/src/hooks/pattern.js b/packages/block-editor/src/hooks/pattern.js index 8def099c7eee71..b793d6e7200ecd 100644 --- a/packages/block-editor/src/hooks/pattern.js +++ b/packages/block-editor/src/hooks/pattern.js @@ -1,163 +1,55 @@ -/** - * External dependencies - */ -// eslint-disable-next-line import/no-extraneous-dependencies -import { v4 as uuid } from 'uuid'; -// TODO: Fix the unique ID generation to avoid adding another dependency just for this. - /** * WordPress dependencies */ -import { getBlockSupport, getBlockType } from '@wordpress/blocks'; +import { store as blocksStore } from '@wordpress/blocks'; import { createHigherOrderComponent } from '@wordpress/compose'; -import { useDispatch, useRegistry } from '@wordpress/data'; -import { useCallback, useMemo } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { useMemo } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; /** * Internal dependencies */ -import { cleanEmptyObject } from './utils'; +import { useBlockEditContext } from '../components/block-edit'; import { store as blockEditorStore } from '../store'; export const PATTERN_SUPPORT_KEY = '__experimentalPattern'; -function hasPatternSupport( blockType ) { - return !! getBlockSupport( blockType, PATTERN_SUPPORT_KEY ); -} - // TODO: Extract this to a custom source file? -function useSource( { - name, - context: { dynamicContent, patternId }, - attributes, - setAttributes, -} ) { - const { updateBlockAttributes } = useDispatch( blockEditorStore ); - const blockType = getBlockType( name ); - const patternSupport = blockType.supports?.[ PATTERN_SUPPORT_KEY ]; - // Generate unique id to link the block instance with the data in the - // pattern block's dynamic content. +function useSourceAttributes( attributes ) { + const { name, clientId } = useBlockEditContext(); + const dynamicContent = useSelect( + ( select ) => { + const hasPatternSupport = select( blocksStore ).hasBlockSupport( + name, + PATTERN_SUPPORT_KEY + ); + if ( ! hasPatternSupport ) return undefined; + const parentPatternClientId = select( + blockEditorStore + ).getBlockParentsByBlockName( clientId, 'core/block', true )[ 0 ]; + if ( ! parentPatternClientId ) return undefined; + return select( blockEditorStore ).getBlockAttributes( + parentPatternClientId + ).dynamicContent; + }, + [ name, clientId ] + ); const attributesWithSourcedAttributes = useMemo( () => { const id = attributes.metadata?.id; - if ( ! patternSupport || ! id || ! dynamicContent ) { + if ( ! id || ! dynamicContent ) { return attributes; } return { ...attributes, - ...Object.fromEntries( - Object.keys( patternSupport ).map( ( attributeName ) => { - return [ - attributeName, - dynamicContent[ id ]?.[ attributeName ] || - attributes[ attributeName ], - ]; - } ) - ), + ...dynamicContent[ id ], }; - }, [ attributes, dynamicContent, patternSupport ] ); - - const updatedSetAttributes = useCallback( - ( nextAttributes ) => { - const id = attributes.metadata?.id ?? uuid(); - - // Collect the updated dynamic content for the current block. - const updatedDynamicContent = Object.entries( nextAttributes ?? {} ) - .filter( - ( [ key ] ) => patternSupport && key in patternSupport - ) - .map( ( [ key, value ] ) => { - if ( value === '' ) { - return [ key, undefined ]; - } - - return [ key, value ]; - } ); - - // Collect the updated dynamic pattern content. - const nextDynamicContent = cleanEmptyObject( { - ...dynamicContent, - [ id ]: { - ...dynamicContent?.[ id ], - ...Object.fromEntries( updatedDynamicContent ), - }, - } ); - - // Filter out pattern stored attributes so they don't override the - // original attributes that act as a default or fallback. - const updatedAttributes = updatedDynamicContent.length - ? Object.fromEntries( - Object.entries( nextAttributes ?? {} ).filter( - ( [ key ] ) => - ! ( patternSupport && key in patternSupport ) - ) - ) - : nextAttributes; - - // Update the parent pattern instance's dynamic content attribute. - if ( updatedDynamicContent.length ) { - // If we are setting dynamic content on the parent pattern, - // ensure the block's id is saved in its metadata. - if ( ! attributes.metadata?.id ) { - updatedAttributes.metadata = { - ...updatedAttributes.metadata, - id, - }; - } - - updateBlockAttributes( patternId, { - dynamicContent: nextDynamicContent, - } ); - } - - setAttributes( updatedAttributes ); - }, - [ - attributes.metadata?.id, - dynamicContent, - patternId, - patternSupport, - setAttributes, - updateBlockAttributes, - ] - ); + }, [ attributes, dynamicContent ] ); - return { - attributes: attributesWithSourcedAttributes, - setAttributes: updatedSetAttributes, - }; -} - -/** - * Filters registered block settings, extending usesContext to include the - * dynamic content and setter provided by a pattern block. - * - * @param {Object} settings Original block settings. - * - * @return {Object} Filtered block settings. - */ -function extendUsesContext( settings ) { - if ( ! hasPatternSupport( settings ) ) { - return settings; - } - - if ( ! Array.isArray( settings.usesContext ) ) { - settings.usesContext = [ 'dynamicContent', 'patternId' ]; - return settings; - } - - if ( ! settings.usesContext.includes( 'dynamicContent' ) ) { - settings.usesContext.push( 'dynamicContent' ); - } - - if ( ! settings.usesContext.includes( 'patternId' ) ) { - settings.usesContext.push( 'patternId' ); - } - - return settings; + return attributesWithSourcedAttributes; } /** @@ -172,37 +64,11 @@ function extendUsesContext( settings ) { const createEditFunctionWithPatternSource = () => createHigherOrderComponent( ( BlockEdit ) => - ( { name, attributes, setAttributes, context, ...props } ) => { - if ( ! context.patternId ) { - return ( - - ); - } - - const registry = useRegistry(); - const { - attributes: updatedAttributes, - setAttributes: updatedSetAttributes, - } = useSource( { name, attributes, setAttributes, context } ); + ( { attributes, ...props } ) => { + const sourceAttributes = useSourceAttributes( attributes ); return ( - - registry.batch( () => - updatedSetAttributes( newAttributes ) - ) - } - { ...props } - /> + ); } ); @@ -219,10 +85,4 @@ if ( window.__experimentalPatterns ) { 'core/pattern/shimAttributeSource', shimAttributeSource ); - - addFilter( - 'blocks.registerBlockType', - 'core/pattern/extendUsesContext', - extendUsesContext - ); } diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 32108de713f754..57926a9c23614f 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -13,6 +13,7 @@ import { switchToBlockType, synchronizeBlocksWithTemplate, getBlockSupport, + store as blocksStore, } from '@wordpress/blocks'; import { speak } from '@wordpress/a11y'; import { __, _n, sprintf } from '@wordpress/i18n'; @@ -157,18 +158,96 @@ export function receiveBlocks( blocks ) { * @param {boolean} uniqueByBlock true if each block in clientIds array has a unique set of attributes * @return {Object} Action object. */ -export function updateBlockAttributes( - clientIds, - attributes, - uniqueByBlock = false -) { - return { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientIds: castArray( clientIds ), - attributes, - uniqueByBlock, +export const updateBlockAttributes = + ( clientIds, attributes, uniqueByBlock = false ) => + ( { select, dispatch, registry } ) => { + if ( ! window.__experimentalPatterns ) { + dispatch( { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientIds: castArray( clientIds ), + attributes, + uniqueByBlock, + } ); + return; + } + + const updates = {}; + for ( const clientId of castArray( clientIds ) ) { + const attrs = uniqueByBlock ? attributes[ clientId ] : attributes; + const parentBlocks = select.getBlocksByClientId( + select.getBlockParents( clientId ) + ); + const parentPattern = parentBlocks.findLast( + ( parentBlock ) => parentBlock.name === 'core/block' + ); + const block = select.getBlock( clientId ); + if ( + ! parentPattern || + ! registry + .select( blocksStore ) + .hasBlockSupport( block.name, '__experimentalPattern' ) + ) { + updates[ clientId ] = attrs; + continue; + } + + const contentAttributes = registry + .select( blocksStore ) + .getBlockSupport( block.name, '__experimentalPattern' ); + const dynamicContent = {}; + const updatedAttributes = {}; + for ( const attributeKey of Object.keys( attrs ) ) { + if ( Object.hasOwn( contentAttributes, attributeKey ) ) { + dynamicContent[ attributeKey ] = attrs[ attributeKey ]; + } else { + updatedAttributes[ attributeKey ] = attrs[ attributeKey ]; + } + } + if ( Object.keys( dynamicContent ).length > 0 ) { + let id = block.attributes.metadata?.id; + if ( ! id ) { + // The id just has to be unique within the pattern context, so we + // use the block's clientId as a convenient unique identifier. + id = block.clientId; + updatedAttributes.metadata = { + ...block.attributes.metadata, + id, + }; + } + + updates[ parentPattern.clientId ] = { + dynamicContent: { + ...parentPattern.attributes.dynamicContent, + [ id ]: dynamicContent, + }, + }; + } + if ( Object.keys( updatedAttributes ).length > 0 ) { + updates[ clientId ] = updatedAttributes; + } + } + + if ( + Object.values( updates ).every( + ( updatedAttributes, _index, arr ) => + updatedAttributes === arr[ 0 ] + ) + ) { + dispatch( { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientIds: Object.keys( updates ), + attributes: Object.values( updates )[ 0 ], + uniqueByBlock: false, + } ); + } else { + dispatch( { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientIds: Object.keys( updates ), + attributes: updates, + uniqueByBlock: true, + } ); + } }; -} /** * Action that updates the block with the specified client ID. diff --git a/packages/block-library/src/block/block.json b/packages/block-library/src/block/block.json index 3b1508a362cca9..d66234f9b5134b 100644 --- a/packages/block-library/src/block/block.json +++ b/packages/block-library/src/block/block.json @@ -11,17 +11,10 @@ "dynamicContent": { "type": "object" }, - "patternId": { - "type": "string" - }, "ref": { "type": "number" } }, - "providesContext": { - "dynamicContent": "dynamicContent", - "patternId": "patternId" - }, "supports": { "customClassName": false, "html": false, diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 21586854fb6d05..13745ae0fd6dec 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -19,30 +19,11 @@ import { __experimentalUseHasRecursion as useHasRecursion, InnerBlocks, InspectorControls, - store as blockEditorStore, useBlockProps, Warning, } from '@wordpress/block-editor'; -import { useDispatch } from '@wordpress/data'; -import { useEffect } from '@wordpress/element'; - -export default function ReusableBlockEdit( { - attributes: { ref }, - clientId, - setAttributes, -} ) { - const { __unstableMarkNextChangeAsNotPersistent } = - useDispatch( blockEditorStore ); - - // To leverage updateBlockAttributes in the pattern block support we need - // an ID, otherwise we'd need to pass a setter down through context. - // The `clientId` is a prop and so can't be passed directly through block - // context. Instead, we set this on a dedicated block attribute. - useEffect( () => { - __unstableMarkNextChangeAsNotPersistent(); - setAttributes( { patternId: clientId } ); - }, [] ); +export default function ReusableBlockEdit( { attributes: { ref } } ) { const hasAlreadyRendered = useHasRecursion( ref ); const { record, hasResolved } = useEntityRecord( 'postType',