From dd59572c7f725221754edbd0787d03dbd075ca68 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 9 Jun 2023 12:00:27 +0300 Subject: [PATCH] Add custom attributes sources block support --- docs/reference-guides/core-blocks.md | 2 +- gutenberg.php | 74 +++++++ .../block-library/src/paragraph/block.json | 6 +- packages/block-library/src/paragraph/index.js | 16 ++ .../editor/src/hooks/custom-sources-v2.js | 180 ++++++++++++++++++ packages/editor/src/hooks/index.js | 1 + 6 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 packages/editor/src/hooks/custom-sources-v2.js diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 5199ce7c7b8cc9..506f3610e6e2d2 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -470,7 +470,7 @@ Start with the basic building block of all narrative. ([Source](https://github.c - **Name:** core/paragraph - **Category:** text -- **Supports:** __unstablePasteTextInline, anchor, color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~className~~ +- **Supports:** __unstablePasteTextInline, anchor, color (background, gradients, link, text), customSources (content), spacing (margin, padding), typography (fontSize, lineHeight), ~~className~~ - **Attributes:** align, content, direction, dropCap, placeholder ## Pattern diff --git a/gutenberg.php b/gutenberg.php index 1200a0b24d55c3..1d5997eb2d6c1a 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -71,3 +71,77 @@ function gutenberg_pre_init() { require_once __DIR__ . '/lib/load.php'; } + +/** + * Renders the block meta attributes. + * + * @param string $block_content Block Content. + * @param array $block Block attributes. + * @param string $block_instance The block instance. + */ +function render_custom_sources( $block_content, $block, $block_instance ) { + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + if ( null === $block_type ) { + return $block_content; + } + + + $custom_sources = _wp_array_get( $block_type->supports, 'customSources', false ); + if ( ! $custom_sources ) { + // TODO: for some reason the "customSources" support is not being registered as it should. + // return $block_content; + } + + $attribute_sources = _wp_array_get( $block['attrs'], array( 'source' ), array() ); + foreach ( $attribute_sources as $attribute_name => $attribute_source ) { + $attribute_config = _wp_array_get( $block_type->attributes, array( $attribute_name ), false ); + if ( ! $attribute_config || ! $attribute_source || 'meta' !== $attribute_source['type'] ) { + continue; + } + $meta_field = $attribute_source['name']; + $meta_value = get_post_meta( $block_instance->context['postId'], $meta_field, true ); + $p = new WP_HTML_Tag_Processor( $block_content ); + $found = $p->next_tag( + array( + // TODO: build the query from CSS selector. + 'tag_name' => $attribute_config['selector'], + ) + ); + if ( ! $found ) { + continue; + } + $tag_name = $p->get_tag(); + $markup = "<$tag_name>$meta_value"; + $p2 = new WP_HTML_Tag_Processor( $markup ); + $p2->next_tag(); + $names = $p->get_attribute_names_with_prefix( '' ); + foreach ( $names as $name ) { + $p2->set_attribute( $name, $p->get_attribute( $name ) ); + } + + $block_content = $p2 . ''; + } + + return $block_content; +} + +add_filter( 'render_block', 'render_custom_sources', 10, 3 ); + +// ----- what follows is just random test code. + +/** + * Registers a custom meta for use by the test paragraph variation. + */ +function init_test_summary_meta_field() { + register_meta( + 'post', + 'summary', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + ) + ); +} + +add_action( 'init', 'init_test_summary_meta_field' ); diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json index cbabc108eca311..6b50684788b50c 100644 --- a/packages/block-library/src/paragraph/block.json +++ b/packages/block-library/src/paragraph/block.json @@ -7,6 +7,7 @@ "description": "Start with the basic building block of all narrative.", "keywords": [ "text" ], "textdomain": "default", + "usesContext": [ "postType", "postId" ], "attributes": { "align": { "type": "string" @@ -63,7 +64,10 @@ } }, "__experimentalSelector": "p", - "__unstablePasteTextInline": true + "__unstablePasteTextInline": true, + "customSources": { + "content": true + } }, "editorStyle": "wp-block-paragraph-editor", "style": "wp-block-paragraph" diff --git a/packages/block-library/src/paragraph/index.js b/packages/block-library/src/paragraph/index.js index bceff881367074..2ef0a3b6283ea9 100644 --- a/packages/block-library/src/paragraph/index.js +++ b/packages/block-library/src/paragraph/index.js @@ -44,6 +44,22 @@ export const settings = { }, edit, save, + variations: [ + { + name: 'core/post-custom-field', + title: __( 'Post Summary' ), + description: __( + 'Just a block to edit a custom field named "summary".' + ), + attributes: { + source: { content: { type: 'meta', name: 'summary' } }, + }, + isActive: ( blockAttributes ) => + blockAttributes.source?.content?.type === 'meta' && + blockAttributes.source?.content?.name === 'summary', + scope: [ 'block', 'inserter', 'transform' ], + }, + ], }; export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/editor/src/hooks/custom-sources-v2.js b/packages/editor/src/hooks/custom-sources-v2.js new file mode 100644 index 00000000000000..465ae5f6477215 --- /dev/null +++ b/packages/editor/src/hooks/custom-sources-v2.js @@ -0,0 +1,180 @@ +/** + * WordPress dependencies + */ +import { getBlockType, hasBlockSupport } from '@wordpress/blocks'; +import { useRegistry } from '@wordpress/data'; +import { useEntityProp } from '@wordpress/core-data'; +import { useMemo } from '@wordpress/element'; +import { createHigherOrderComponent } from '@wordpress/compose'; +import { addFilter } from '@wordpress/hooks'; + +/** + * Filters registered block settings, extending attributes to include `style` attribute. + * + * @param {Object} settings Original block settings. + * + * @return {Object} Filtered block settings. + */ +function addAttribute( settings ) { + if ( ! hasBlockSupport( settings, 'customSources' ) ) { + return settings; + } + + // Allow blocks to specify their own attribute definition with default values if needed. + if ( ! settings.attributes.source ) { + Object.assign( settings.attributes, { + source: { + type: 'object', + }, + } ); + } + + return settings; +} + +/** @typedef {import('@wordpress/compose').WPHigherOrderComponent} WPHigherOrderComponent */ +/** @typedef {import('@wordpress/blocks').WPBlockSettings} WPBlockSettings */ + +/** + * Given a mapping of attribute names (meta source attributes) to their + * associated meta key, returns a higher order component that overrides its + * `attributes` and `setAttributes` props to sync any changes with the edited + * post's meta keys. + * + * @return {WPHigherOrderComponent} Higher-order component. + */ +const createEditFunctionWithCustomSources = () => + createHigherOrderComponent( + ( BlockEdit ) => + ( { + name, + attributes, + setAttributes, + context: { postType, postId }, + ...props + } ) => { + const registry = useRegistry(); + const [ meta, setMeta ] = useEntityProp( + 'postType', + postType, + 'meta', + postId + ); + + const blockType = getBlockType( name ); + + const mergedAttributes = useMemo( () => { + if ( ! blockType.supports?.customSources ) { + return attributes; + } + return { + ...attributes, + ...Object.fromEntries( + Object.keys( blockType.supports.customSources ).map( + ( attributeName ) => { + if ( + attributes.source?.[ attributeName ] + ?.type === 'meta' + ) { + return [ + attributeName, + meta?.[ + attributes.source?.[ + attributeName + ]?.name + ], + ]; + } + return [ + attributeName, + attributes[ attributeName ], + ]; + } + ) + ), + }; + }, [ blockType.supports?.customSources, attributes, meta ] ); + + return ( + { + const nextMeta = Object.fromEntries( + Object.entries( nextAttributes ?? {} ) + .filter( + // Filter to intersection of keys between the updated + // attributes and those with an associated meta key. + ( [ key ] ) => + blockType.supports?.customSources && + key in + blockType.supports + ?.customSources && + attributes.source?.[ key ]?.type === + 'meta' + ) + .map( ( [ attributeKey, value ] ) => [ + // Rename the keys to the expected meta key name. + attributes.source?.[ attributeKey ] + ?.name, + value, + ] ) + ); + + const updatedAttributes = Object.entries( nextMeta ) + .length + ? Object.fromEntries( + Object.entries( nextAttributes ).filter( + ( [ key ] ) => + ! ( + blockType.supports + ?.customSources && + key in + blockType.supports + ?.customSources && + attributes.source?.[ key ] + ?.type === 'meta' + ) + ) + ) + : nextAttributes; + + registry.batch( () => { + if ( Object.entries( nextMeta ).length ) { + setMeta( nextMeta ); + } + + setAttributes( updatedAttributes ); + } ); + } } + { ...props } + /> + ); + }, + 'withCustomSources' + ); + +/** + * Filters a registered block's settings to enhance a block's `edit` component + * to upgrade meta-sourced attributes to use the post's meta entity property. + * + * @param {WPBlockSettings} settings Registered block settings. + * + * @return {WPBlockSettings} Filtered block settings. + */ +function shimAttributeSource( settings ) { + settings.edit = createEditFunctionWithCustomSources()( settings.edit ); + + return settings; +} + +addFilter( + 'blocks.registerBlockType', + 'core/editor/custom-sources-backwards-compatibility/shim-attribute-source', + shimAttributeSource +); + +addFilter( + 'blocks.registerBlockType', + 'core/custom-sources-v2/addAttribute', + addAttribute +); diff --git a/packages/editor/src/hooks/index.js b/packages/editor/src/hooks/index.js index 6e0934d63c0cfa..3b34b42936b80a 100644 --- a/packages/editor/src/hooks/index.js +++ b/packages/editor/src/hooks/index.js @@ -2,4 +2,5 @@ * Internal dependencies */ import './custom-sources-backwards-compatibility'; +import './custom-sources-v2'; import './default-autocompleters';