diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 68b4349197a8b7..71808d6da39ad7 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -42,7 +42,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~~, ~~renaming~~ -- **Attributes:** ref +- **Attributes:** dynamicContent, ref ## Button diff --git a/lib/block-supports/pattern.php b/lib/block-supports/pattern.php new file mode 100644 index 00000000000000..1c761742662485 --- /dev/null +++ b/lib/block-supports/pattern.php @@ -0,0 +1,31 @@ +supports, array( '__experimentalConnections' ), false ) : false; + + if ( $pattern_support ) { + if ( ! $block_type->uses_context ) { + $block_type->uses_context = array(); + } + + if ( ! in_array( 'dynamicContent', $block_type->uses_context, true ) ) { + $block_type->uses_context[] = 'dynamicContent'; + } + } + } + + // Register the block support. + WP_Block_Supports::get_instance()->register( + 'pattern', + array( + 'register_attribute' => 'gutenberg_register_pattern_support', + ) + ); +} diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index f9f2412ae51205..a18e082e1d663d 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -101,6 +101,7 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst $blocks_attributes_allowlist = array( 'core/paragraph' => array( 'content' ), 'core/image' => array( 'url' ), + 'core/heading' => array( 'content' ), ); // Whitelist of the block types that support block connections. @@ -132,9 +133,8 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst continue; } - // If the source value is not "meta_fields", skip it because the only supported - // connection source is meta (custom fields) for now. - if ( 'meta_fields' !== $attribute_value['source'] ) { + // Skip if the source value is not "meta_fields" or "pattern_attributes". + if ( 'meta_fields' !== $attribute_value['source'] && 'pattern_attributes' !== $attribute_value['source'] ) { continue; } @@ -154,6 +154,10 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst $attribute_value['value'] ); + if ( false === $custom_value ) { + continue; + } + $tags = new WP_HTML_Tag_Processor( $block_content ); $found = $tags->next_tag( array( @@ -181,5 +185,6 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst return $block_content; } + add_filter( 'render_block', 'gutenberg_render_block_connections', 10, 3 ); } diff --git a/lib/experimental/connection-sources/index.php b/lib/experimental/connection-sources/index.php index b63abcad96f628..34ee091eb117b5 100644 --- a/lib/experimental/connection-sources/index.php +++ b/lib/experimental/connection-sources/index.php @@ -12,4 +12,7 @@ // if it doesn't, `get_post_meta()` will just return an empty string. return get_post_meta( $block_instance->context['postId'], $meta_field, true ); }, + 'pattern_attributes' => function ( $block_instance, $meta_field ) { + return _wp_array_get( $block_instance->context, array( 'dynamicContent', $meta_field ), false ); + } ); diff --git a/lib/load.php b/lib/load.php index 2b178af5fb9bfe..2edf9ccefa13f5 100644 --- a/lib/load.php +++ b/lib/load.php @@ -256,6 +256,7 @@ function () { require __DIR__ . '/block-supports/shadow.php'; require __DIR__ . '/block-supports/background.php'; require __DIR__ . '/block-supports/behaviors.php'; +require __DIR__ . '/block-supports/pattern.php'; // Data views. require_once __DIR__ . '/experimental/data-views.php'; diff --git a/packages/block-editor/src/hooks/custom-fields.js b/packages/block-editor/src/hooks/custom-fields.js index 9affb55c3ea71f..1d59c274040cf6 100644 --- a/packages/block-editor/src/hooks/custom-fields.js +++ b/packages/block-editor/src/hooks/custom-fields.js @@ -1,8 +1,9 @@ /** * WordPress dependencies */ +import { useRegistry } from '@wordpress/data'; import { addFilter } from '@wordpress/hooks'; -import { PanelBody, TextControl } from '@wordpress/components'; +import { PanelBody, TextControl, SelectControl } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { hasBlockSupport } from '@wordpress/blocks'; import { createHigherOrderComponent } from '@wordpress/compose'; @@ -55,7 +56,11 @@ const withInspectorControl = createHigherOrderComponent( ( BlockEdit ) => { // Check if the current block is a paragraph or image block. // Currently, only these two blocks are supported. - if ( ! [ 'core/paragraph', 'core/image' ].includes( props.name ) ) { + if ( + ! [ 'core/paragraph', 'core/image', 'core/heading' ].includes( + props.name + ) + ) { return ; } @@ -65,6 +70,41 @@ const withInspectorControl = createHigherOrderComponent( ( BlockEdit ) => { let attributeName; if ( props.name === 'core/paragraph' ) attributeName = 'content'; if ( props.name === 'core/image' ) attributeName = 'url'; + if ( props.name === 'core/heading' ) attributeName = 'content'; + + const connectionSource = + props.attributes?.connections?.attributes?.[ attributeName ] + ?.source || ''; + const connectionValue = + props.attributes?.connections?.attributes?.[ attributeName ] + ?.value || ''; + + function updateConnections( source, value ) { + if ( value === '' ) { + props.setAttributes( { + connections: undefined, + placeholder: undefined, + } ); + } else { + props.setAttributes( { + connections: { + attributes: { + // The attributeName will be either `content` or `url`. + [ attributeName ]: { + // Source will be variable, could be post_meta, user_meta, term_meta, etc. + // Could even be a custom source like a social media attribute. + source, + value, + }, + }, + }, + placeholder: sprintf( + 'This content will be replaced on the frontend by the value of "%s" custom field.', + value + ), + } ); + } + } if ( hasCustomFieldsSupport && props.isSelected ) { return ( @@ -76,42 +116,40 @@ const withInspectorControl = createHigherOrderComponent( ( BlockEdit ) => { title={ __( 'Connections' ) } initialOpen={ true } > + { + updateConnections( + nextSource, + connectionValue + ); + } } + /> { - if ( nextValue === '' ) { - props.setAttributes( { - connections: undefined, - [ attributeName ]: undefined, - placeholder: undefined, - } ); - } else { - props.setAttributes( { - connections: { - attributes: { - // The attributeName will be either `content` or `url`. - [ attributeName ]: { - // Source will be variable, could be post_meta, user_meta, term_meta, etc. - // Could even be a custom source like a social media attribute. - source: 'meta_fields', - value: nextValue, - }, - }, - }, - [ attributeName ]: undefined, - placeholder: sprintf( - 'This content will be replaced on the frontend by the value of "%s" custom field.', - nextValue - ), - } ); - } + updateConnections( + connectionSource, + nextValue + ); } } /> @@ -125,6 +163,30 @@ const withInspectorControl = createHigherOrderComponent( ( BlockEdit ) => { }; }, 'withInspectorControl' ); +const createEditFunctionWithPatternSource = () => + createHigherOrderComponent( + ( BlockEdit ) => + ( { attributes, ...props } ) => { + const registry = useRegistry(); + const sourceAttributes = + registry._selectAttributes?.( { + clientId: props.clientId, + name: props.name, + attributes, + } ) ?? attributes; + + return ( + + ); + } + ); + +function shimAttributeSource( settings ) { + settings.edit = createEditFunctionWithPatternSource()( settings.edit ); + + return settings; +} + if ( window.__experimentalConnections ) { addFilter( 'blocks.registerBlockType', @@ -136,4 +198,9 @@ if ( window.__experimentalConnections ) { 'core/connections/with-inspector-control', withInspectorControl ); + addFilter( + 'blocks.registerBlockType', + 'core/pattern/shimAttributeSource', + shimAttributeSource + ); } diff --git a/packages/block-library/src/block/block.json b/packages/block-library/src/block/block.json index aeccdbfc1051db..690ec196866445 100644 --- a/packages/block-library/src/block/block.json +++ b/packages/block-library/src/block/block.json @@ -8,6 +8,9 @@ "keywords": [ "reusable" ], "textdomain": "default", "attributes": { + "dynamicContent": { + "type": "object" + }, "ref": { "type": "number" } diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 89997d2c066c85..107cd85f78a39f 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -6,6 +6,8 @@ import classnames from 'classnames'; /** * WordPress dependencies */ +import { RegistryProvider, useRegistry } from '@wordpress/data'; +import { useRef, useMemo } from '@wordpress/element'; import { useEntityBlockEditor, useEntityProp, @@ -27,14 +29,86 @@ import { useBlockProps, Warning, privateApis as blockEditorPrivateApis, + store as blockEditorStore, } from '@wordpress/block-editor'; -import { useRef, useMemo } from '@wordpress/element'; +import { getBlockSupport } from '@wordpress/blocks'; /** * Internal dependencies */ import { unlock } from '../lock-unlock'; +function hasAttributeSynced( block ) { + return ( + !! getBlockSupport( block.name, '__experimentalConnections', false ) && + !! block.attributes.connections?.attributes && + Object.values( block.attributes.connections.attributes ).some( + ( connection ) => connection.source === 'pattern_attributes' + ) + ); +} +function getAttributeSynced( block ) { + const attributes = {}; + for ( const [ attribute, connection ] of Object.entries( + block.attributes.connections.attributes + ) ) { + if ( connection.source !== 'pattern_attributes' ) continue; + attributes[ attribute ] = connection.value; + } + return attributes; +} + +const updateBlockAttributes = + ( patternClientId ) => + ( clientIds, attributes, uniqueByBlock = false ) => + ( { select, dispatch } ) => { + const updates = {}; + for ( const clientId of [].concat( clientIds ) ) { + const attrs = uniqueByBlock ? attributes[ clientId ] : attributes; + const parentPattern = select.getBlock( patternClientId ); + const block = select.getBlock( clientId ); + if ( ! parentPattern || ! hasAttributeSynced( block ) ) { + continue; + } + + const contentAttributes = getAttributeSynced( block ); + const dynamicContent = {}; + for ( const attributeKey of Object.keys( attrs ) ) { + if ( Object.hasOwn( contentAttributes, attributeKey ) ) { + dynamicContent[ contentAttributes[ attributeKey ] ] = + attrs[ attributeKey ]; + } + } + if ( Object.keys( dynamicContent ).length > 0 ) { + updates[ parentPattern.clientId ] = { + dynamicContent: { + ...parentPattern.attributes.dynamicContent, + ...dynamicContent, + }, + }; + } + } + + if ( + Object.values( updates ).every( + ( updatedAttributes, _index, arr ) => + updatedAttributes === arr[ 0 ] + ) + ) { + dispatch.updateBlockAttributes( + Object.keys( updates ), + Object.values( updates )[ 0 ], + false + ); + } else { + dispatch.updateBlockAttributes( + Object.keys( updates ), + updates, + true + ); + } + }; + const fullAlignments = [ 'full', 'wide', 'left', 'right' ]; const useInferredLayout = ( blocks, parentLayout ) => { @@ -70,7 +144,9 @@ export default function ReusableBlockEdit( { name, attributes: { ref }, __unstableParentLayout: parentLayout, + clientId: patternClientId, } ) { + const registry = useRegistry(); const { useLayoutClasses } = unlock( blockEditorPrivateApis ); const hasAlreadyRendered = useHasRecursion( ref ); const { record, hasResolved } = useEntityRecord( @@ -114,6 +190,57 @@ export default function ReusableBlockEdit( { : InnerBlocks.ButtonBlockAppender, } ); + const subRegistry = useMemo( () => { + return { + ...registry, + _selectAttributes( block ) { + if ( ! hasAttributeSynced( block ) ) return block.attributes; + const { dynamicContent } = registry + .select( blockEditorStore ) + .getBlockAttributes( patternClientId ); + if ( ! dynamicContent ) return block.attributes; + const attributeIds = getAttributeSynced( block ); + const newAttributes = { ...block.attributes }; + for ( const [ attributeKey, id ] of Object.entries( + attributeIds + ) ) { + if ( dynamicContent[ id ] ) { + newAttributes[ attributeKey ] = dynamicContent[ id ]; + } + } + return newAttributes; + }, + dispatch( store ) { + if ( + store !== blockEditorStore && + store !== blockEditorStore.name + ) { + return registry.dispatch( store ); + } + const dispatch = registry.dispatch( store ); + const select = registry.select( store ); + return { + ...dispatch, + updateBlockAttributes( + clientId, + attributes, + uniqueByBlock + ) { + return updateBlockAttributes( patternClientId )( + clientId, + attributes, + uniqueByBlock + )( { + registry, + select, + dispatch, + } ); + }, + }; + }, + }; + }, [ registry, patternClientId ] ); + if ( hasAlreadyRendered ) { return (
@@ -145,18 +272,20 @@ export default function ReusableBlockEdit( { } return ( - - - - - - -
- + + + + + + + +
+ + ); } diff --git a/packages/block-library/src/block/index.php b/packages/block-library/src/block/index.php index d51b35d68b23d9..b7d3474e537e1b 100644 --- a/packages/block-library/src/block/index.php +++ b/packages/block-library/src/block/index.php @@ -41,6 +41,21 @@ function render_block_core_block( $attributes ) { $seen_refs[ $attributes['ref'] ] = true; + $filter_block_context = static function( $context ) use ( $attributes ) { + if ( isset( $attributes['dynamicContent'] ) && $attributes['dynamicContent'] ) { + $context['dynamicContent'] = $attributes['dynamicContent']; + } + + return $context; + }; + + /** + * We set the `dynamicContent` context through the `render_block_context` + * filter so that it is available when a pattern's inner blocks are + * rendering via do_blocks given it only receives the inner content. + */ + add_filter( 'render_block_context', $filter_block_context, 1 ); + // Handle embeds for reusable blocks. global $wp_embed; $content = $wp_embed->run_shortcode( $reusable_block->post_content ); @@ -48,6 +63,9 @@ function render_block_core_block( $attributes ) { $content = do_blocks( $content ); unset( $seen_refs[ $attributes['ref'] ] ); + + remove_filter( 'render_block_context', $filter_block_context, 1 ); + return $content; } diff --git a/packages/block-library/src/heading/block.json b/packages/block-library/src/heading/block.json index 7c018f8472cb4a..9cbeff8e89eeb5 100644 --- a/packages/block-library/src/heading/block.json +++ b/packages/block-library/src/heading/block.json @@ -14,7 +14,7 @@ "content": { "type": "string", "source": "html", - "selector": "h1,h2,h3,h4,h5,h6", + "selector": "h2", "default": "", "__experimentalRole": "content" }, @@ -63,7 +63,8 @@ } }, "__unstablePasteTextInline": true, - "__experimentalSlashInserter": true + "__experimentalSlashInserter": true, + "__experimentalConnections": true }, "editorStyle": "wp-block-heading-editor", "style": "wp-block-heading"