From 24d575b1accaf972b2e4bfe2020e159fd0615546 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 07:53:37 +0100 Subject: [PATCH 01/17] 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 a783135c793e3f..f9dd1b4b44248a 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 88e46b478389d2..a85cf98937ee2b 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 5f61684e8b1342..729376cf030dd9 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 b77a69b692ff1f..c407e106a34173 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 9b677933adc138..5dc82473ef40eb 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 385b9fe6b1511e..fa5ab255f8b6e2 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 fbfef0b4cf1778..de3921084af866 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 3fe4fbb34e1029..96ec9c778c0960 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 40bd1e16dfc00d..976efebb720f6e 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/17] 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 5dc82473ef40eb..00000000000000 --- 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 fa5ab255f8b6e2..f17c0a22166e4e 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 96ec9c778c0960..a81d754d8ca1be 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/17] 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 00000000000000..9fb629d5e28218 --- /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 00000000000000..cca857e93702f3 --- /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 bf89ba177b6e94..00000000000000 --- 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/17] 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 00000000000000..4948e1d8fdd032 --- /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/17] 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 00000000000000..3220b3c6defb2f --- /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/17] 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 232829be487ba4..ebc65f9dfbe89c 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 de3921084af866..c8290e5e30090f 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 a81d754d8ca1be..70f43d3793bcaa 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 42c39ce69e87bf..10f812d29fd213 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/17] 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 4948e1d8fdd032..c2549b60489282 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 3220b3c6defb2f..d8b091d97aade1 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/17] 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 d8b091d97aade1..9e2bf22aa8b734 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/17] 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 5917ac235505cb..d0870237419533 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 ee3b2692b369a8..21f33acfc274db 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 00000000000000..c34aa8b46c6e36 --- /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/17] 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 10f812d29fd213..700a05b10c896c 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/17] 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 00000000000000..3827deae790c42 --- /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 00000000000000..9d23f55b601a95 --- /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 00000000000000..738c7479884809 --- /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 33a18e6f9a6ad2..94cf5c50169d03 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 50359984af1628..4bec3804bb1e87 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/17] 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 00000000000000..ba28797940b19e --- /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 9d23f55b601a95..600523bc9bbf75 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/17] 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 63656899e587c0..e138089b2b9169 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 00000000000000..90bf447f162fa0 --- /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 5a48ec1bf49566..8d52e5a36e6631 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 b761a5d8392b883183a846e6b184e57d1cdcaabf Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 13:30:11 +0100 Subject: [PATCH 14/17] Add "post data" source PHP support --- lib/experimental/block-bindings/index.php | 1 + .../block-bindings/sources/post-data.php | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 lib/experimental/block-bindings/sources/post-data.php diff --git a/lib/experimental/block-bindings/index.php b/lib/experimental/block-bindings/index.php index cca857e93702f3..fa0060c0fb5d8e 100644 --- a/lib/experimental/block-bindings/index.php +++ b/lib/experimental/block-bindings/index.php @@ -16,5 +16,6 @@ } if ( array_key_exists( 'gutenberg-block-bindings', $gutenberg_experiments ) ) { require_once __DIR__ . '/sources/post-meta.php'; + require_once __DIR__ . '/sources/post-data.php'; } } diff --git a/lib/experimental/block-bindings/sources/post-data.php b/lib/experimental/block-bindings/sources/post-data.php new file mode 100644 index 00000000000000..f4f565781efc32 --- /dev/null +++ b/lib/experimental/block-bindings/sources/post-data.php @@ -0,0 +1,26 @@ +context['postId'] but it wasn't available in the image block. + $post_id = get_the_ID(); + } + return get_post( $post_id )->{$source_attrs['value']}; + }; + register_block_bindings_source( + 'post_data', + array( + 'label' => __( 'Post Data' ), + 'apply' => $post_data_source_callback, + ) + ); +} From 5db13ade997fbfbf2daaf0564166029726db15fe Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 13:30:40 +0100 Subject: [PATCH 15/17] Add "post data" source to block bindings UI --- .../hooks/block-bindings-sources/post-data.js | 91 +++++++++++++++++++ packages/editor/src/hooks/index.js | 1 + 2 files changed, 92 insertions(+) create mode 100644 packages/editor/src/hooks/block-bindings-sources/post-data.js diff --git a/packages/editor/src/hooks/block-bindings-sources/post-data.js b/packages/editor/src/hooks/block-bindings-sources/post-data.js new file mode 100644 index 00000000000000..a42379416ff2dd --- /dev/null +++ b/packages/editor/src/hooks/block-bindings-sources/post-data.js @@ -0,0 +1,91 @@ +/** + * 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 PostData = ( props ) => { + const { context } = props; + + // Fetching the REST API to get the post data. + // 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. + // TODO: Ensure the key and label work with translations. + const fields = [ + { + key: 'post_title', + label: 'Post title', + value: data.title.rendered, + }, + { + key: 'post_date', + label: 'Post date', + value: data.date, + }, + { + key: 'guid', + label: 'Post link', + value: data.link, + }, + ]; + + return ( + + ); +}; + +if ( window.__experimentalBlockBindings ) { + // TODO: Read the context somehow to decide if we should add the source. + + 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 8d52e5a36e6631..acaaff3831a421 100644 --- a/packages/editor/src/hooks/index.js +++ b/packages/editor/src/hooks/index.js @@ -7,3 +7,4 @@ import './pattern-partial-syncing'; // Block bindings sources. import './block-bindings-sources/post-meta'; +import './block-bindings-sources/post-data'; From 845680efcc679e58181928210d1e6eb1a88958ef Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 13:35:05 +0100 Subject: [PATCH 16/17] Remove unused attributes --- lib/experimental/block-bindings/sources/post-data.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/experimental/block-bindings/sources/post-data.php b/lib/experimental/block-bindings/sources/post-data.php index f4f565781efc32..d20fdbd5653ed7 100644 --- a/lib/experimental/block-bindings/sources/post-data.php +++ b/lib/experimental/block-bindings/sources/post-data.php @@ -6,7 +6,7 @@ */ if ( function_exists( 'register_block_bindings_source' ) ) { - $post_data_source_callback = function ( $source_attrs, $block_content, $block, $block_instance ) { + $post_data_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 a6bd5edfae134186ad4dbe1145542b881f21c6a2 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 13:38:52 +0100 Subject: [PATCH 17/17] Add translation namespace --- lib/experimental/block-bindings/sources/post-data.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/experimental/block-bindings/sources/post-data.php b/lib/experimental/block-bindings/sources/post-data.php index d20fdbd5653ed7..5652a242703904 100644 --- a/lib/experimental/block-bindings/sources/post-data.php +++ b/lib/experimental/block-bindings/sources/post-data.php @@ -19,7 +19,7 @@ register_block_bindings_source( 'post_data', array( - 'label' => __( 'Post Data' ), + 'label' => __( 'Post Data', 'gutenberg' ), 'apply' => $post_data_source_callback, ) );