From 24d575b1accaf972b2e4bfe2020e159fd0615546 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 07:53:37 +0100 Subject: [PATCH 01/20] Change block bindings experiment name --- lib/block-supports/pattern.php | 2 +- lib/experimental/blocks.php | 4 ++-- lib/experimental/editor-settings.php | 4 ++-- lib/experiments-page.php | 6 +++--- packages/block-editor/src/hooks/custom-fields.js | 6 +++--- packages/block-editor/src/hooks/index.js | 2 +- packages/block-library/src/block/edit.js | 6 +++++- packages/block-library/src/paragraph/block.json | 2 +- packages/editor/src/hooks/pattern-partial-syncing.js | 2 +- 9 files changed, 19 insertions(+), 15 deletions(-) diff --git a/lib/block-supports/pattern.php b/lib/block-supports/pattern.php index a783135c793e3..f9dd1b4b44248 100644 --- a/lib/block-supports/pattern.php +++ b/lib/block-supports/pattern.php @@ -13,7 +13,7 @@ * @param WP_Block_Type $block_type Block Type. */ function gutenberg_register_pattern_support( $block_type ) { - $pattern_support = property_exists( $block_type, 'supports' ) ? _wp_array_get( $block_type->supports, array( '__experimentalConnections' ), false ) : false; + $pattern_support = property_exists( $block_type, 'supports' ) ? _wp_array_get( $block_type->supports, array( '__experimentalBlockBindings' ), false ) : false; if ( $pattern_support ) { if ( ! $block_type->uses_context ) { diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index 88e46b478389d..a85cf98937ee2 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -83,7 +83,7 @@ function wp_enqueue_block_view_script( $block_name, $args ) { $gutenberg_experiments = get_option( 'gutenberg-experiments' ); if ( $gutenberg_experiments && ( - array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) || + array_key_exists( 'gutenberg-block-bindings', $gutenberg_experiments ) || array_key_exists( 'gutenberg-pattern-partial-syncing', $gutenberg_experiments ) ) ) { /** @@ -118,7 +118,7 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst } // If the block does not have support for block connections, skip it. - if ( ! block_has_support( $block_type, array( '__experimentalConnections' ), false ) ) { + if ( ! block_has_support( $block_type, array( '__experimentalBlockBindings' ), false ) ) { return $block_content; } diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index 5f61684e8b134..729376cf030dd 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -26,8 +26,8 @@ function gutenberg_enable_experiments() { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableGroupGridVariation = true', 'before' ); } - if ( $gutenberg_experiments && array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) ) { - wp_add_inline_script( 'wp-block-editor', 'window.__experimentalConnections = true', 'before' ); + if ( $gutenberg_experiments && array_key_exists( 'gutenberg-block-bindings', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalBlockBindings = true', 'before' ); } if ( gutenberg_is_experiment_enabled( 'gutenberg-no-tinymce' ) ) { diff --git a/lib/experiments-page.php b/lib/experiments-page.php index b77a69b692ff1..c407e106a3417 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -128,13 +128,13 @@ function gutenberg_initialize_experiments_settings() { add_settings_field( 'gutenberg-custom-fields', - __( 'Connections', 'gutenberg' ), + __( 'Block Bindings & Custom Fields', 'gutenberg' ), 'gutenberg_display_experiment_field', 'gutenberg-experiments', 'gutenberg_experiments_section', array( - 'label' => __( 'Test Connections', 'gutenberg' ), - 'id' => 'gutenberg-connections', + 'label' => __( 'Test connecting block attributes to different sources like custom fields', 'gutenberg' ), + 'id' => 'gutenberg-block-bindings', ) ); diff --git a/packages/block-editor/src/hooks/custom-fields.js b/packages/block-editor/src/hooks/custom-fields.js index 9b677933adc13..5dc82473ef40e 100644 --- a/packages/block-editor/src/hooks/custom-fields.js +++ b/packages/block-editor/src/hooks/custom-fields.js @@ -20,7 +20,7 @@ import { useBlockEditingMode } from '../components/block-editing-mode'; * @return {Object} Filtered block settings. */ function addAttribute( settings ) { - if ( hasBlockSupport( settings, '__experimentalConnections', true ) ) { + if ( hasBlockSupport( settings, '__experimentalBlockBindings', true ) ) { // Gracefully handle if settings.attributes.connections is undefined. settings.attributes = { ...settings.attributes, @@ -95,7 +95,7 @@ export default { attributeKeys: [ 'connections' ], hasSupport( name ) { return ( - hasBlockSupport( name, '__experimentalConnections', false ) && + hasBlockSupport( name, '__experimentalBlockBindings', false ) && // Check if the current block is a paragraph or image block. // Currently, only these two blocks are supported. [ 'core/paragraph', 'core/image' ].includes( name ) @@ -104,7 +104,7 @@ export default { }; if ( - window.__experimentalConnections || + window.__experimentalBlockBindings || window.__experimentalPatternPartialSyncing ) { addFilter( diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 385b9fe6b1511..fa5ab255f8b6e 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -39,7 +39,7 @@ createBlockEditFilter( position, layout, contentLockUI, - window.__experimentalConnections ? customFields : null, + window.__experimentalBlockBindings ? customFields : null, blockHooks, blockRenaming, ].filter( Boolean ) diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index fbfef0b4cf177..de3921084af86 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -38,7 +38,11 @@ const { useLayoutClasses } = unlock( blockEditorPrivateApis ); function isPartiallySynced( block ) { return ( - !! getBlockSupport( block.name, '__experimentalConnections', false ) && + !! getBlockSupport( + block.name, + '__experimentalBlockBindings', + false + ) && !! block.attributes.connections?.attributes && Object.values( block.attributes.connections.attributes ).some( ( connection ) => connection.source === 'pattern_attributes' diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json index 3fe4fbb34e102..96ec9c778c096 100644 --- a/packages/block-library/src/paragraph/block.json +++ b/packages/block-library/src/paragraph/block.json @@ -41,7 +41,7 @@ "text": true } }, - "__experimentalConnections": true, + "__experimentalBlockBindings": true, "spacing": { "margin": true, "padding": true, diff --git a/packages/editor/src/hooks/pattern-partial-syncing.js b/packages/editor/src/hooks/pattern-partial-syncing.js index 40bd1e16dfc00..976efebb720f6 100644 --- a/packages/editor/src/hooks/pattern-partial-syncing.js +++ b/packages/editor/src/hooks/pattern-partial-syncing.js @@ -34,7 +34,7 @@ const withPartialSyncingControls = createHigherOrderComponent( const blockEditingMode = useBlockEditingMode(); const hasCustomFieldsSupport = hasBlockSupport( props.name, - '__experimentalConnections', + '__experimentalBlockBindings', false ); const isEditingPattern = useSelect( From 5859b60bf8c26d69c43cbe01c0ced012931f46bc Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 07:58:32 +0100 Subject: [PATCH 02/20] Remove old custom fields UI --- .../block-editor/src/hooks/custom-fields.js | 115 ------------------ packages/block-editor/src/hooks/index.js | 2 - .../block-library/src/paragraph/block.json | 1 - 3 files changed, 118 deletions(-) delete mode 100644 packages/block-editor/src/hooks/custom-fields.js diff --git a/packages/block-editor/src/hooks/custom-fields.js b/packages/block-editor/src/hooks/custom-fields.js deleted file mode 100644 index 5dc82473ef40e..0000000000000 --- a/packages/block-editor/src/hooks/custom-fields.js +++ /dev/null @@ -1,115 +0,0 @@ -/** - * WordPress dependencies - */ -import { addFilter } from '@wordpress/hooks'; -import { PanelBody, TextControl } from '@wordpress/components'; -import { __, sprintf } from '@wordpress/i18n'; -import { hasBlockSupport } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import { InspectorControls } from '../components'; -import { useBlockEditingMode } from '../components/block-editing-mode'; - -/** - * Filters registered block settings, extending attributes to include `connections`. - * - * @param {Object} settings Original block settings. - * - * @return {Object} Filtered block settings. - */ -function addAttribute( settings ) { - if ( hasBlockSupport( settings, '__experimentalBlockBindings', true ) ) { - // Gracefully handle if settings.attributes.connections is undefined. - settings.attributes = { - ...settings.attributes, - connections: { - type: 'object', - }, - }; - } - - return settings; -} - -function CustomFieldsControlPure( { name, connections, setAttributes } ) { - const blockEditingMode = useBlockEditingMode(); - if ( blockEditingMode !== 'default' ) { - return null; - } - - // If the block is a paragraph or image block, we need to know which - // attribute to use for the connection. Only the `content` attribute - // of the paragraph block and the `url` attribute of the image block are supported. - let attributeName; - if ( name === 'core/paragraph' ) attributeName = 'content'; - if ( name === 'core/image' ) attributeName = 'url'; - - return ( - - - { - if ( nextValue === '' ) { - setAttributes( { - connections: undefined, - [ attributeName ]: undefined, - placeholder: undefined, - } ); - } else { - 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 - ), - } ); - } - } } - /> - - - ); -} - -export default { - edit: CustomFieldsControlPure, - attributeKeys: [ 'connections' ], - hasSupport( name ) { - return ( - hasBlockSupport( name, '__experimentalBlockBindings', false ) && - // Check if the current block is a paragraph or image block. - // Currently, only these two blocks are supported. - [ 'core/paragraph', 'core/image' ].includes( name ) - ); - }, -}; - -if ( - window.__experimentalBlockBindings || - window.__experimentalPatternPartialSyncing -) { - addFilter( - 'blocks.registerBlockType', - 'core/editor/connections/attribute', - addAttribute - ); -} diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index fa5ab255f8b6e..f17c0a22166e4 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -25,7 +25,6 @@ import layout from './layout'; import childLayout from './layout-child'; import contentLockUI from './content-lock-ui'; import './metadata'; -import customFields from './custom-fields'; import blockHooks from './block-hooks'; import blockRenaming from './block-renaming'; @@ -39,7 +38,6 @@ createBlockEditFilter( position, layout, contentLockUI, - window.__experimentalBlockBindings ? customFields : null, blockHooks, blockRenaming, ].filter( Boolean ) diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json index 96ec9c778c096..a81d754d8ca1b 100644 --- a/packages/block-library/src/paragraph/block.json +++ b/packages/block-library/src/paragraph/block.json @@ -41,7 +41,6 @@ "text": true } }, - "__experimentalBlockBindings": true, "spacing": { "margin": true, "padding": true, From da3934b71c77c215cb3683738a41599ed1c9a071 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 08:11:09 +0100 Subject: [PATCH 03/20] Add block bindings PHP logic --- .../block-bindings/html-processing.php | 112 ++++++++++++ lib/experimental/block-bindings/index.php | 20 +++ .../block-bindings/sources/index.php | 24 +++ lib/experimental/blocks.php | 164 +++++++----------- lib/experimental/connection-sources/index.php | 19 -- 5 files changed, 222 insertions(+), 117 deletions(-) create mode 100644 lib/experimental/block-bindings/html-processing.php create mode 100644 lib/experimental/block-bindings/index.php create mode 100644 lib/experimental/block-bindings/sources/index.php delete mode 100644 lib/experimental/connection-sources/index.php diff --git a/lib/experimental/block-bindings/html-processing.php b/lib/experimental/block-bindings/html-processing.php new file mode 100644 index 0000000000000..9fb629d5e2821 --- /dev/null +++ b/lib/experimental/block-bindings/html-processing.php @@ -0,0 +1,112 @@ +get_registered( $block_name ); + if ( null === $block_type ) { + return; + } + + // Depending on the attribute source, the processing will be different. + // TODO: Get the type from the block attribute definition and modify/validate the value returned by the source if needed. + switch ( $block_type->attributes[ $block_attr ]['source'] ) { + case 'html': + case 'rich-text': + $p = new WP_HTML_Tag_Processor( $block_content ); + + // TODO: Support for CSS selectors whenever they are ready in the HTML API. + // In the meantime, support comma-separated selectors by exploding them into an array. + $selectors = explode( ',', $block_type->attributes[ $block_attr ]['selector'] ); + // Add a bookmark to the first tag to be able to iterate over the selectors. + $p->next_tag(); + $p->set_bookmark( 'iterate-selectors' ); + + // TODO: This shouldn't be needed when the `set_inner_html` function is ready. + // Store the parent tag and its attributes to be able to restore them later in the button. + // The button block has a wrapper while the paragraph and heading blocks don't. + if ( 'core/button' === $block_name ) { + $parent_tag = $p->get_tag(); + $parent_tag_names = $p->get_attribute_names_with_prefix( '' ); + $parent_tag_attrs = array(); + foreach ( $parent_tag_names as $name ) { + $parent_tag_attrs[ $name ] = $p->get_attribute( $name ); + } + } + + foreach ( $selectors as $selector ) { + // If the parent tag, or any of its children, matches the selector, replace the HTML. + if ( strcasecmp( $p->get_tag( $selector ), $selector ) === 0 || $p->next_tag( + array( + 'tag_name' => $selector, + ) + ) ) { + $p->release_bookmark( 'iterate-selectors' ); + + // TODO: Use `set_inner_html` method whenever it's ready in the HTML API. + // Until then, it is hardcoded for the paragraph, heading, and button blocks. + // Store the tag and its attributes to be able to restore them later. + $selector_tag_names = $p->get_attribute_names_with_prefix( '' ); + $selector_tag_attrs = array(); + foreach ( $selector_tag_names as $name ) { + $selector_tag_attrs[ $name ] = $p->get_attribute( $name ); + } + $selector_markup = "<$selector>" . esc_html( $source_value ) . ""; + $p2 = new WP_HTML_Tag_Processor( $selector_markup ); + $p2->next_tag(); + foreach ( $selector_tag_attrs as $attribute_key => $attribute_value ) { + $p2->set_attribute( $attribute_key, $attribute_value ); + } + $selector_updated_html = $p2->get_updated_html(); + if ( 'core/paragraph' === $block_name || 'core/heading' === $block_name ) { + return $selector_updated_html; + } + if ( 'core/button' === $block_name ) { + $markup = "<$parent_tag>$selector_updated_html"; + $p3 = new WP_HTML_Tag_Processor( $markup ); + $p3->next_tag(); + foreach ( $parent_tag_attrs as $attribute_key => $attribute_value ) { + $p3->set_attribute( $attribute_key, $attribute_value ); + } + return $p3->get_updated_html(); + } + } else { + $p->seek( 'iterate-selectors' ); + } + } + $p->release_bookmark( 'iterate-selectors' ); + return $block_content; + + case 'attribute': + $p = new WP_HTML_Tag_Processor( $block_content ); + if ( ! $p->next_tag( + array( + // TODO: build the query from CSS selector. + 'tag_name' => $block_type->attributes[ $block_attr ]['selector'], + ) + ) ) { + return $block_content; + } + $p->set_attribute( $block_type->attributes[ $block_attr ]['attribute'], esc_attr( $source_value ) ); + return $p->get_updated_html(); + break; + + default: + return $block_content; + break; + } + return; + } +} diff --git a/lib/experimental/block-bindings/index.php b/lib/experimental/block-bindings/index.php new file mode 100644 index 0000000000000..cca857e93702f --- /dev/null +++ b/lib/experimental/block-bindings/index.php @@ -0,0 +1,20 @@ +block_type; - - // Allowlist of blocks that support block connections. - // Currently, we only allow the following blocks and attributes: - // - Paragraph: content. - // - Image: url. - $blocks_attributes_allowlist = array( - 'core/paragraph' => array( 'content' ), - 'core/image' => array( 'url' ), - ); - - // Whitelist of the block types that support block connections. - // Currently, we only allow the Paragraph and Image blocks to use block connections. - if ( ! in_array( $block['blockName'], array_keys( $blocks_attributes_allowlist ), true ) ) { - return $block_content; - } - - // If for some reason, the block type is not found, skip it. - if ( null === $block_type ) { - return $block_content; - } - - // If the block does not have support for block connections, skip it. - if ( ! block_has_support( $block_type, array( '__experimentalBlockBindings' ), false ) ) { - return $block_content; - } - - // Get all the attributes that have a connection. - $connected_attributes = $block['attrs']['connections']['attributes'] ?? false; - if ( ! $connected_attributes ) { - return $block_content; - } - - foreach ( $connected_attributes as $attribute_name => $attribute_value ) { - - // If the attribute is not in the allowlist, skip it. - if ( ! in_array( $attribute_name, $blocks_attributes_allowlist[ $block['blockName'] ], true ) ) { - continue; - } - - // Skip if the source value is not "meta_fields" or "pattern_attributes". - if ( 'meta_fields' !== $attribute_value['source'] && 'pattern_attributes' !== $attribute_value['source'] ) { - continue; - } - // If the attribute does not have a source, skip it. - if ( ! isset( $block_type->attributes[ $attribute_name ]['source'] ) ) { - continue; + require_once __DIR__ . '/block-bindings/index.php'; + // Whitelist of blocks that support block bindings. + // TODO: Look for a mechanism to opt-in for this. Maybe adding a property to block attributes? + global $block_bindings_whitelist; + $block_bindings_whitelist = array( + 'core/paragraph' => array( 'content' ), + 'core/heading' => array( 'content' ), + 'core/image' => array( 'url', 'title' ), + 'core/button' => array( 'url', 'text' ), + ); + if ( ! function_exists( 'process_block_bindings' ) ) { + /** + * Process the block bindings attribute. + * + * @param string $block_content Block Content. + * @param array $block Block attributes. + * @param WP_Block $block_instance The block instance. + */ + function process_block_bindings( $block_content, $block, $block_instance ) { + // If the block doesn't have the bindings property, return. + if ( ! isset( $block['attrs']['metadata']['bindings'] ) ) { + return $block_content; } - if ( 'pattern_attributes' === $attribute_value['source'] ) { - if ( ! _wp_array_get( $block_instance->attributes, array( 'metadata', 'id' ), false ) ) { + // TODO: Review the bindings syntax. + // Assuming the following format for the bindings property of the "metadata" attribute: + // + // "bindings": { + // "title": { + // "source": { + // "name": "post_meta", + // "attributes": { "value": "text_custom_field" } + // } + // }, + // "url": { + // "source": { + // "name": "post_meta", + // "attributes": { "value": "text_custom_field" } + // } + // } + // }, + // . + global $block_bindings_whitelist; + global $block_bindings_sources; + $modified_block_content = $block_content; + foreach ( $block['attrs']['metadata']['bindings'] as $binding_attribute => $binding_source ) { + // If the block is not in the whitelist, stop processing. + if ( ! isset( $block_bindings_whitelist[ $block['blockName'] ] ) ) { + return $block_content; + } + // If the attribute is not in the whitelist, process next attribute. + if ( ! in_array( $binding_attribute, $block_bindings_whitelist[ $block['blockName'] ], true ) ) { continue; } - - $custom_value = $connection_sources[ $attribute_value['source'] ]( $block_instance ); - } else { - // If the attribute does not specify the name of the custom field, skip it. - if ( ! isset( $attribute_value['value'] ) ) { + // If no source is provided, or that source is not registered, process next attribute. + if ( ! isset( $binding_source['source'] ) || ! isset( $binding_source['source']['name'] ) || ! isset( $block_bindings_sources[ $binding_source['source']['name'] ] ) ) { continue; } - // Get the content from the connection source. - $custom_value = $connection_sources[ $attribute_value['source'] ]( - $block_instance, - $attribute_value['value'] - ); - } - - if ( false === $custom_value ) { - continue; - } + $source_callback = $block_bindings_sources[ $binding_source['source']['name'] ]['apply']; + // Get the value based on the source. + $source_value = $source_callback( $binding_source['source']['attributes'], $block_content, $block, $block_instance ); + // If the value is null, process next attribute. + if ( is_null( $source_value ) ) { + continue; + } - $tags = new WP_HTML_Tag_Processor( $block_content ); - $found = $tags->next_tag( - array( - // TODO: In the future, when blocks other than Paragraph and Image are - // supported, we should build the full query from CSS selector. - 'tag_name' => $block_type->attributes[ $attribute_name ]['selector'], - ) - ); - if ( ! $found ) { - return $block_content; + // Process the HTML based on the block and the attribute. + $modified_block_content = block_bindings_replace_html( $modified_block_content, $block['blockName'], $binding_attribute, $source_value ); } - $tag_name = $tags->get_tag(); - $markup = "<$tag_name>$custom_value"; - $updated_tags = new WP_HTML_Tag_Processor( $markup ); - $updated_tags->next_tag(); - - // Get all the attributes from the original block and add them to the new markup. - $names = $tags->get_attribute_names_with_prefix( '' ); - foreach ( $names as $name ) { - $updated_tags->set_attribute( $name, $tags->get_attribute( $name ) ); - } - - return $updated_tags->get_updated_html(); + return $modified_block_content; } - return $block_content; + // Add filter only to the blocks in the whitelist. + foreach ( $block_bindings_whitelist as $block_name => $attributes ) { + add_filter( 'render_block_' . $block_name, 'process_block_bindings', 20, 3 ); + } } - - 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 deleted file mode 100644 index bf89ba177b6e9..0000000000000 --- a/lib/experimental/connection-sources/index.php +++ /dev/null @@ -1,19 +0,0 @@ - 'meta', - 'meta_fields' => function ( $block_instance, $meta_field ) { - // We should probably also check if the meta field exists but for now it's okay because - // 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 ) { - $block_id = $block_instance->attributes['metadata']['id']; - return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id ), false ); - }, -); From e55b1a84716d8be9837d80b6eda87e9ac1cbbeb3 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 08:11:27 +0100 Subject: [PATCH 04/20] Add pattern source --- .../block-bindings/sources/pattern.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 lib/experimental/block-bindings/sources/pattern.php diff --git a/lib/experimental/block-bindings/sources/pattern.php b/lib/experimental/block-bindings/sources/pattern.php new file mode 100644 index 0000000000000..4948e1d8fdd03 --- /dev/null +++ b/lib/experimental/block-bindings/sources/pattern.php @@ -0,0 +1,23 @@ +attributes, array( 'metadata', 'id' ), false ) ) { + return; + } + $block_id = $block_instance->attributes['metadata']['id']; + return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id ), false ); + }; + register_block_bindings_source( + 'pattern_attributes', + array( + 'label' => __( 'Pattern Attributes' ), + 'apply' => $pattern_source_callback, + ) + ); +} From 3ae0f422ed1181a7cda6a6d45e66c8c8040e0ea5 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 08:11:34 +0100 Subject: [PATCH 05/20] Add post meta source --- .../block-bindings/sources/post-meta.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 lib/experimental/block-bindings/sources/post-meta.php diff --git a/lib/experimental/block-bindings/sources/post-meta.php b/lib/experimental/block-bindings/sources/post-meta.php new file mode 100644 index 0000000000000..3220b3c6defb2 --- /dev/null +++ b/lib/experimental/block-bindings/sources/post-meta.php @@ -0,0 +1,27 @@ +context['postId'] but it wasn't available in the image block. + $post_id = get_the_ID(); + } + + return get_post_meta( $post_id, $source_attrs['value'], true ); + }; + register_block_bindings_source( + 'post_meta', + array( + 'label' => __( 'Post Meta' ), + 'apply' => $post_meta_source_callback, + ) + ); +} From f26e17378e7288c2d5353d33fae86b20a7fe9940 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 08:40:57 +0100 Subject: [PATCH 06/20] Adapt partially synced patterns experiment --- lib/experimental/blocks.php | 7 ++- packages/block-library/src/block/edit.js | 10 ++-- .../block-library/src/paragraph/block.json | 1 + .../components/partial-syncing-controls.js | 52 ++++++++----------- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index 232829be487ba..ebc65f9dfbe89 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -148,7 +148,12 @@ function process_block_bindings( $block_content, $block, $block_instance ) { $source_callback = $block_bindings_sources[ $binding_source['source']['name'] ]['apply']; // Get the value based on the source. - $source_value = $source_callback( $binding_source['source']['attributes'], $block_content, $block, $block_instance ); + if ( ! isset( $binding_source['source']['attributes'] ) ) { + $source_args = array(); + } else { + $source_args = $binding_source['source']['attributes']; + } + $source_value = $source_callback( $source_args, $block_content, $block, $block_instance ); // If the value is null, process next attribute. if ( is_null( $source_value ) ) { continue; diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index de3921084af86..c8290e5e30090 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -43,16 +43,16 @@ function isPartiallySynced( block ) { '__experimentalBlockBindings', false ) && - !! block.attributes.connections?.attributes && - Object.values( block.attributes.connections.attributes ).some( - ( connection ) => connection.source === 'pattern_attributes' + !! block.attributes.metadata?.bindings && + Object.values( block.attributes.metadata.bindings ).some( + ( binding ) => binding.source.name === 'pattern_attributes' ) ); } function getPartiallySyncedAttributes( block ) { - return Object.entries( block.attributes.connections.attributes ) + return Object.entries( block.attributes.metadata.bindings ) .filter( - ( [ , connection ] ) => connection.source === 'pattern_attributes' + ( [ , binding ] ) => binding.source.name === 'pattern_attributes' ) .map( ( [ attributeKey ] ) => attributeKey ); } diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json index a81d754d8ca1b..70f43d3793bca 100644 --- a/packages/block-library/src/paragraph/block.json +++ b/packages/block-library/src/paragraph/block.json @@ -63,6 +63,7 @@ "fontSize": true } }, + "__experimentalBlockBindings": true, "__experimentalSelector": "p", "__unstablePasteTextInline": true }, diff --git a/packages/patterns/src/components/partial-syncing-controls.js b/packages/patterns/src/components/partial-syncing-controls.js index 42c39ce69e87b..10f812d29fd21 100644 --- a/packages/patterns/src/components/partial-syncing-controls.js +++ b/packages/patterns/src/components/partial-syncing-controls.js @@ -18,51 +18,45 @@ import { PARTIAL_SYNCING_SUPPORTED_BLOCKS } from '../constants'; function PartialSyncingControls( { name, attributes, setAttributes } ) { const syncedAttributes = PARTIAL_SYNCING_SUPPORTED_BLOCKS[ name ]; - function updateConnections( attributeName, isChecked ) { + function updateBindings( attributeName, isChecked ) { if ( ! isChecked ) { - let updatedConnections = { - ...attributes.connections, - attributes: { - ...attributes.connections?.attributes, - [ attributeName ]: undefined, - }, + let updatedBindings = { + ...attributes?.metadata?.bindings, + [ attributeName ]: undefined, }; - if ( Object.keys( updatedConnections.attributes ).length === 1 ) { - updatedConnections.attributes = undefined; - } - if ( - Object.keys( updatedConnections ).length === 1 && - updateConnections.attributes === undefined - ) { - updatedConnections = undefined; + if ( Object.keys( updatedBindings ).length === 1 ) { + updatedBindings = undefined; } setAttributes( { - connections: updatedConnections, + metadata: { + ...attributes.metadata, + bindings: updatedBindings, + }, } ); return; } - const updatedConnections = { - ...attributes.connections, - attributes: { - ...attributes.connections?.attributes, - [ attributeName ]: { - source: 'pattern_attributes', - }, - }, + const updatedBindings = { + ...attributes?.metadata?.bindings, + [ attributeName ]: { source: { name: 'pattern_attributes' } }, }; if ( typeof attributes.metadata?.id === 'string' ) { - setAttributes( { connections: updatedConnections } ); + setAttributes( { + metadata: { + ...attributes.metadata, + bindings: updatedBindings, + }, + } ); return; } const id = nanoid( 6 ); setAttributes( { - connections: updatedConnections, metadata: { ...attributes.metadata, id, + bindings: updatedBindings, }, } ); } @@ -80,12 +74,12 @@ function PartialSyncingControls( { name, attributes, setAttributes } ) { __nextHasNoMarginBottom label={ label } checked={ - attributes.connections?.attributes?.[ + attributes?.metadata?.bindings?.[ attributeName - ]?.source === 'pattern_attributes' + ]?.source?.name === 'pattern_attributes' } onChange={ ( isChecked ) => { - updateConnections( attributeName, isChecked ); + updateBindings( attributeName, isChecked ); } } /> ) From c8b77194d01f7537d8015e5d6e84662ec62f8c76 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 09:47:07 +0100 Subject: [PATCH 07/20] Add domain to the translations in sources --- lib/experimental/block-bindings/sources/pattern.php | 2 +- lib/experimental/block-bindings/sources/post-meta.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/experimental/block-bindings/sources/pattern.php b/lib/experimental/block-bindings/sources/pattern.php index 4948e1d8fdd03..c2549b6048928 100644 --- a/lib/experimental/block-bindings/sources/pattern.php +++ b/lib/experimental/block-bindings/sources/pattern.php @@ -16,7 +16,7 @@ register_block_bindings_source( 'pattern_attributes', array( - 'label' => __( 'Pattern Attributes' ), + 'label' => __( 'Pattern Attributes', 'gutenberg' ), 'apply' => $pattern_source_callback, ) ); diff --git a/lib/experimental/block-bindings/sources/post-meta.php b/lib/experimental/block-bindings/sources/post-meta.php index 3220b3c6defb2..d8b091d97aade 100644 --- a/lib/experimental/block-bindings/sources/post-meta.php +++ b/lib/experimental/block-bindings/sources/post-meta.php @@ -20,7 +20,7 @@ register_block_bindings_source( 'post_meta', array( - 'label' => __( 'Post Meta' ), + 'label' => __( 'Post Meta', 'gutenberg' ), 'apply' => $post_meta_source_callback, ) ); From f19f3b63648611d9c665a3a5cb3df7643309b82f Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 09:55:20 +0100 Subject: [PATCH 08/20] Remove unused post_meta attrs --- lib/experimental/block-bindings/sources/post-meta.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/experimental/block-bindings/sources/post-meta.php b/lib/experimental/block-bindings/sources/post-meta.php index d8b091d97aade..9e2bf22aa8b73 100644 --- a/lib/experimental/block-bindings/sources/post-meta.php +++ b/lib/experimental/block-bindings/sources/post-meta.php @@ -6,7 +6,7 @@ */ if ( function_exists( 'register_block_bindings_source' ) ) { - $post_meta_source_callback = function ( $source_attrs, $block_content, $block, $block_instance ) { + $post_meta_source_callback = function ( $source_attrs ) { // Use the postId attribute if available, otherwise use the context. if ( isset( $source_attrs['postId'] ) ) { $post_id = $source_attrs['postId']; From a51b743ed55b78e20fa7da65cb88ed7b247c4418 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 11:24:18 +0100 Subject: [PATCH 09/20] Create helper to update bindings attribute --- packages/block-editor/README.md | 12 ++++ packages/block-editor/src/utils/index.js | 1 + .../src/utils/update-block-bindings.js | 68 +++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 packages/block-editor/src/utils/update-block-bindings.js diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 5917ac235505c..d087023741953 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -812,6 +812,18 @@ _Properties_ Ensures that the text selection keeps the same vertical distance from the viewport during keyboard events within this component. The vertical distance can vary. It is the last clicked or scrolled to position. +### updateBlockBindingsAttribute + +Helper to update the bindings attribute used by the Block Bindings API. + +_Parameters_ + +- _blockAttributes_ `Object`: - The original block attributes. +- _setAttributes_ `Function`: - setAttributes function to modify the bindings property. +- _attributeName_ `string`: - The attribute in the bindings object to update. +- _sourceName_ `string`: - The source name added to the bindings property. +- _sourceAttributes_ `string`: - The source attributes added to the bindings property. + ### URLInput _Related_ diff --git a/packages/block-editor/src/utils/index.js b/packages/block-editor/src/utils/index.js index ee3b2692b369a..21f33acfc274d 100644 --- a/packages/block-editor/src/utils/index.js +++ b/packages/block-editor/src/utils/index.js @@ -1,3 +1,4 @@ export { default as transformStyles } from './transform-styles'; export * from './block-variation-transforms'; export { default as getPxFromCssUnit } from './get-px-from-css-unit'; +export * from './update-block-bindings'; diff --git a/packages/block-editor/src/utils/update-block-bindings.js b/packages/block-editor/src/utils/update-block-bindings.js new file mode 100644 index 0000000000000..c34aa8b46c6e3 --- /dev/null +++ b/packages/block-editor/src/utils/update-block-bindings.js @@ -0,0 +1,68 @@ +/** + * Helper to update the bindings attribute used by the Block Bindings API. + * + * @param {Object} blockAttributes - The original block attributes. + * @param {Function} setAttributes - setAttributes function to modify the bindings property. + * @param {string} attributeName - The attribute in the bindings object to update. + * @param {string} sourceName - The source name added to the bindings property. + * @param {string} sourceAttributes - The source attributes added to the bindings property. + */ +export const updateBlockBindingsAttribute = ( + blockAttributes, + setAttributes, + attributeName, + sourceName, + sourceAttributes +) => { + // TODO: Review if we can create a React Hook for this. + + // Assuming the following format for the bindings property of the "metadata" attribute: + // + // "bindings": { + // "title": { + // "source": { + // "name": "metadata", + // "attributes": { "value": "text_custom_field" } + // } + // }, + // "url": { + // "source": { + // "name": "metadata", + // "attributes": { "value": "text_custom_field" } + // } + // } + // }, + // . + + let updatedBindings = {}; + // // If no sourceName is provided, remove the attribute from the bindings. + if ( sourceName === null ) { + if ( ! blockAttributes?.metadata.bindings ) { + return blockAttributes?.metadata; + } + + updatedBindings = { + ...blockAttributes?.metadata?.bindings, + [ attributeName ]: undefined, + }; + if ( Object.keys( updatedBindings ).length === 1 ) { + updatedBindings = undefined; + } + } else { + updatedBindings = { + ...blockAttributes?.metadata?.bindings, + [ attributeName ]: { + source: { name: sourceName, attributes: sourceAttributes }, + }, + }; + } + + setAttributes( { + metadata: { + ...blockAttributes.metadata, + bindings: updatedBindings, + }, + } ); + + return blockAttributes.metadata; +}; From 32486c35e137d321edc66839db77a6d252be9ff2 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 11:25:18 +0100 Subject: [PATCH 10/20] Use helper in patterns --- .../components/partial-syncing-controls.js | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/patterns/src/components/partial-syncing-controls.js b/packages/patterns/src/components/partial-syncing-controls.js index 10f812d29fd21..700a05b10c896 100644 --- a/packages/patterns/src/components/partial-syncing-controls.js +++ b/packages/patterns/src/components/partial-syncing-controls.js @@ -6,7 +6,10 @@ import { nanoid } from 'nanoid'; /** * WordPress dependencies */ -import { InspectorControls } from '@wordpress/block-editor'; +import { + InspectorControls, + updateBlockBindingsAttribute, +} from '@wordpress/block-editor'; import { BaseControl, CheckboxControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; @@ -20,43 +23,40 @@ function PartialSyncingControls( { name, attributes, setAttributes } ) { function updateBindings( attributeName, isChecked ) { if ( ! isChecked ) { - let updatedBindings = { - ...attributes?.metadata?.bindings, - [ attributeName ]: undefined, - }; - if ( Object.keys( updatedBindings ).length === 1 ) { - updatedBindings = undefined; - } - setAttributes( { - metadata: { - ...attributes.metadata, - bindings: updatedBindings, - }, - } ); + updateBlockBindingsAttribute( + attributes, + setAttributes, + attributeName, + null, + null + ); return; } - const updatedBindings = { - ...attributes?.metadata?.bindings, - [ attributeName ]: { source: { name: 'pattern_attributes' } }, - }; - if ( typeof attributes.metadata?.id === 'string' ) { - setAttributes( { - metadata: { - ...attributes.metadata, - bindings: updatedBindings, - }, - } ); + updateBlockBindingsAttribute( + attributes, + setAttributes, + attributeName, + 'pattern_attributes', + null + ); return; } const id = nanoid( 6 ); + const newMetadata = updateBlockBindingsAttribute( + attributes, + setAttributes, + attributeName, + 'pattern_attributes', + null + ); + setAttributes( { metadata: { - ...attributes.metadata, + ...newMetadata, id, - bindings: updatedBindings, }, } ); } From 699fbb27a363c866889b4d222a8d935171d9e51a Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 11:51:49 +0100 Subject: [PATCH 11/20] Add UI for block bindings with a Fill for sources --- .../components/block-bindings/bindings-ui.js | 274 ++++++++++++++++++ .../src/components/block-bindings/index.js | 4 + .../src/components/block-bindings/style.scss | 30 ++ packages/editor/src/components/index.js | 3 + packages/editor/src/style.scss | 1 + 5 files changed, 312 insertions(+) create mode 100644 packages/editor/src/components/block-bindings/bindings-ui.js create mode 100644 packages/editor/src/components/block-bindings/index.js create mode 100644 packages/editor/src/components/block-bindings/style.scss diff --git a/packages/editor/src/components/block-bindings/bindings-ui.js b/packages/editor/src/components/block-bindings/bindings-ui.js new file mode 100644 index 0000000000000..3827deae790c4 --- /dev/null +++ b/packages/editor/src/components/block-bindings/bindings-ui.js @@ -0,0 +1,274 @@ +/** + * WordPress dependencies + */ +import { useState, cloneElement, Fragment } from '@wordpress/element'; +import { + BlockControls, + updateBlockBindingsAttribute, +} from '@wordpress/block-editor'; +import { + Button, + createSlotFill, + MenuItem, + MenuGroup, + Popover, +} from '@wordpress/components'; +import { + plugins as pluginsIcon, + chevronDown, + chevronUp, +} from '@wordpress/icons'; +import { addFilter } from '@wordpress/hooks'; + +const blockBindingsWhitelist = { + 'core/paragraph': [ 'content' ], + 'core/heading': [ 'content' ], + 'core/image': [ 'url', 'title' ], + 'core/button': [ 'url', 'text' ], +}; + +const { Slot, Fill } = createSlotFill( 'BlockBindingsUI' ); + +const BlockBindingsFill = ( { children, source, label } ) => { + return ( + + { ( props ) => { + return ( + <> + { cloneElement( children, { + source, + label, + ...props, + } ) } + + ); + } } + + ); +}; + +export default BlockBindingsFill; + +const BlockBindingsUI = ( props ) => { + const [ addingBinding, setAddingBinding ] = useState( false ); + const [ popoverAnchor, setPopoverAnchor ] = useState(); + return ( + <> + + + { addingBinding && ( + { + setAddingBinding( false ); + } } + onFocusOutside={ () => { + setAddingBinding( false ); + } } + placement="bottom" + shift + className="block-bindings-ui-popover" + { ...props } + > + + + ) } + + + ); +}; + +function AttributesLayer( props ) { + const [ activeAttribute, setIsActiveAttribute ] = useState( false ); + const [ activeSource, setIsActiveSource ] = useState( false ); + return ( + + { blockBindingsWhitelist[ props.name ].map( ( attribute ) => ( +
+ + setIsActiveAttribute( + activeAttribute === attribute + ? false + : attribute + ) + } + className="block-bindings-attribute-picker-button" + > + { attribute } + + { activeAttribute === attribute && ( + <> + + { /* Sources can fill this slot */ } + + { ( fills ) => { + if ( ! fills.length ) { + return null; + } + + return ( + <> + { fills.map( + ( fill, index ) => { + // TODO: Check better way to get the source and label. + const source = + fill[ 0 ].props + .children.props + .source; + const sourceLabel = + fill[ 0 ].props + .children.props + .label; + const isSourceSelected = + activeSource === + source; + + return ( + + + setIsActiveSource( + isSourceSelected + ? false + : source + ) + } + className="block-bindings-source-picker-button" + > + { + sourceLabel + } + + { isSourceSelected && + fill } + + ); + } + ) } + + ); + } } + + + + + ) } +
+ ) ) } +
+ ); +} + +function RemoveBindingButton( props ) { + return ( + + ); +} + +if ( window.__experimentalBlockBindings ) { + addFilter( + 'blocks.registerBlockType', + 'core/block-bindings-ui', + ( settings, name ) => { + if ( ! ( name in blockBindingsWhitelist ) ) { + return settings; + } + + // TODO: Review the implications of this and the code. + // Add the necessary context to the block. + const contextItems = [ 'postId', 'postType', 'queryId' ]; + const usesContextArray = settings.usesContext; + const oldUsesContextArray = new Set( usesContextArray ); + contextItems.forEach( ( item ) => { + if ( ! oldUsesContextArray.has( item ) ) { + usesContextArray.push( item ); + } + } ); + settings.usesContext = usesContextArray; + + // Add bindings button to the block toolbar. + const OriginalComponent = settings.edit; + settings.edit = ( props ) => { + return ( + <> + + + + ); + }; + + return settings; + } + ); +} + +// TODO: Add also some components to the sidebar. diff --git a/packages/editor/src/components/block-bindings/index.js b/packages/editor/src/components/block-bindings/index.js new file mode 100644 index 0000000000000..9d23f55b601a9 --- /dev/null +++ b/packages/editor/src/components/block-bindings/index.js @@ -0,0 +1,4 @@ +/** + * Internal dependencies + */ +export { default as BlockBindingsFill } from './bindings-ui'; diff --git a/packages/editor/src/components/block-bindings/style.scss b/packages/editor/src/components/block-bindings/style.scss new file mode 100644 index 0000000000000..738c747988480 --- /dev/null +++ b/packages/editor/src/components/block-bindings/style.scss @@ -0,0 +1,30 @@ +// TODO: Change the styles. +.block-bindings-ui-popover { + margin-top: 12px; + width: 300px; + .components-popover__content { + width: 100%; + } + + .block-bindings-attribute-picker-container { + border-bottom: 1px solid #0002; + } + + .block-bindings-fields-list-ui { + padding: 12px; + li { + margin: 20px 8px; + cursor: pointer; + } + .selected-meta-field { + font-weight: bold; + } + .selected-meta-field::before { + content: "✔ "; + margin-left: -16px; + } + } + .block-bindings-remove-button { + color: var(--wp-admin-theme-color, #3858e9); + } +} diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index 33a18e6f9a6ad..94cf5c50169d0 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -102,3 +102,6 @@ export { default as EditorProvider } from './provider'; export * from './deprecated'; export const VisualEditorGlobalKeyboardShortcuts = EditorKeyboardShortcuts; export const TextEditorGlobalKeyboardShortcuts = EditorKeyboardShortcuts; + +// Block Bindings Components. +export * from './block-bindings'; diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss index 50359984af162..4bec3804bb1e8 100644 --- a/packages/editor/src/style.scss +++ b/packages/editor/src/style.scss @@ -1,4 +1,5 @@ @import "./components/autocompleters/style.scss"; +@import "./components/block-bindings/style.scss"; @import "./components/document-bar/style.scss"; @import "./components/document-outline/style.scss"; @import "./components/editor-notices/style.scss"; From 7a9a81b75ce73ce3a7545465a209149cff21b4e0 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 11:52:33 +0100 Subject: [PATCH 12/20] Add BlockBindingsFieldsList component --- .../components/block-bindings/fields-list.js | 60 +++++++++++++++++++ .../src/components/block-bindings/index.js | 1 + 2 files changed, 61 insertions(+) create mode 100644 packages/editor/src/components/block-bindings/fields-list.js diff --git a/packages/editor/src/components/block-bindings/fields-list.js b/packages/editor/src/components/block-bindings/fields-list.js new file mode 100644 index 0000000000000..ba28797940b19 --- /dev/null +++ b/packages/editor/src/components/block-bindings/fields-list.js @@ -0,0 +1,60 @@ +/** + * WordPress dependencies + */ +import { updateBlockBindingsAttribute } from '@wordpress/block-editor'; +import { MenuItem, MenuGroup } from '@wordpress/components'; + +export default function BlockBindingsFieldsList( props ) { + const { + attributes, + setAttributes, + setIsActiveAttribute, + currentAttribute, + fields, + source, + setAddingBinding, + } = props; + + // TODO: Try to abstract this function to be reused across all the sources. + function selectItem( item ) { + // Modify the attribute we are binding. + // TODO: Not sure if we should do this. We might need to process the bindings attribute somehow in the editor to modify the content with context. + // TODO: Get the type from the block attribute definition and modify/validate the value returned by the source if needed. + const newAttributes = {}; + newAttributes[ currentAttribute ] = item.value; + setAttributes( newAttributes ); + + // Update the bindings property. + updateBlockBindingsAttribute( + attributes, + setAttributes, + currentAttribute, + source, + { value: item.key } + ); + + setIsActiveAttribute( false ); + setAddingBinding( false ); + } + + return ( + + { fields.map( ( item ) => ( + selectItem( item ) } + className={ + attributes.metadata?.bindings?.[ currentAttribute ] + ?.source?.name === source && + attributes.metadata?.bindings?.[ currentAttribute ] + ?.source?.attributes?.value === item.key + ? 'selected-meta-field' + : '' + } + > + { item.label } + + ) ) } + + ); +} diff --git a/packages/editor/src/components/block-bindings/index.js b/packages/editor/src/components/block-bindings/index.js index 9d23f55b601a9..600523bc9bbf7 100644 --- a/packages/editor/src/components/block-bindings/index.js +++ b/packages/editor/src/components/block-bindings/index.js @@ -2,3 +2,4 @@ * Internal dependencies */ export { default as BlockBindingsFill } from './bindings-ui'; +export { default as BlockBindingsFieldsList } from './fields-list'; From c3f709a329dba397e46f78ce784eeed3017dc989 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 12:26:26 +0100 Subject: [PATCH 13/20] Add post meta source using Fill and component --- packages/editor/package.json | 2 +- .../hooks/block-bindings-sources/post-meta.js | 90 +++++++++++++++++++ packages/editor/src/hooks/index.js | 3 + 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 packages/editor/src/hooks/block-bindings-sources/post-meta.js diff --git a/packages/editor/package.json b/packages/editor/package.json index 63656899e587c..e138089b2b916 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -27,7 +27,7 @@ "sideEffects": [ "build-style/**", "src/**/*.scss", - "{src,build,build-module}/{index.js,store/index.js,hooks/**}" + "{src,build,build-module}/{index.js,store/index.js,hooks/**,hooks/block-bindings-sources/**}" ], "dependencies": { "@babel/runtime": "^7.16.0", diff --git a/packages/editor/src/hooks/block-bindings-sources/post-meta.js b/packages/editor/src/hooks/block-bindings-sources/post-meta.js new file mode 100644 index 0000000000000..90bf447f162fa --- /dev/null +++ b/packages/editor/src/hooks/block-bindings-sources/post-meta.js @@ -0,0 +1,90 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { createHigherOrderComponent } from '@wordpress/compose'; +import { addFilter } from '@wordpress/hooks'; +/** + * Internal dependencies + */ +import BlockBindingsFill from '../../components/block-bindings/bindings-ui'; +import BlockBindingsFieldsList from '../../components/block-bindings/fields-list'; + +const PostMeta = ( props ) => { + const { context } = props; + + // Fetching the REST API to get the available custom fields. + // TODO: Explore how it should work in templates. + // TODO: Explore if it makes sense to create a custom endpoint for this. + const data = useSelect( + ( select ) => { + const { getEntityRecord } = select( coreStore ); + return getEntityRecord( + 'postType', + context.postType, + context.postId + ); + }, + [ context.postType, context.postId ] + ); + + // Adapt the data to the format expected by the fields list. + const fields = []; + // Prettifying the name until we receive the label from the REST API endpoint. + const keyToLabel = ( key ) => { + return key + .split( '_' ) + .map( ( word ) => word.charAt( 0 ).toUpperCase() + word.slice( 1 ) ) + .join( ' ' ); + }; + Object.entries( data.meta ).forEach( ( [ key, value ] ) => { + fields.push( { + key, + label: keyToLabel( key ), + value, + } ); + } ); + + return ( + + ); +}; + +if ( window.__experimentalBlockBindings ) { + // TODO: Read the context somehow to decide if we should add the source. + // const data = useSelect( editorStore ); + + // External sources could do something similar. + const withCoreSources = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + const { isSelected } = props; + return ( + <> + { isSelected && ( + <> + + + + + ) } + + + ); + }, + 'withToolbarControls' + ); + + addFilter( + 'editor.BlockEdit', + 'core/block-bindings-ui/add-sources', + withCoreSources + ); +} diff --git a/packages/editor/src/hooks/index.js b/packages/editor/src/hooks/index.js index 5a48ec1bf4956..8d52e5a36e663 100644 --- a/packages/editor/src/hooks/index.js +++ b/packages/editor/src/hooks/index.js @@ -4,3 +4,6 @@ import './custom-sources-backwards-compatibility'; import './default-autocompleters'; import './pattern-partial-syncing'; + +// Block bindings sources. +import './block-bindings-sources/post-meta'; From e047ecaeb9b257f5813e5eb39c38adfbe9b8a820 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 15:25:25 +0100 Subject: [PATCH 14/20] Change variables names --- lib/experimental/blocks.php | 30 ++++++++++++------- .../components/block-bindings/bindings-ui.js | 6 ++-- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index ebc65f9dfbe89..922774ba4ca97 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -88,10 +88,10 @@ function wp_enqueue_block_view_script( $block_name, $args ) { ) ) { require_once __DIR__ . '/block-bindings/index.php'; - // Whitelist of blocks that support block bindings. + // Allowed blocks that support block bindings. // TODO: Look for a mechanism to opt-in for this. Maybe adding a property to block attributes? - global $block_bindings_whitelist; - $block_bindings_whitelist = array( + global $block_bindings_allowed_blocks; + $block_bindings_allowed_blocks = array( 'core/paragraph' => array( 'content' ), 'core/heading' => array( 'content' ), 'core/image' => array( 'url', 'title' ), @@ -129,16 +129,16 @@ function process_block_bindings( $block_content, $block, $block_instance ) { // } // }, // . - global $block_bindings_whitelist; + global $block_bindings_allowed_blocks; global $block_bindings_sources; $modified_block_content = $block_content; foreach ( $block['attrs']['metadata']['bindings'] as $binding_attribute => $binding_source ) { - // If the block is not in the whitelist, stop processing. - if ( ! isset( $block_bindings_whitelist[ $block['blockName'] ] ) ) { + // If the block is not in the list, stop processing. + if ( ! isset( $block_bindings_allowed_blocks[ $block['blockName'] ] ) ) { return $block_content; } - // If the attribute is not in the whitelist, process next attribute. - if ( ! in_array( $binding_attribute, $block_bindings_whitelist[ $block['blockName'] ], true ) ) { + // If the attribute is not in the list, process next attribute. + if ( ! in_array( $binding_attribute, $block_bindings_allowed_blocks[ $block['blockName'] ], true ) ) { continue; } // If no source is provided, or that source is not registered, process next attribute. @@ -165,8 +165,18 @@ function process_block_bindings( $block_content, $block, $block_instance ) { return $modified_block_content; } - // Add filter only to the blocks in the whitelist. - foreach ( $block_bindings_whitelist as $block_name => $attributes ) { + // Add filter only to the blocks in the list. + foreach ( $block_bindings_allowed_blocks as $block_name => $attributes ) { + // if ( ! function_exists( 'process_block_bindings' ) ) { + // function filter_metadata_registration( $metadata ) { + // var_dump( $metadata ); + // die; + // $metadata['usesContext'] = array( 'postId', 'postType', 'queryId' ); + // return $metadata; + // } + // add_filter( 'block_type_metadata', 'filter_metadata_registration' ); + // } + add_filter( 'render_block_' . $block_name, 'process_block_bindings', 20, 3 ); } } diff --git a/packages/editor/src/components/block-bindings/bindings-ui.js b/packages/editor/src/components/block-bindings/bindings-ui.js index 3827deae790c4..c9cffde8eba9f 100644 --- a/packages/editor/src/components/block-bindings/bindings-ui.js +++ b/packages/editor/src/components/block-bindings/bindings-ui.js @@ -20,7 +20,7 @@ import { } from '@wordpress/icons'; import { addFilter } from '@wordpress/hooks'; -const blockBindingsWhitelist = { +const blockBindingsAllowedBlocks = { 'core/paragraph': [ 'content' ], 'core/heading': [ 'content' ], 'core/image': [ 'url', 'title' ], @@ -93,7 +93,7 @@ function AttributesLayer( props ) { const [ activeSource, setIsActiveSource ] = useState( false ); return ( - { blockBindingsWhitelist[ props.name ].map( ( attribute ) => ( + { blockBindingsAllowedBlocks[ props.name ].map( ( attribute ) => (
{ - if ( ! ( name in blockBindingsWhitelist ) ) { + if ( ! ( name in blockBindingsAllowedBlocks ) ) { return settings; } From d43ed33ae8a2f69e085f87549623dbf185c38092 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 16:49:41 +0100 Subject: [PATCH 15/20] Get context from "post meta" source --- .../components/block-bindings/bindings-ui.js | 12 -- .../hooks/block-bindings-sources/post-meta.js | 109 ++++++++++-------- 2 files changed, 59 insertions(+), 62 deletions(-) diff --git a/packages/editor/src/components/block-bindings/bindings-ui.js b/packages/editor/src/components/block-bindings/bindings-ui.js index c9cffde8eba9f..0e1ce6fc87e5f 100644 --- a/packages/editor/src/components/block-bindings/bindings-ui.js +++ b/packages/editor/src/components/block-bindings/bindings-ui.js @@ -243,18 +243,6 @@ if ( window.__experimentalBlockBindings ) { return settings; } - // TODO: Review the implications of this and the code. - // Add the necessary context to the block. - const contextItems = [ 'postId', 'postType', 'queryId' ]; - const usesContextArray = settings.usesContext; - const oldUsesContextArray = new Set( usesContextArray ); - contextItems.forEach( ( item ) => { - if ( ! oldUsesContextArray.has( item ) ) { - usesContextArray.push( item ); - } - } ); - settings.usesContext = usesContextArray; - // Add bindings button to the block toolbar. const OriginalComponent = settings.edit; settings.edit = ( props ) => { diff --git a/packages/editor/src/hooks/block-bindings-sources/post-meta.js b/packages/editor/src/hooks/block-bindings-sources/post-meta.js index 90bf447f162fa..c9458ae6f7e0c 100644 --- a/packages/editor/src/hooks/block-bindings-sources/post-meta.js +++ b/packages/editor/src/hooks/block-bindings-sources/post-meta.js @@ -10,68 +10,77 @@ import { addFilter } from '@wordpress/hooks'; */ import BlockBindingsFill from '../../components/block-bindings/bindings-ui'; import BlockBindingsFieldsList from '../../components/block-bindings/fields-list'; - -const PostMeta = ( props ) => { - const { context } = props; - - // Fetching the REST API to get the available custom fields. - // TODO: Explore how it should work in templates. - // TODO: Explore if it makes sense to create a custom endpoint for this. - const data = useSelect( - ( select ) => { - const { getEntityRecord } = select( coreStore ); - return getEntityRecord( - 'postType', - context.postType, - context.postId - ); - }, - [ context.postType, context.postId ] - ); - - // Adapt the data to the format expected by the fields list. - const fields = []; - // Prettifying the name until we receive the label from the REST API endpoint. - const keyToLabel = ( key ) => { - return key - .split( '_' ) - .map( ( word ) => word.charAt( 0 ).toUpperCase() + word.slice( 1 ) ) - .join( ' ' ); - }; - Object.entries( data.meta ).forEach( ( [ key, value ] ) => { - fields.push( { - key, - label: keyToLabel( key ), - value, - } ); - } ); - - return ( - - ); -}; +import { store as editorStore } from '../../store'; if ( window.__experimentalBlockBindings ) { - // TODO: Read the context somehow to decide if we should add the source. - // const data = useSelect( editorStore ); - // External sources could do something similar. + const withCoreSources = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { - const { isSelected } = props; + const blockBindingsAllowedBlocks = { + 'core/paragraph': [ 'content' ], + 'core/heading': [ 'content' ], + 'core/image': [ 'url', 'title' ], + 'core/button': [ 'url', 'text' ], + }; + const { name, isSelected, context } = props; + // If the block is not allowed, return the original BlockEdit. + if ( ! blockBindingsAllowedBlocks[ name ] ) { + return ; + } + const fields = []; + if ( isSelected ) { + const data = useSelect( ( select ) => { + const postId = context.postId + ? context.postId + : select( editorStore ).getCurrentPostId(); + const postType = context.postType + ? context.postType + : select( editorStore ).getCurrentPostType(); + // If not a post type, return null. + if ( postType !== 'post' && postType !== 'page' ) { + return null; + } + const { getEntityRecord } = select( coreStore ); + return getEntityRecord( 'postType', postType, postId ); + }, [] ); + + if ( data ) { + // Adapt the data to the format expected by the fields list. + // Prettifying the name until we receive the label from the REST API endpoint. + const keyToLabel = ( key ) => { + return key + .split( '_' ) + .map( + ( word ) => + word.charAt( 0 ).toUpperCase() + + word.slice( 1 ) + ) + .join( ' ' ); + }; + Object.entries( data.meta ).forEach( ( [ key, value ] ) => { + fields.push( { + key, + label: keyToLabel( key ), + value, + } ); + } ); + } + } + return ( <> - { isSelected && ( + { isSelected && fields.length !== 0 && ( <> - + ) } From 42288ab5a014e03d3be0124107166bf56fe03949 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 17:26:15 +0100 Subject: [PATCH 16/20] Create global `BLOCK_BINDINGS_ALLOWED_BLOCKS` constant --- .../components/block-bindings/bindings-ui.js | 215 +++++++++--------- .../hooks/block-bindings-sources/post-meta.js | 9 +- packages/editor/src/store/constants.js | 6 + 3 files changed, 118 insertions(+), 112 deletions(-) diff --git a/packages/editor/src/components/block-bindings/bindings-ui.js b/packages/editor/src/components/block-bindings/bindings-ui.js index 0e1ce6fc87e5f..b7677a1d4a8dd 100644 --- a/packages/editor/src/components/block-bindings/bindings-ui.js +++ b/packages/editor/src/components/block-bindings/bindings-ui.js @@ -19,13 +19,10 @@ import { chevronUp, } from '@wordpress/icons'; import { addFilter } from '@wordpress/hooks'; - -const blockBindingsAllowedBlocks = { - 'core/paragraph': [ 'content' ], - 'core/heading': [ 'content' ], - 'core/image': [ 'url', 'title' ], - 'core/button': [ 'url', 'text' ], -}; +/** + * Internal dependencies + */ +import { BLOCK_BINDINGS_ALLOWED_BLOCKS } from '../../store/constants'; const { Slot, Fill } = createSlotFill( 'BlockBindingsUI' ); @@ -93,108 +90,116 @@ function AttributesLayer( props ) { const [ activeSource, setIsActiveSource ] = useState( false ); return ( - { blockBindingsAllowedBlocks[ props.name ].map( ( attribute ) => ( -
- - setIsActiveAttribute( - activeAttribute === attribute - ? false - : attribute - ) - } - className="block-bindings-attribute-picker-button" + { BLOCK_BINDINGS_ALLOWED_BLOCKS[ props.name ].map( + ( attribute ) => ( +
- { attribute } - - { activeAttribute === attribute && ( - <> - - { /* Sources can fill this slot */ } - - { ( fills ) => { - if ( ! fills.length ) { - return null; - } + + setIsActiveAttribute( + activeAttribute === attribute + ? false + : attribute + ) + } + className="block-bindings-attribute-picker-button" + > + { attribute } + + { activeAttribute === attribute && ( + <> + + { /* Sources can fill this slot */ } + + { ( fills ) => { + if ( ! fills.length ) { + return null; + } - return ( - <> - { fills.map( - ( fill, index ) => { - // TODO: Check better way to get the source and label. - const source = - fill[ 0 ].props - .children.props - .source; - const sourceLabel = - fill[ 0 ].props - .children.props - .label; - const isSourceSelected = - activeSource === - source; + return ( + <> + { fills.map( + ( fill, index ) => { + // TODO: Check better way to get the source and label. + const source = + fill[ 0 ].props + .children + .props + .source; + const sourceLabel = + fill[ 0 ].props + .children + .props + .label; + const isSourceSelected = + activeSource === + source; - return ( - - - setIsActiveSource( - isSourceSelected - ? false - : source - ) - } - className="block-bindings-source-picker-button" > - { - sourceLabel - } - - { isSourceSelected && - fill } - - ); - } - ) } - - ); - } } - - - - - ) } -
- ) ) } + + setIsActiveSource( + isSourceSelected + ? false + : source + ) + } + className="block-bindings-source-picker-button" + > + { + sourceLabel + } + + { isSourceSelected && + fill } + + ); + } + ) } + + ); + } } + + + + + ) } +
+ ) + ) }
); } @@ -239,7 +244,7 @@ if ( window.__experimentalBlockBindings ) { 'blocks.registerBlockType', 'core/block-bindings-ui', ( settings, name ) => { - if ( ! ( name in blockBindingsAllowedBlocks ) ) { + if ( ! ( name in BLOCK_BINDINGS_ALLOWED_BLOCKS ) ) { return settings; } diff --git a/packages/editor/src/hooks/block-bindings-sources/post-meta.js b/packages/editor/src/hooks/block-bindings-sources/post-meta.js index c9458ae6f7e0c..bbc1941e03b59 100644 --- a/packages/editor/src/hooks/block-bindings-sources/post-meta.js +++ b/packages/editor/src/hooks/block-bindings-sources/post-meta.js @@ -11,21 +11,16 @@ import { addFilter } from '@wordpress/hooks'; import BlockBindingsFill from '../../components/block-bindings/bindings-ui'; import BlockBindingsFieldsList from '../../components/block-bindings/fields-list'; import { store as editorStore } from '../../store'; +import { BLOCK_BINDINGS_ALLOWED_BLOCKS } from '../../store/constants'; if ( window.__experimentalBlockBindings ) { // External sources could do something similar. const withCoreSources = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { - const blockBindingsAllowedBlocks = { - 'core/paragraph': [ 'content' ], - 'core/heading': [ 'content' ], - 'core/image': [ 'url', 'title' ], - 'core/button': [ 'url', 'text' ], - }; const { name, isSelected, context } = props; // If the block is not allowed, return the original BlockEdit. - if ( ! blockBindingsAllowedBlocks[ name ] ) { + if ( ! BLOCK_BINDINGS_ALLOWED_BLOCKS[ name ] ) { return ; } const fields = []; diff --git a/packages/editor/src/store/constants.js b/packages/editor/src/store/constants.js index 7882ba53e64db..c8d1c71f56abe 100644 --- a/packages/editor/src/store/constants.js +++ b/packages/editor/src/store/constants.js @@ -18,3 +18,9 @@ export const TRASH_POST_NOTICE_ID = 'TRASH_POST_NOTICE_ID'; export const PERMALINK_POSTNAME_REGEX = /%(?:postname|pagename)%/; export const ONE_MINUTE_IN_MS = 60 * 1000; export const AUTOSAVE_PROPERTIES = [ 'title', 'excerpt', 'content' ]; +export const BLOCK_BINDINGS_ALLOWED_BLOCKS = { + 'core/paragraph': [ 'content' ], + 'core/heading': [ 'content' ], + 'core/image': [ 'url', 'title' ], + 'core/button': [ 'url', 'text' ], +}; From 201db326147f350fda3755743ace77146e2fc005 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 21 Dec 2023 08:57:18 +0100 Subject: [PATCH 17/20] Add context back until we find a proper way --- .../components/block-bindings/bindings-ui.js | 12 ++++ .../hooks/block-bindings-sources/post-meta.js | 71 ++++++++++--------- 2 files changed, 49 insertions(+), 34 deletions(-) diff --git a/packages/editor/src/components/block-bindings/bindings-ui.js b/packages/editor/src/components/block-bindings/bindings-ui.js index b7677a1d4a8dd..bd6ecf6cd3b5b 100644 --- a/packages/editor/src/components/block-bindings/bindings-ui.js +++ b/packages/editor/src/components/block-bindings/bindings-ui.js @@ -248,6 +248,18 @@ if ( window.__experimentalBlockBindings ) { return settings; } + // TODO: Review the implications of this and the code. + // Add the necessary context to the block. + const contextItems = [ 'postId', 'postType', 'queryId' ]; + const usesContextArray = settings.usesContext; + const oldUsesContextArray = new Set( usesContextArray ); + contextItems.forEach( ( item ) => { + if ( ! oldUsesContextArray.has( item ) ) { + usesContextArray.push( item ); + } + } ); + settings.usesContext = usesContextArray; + // Add bindings button to the block toolbar. const OriginalComponent = settings.edit; settings.edit = ( props ) => { diff --git a/packages/editor/src/hooks/block-bindings-sources/post-meta.js b/packages/editor/src/hooks/block-bindings-sources/post-meta.js index bbc1941e03b59..c01ea97b1604b 100644 --- a/packages/editor/src/hooks/block-bindings-sources/post-meta.js +++ b/packages/editor/src/hooks/block-bindings-sources/post-meta.js @@ -25,42 +25,43 @@ if ( window.__experimentalBlockBindings ) { } const fields = []; if ( isSelected ) { - const data = useSelect( ( select ) => { - const postId = context.postId - ? context.postId - : select( editorStore ).getCurrentPostId(); - const postType = context.postType - ? context.postType - : select( editorStore ).getCurrentPostType(); - // If not a post type, return null. - if ( postType !== 'post' && postType !== 'page' ) { - return null; - } - const { getEntityRecord } = select( coreStore ); - return getEntityRecord( 'postType', postType, postId ); - }, [] ); + const data = useSelect( + ( select ) => { + const postId = context.postId + ? context.postId + : select( editorStore ).getCurrentPostId(); + const postType = context.postType + ? context.postType + : select( editorStore ).getCurrentPostType(); + const { getEntityRecord } = select( coreStore ); + return getEntityRecord( 'postType', postType, postId ); + }, + [ context.postId, context.postType ] + ); - if ( data ) { - // Adapt the data to the format expected by the fields list. - // Prettifying the name until we receive the label from the REST API endpoint. - const keyToLabel = ( key ) => { - return key - .split( '_' ) - .map( - ( word ) => - word.charAt( 0 ).toUpperCase() + - word.slice( 1 ) - ) - .join( ' ' ); - }; - Object.entries( data.meta ).forEach( ( [ key, value ] ) => { - fields.push( { - key, - label: keyToLabel( key ), - value, - } ); - } ); + // TODO: Explore how to get the list of available fields depending on the template. + if ( ! data || ! data.meta ) { + return ; } + + // Adapt the data to the format expected by the fields list. + // Prettifying the name until we receive the label from the REST API endpoint. + const keyToLabel = ( key ) => { + return key + .split( '_' ) + .map( + ( word ) => + word.charAt( 0 ).toUpperCase() + word.slice( 1 ) + ) + .join( ' ' ); + }; + Object.entries( data.meta ).forEach( ( [ key, value ] ) => { + fields.push( { + key, + label: keyToLabel( key ), + value, + } ); + } ); } return ( @@ -86,6 +87,8 @@ if ( window.__experimentalBlockBindings ) { 'withToolbarControls' ); + // TODO: Review if there is a better filter for this. + // This runs for every block. addFilter( 'editor.BlockEdit', 'core/block-bindings-ui/add-sources', From 987ee65304e3fc7669124468715cb953c435423f Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 21 Dec 2023 16:56:13 +0100 Subject: [PATCH 18/20] Add placeholder field --- .../editor/src/components/block-bindings/bindings-ui.js | 1 + .../editor/src/components/block-bindings/fields-list.js | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/editor/src/components/block-bindings/bindings-ui.js b/packages/editor/src/components/block-bindings/bindings-ui.js index bd6ecf6cd3b5b..7f384bfc7c7ca 100644 --- a/packages/editor/src/components/block-bindings/bindings-ui.js +++ b/packages/editor/src/components/block-bindings/bindings-ui.js @@ -222,6 +222,7 @@ function RemoveBindingButton( props ) { // Modify the attribute we are binding. const newAttributes = {}; newAttributes[ currentAttribute ] = ''; + newAttributes.placeholder = null; props.setAttributes( newAttributes ); updateBlockBindingsAttribute( diff --git a/packages/editor/src/components/block-bindings/fields-list.js b/packages/editor/src/components/block-bindings/fields-list.js index ba28797940b19..007abfed0d2d9 100644 --- a/packages/editor/src/components/block-bindings/fields-list.js +++ b/packages/editor/src/components/block-bindings/fields-list.js @@ -18,10 +18,11 @@ export default function BlockBindingsFieldsList( props ) { // TODO: Try to abstract this function to be reused across all the sources. function selectItem( item ) { // Modify the attribute we are binding. - // TODO: Not sure if we should do this. We might need to process the bindings attribute somehow in the editor to modify the content with context. - // TODO: Get the type from the block attribute definition and modify/validate the value returned by the source if needed. + // TODO: Inherit the value from the bindings instead of setting it manually. const newAttributes = {}; - newAttributes[ currentAttribute ] = item.value; + newAttributes[ currentAttribute ] = item.value ? item.value : ''; + // TODO: Improve the way we manage placeholders. + newAttributes.placeholder = '{ ' + item.placeholder + ' }'; setAttributes( newAttributes ); // Update the bindings property. From 5321a043b4678559951c39bac269010a2076d6b5 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 21 Dec 2023 16:56:50 +0100 Subject: [PATCH 19/20] Support templates in "post meta" source --- .../hooks/block-bindings-sources/post-meta.js | 134 ++++++++++++++++-- 1 file changed, 123 insertions(+), 11 deletions(-) diff --git a/packages/editor/src/hooks/block-bindings-sources/post-meta.js b/packages/editor/src/hooks/block-bindings-sources/post-meta.js index c01ea97b1604b..2ebbbfe5bd076 100644 --- a/packages/editor/src/hooks/block-bindings-sources/post-meta.js +++ b/packages/editor/src/hooks/block-bindings-sources/post-meta.js @@ -33,14 +33,123 @@ if ( window.__experimentalBlockBindings ) { const postType = context.postType ? context.postType : select( editorStore ).getCurrentPostType(); - const { getEntityRecord } = select( coreStore ); - return getEntityRecord( 'postType', postType, postId ); + const { getEntityRecord, getEntityRecords } = + select( coreStore ); + + let isTemplate = false; + // If it is a template, get example data if it is a post/page/cpt. + if ( postType === 'wp_template' ) { + isTemplate = true; + const { slug: templateSlug } = getEntityRecord( + 'postType', + 'wp_template', + postId + ); + + // Get the post type from the template slug. + + // Match "page-{slug}". + const pagePattern = /^page(?:-(.+))?$/; + // Match "single-{postType}-{slug}". + const postPattern = /^single-([^-]+)(?:-(.+))?$/; + // Match "wp-custom-template-{slug}". + const customTemplatePattern = + /^wp-custom-template-(.+)$/; + // If it doesn't match any of the accepted patterns, return. + if ( + ! templateSlug !== 'index' && + ! templateSlug !== 'page' && + ! pagePattern.test( templateSlug ) && + ! postPattern.test( templateSlug ) && + ! customTemplatePattern.test( templateSlug ) + ) { + return null; + } + + let records = []; + // If it is an index or a generic page template, return any page. + if ( + templateSlug === 'index' || + templateSlug === 'page' + ) { + records = getEntityRecords( + 'postType', + 'page', + { + per_page: 1, + } + ); + } + + // If it is specific page template, return that one. + if ( pagePattern.test( templateSlug ) ) { + records = getEntityRecords( + 'postType', + 'page', + { + slug: templateSlug.match( + pagePattern + )[ 1 ], + } + ); + } + + // If it is post/cpt template. + if ( postPattern.test( templateSlug ) ) { + const [ , entityPostType, entitySlug ] = + templateSlug.match( postPattern ); + + // If it is a specific post. + if ( entitySlug ) { + records = getEntityRecords( + 'postType', + entityPostType, + { + slug: entitySlug, + } + ); + } else { + // If it is a generic template, return any post. + records = getEntityRecords( + 'postType', + entityPostType, + { + per_page: 1, + } + ); + } + } + + // If it is a custom template, get the fields from any page. + if ( + customTemplatePattern.test( templateSlug ) || + ! records + ) { + records = getEntityRecords( + 'postType', + 'page', + { + per_page: 1, + } + ); + } + + return { isTemplate, fields: records?.[ 0 ] }; + } + + return { + isTemplate, + fields: getEntityRecord( + 'postType', + postType, + postId + ), + }; }, [ context.postId, context.postType ] ); - // TODO: Explore how to get the list of available fields depending on the template. - if ( ! data || ! data.meta ) { + if ( ! data || ! data?.fields?.meta ) { return ; } @@ -55,13 +164,16 @@ if ( window.__experimentalBlockBindings ) { ) .join( ' ' ); }; - Object.entries( data.meta ).forEach( ( [ key, value ] ) => { - fields.push( { - key, - label: keyToLabel( key ), - value, - } ); - } ); + Object.entries( data.fields.meta ).forEach( + ( [ key, value ] ) => { + fields.push( { + key, + label: keyToLabel( key ), + value: data.isTemplate ? null : value, + placeholder: keyToLabel( key ), + } ); + } + ); } return ( From d3667090626d6eb8b28e46f1dd763e2324492582 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Fri, 22 Dec 2023 09:13:21 +0100 Subject: [PATCH 20/20] Don't add placeholder if value exists --- packages/editor/src/components/block-bindings/fields-list.js | 3 ++- packages/editor/src/hooks/block-bindings-sources/post-meta.js | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/components/block-bindings/fields-list.js b/packages/editor/src/components/block-bindings/fields-list.js index 007abfed0d2d9..3c83cdb825fb1 100644 --- a/packages/editor/src/components/block-bindings/fields-list.js +++ b/packages/editor/src/components/block-bindings/fields-list.js @@ -22,7 +22,8 @@ export default function BlockBindingsFieldsList( props ) { const newAttributes = {}; newAttributes[ currentAttribute ] = item.value ? item.value : ''; // TODO: Improve the way we manage placeholders. - newAttributes.placeholder = '{ ' + item.placeholder + ' }'; + if ( item.placeholder && ! item.value ) + newAttributes.placeholder = '{ ' + item.placeholder + ' }'; setAttributes( newAttributes ); // Update the bindings property. diff --git a/packages/editor/src/hooks/block-bindings-sources/post-meta.js b/packages/editor/src/hooks/block-bindings-sources/post-meta.js index 2ebbbfe5bd076..bfd76262d0696 100644 --- a/packages/editor/src/hooks/block-bindings-sources/post-meta.js +++ b/packages/editor/src/hooks/block-bindings-sources/post-meta.js @@ -170,7 +170,9 @@ if ( window.__experimentalBlockBindings ) { key, label: keyToLabel( key ), value: data.isTemplate ? null : value, - placeholder: keyToLabel( key ), + placeholder: data.isTemplate + ? keyToLabel( key ) + : null, } ); } );