From 4100933a491185036880a002166a94dd5c22d0c0 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 29 Nov 2023 20:34:14 +0100 Subject: [PATCH 01/30] Add the basis of the Block Bindings API --- .../block-bindings-api/html-processing.php | 67 ++++++++ lib/experimental/block-bindings-api/index.php | 10 ++ .../block-bindings-api/sources/index.php | 18 +++ lib/experimental/blocks.php | 149 +++++++----------- 4 files changed, 149 insertions(+), 95 deletions(-) create mode 100644 lib/experimental/block-bindings-api/html-processing.php create mode 100644 lib/experimental/block-bindings-api/index.php create mode 100644 lib/experimental/block-bindings-api/sources/index.php diff --git a/lib/experimental/block-bindings-api/html-processing.php b/lib/experimental/block-bindings-api/html-processing.php new file mode 100644 index 0000000000000..e0ebf5b27d3b9 --- /dev/null +++ b/lib/experimental/block-bindings-api/html-processing.php @@ -0,0 +1,67 @@ +get_registered( $block_name ); + if ( null === $block_type ) { + return; + } + + // Depending on the attribute source, the processing will be different. + switch ( $block_type->attributes[ $block_attr ]['source'] ) { + case 'html': + $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; + } + // TODO: We should use a `set_inner_html` method once available. + $tag_name = $p->get_tag(); + $markup = "<$tag_name>" . esc_html( $source_value ) . ""; + $p2 = new WP_HTML_Tag_Processor( $markup ); + $p2->next_tag(); + $names = $p->get_attribute_names_with_prefix( '' ); + foreach ( $names as $name ) { + $p2->set_attribute( $name, $p->get_attribute( $name ) ); + } + return $p2->get_updated_html(); + break; + + 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-api/index.php b/lib/experimental/block-bindings-api/index.php new file mode 100644 index 0000000000000..58f1d81537283 --- /dev/null +++ b/lib/experimental/block-bindings-api/index.php @@ -0,0 +1,10 @@ + $source_callback ); + } +} diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index f9f2412ae5120..3e31a93c6e30b 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -78,108 +78,67 @@ 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 ) ) { - /** - * Renders the block meta attributes. - * - * @param string $block_content Block Content. - * @param array $block Block attributes. - * @param WP_Block $block_instance The block instance. - */ - function gutenberg_render_block_connections( $block_content, $block, $block_instance ) { - $connection_sources = require __DIR__ . '/connection-sources/index.php'; - $block_type = $block_instance->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( '__experimentalConnections' ), 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; - } - - // If the source value is not "meta_fields", skip it because the only supported - // connection source is meta (custom fields) for now. - if ( 'meta_fields' !== $attribute_value['source'] ) { - continue; - } - - // If the attribute does not have a source, skip it. - if ( ! isset( $block_type->attributes[ $attribute_name ]['source'] ) ) { - continue; - } - - // If the attribute does not specify the name of the custom field, skip it. - if ( ! isset( $attribute_value['value'] ) ) { - continue; - } - - // Get the content from the connection source. - $custom_value = $connection_sources[ $attribute_value['source'] ]( - $block_instance, - $attribute_value['value'] - ); - - $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 ) { + require_once __DIR__ . '/block-bindings-api/index.php'; + // Whitelist of blocks that support block bindings. + // We should 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/image' => array( 'url', 'title' ), + ); + 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 attribute, return. + if ( ! isset( $block['attrs']['bindings'] ) ) { return $block_content; } - $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 ) ); + // Assuming the following format for the bindings attribute: + // + // "bindings": [ + // { + // "attribute": "title", + // "source": { "name": "metadata", "params": "custom_field_1" } + // }, + // { + // "attribute": "url", + // "source": { "name": "metadata", "params": "custom_field_2" } + // }, + // ] + // . + global $block_bindings_whitelist; + global $block_bindings_sources; + $modified_block_content = $block_content; + foreach ( $block['attrs']['bindings'] as $binding ) { + if ( ! isset( $block_bindings_whitelist[ $block['blockName'] ] ) ) { + continue; + } + if ( ! in_array( $binding['attribute'], $block_bindings_whitelist[ $block['blockName'] ], true ) ) { + continue; + } + // Get the value based on the source. + // We might want to move this to its own function if it gets more complex. + // We pass $block_content, $block, $block_instance to the source callback in case sources want to use them. + $source_value = $block_bindings_sources[ $binding['source']['name'] ]['apply_source']( $binding['source']['params'], $block_content, $block, $block_instance ); + + // 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 ); } - - 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 ); } From edee28acf70b406636c0c29db7adf58349ae1785 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 29 Nov 2023 20:34:28 +0100 Subject: [PATCH 02/30] Add the first PHP logic of the `metadata` source --- .../block-bindings-api/sources/metadata.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 lib/experimental/block-bindings-api/sources/metadata.php diff --git a/lib/experimental/block-bindings-api/sources/metadata.php b/lib/experimental/block-bindings-api/sources/metadata.php new file mode 100644 index 0000000000000..d5ba31d267d05 --- /dev/null +++ b/lib/experimental/block-bindings-api/sources/metadata.php @@ -0,0 +1,27 @@ +context['postId'] but it wasn't available in the image block. + $post_id = get_the_ID(); + } + + // TODO: Add logic to handle other meta types. + if ( isset( $source_attrs['metaType'] ) ) { + $meta_type = $source_attrs['metaType']; + } else { + $meta_type = 'post'; + } + return get_metadata( $meta_type, $post_id, $source_attrs['value'], true ); + }; + register_block_bindings_source( 'metadata', $metadata_source_callback ); +} From 01189c709f0e568eb062f0b7eddcc33158610805 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 Nov 2023 09:05:57 +0100 Subject: [PATCH 03/30] Update metadata folder structure --- lib/experimental/block-bindings-api/index.php | 2 +- lib/experimental/block-bindings-api/sources/index.php | 7 ++++++- .../sources/{metadata.php => metadata/index.php} | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) rename lib/experimental/block-bindings-api/sources/{metadata.php => metadata/index.php} (92%) diff --git a/lib/experimental/block-bindings-api/index.php b/lib/experimental/block-bindings-api/index.php index 58f1d81537283..6d3c86e5f8ae7 100644 --- a/lib/experimental/block-bindings-api/index.php +++ b/lib/experimental/block-bindings-api/index.php @@ -6,5 +6,5 @@ */ require_once __DIR__ . '/sources/index.php'; -require_once __DIR__ . '/sources/metadata.php'; +require_once __DIR__ . '/sources/metadata/index.php'; require_once __DIR__ . '/html-processing.php'; diff --git a/lib/experimental/block-bindings-api/sources/index.php b/lib/experimental/block-bindings-api/sources/index.php index 1c2b707c5e653..56470e6f4e9dc 100644 --- a/lib/experimental/block-bindings-api/sources/index.php +++ b/lib/experimental/block-bindings-api/sources/index.php @@ -8,7 +8,12 @@ global $block_bindings_sources; $block_bindings_sources = array(); if ( ! function_exists( 'register_block_bindings_source' ) ) { - // Function to register a new source. + /** + * Function to register a new source. + * + * @param string $source_name The name of the source. + * @param function $source_callback The callback executed when the source is processed in the server. + */ function register_block_bindings_source( $source_name, $source_callback ) { // We might want to add some validation here, for the name and for the apply_source callback. // To ensure the register sources are valid. diff --git a/lib/experimental/block-bindings-api/sources/metadata.php b/lib/experimental/block-bindings-api/sources/metadata/index.php similarity index 92% rename from lib/experimental/block-bindings-api/sources/metadata.php rename to lib/experimental/block-bindings-api/sources/metadata/index.php index d5ba31d267d05..9a0d0b41e6fba 100644 --- a/lib/experimental/block-bindings-api/sources/metadata.php +++ b/lib/experimental/block-bindings-api/sources/metadata/index.php @@ -21,6 +21,8 @@ } else { $meta_type = 'post'; } + + // TODO: Add a filter/mechanism to limit the meta keys that can be used. return get_metadata( $meta_type, $post_id, $source_attrs['value'], true ); }; register_block_bindings_source( 'metadata', $metadata_source_callback ); From 5e38246f4160f70b0bbfa97a011eba778a90688d Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 Nov 2023 12:51:48 +0100 Subject: [PATCH 04/30] Add initial version of the block bindings editor UI --- packages/block-library/src/image/block.json | 3 + .../block-library/src/paragraph/block.json | 3 + .../src/components/block-bindings/index.js | 97 +++++++++++++++ .../src/components/block-bindings/metadata.js | 117 ++++++++++++++++++ .../src/components/block-bindings/style.scss | 26 ++++ packages/editor/src/components/index.js | 4 + packages/editor/src/style.scss | 1 + 7 files changed, 251 insertions(+) create mode 100644 packages/editor/src/components/block-bindings/index.js create mode 100644 packages/editor/src/components/block-bindings/metadata.js create mode 100644 packages/editor/src/components/block-bindings/style.scss diff --git a/packages/block-library/src/image/block.json b/packages/block-library/src/image/block.json index b46829e5059a2..2aaad383d04be 100644 --- a/packages/block-library/src/image/block.json +++ b/packages/block-library/src/image/block.json @@ -24,6 +24,9 @@ "default": "", "__experimentalRole": "content" }, + "bindings": { + "type": "object" + }, "caption": { "type": "string", "source": "html", diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json index 85f56f4a838f5..bbe04fa23186d 100644 --- a/packages/block-library/src/paragraph/block.json +++ b/packages/block-library/src/paragraph/block.json @@ -12,6 +12,9 @@ "align": { "type": "string" }, + "bindings": { + "type": "object" + }, "content": { "type": "string", "source": "html", 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..5138419e10421 --- /dev/null +++ b/packages/editor/src/components/block-bindings/index.js @@ -0,0 +1,97 @@ +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; +import { BlockControls } from '@wordpress/block-editor'; +import { Button, Popover } from '@wordpress/components'; +import { plugins as pluginsIcon } from '@wordpress/icons'; +import { addFilter } from '@wordpress/hooks'; +/** + * Internal dependencies + */ +import MetadataSourceUI from './metadata.js'; + +const blockBindingsWhitelist = { + 'core/paragraph': [ 'content' ], + 'core/image': [ 'url', 'title' ], +}; + +export default function BlockBindingsButton( BlockEdit ) { + return ( props ) => { + // Only add the Block Bindings button to the blocks in the whitelist. + if ( ! ( props.name in blockBindingsWhitelist ) ) { + return ; + } + const { setAttributes } = props; + + const [ addingBinding, setAddingBinding ] = useState( false ); + function BindingsUI() { + return ( + { + setAddingBinding( false ); + } } + onFocusOutside={ () => { + setAddingBinding( false ); + } } + placement="bottom" + shift + className="block-bindings-ui-popover" + { ...props } + > + { /* TODO: Add logic to select the attribute to bind */ } + + { /* TODO: This component could potentially be defined by each source. */ } + + + { /* + TODO: Add a better way to "clear" the binding. + We don't have to remove the whole bindings attribute but just the one we are binding. + We can explore if we can get back to the previous content or keep the value of the custom field. + */ } + + + ); + } + + const [ popoverAnchor, setPopoverAnchor ] = useState(); + return ( + <> + + + + { addingBinding && } + + + ); + }; +} + +addFilter( 'editor.BlockEdit', 'core', BlockBindingsButton ); + +// TODO: Add also some components to the sidebar. diff --git a/packages/editor/src/components/block-bindings/metadata.js b/packages/editor/src/components/block-bindings/metadata.js new file mode 100644 index 0000000000000..d5828fae96486 --- /dev/null +++ b/packages/editor/src/components/block-bindings/metadata.js @@ -0,0 +1,117 @@ +/** + * WordPress dependencies + */ +import { useState, useEffect } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { SearchControl } from '@wordpress/components'; +import apiFetch from '@wordpress/api-fetch'; + +export default function MetadataSourceUI( props ) { + const { setAttributes, setAddingBinding } = props; + // Fetching the REST API to get the available custom fields. + // + // Ensure we have the full context. + // Check if it is a page, a post, a CPT, a taxonomy... + // Ensure that context is available in all the blocks. It is not in the images. + // TODO: This is not working yet. Hardcoded to a post. + // TODO: Only run the fetch once. Right now it is triggered each time I click the button. + const [ metadata, setMetadata ] = useState( [] ); + useEffect( () => { + apiFetch( { + path: '/wp/v2/posts/1', + } ).then( ( posts ) => { + // TODO: Add filter in case plugins want to add/remove/modify fields. + // metadataaa = posts.meta; + let fetchedMetadata = []; + Object.entries( posts.meta ).forEach( ( [ key, value ] ) => { + // Prettifying the name. But I guess it is not necessary. + // Plugins could provide it somehow. + const prettyName = key + .split( '_' ) + .map( + ( word ) => + word.charAt( 0 ).toUpperCase() + word.slice( 1 ) + ) + .join( ' ' ); + fetchedMetadata = [ + ...fetchedMetadata, + { + name: prettyName, + key, + value, + }, + ]; + } ); + setMetadata( fetchedMetadata ); + } ); + }, [] ); + + const [ selectedField, setSelectedField ] = useState( null ); + // TODO: Try to abstract this function to be reused across all the sources. + function selectItem( item ) { + setSelectedField( item ); + // TODO: Add the ability to select the attribute instead of hardcoding it and check it exists for the block. + + // TODO: Add a better way to "clear" the binding. + // We don't have to remove the whole bindings attribte but just the one we are binding. + switch ( props.name ) { + case 'core/paragraph': + setAttributes( { + content: item.value, + bindings: [ + { + attribute: 'content', + source: { + name: 'metadata', + params: { value: item.value }, + }, + }, + ], + } ); + break; + case 'core/image': + setAttributes( { + url: item.value, + bindings: [ + { + attribute: 'url', + source: { + name: 'metadata', + params: { value: item.value }, + }, + }, + ], + } ); + break; + } + setAddingBinding( false ); + } + + const [ searchInput, setSearchInput ] = useState( '' ); + + return ( +
+ +
    + { metadata.map( ( item ) => ( +
  • selectItem( item, props ) } + className={ + selectedField?.key === item.key + ? 'selected-meta-field' + : '' + } + > + { item.name } +
  • + ) ) } +
+
+ ); +} 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..51cddcf6a37b7 --- /dev/null +++ b/packages/editor/src/components/block-bindings/style.scss @@ -0,0 +1,26 @@ +// TODO: Change the styles. +.block-bindings-ui-popover { + margin-top: 12px; + .block-bindings-metadata-source-ui { + min-width: 300px; + + li { + margin: 20px 32px; + cursor: pointer; + } + .selected-meta-field { + font-weight: bold; + } + .selected-meta-field::before { + content: "✔ "; + margin-left: -16px; + } + } + .block-bindings-clear-button { + margin: 10px; + background: transparent; + border: none; + color: var(--wp-admin-theme-color, #3858e9); + cursor: pointer; + } +} diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index 5fefc5506a02f..ce73cfd8e79d4 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -94,3 +94,7 @@ export { default as EditorProvider } from './provider'; export * from './deprecated'; export const VisualEditorGlobalKeyboardShortcuts = EditorKeyboardShortcuts; export const TextEditorGlobalKeyboardShortcuts = EditorKeyboardShortcuts; + +// Block Bindings Components + +export { default as BlockBindingsButton } from './block-bindings'; diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss index 986cb645c271f..7c79577c05da5 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-outline/style.scss"; @import "./components/editor-notices/style.scss"; @import "./components/entities-saved-states/style.scss"; From 35d03dd6757fa10b100fe5e6d4d0855a0f0511fd Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 Nov 2023 15:48:58 +0100 Subject: [PATCH 05/30] Filter to add the bindings attribute automatically --- packages/block-library/src/image/block.json | 3 - .../block-library/src/paragraph/block.json | 3 - .../src/components/block-bindings/index.js | 157 ++++++++++-------- 3 files changed, 90 insertions(+), 73 deletions(-) diff --git a/packages/block-library/src/image/block.json b/packages/block-library/src/image/block.json index 2aaad383d04be..b46829e5059a2 100644 --- a/packages/block-library/src/image/block.json +++ b/packages/block-library/src/image/block.json @@ -24,9 +24,6 @@ "default": "", "__experimentalRole": "content" }, - "bindings": { - "type": "object" - }, "caption": { "type": "string", "source": "html", diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json index bbe04fa23186d..85f56f4a838f5 100644 --- a/packages/block-library/src/paragraph/block.json +++ b/packages/block-library/src/paragraph/block.json @@ -12,9 +12,6 @@ "align": { "type": "string" }, - "bindings": { - "type": "object" - }, "content": { "type": "string", "source": "html", diff --git a/packages/editor/src/components/block-bindings/index.js b/packages/editor/src/components/block-bindings/index.js index 5138419e10421..87f430709fb98 100644 --- a/packages/editor/src/components/block-bindings/index.js +++ b/packages/editor/src/components/block-bindings/index.js @@ -16,82 +16,105 @@ const blockBindingsWhitelist = { 'core/image': [ 'url', 'title' ], }; -export default function BlockBindingsButton( BlockEdit ) { - return ( props ) => { - // Only add the Block Bindings button to the blocks in the whitelist. - if ( ! ( props.name in blockBindingsWhitelist ) ) { - return ; - } - const { setAttributes } = props; +export default function BlockBindingsButton( props ) { + const { setAttributes } = props; - const [ addingBinding, setAddingBinding ] = useState( false ); - function BindingsUI() { - return ( - { - setAddingBinding( false ); - } } - onFocusOutside={ () => { - setAddingBinding( false ); - } } - placement="bottom" - shift - className="block-bindings-ui-popover" - { ...props } - > - { /* TODO: Add logic to select the attribute to bind */ } + const [ addingBinding, setAddingBinding ] = useState( false ); + function BindingsUI() { + return ( + { + setAddingBinding( false ); + } } + onFocusOutside={ () => { + setAddingBinding( false ); + } } + placement="bottom" + shift + className="block-bindings-ui-popover" + { ...props } + > + { /* TODO: Add logic to select the attribute to bind */ } - { /* TODO: This component could potentially be defined by each source. */ } - + { /* TODO: This component could potentially be defined by each source. */ } + - { /* + { /* TODO: Add a better way to "clear" the binding. We don't have to remove the whole bindings attribute but just the one we are binding. We can explore if we can get back to the previous content or keep the value of the custom field. */ } - - - ); - } - - const [ popoverAnchor, setPopoverAnchor ] = useState(); - return ( - <> - - - - { addingBinding && } - - + + ); - }; + } + + const [ popoverAnchor, setPopoverAnchor ] = useState(); + return ( + <> + + + { addingBinding && } + + + ); } -addFilter( 'editor.BlockEdit', 'core', BlockBindingsButton ); +if ( window.__experimentalConnections ) { + addFilter( + 'blocks.registerBlockType', + 'block-directory/fallback', + ( settings, name ) => { + if ( ! ( name in blockBindingsWhitelist ) ) { + return settings; + } + + // Add "bindings" attribute. + if ( ! settings.attributes.bindings ) { + settings.attributes.bindings = { + type: 'object', + }; + } + + // 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. From a19009c4f1525679e9ad753db53ba77186ff946c Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 Nov 2023 19:05:31 +0100 Subject: [PATCH 06/30] Logic to handle different attributes in the editor --- .../src/components/block-bindings/index.js | 113 +++++++++++++----- .../src/components/block-bindings/metadata.js | 68 +++++------ .../src/components/block-bindings/style.scss | 20 ++-- 3 files changed, 128 insertions(+), 73 deletions(-) diff --git a/packages/editor/src/components/block-bindings/index.js b/packages/editor/src/components/block-bindings/index.js index 87f430709fb98..50622b00783fc 100644 --- a/packages/editor/src/components/block-bindings/index.js +++ b/packages/editor/src/components/block-bindings/index.js @@ -3,8 +3,12 @@ */ import { useState } from '@wordpress/element'; import { BlockControls } from '@wordpress/block-editor'; -import { Button, Popover } from '@wordpress/components'; -import { plugins as pluginsIcon } from '@wordpress/icons'; +import { Button, MenuItem, MenuGroup, Popover } from '@wordpress/components'; +import { + plugins as pluginsIcon, + chevronDown, + chevronUp, +} from '@wordpress/icons'; import { addFilter } from '@wordpress/hooks'; /** * Internal dependencies @@ -20,6 +24,7 @@ export default function BlockBindingsButton( props ) { const { setAttributes } = props; const [ addingBinding, setAddingBinding ] = useState( false ); + // TODO: Triage why it is reloading after selecting a binding. function BindingsUI() { return ( - { /* TODO: Add logic to select the attribute to bind */ } + + + ); + } - { /* TODO: This component could potentially be defined by each source. */ } - + function AttributesLayer( props ) { + const [ activeAttribute, setIsActiveAttribute ] = useState( false ); + return ( + + { blockBindingsWhitelist[ props.name ].map( ( attribute ) => ( +
+ + setIsActiveAttribute( + activeAttribute === attribute + ? false + : attribute + ) + } + className="block-bindings-attribute-picker-button" + > + { attribute } + + { activeAttribute === attribute && ( + <> + { /* TODO: This component could potentially be defined by each source. */ } + + + + ) } +
+ ) ) } +
+ ); + } - { /* - TODO: Add a better way to "clear" the binding. - We don't have to remove the whole bindings attribute but just the one we are binding. - We can explore if we can get back to the previous content or keep the value of the custom field. - */ } - - + function RemoveBindingButton( props ) { + return ( + ); } @@ -97,7 +150,7 @@ if ( window.__experimentalConnections ) { // Add "bindings" attribute. if ( ! settings.attributes.bindings ) { settings.attributes.bindings = { - type: 'object', + type: 'array', }; } diff --git a/packages/editor/src/components/block-bindings/metadata.js b/packages/editor/src/components/block-bindings/metadata.js index d5828fae96486..fef57da95651c 100644 --- a/packages/editor/src/components/block-bindings/metadata.js +++ b/packages/editor/src/components/block-bindings/metadata.js @@ -7,7 +7,7 @@ import { SearchControl } from '@wordpress/components'; import apiFetch from '@wordpress/api-fetch'; export default function MetadataSourceUI( props ) { - const { setAttributes, setAddingBinding } = props; + const { setAttributes, setIsActiveAttribute } = props; // Fetching the REST API to get the available custom fields. // // Ensure we have the full context. @@ -48,49 +48,47 @@ export default function MetadataSourceUI( props ) { const [ selectedField, setSelectedField ] = useState( null ); // TODO: Try to abstract this function to be reused across all the sources. - function selectItem( item ) { + function selectItem( item, props ) { + const { currentAttribute } = props; setSelectedField( item ); - // TODO: Add the ability to select the attribute instead of hardcoding it and check it exists for the block. - // TODO: Add a better way to "clear" the binding. - // We don't have to remove the whole bindings attribte but just the one we are binding. - switch ( props.name ) { - case 'core/paragraph': - setAttributes( { - content: item.value, - bindings: [ - { - attribute: 'content', - source: { - name: 'metadata', - params: { value: item.value }, - }, - }, - ], - } ); - break; - case 'core/image': - setAttributes( { - url: item.value, - bindings: [ - { - attribute: 'url', - source: { - name: 'metadata', - params: { value: item.value }, - }, - }, - ], - } ); - break; + const newAttributes = {}; + // Modify the attribute we are binding. + newAttributes[ currentAttribute ] = item.value; + + // If the attribute exists in the bindings, update it. + // Otherwise, add it. + const newBindings = props.attributes.bindings + ? props.attributes.bindings + : []; + let attributeExists = false; + newBindings.forEach( ( binding ) => { + if ( binding.attribute === currentAttribute ) { + binding.source.name = 'metadata'; + binding.source.params.value = item.key; + attributeExists = true; + } + } ); + if ( ! attributeExists ) { + newBindings.push( { + attribute: currentAttribute, + source: { + name: 'metadata', + params: { value: item.key }, + }, + } ); } - setAddingBinding( false ); + newAttributes.bindings = newBindings; + setAttributes( newAttributes ); + + setIsActiveAttribute( false ); } const [ searchInput, setSearchInput ] = useState( '' ); return (
+ { /* TODO: Implement the Search logic. */ } Date: Thu, 30 Nov 2023 20:19:40 +0100 Subject: [PATCH 07/30] Fetch metadata from REST API correctly --- .../block-bindings-api/html-processing.php | 1 + .../src/components/block-bindings/index.js | 12 +++ .../src/components/block-bindings/metadata.js | 79 +++++++++++-------- 3 files changed, 60 insertions(+), 32 deletions(-) diff --git a/lib/experimental/block-bindings-api/html-processing.php b/lib/experimental/block-bindings-api/html-processing.php index e0ebf5b27d3b9..b351de4190b24 100644 --- a/lib/experimental/block-bindings-api/html-processing.php +++ b/lib/experimental/block-bindings-api/html-processing.php @@ -21,6 +21,7 @@ function block_bindings_replace_html( $block_content, $block_name, $block_attr, } // 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': $p = new WP_HTML_Tag_Processor( $block_content ); diff --git a/packages/editor/src/components/block-bindings/index.js b/packages/editor/src/components/block-bindings/index.js index 50622b00783fc..a5f6de2ddd207 100644 --- a/packages/editor/src/components/block-bindings/index.js +++ b/packages/editor/src/components/block-bindings/index.js @@ -154,6 +154,18 @@ if ( window.__experimentalConnections ) { }; } + // TODO: Review the implications of this and the code. + // Add the necessary context to the block. + const contextItems = [ 'postId', 'postType', 'queryId' ]; + let 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/components/block-bindings/metadata.js b/packages/editor/src/components/block-bindings/metadata.js index fef57da95651c..57a26027388d6 100644 --- a/packages/editor/src/components/block-bindings/metadata.js +++ b/packages/editor/src/components/block-bindings/metadata.js @@ -7,42 +7,55 @@ import { SearchControl } from '@wordpress/components'; import apiFetch from '@wordpress/api-fetch'; export default function MetadataSourceUI( props ) { - const { setAttributes, setIsActiveAttribute } = props; - // Fetching the REST API to get the available custom fields. - // - // Ensure we have the full context. - // Check if it is a page, a post, a CPT, a taxonomy... - // Ensure that context is available in all the blocks. It is not in the images. - // TODO: This is not working yet. Hardcoded to a post. - // TODO: Only run the fetch once. Right now it is triggered each time I click the button. + const { setAttributes, setIsActiveAttribute, context } = props; + const [ metadata, setMetadata ] = useState( [] ); useEffect( () => { + // Fetching the REST API to get the available custom fields. + // TODO: Only run the fetch once. Right now it is triggered each time I click the button. + // TODO: Review if we can avoid the first fetch. Using it right now to get the rest_base and the rest_namespace. + // TODO: Review if it works with taxonomies. apiFetch( { - path: '/wp/v2/posts/1', - } ).then( ( posts ) => { - // TODO: Add filter in case plugins want to add/remove/modify fields. - // metadataaa = posts.meta; - let fetchedMetadata = []; - Object.entries( posts.meta ).forEach( ( [ key, value ] ) => { - // Prettifying the name. But I guess it is not necessary. - // Plugins could provide it somehow. - const prettyName = key - .split( '_' ) - .map( - ( word ) => - word.charAt( 0 ).toUpperCase() + word.slice( 1 ) - ) - .join( ' ' ); - fetchedMetadata = [ - ...fetchedMetadata, - { - name: prettyName, - key, - value, - }, - ]; + path: '/wp/v2/types/' + context.postType, + } ).then( ( type ) => { + apiFetch( { + path: + '/' + + type.rest_namespace + + '/' + + type.rest_base + + '/' + + context.postId, + } ).then( ( posts ) => { + let fetchedMetadata = []; + function addMetadata( array, newData ) { + Object.entries( newData ).forEach( ( [ key, value ] ) => { + // Prettifying the name. But I guess it is not necessary. + // Plugins could provide it somehow. + const prettyName = key + .split( '_' ) + .map( + ( word ) => + word.charAt( 0 ).toUpperCase() + + word.slice( 1 ) + ) + .join( ' ' ); + array.push( { + name: prettyName, + key, + value, + } ); + return array; + } ); + } + addMetadata( fetchedMetadata, posts.meta ); + + // TODO: Add filter in case plugins want to add/remove/modify fields. + // For example, ACF has its own field named "acf". Adding it manually. + addMetadata( fetchedMetadata, posts.acf ); + + setMetadata( fetchedMetadata ); } ); - setMetadata( fetchedMetadata ); } ); }, [] ); @@ -54,6 +67,8 @@ export default function MetadataSourceUI( props ) { const newAttributes = {}; // 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. newAttributes[ currentAttribute ] = item.value; // If the attribute exists in the bindings, update it. From 081ed5c3b028045475ab52ae601637c3c9379d60 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Thu, 30 Nov 2023 20:20:16 +0100 Subject: [PATCH 08/30] Use const instead of let --- packages/editor/src/components/block-bindings/index.js | 2 +- packages/editor/src/components/block-bindings/metadata.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/components/block-bindings/index.js b/packages/editor/src/components/block-bindings/index.js index a5f6de2ddd207..044040af39b02 100644 --- a/packages/editor/src/components/block-bindings/index.js +++ b/packages/editor/src/components/block-bindings/index.js @@ -157,7 +157,7 @@ if ( window.__experimentalConnections ) { // TODO: Review the implications of this and the code. // Add the necessary context to the block. const contextItems = [ 'postId', 'postType', 'queryId' ]; - let usesContextArray = settings.usesContext; + const usesContextArray = settings.usesContext; const oldUsesContextArray = new Set( usesContextArray ); contextItems.forEach( ( item ) => { if ( ! oldUsesContextArray.has( item ) ) { diff --git a/packages/editor/src/components/block-bindings/metadata.js b/packages/editor/src/components/block-bindings/metadata.js index 57a26027388d6..65d9ad5bf9e1a 100644 --- a/packages/editor/src/components/block-bindings/metadata.js +++ b/packages/editor/src/components/block-bindings/metadata.js @@ -27,7 +27,7 @@ export default function MetadataSourceUI( props ) { '/' + context.postId, } ).then( ( posts ) => { - let fetchedMetadata = []; + const fetchedMetadata = []; function addMetadata( array, newData ) { Object.entries( newData ).forEach( ( [ key, value ] ) => { // Prettifying the name. But I guess it is not necessary. From ecd13770c0778e8c654abe084415ce9022c4ef05 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Fri, 1 Dec 2023 09:40:47 +0100 Subject: [PATCH 09/30] Move bindings inside metadata attribute --- lib/experimental/blocks.php | 8 +++---- .../src/components/block-bindings/index.js | 21 ++++++++----------- .../src/components/block-bindings/metadata.js | 18 ++++++++++------ 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index 3e31a93c6e30b..7a22419ef8119 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -97,12 +97,12 @@ function wp_enqueue_block_view_script( $block_name, $args ) { * @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 attribute, return. - if ( ! isset( $block['attrs']['bindings'] ) ) { + // If the block doesn't have the bindings property, return. + if ( ! isset( $block['attrs']['metadata']['bindings'] ) ) { return $block_content; } - // Assuming the following format for the bindings attribute: + // Assuming the following format for the bindings property of the "metadata" attribute: // // "bindings": [ // { @@ -118,7 +118,7 @@ function process_block_bindings( $block_content, $block, $block_instance ) { global $block_bindings_whitelist; global $block_bindings_sources; $modified_block_content = $block_content; - foreach ( $block['attrs']['bindings'] as $binding ) { + foreach ( $block['attrs']['metadata']['bindings'] as $binding ) { if ( ! isset( $block_bindings_whitelist[ $block['blockName'] ] ) ) { continue; } diff --git a/packages/editor/src/components/block-bindings/index.js b/packages/editor/src/components/block-bindings/index.js index 044040af39b02..47e25456173fc 100644 --- a/packages/editor/src/components/block-bindings/index.js +++ b/packages/editor/src/components/block-bindings/index.js @@ -102,16 +102,20 @@ export default function BlockBindingsButton( props ) { + ); + } + + const [ popoverAnchor, setPopoverAnchor ] = useState(); + return ( + <> + + + { addingBinding && } + + + ); +}; + +if ( window.__experimentalConnections ) { + 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/fields-list.js b/packages/editor/src/components/block-bindings/fields-list.js new file mode 100644 index 0000000000000..1971e551e74f0 --- /dev/null +++ b/packages/editor/src/components/block-bindings/fields-list.js @@ -0,0 +1,91 @@ +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; +import { updateBlockBindingsAttribute } from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; +import { SearchControl, MenuItem } from '@wordpress/components'; +import { chevronDown, chevronUp } from '@wordpress/icons'; + +export default function BlockBindingsFieldsList( props ) { + const { + attributes, + setAttributes, + setIsActiveAttribute, + currentAttribute, + activeSource, + setIsActiveSource, + fields, + source, + label, + } = 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 ); + } + + const [ searchInput, setSearchInput ] = useState( '' ); + + return ( +
+ + setIsActiveSource( + activeSource === source ? false : source + ) + } + className="block-bindings-source-picker-button" + > + { label } + + { /* TODO: Implement the Search logic. */ } + { /* */ } + { activeSource === source && ( +
    + { 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 ef75d701fb1c4..8bc50dcbc9f80 100644 --- a/packages/editor/src/components/block-bindings/index.js +++ b/packages/editor/src/components/block-bindings/index.js @@ -1,190 +1,9 @@ -/** - * WordPress dependencies - */ -import { useState } from '@wordpress/element'; -import { - BlockControls, - updateBlockBindingsAttribute, -} from '@wordpress/block-editor'; -import { Button, MenuItem, MenuGroup, Popover } from '@wordpress/components'; -import { - plugins as pluginsIcon, - chevronDown, - chevronUp, -} from '@wordpress/icons'; -import { addFilter } from '@wordpress/hooks'; /** * Internal dependencies */ -import MetadataSourceUI from './metadata.js'; - -const blockBindingsWhitelist = { - 'core/paragraph': [ 'content' ], - 'core/heading': [ 'content' ], - 'core/image': [ 'url', 'title' ], - 'core/button': [ 'url', 'text' ], -}; - -export default function BlockBindingsButton( props ) { - const { setAttributes } = props; - - const [ addingBinding, setAddingBinding ] = useState( false ); - // TODO: Triage why it is reloading after selecting a binding. - function BindingsUI() { - return ( - { - setAddingBinding( false ); - } } - onFocusOutside={ () => { - setAddingBinding( false ); - } } - placement="bottom" - shift - className="block-bindings-ui-popover" - { ...props } - > - - - ); - } - - function AttributesLayer( props ) { - const [ activeAttribute, setIsActiveAttribute ] = useState( false ); - return ( - - { blockBindingsWhitelist[ props.name ].map( ( attribute ) => ( -
- - setIsActiveAttribute( - activeAttribute === attribute - ? false - : attribute - ) - } - className="block-bindings-attribute-picker-button" - > - { attribute } - - { activeAttribute === attribute && ( - <> - { /* TODO: This component could potentially be defined by each source. */ } - - - - ) } -
- ) ) } -
- ); - } - - function RemoveBindingButton( props ) { - return ( - - ); - } - - const [ popoverAnchor, setPopoverAnchor ] = useState(); - return ( - <> - - - { addingBinding && } - - - ); -} - -// TODO: Review if this is needed for other sources or just the metadata. -if ( window.__experimentalConnections ) { - addFilter( - 'blocks.registerBlockType', - 'block-directory/fallback', - ( 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. +export { default as BlockBindingsFill } from './bindings-ui'; +export { default as BlockBindingsFieldsList } from './fields-list'; +// TODO: Review where this files should go. +export { default as PostMeta } from './sources/post-meta'; +export { default as PostData } from './sources/post-data'; +export { default as SiteData } from './sources/site-data'; diff --git a/packages/editor/src/components/block-bindings/metadata.js b/packages/editor/src/components/block-bindings/metadata.js deleted file mode 100644 index a92adf473c2c9..0000000000000 --- a/packages/editor/src/components/block-bindings/metadata.js +++ /dev/null @@ -1,132 +0,0 @@ -/** - * WordPress dependencies - */ -import { useState } from '@wordpress/element'; -import { updateBlockBindingsAttribute } from '@wordpress/block-editor'; -import { useSelect } from '@wordpress/data'; -import { store as coreStore } from '@wordpress/core-data'; -import { __ } from '@wordpress/i18n'; -import { SearchControl } from '@wordpress/components'; - -export default function MetadataSourceUI( props ) { - const { - attributes, - setAttributes, - setIsActiveAttribute, - currentAttribute, - context, - } = props; - - // Fetching the REST API to get the available custom fields. - // TODO: Review if it works with taxonomies. - // 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 ] - ); - const metadata = []; - function addMetadata( array, newData ) { - Object.entries( newData ).forEach( ( [ key, value ] ) => { - // Prettifying the name. But I guess it is not necessary. - // Plugins could provide it somehow. - const prettyName = key - .split( '_' ) - .map( - ( word ) => word.charAt( 0 ).toUpperCase() + word.slice( 1 ) - ) - .join( ' ' ); - array.push( { - name: prettyName, - key, - value, - } ); - return array; - } ); - } - - addMetadata( metadata, data.meta ); - - // TODO: Decide if these should be added here or as a separate source. - // Example 1: Extend the list with the ACF fields. - addMetadata( metadata, data.acf ); - - // Example 2: Add post data - addMetadata( metadata, { - post_title: data.title.rendered, - post_date: data.date, - } ); - - // Example 3: Add site data - const siteData = useSelect( - ( select ) => { - const { getEntityRecord } = select( coreStore ); - return getEntityRecord( 'root', 'site' ); - }, - [ context.postType, context.postId ] - ); - addMetadata( metadata, { - site_title: siteData.title, - site_url: siteData.url, - } ); - - // 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, - 'metadata', - { value: item.key } - ); - - setIsActiveAttribute( false ); - } - - const [ searchInput, setSearchInput ] = useState( '' ); - - return ( -
- { /* TODO: Implement the Search logic. */ } - -
    - { metadata.map( ( item ) => ( -
  • selectItem( item ) } - className={ - attributes.metadata?.bindings?.[ currentAttribute ] - ?.source?.name === 'metadata' && - attributes.metadata?.bindings?.[ currentAttribute ] - ?.source?.attributes?.value === item.key - ? 'selected-meta-field' - : '' - } - > - { item.name } -
  • - ) ) } -
-
- ); -} diff --git a/packages/editor/src/components/block-bindings/sources/post-meta.js b/packages/editor/src/components/block-bindings/sources/post-meta.js new file mode 100644 index 0000000000000..69152193513a0 --- /dev/null +++ b/packages/editor/src/components/block-bindings/sources/post-meta.js @@ -0,0 +1,89 @@ +/** + * 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 '../bindings-ui.js'; +import BlockBindingsFieldsList from '../fields-list.js'; + +const PostMeta = ( props ) => { + const { context } = props; + + // Fetching the REST API to get the available custom fields. + // TODO: Review if it works with taxonomies. + // 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.__experimentalConnections ) { + // 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/components/block-bindings/style.scss b/packages/editor/src/components/block-bindings/style.scss index f2041afef4637..738c747988480 100644 --- a/packages/editor/src/components/block-bindings/style.scss +++ b/packages/editor/src/components/block-bindings/style.scss @@ -10,7 +10,7 @@ border-bottom: 1px solid #0002; } - .block-bindings-metadata-source-ui { + .block-bindings-fields-list-ui { padding: 12px; li { margin: 20px 8px; diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index ce73cfd8e79d4..24c25c8a7451f 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -95,6 +95,5 @@ export * from './deprecated'; export const VisualEditorGlobalKeyboardShortcuts = EditorKeyboardShortcuts; export const TextEditorGlobalKeyboardShortcuts = EditorKeyboardShortcuts; -// Block Bindings Components - -export { default as BlockBindingsButton } from './block-bindings'; +// Block Bindings Components. +export * from './block-bindings'; From 159cb0a39b565c2bf031e27644d8ee74e1dfb204 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 18 Dec 2023 18:57:02 +0100 Subject: [PATCH 23/30] Add post-data and site-data sources in the editor --- .../block-bindings/sources/post-data.js | 89 +++++++++++++++++++ .../block-bindings/sources/site-data.js | 77 ++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 packages/editor/src/components/block-bindings/sources/post-data.js create mode 100644 packages/editor/src/components/block-bindings/sources/site-data.js diff --git a/packages/editor/src/components/block-bindings/sources/post-data.js b/packages/editor/src/components/block-bindings/sources/post-data.js new file mode 100644 index 0000000000000..569495af4b1bd --- /dev/null +++ b/packages/editor/src/components/block-bindings/sources/post-data.js @@ -0,0 +1,89 @@ +/** + * 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 '../bindings-ui.js'; +import BlockBindingsFieldsList from '../fields-list.js'; + +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.__experimentalConnections ) { + // 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/components/block-bindings/sources/site-data.js b/packages/editor/src/components/block-bindings/sources/site-data.js new file mode 100644 index 0000000000000..a3ffecafd57eb --- /dev/null +++ b/packages/editor/src/components/block-bindings/sources/site-data.js @@ -0,0 +1,77 @@ +/** + * 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 '../bindings-ui.js'; +import BlockBindingsFieldsList from '../fields-list.js'; + +const SiteData = ( props ) => { + // TODO: Explore if it makes sense to create a custom endpoint for this. + const siteData = useSelect( ( select ) => { + const { getEntityRecord } = select( coreStore ); + return getEntityRecord( 'root', 'site' ); + }, [] ); + + // Adapt the data to the format expected by the fields list. + // TODO: Ensure the key and label work with translations. + const fields = [ + { + key: 'blogname', + label: 'Site title', + value: siteData.title, + }, + { + key: 'blogdescription', + label: 'Site description', + value: siteData.description, + }, + { + key: 'siteurl', + label: 'Site url', + value: siteData.description, + }, + ]; + + return ( + + ); +}; + +if ( window.__experimentalConnections ) { + const withCoreSources = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + const { isSelected } = props; + + return ( + <> + { isSelected && ( + <> + + + + + ) } + + + ); + }, + 'withToolbarControls' + ); + + addFilter( + 'editor.BlockEdit', + 'core/block-bindings-ui/add-sources', + withCoreSources + ); +} From 1178ba8c9cc84ece90553ccfcc50c62c4c0845df Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Mon, 18 Dec 2023 18:58:09 +0100 Subject: [PATCH 24/30] Add core sources in PHP --- lib/experimental/block-bindings-api/index.php | 4 ++- .../block-bindings-api/sources/metadata.php | 29 ------------------- .../block-bindings-api/sources/post-data.php | 20 +++++++++++++ .../block-bindings-api/sources/post-meta.php | 21 ++++++++++++++ .../block-bindings-api/sources/site-data.php | 13 +++++++++ 5 files changed, 57 insertions(+), 30 deletions(-) delete mode 100644 lib/experimental/block-bindings-api/sources/metadata.php create mode 100644 lib/experimental/block-bindings-api/sources/post-data.php create mode 100644 lib/experimental/block-bindings-api/sources/post-meta.php create mode 100644 lib/experimental/block-bindings-api/sources/site-data.php diff --git a/lib/experimental/block-bindings-api/index.php b/lib/experimental/block-bindings-api/index.php index edf7a3209da86..4ca10b1057483 100644 --- a/lib/experimental/block-bindings-api/index.php +++ b/lib/experimental/block-bindings-api/index.php @@ -15,6 +15,8 @@ require_once __DIR__ . '/sources/pattern.php'; } if ( array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) ) { - require_once __DIR__ . '/sources/metadata.php'; + require_once __DIR__ . '/sources/post-meta.php'; + require_once __DIR__ . '/sources/post-data.php'; + require_once __DIR__ . '/sources/site-data.php'; } } diff --git a/lib/experimental/block-bindings-api/sources/metadata.php b/lib/experimental/block-bindings-api/sources/metadata.php deleted file mode 100644 index 9a0d0b41e6fba..0000000000000 --- a/lib/experimental/block-bindings-api/sources/metadata.php +++ /dev/null @@ -1,29 +0,0 @@ -context['postId'] but it wasn't available in the image block. - $post_id = get_the_ID(); - } - - // TODO: Add logic to handle other meta types. - if ( isset( $source_attrs['metaType'] ) ) { - $meta_type = $source_attrs['metaType']; - } else { - $meta_type = 'post'; - } - - // TODO: Add a filter/mechanism to limit the meta keys that can be used. - return get_metadata( $meta_type, $post_id, $source_attrs['value'], true ); - }; - register_block_bindings_source( 'metadata', $metadata_source_callback ); -} diff --git a/lib/experimental/block-bindings-api/sources/post-data.php b/lib/experimental/block-bindings-api/sources/post-data.php new file mode 100644 index 0000000000000..d1c56224f1b0a --- /dev/null +++ b/lib/experimental/block-bindings-api/sources/post-data.php @@ -0,0 +1,20 @@ +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', $post_data_source_callback ); +} diff --git a/lib/experimental/block-bindings-api/sources/post-meta.php b/lib/experimental/block-bindings-api/sources/post-meta.php new file mode 100644 index 0000000000000..aad710f5f0748 --- /dev/null +++ b/lib/experimental/block-bindings-api/sources/post-meta.php @@ -0,0 +1,21 @@ +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', $post_meta_source_callback ); +} diff --git a/lib/experimental/block-bindings-api/sources/site-data.php b/lib/experimental/block-bindings-api/sources/site-data.php new file mode 100644 index 0000000000000..8ad8a5ce28880 --- /dev/null +++ b/lib/experimental/block-bindings-api/sources/site-data.php @@ -0,0 +1,13 @@ + Date: Tue, 19 Dec 2023 17:37:59 +0100 Subject: [PATCH 25/30] Abstract block bindings UI --- .../components/block-bindings/bindings-ui.js | 80 +++++++++++++++++-- .../components/block-bindings/fields-list.js | 70 +++++----------- .../block-bindings/sources/post-data.js | 6 +- .../block-bindings/sources/post-meta.js | 6 +- .../block-bindings/sources/site-data.js | 6 +- 5 files changed, 105 insertions(+), 63 deletions(-) diff --git a/packages/editor/src/components/block-bindings/bindings-ui.js b/packages/editor/src/components/block-bindings/bindings-ui.js index 9490ceadd1ca9..c5ea226d38a26 100644 --- a/packages/editor/src/components/block-bindings/bindings-ui.js +++ b/packages/editor/src/components/block-bindings/bindings-ui.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useState, cloneElement } from '@wordpress/element'; +import { useState, cloneElement, Fragment } from '@wordpress/element'; import { BlockControls, updateBlockBindingsAttribute, @@ -29,11 +29,19 @@ const blockBindingsWhitelist = { const { Slot, Fill } = createSlotFill( 'BlockBindingsUI' ); -const BlockBindingsFill = ( { children } ) => { +const BlockBindingsFill = ( { children, source, label } ) => { return ( { ( props ) => { - return <>{ cloneElement( children, props ) }; + return ( + <> + { cloneElement( children, { + source, + label, + ...props, + } ) } + + ); } } ); @@ -103,10 +111,70 @@ const BlockBindingsUI = ( props ) => { ...props, currentAttribute: attribute, setIsActiveAttribute, - activeSource, - setIsActiveSource, } } - /> + > + { ( 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 } + + ); + } + ) } + + ); + } } + - - setIsActiveSource( - activeSource === source ? false : source - ) - } - className="block-bindings-source-picker-button" - > - { label } - - { /* TODO: Implement the Search logic. */ } - { /* */ } - { activeSource === source && ( -
    - { 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 } -
  • - ) ) } -
- ) } -
+ + { 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/sources/post-data.js b/packages/editor/src/components/block-bindings/sources/post-data.js index 569495af4b1bd..2d27bde25231a 100644 --- a/packages/editor/src/components/block-bindings/sources/post-data.js +++ b/packages/editor/src/components/block-bindings/sources/post-data.js @@ -52,7 +52,6 @@ const PostData = ( props ) => { ); @@ -69,7 +68,10 @@ if ( window.__experimentalConnections ) { <> { isSelected && ( <> - + diff --git a/packages/editor/src/components/block-bindings/sources/post-meta.js b/packages/editor/src/components/block-bindings/sources/post-meta.js index 69152193513a0..cd21f241ea896 100644 --- a/packages/editor/src/components/block-bindings/sources/post-meta.js +++ b/packages/editor/src/components/block-bindings/sources/post-meta.js @@ -51,7 +51,6 @@ const PostMeta = ( props ) => { ); @@ -69,7 +68,10 @@ if ( window.__experimentalConnections ) { <> { isSelected && ( <> - + diff --git a/packages/editor/src/components/block-bindings/sources/site-data.js b/packages/editor/src/components/block-bindings/sources/site-data.js index a3ffecafd57eb..07f3eb514ab0b 100644 --- a/packages/editor/src/components/block-bindings/sources/site-data.js +++ b/packages/editor/src/components/block-bindings/sources/site-data.js @@ -42,7 +42,6 @@ const SiteData = ( props ) => { ); @@ -57,7 +56,10 @@ if ( window.__experimentalConnections ) { <> { isSelected && ( <> - + From cc59e1b039d07547287a3561b530ecd24ce61e08 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Tue, 19 Dec 2023 17:38:22 +0100 Subject: [PATCH 26/30] Remove extra dependency --- packages/editor/src/components/block-bindings/fields-list.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/editor/src/components/block-bindings/fields-list.js b/packages/editor/src/components/block-bindings/fields-list.js index 42697c9b271c1..a70156e453ca2 100644 --- a/packages/editor/src/components/block-bindings/fields-list.js +++ b/packages/editor/src/components/block-bindings/fields-list.js @@ -3,7 +3,6 @@ */ import { updateBlockBindingsAttribute } from '@wordpress/block-editor'; import { MenuItem, MenuGroup } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; export default function BlockBindingsFieldsList( props ) { const { From 4f651c21daa027066792ca847c393a069e74cfc7 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 07:16:00 +0100 Subject: [PATCH 27/30] Modify `register_block_bindings_source` syntax --- .../block-bindings-api/sources/index.php | 11 ++++---- .../block-bindings-api/sources/pattern.php | 8 +++++- .../block-bindings-api/sources/post-data.php | 8 +++++- .../block-bindings-api/sources/post-meta.php | 8 +++++- .../block-bindings-api/sources/site-data.php | 8 +++++- lib/experimental/blocks.php | 25 +++++++++++-------- 6 files changed, 48 insertions(+), 20 deletions(-) diff --git a/lib/experimental/block-bindings-api/sources/index.php b/lib/experimental/block-bindings-api/sources/index.php index 56470e6f4e9dc..5e99f8038976c 100644 --- a/lib/experimental/block-bindings-api/sources/index.php +++ b/lib/experimental/block-bindings-api/sources/index.php @@ -12,12 +12,13 @@ * Function to register a new source. * * @param string $source_name The name of the source. - * @param function $source_callback The callback executed when the source is processed in the server. + * @param function $source_args List of arguments for the block bindings source: + * - label: The label of the source. + * - apply: The callback executed when the source is processed in the server. + * @return void */ - function register_block_bindings_source( $source_name, $source_callback ) { - // We might want to add some validation here, for the name and for the apply_source callback. - // To ensure the register sources are valid. + function register_block_bindings_source( $source_name, $source_args ) { global $block_bindings_sources; - $block_bindings_sources[ $source_name ] = array( 'apply_source' => $source_callback ); + $block_bindings_sources[ $source_name ] = $source_args; } } diff --git a/lib/experimental/block-bindings-api/sources/pattern.php b/lib/experimental/block-bindings-api/sources/pattern.php index ac71166a9777a..4948e1d8fdd03 100644 --- a/lib/experimental/block-bindings-api/sources/pattern.php +++ b/lib/experimental/block-bindings-api/sources/pattern.php @@ -13,5 +13,11 @@ $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', $pattern_source_callback ); + register_block_bindings_source( + 'pattern_attributes', + array( + 'label' => __( 'Pattern Attributes' ), + 'apply' => $pattern_source_callback, + ) + ); } diff --git a/lib/experimental/block-bindings-api/sources/post-data.php b/lib/experimental/block-bindings-api/sources/post-data.php index d1c56224f1b0a..f4f565781efc3 100644 --- a/lib/experimental/block-bindings-api/sources/post-data.php +++ b/lib/experimental/block-bindings-api/sources/post-data.php @@ -16,5 +16,11 @@ } return get_post( $post_id )->{$source_attrs['value']}; }; - register_block_bindings_source( 'post_data', $post_data_source_callback ); + register_block_bindings_source( + 'post_data', + array( + 'label' => __( 'Post Data' ), + 'apply' => $post_data_source_callback, + ) + ); } diff --git a/lib/experimental/block-bindings-api/sources/post-meta.php b/lib/experimental/block-bindings-api/sources/post-meta.php index aad710f5f0748..3220b3c6defb2 100644 --- a/lib/experimental/block-bindings-api/sources/post-meta.php +++ b/lib/experimental/block-bindings-api/sources/post-meta.php @@ -17,5 +17,11 @@ return get_post_meta( $post_id, $source_attrs['value'], true ); }; - register_block_bindings_source( 'post_meta', $post_meta_source_callback ); + register_block_bindings_source( + 'post_meta', + array( + 'label' => __( 'Post Meta' ), + 'apply' => $post_meta_source_callback, + ) + ); } diff --git a/lib/experimental/block-bindings-api/sources/site-data.php b/lib/experimental/block-bindings-api/sources/site-data.php index 8ad8a5ce28880..5375a81b42385 100644 --- a/lib/experimental/block-bindings-api/sources/site-data.php +++ b/lib/experimental/block-bindings-api/sources/site-data.php @@ -9,5 +9,11 @@ $site_data_source_callback = function ( $source_attrs, $block_content, $block, $block_instance ) { return get_option( $source_attrs['value'] ); }; - register_block_bindings_source( 'site_data', $site_data_source_callback ); + register_block_bindings_source( + 'site_data', + array( + 'label' => __( 'Site Data' ), + 'apply' => $site_data_source_callback, + ) + ); } diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index 47c900aa52244..70eeee6241fd7 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -114,13 +114,13 @@ function process_block_bindings( $block_content, $block, $block_instance ) { // "bindings": { // "title": { // "source": { - // "name": "metadata", + // "name": "post_meta", // "attributes": { "value": "text_custom_field" } // } // }, // "url": { // "source": { - // "name": "metadata", + // "name": "post_meta", // "attributes": { "value": "text_custom_field" } // } // } @@ -130,22 +130,25 @@ function process_block_bindings( $block_content, $block, $block_instance ) { 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'] ] ) ) { - continue; + 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; } + // 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; + } + $source_callback = $block_bindings_sources[ $binding_source['source']['name'] ]['apply']; // Get the value based on the source. - // We might want to move this to its own function if it gets more complex. - // We pass $block_content, $block, $block_instance to the source callback in case sources want to use them. - if ( ! isset( $block_bindings_sources[ $binding_source['source']['name'] ]['apply_source'] ) ) { - return $block_content; - } - $source_value = $block_bindings_sources[ $binding_source['source']['name'] ]['apply_source']( $binding_source['source']['attributes'], $block_content, $block, $block_instance ); - if ( false === $source_value ) { - return $block_content; + $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; } // Process the HTML based on the block and the attribute. From 7dd436803c88d291e526fb79e38cf06f15f5db3a Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 07:34:57 +0100 Subject: [PATCH 28/30] Change block bindings experiment name --- lib/block-supports/pattern.php | 2 +- lib/experimental/block-bindings-api/index.php | 2 +- lib/experimental/blocks.php | 2 +- lib/experimental/editor-settings.php | 4 ++-- lib/experiments-page.php | 8 ++++---- packages/block-editor/src/hooks/custom-fields.js | 8 ++++---- packages/block-library/src/block/edit.js | 6 +++++- packages/block-library/src/paragraph/block.json | 1 - .../editor/src/components/block-bindings/bindings-ui.js | 2 +- .../src/components/block-bindings/sources/post-data.js | 2 +- .../src/components/block-bindings/sources/post-meta.js | 2 +- .../src/components/block-bindings/sources/site-data.js | 2 +- packages/editor/src/hooks/pattern-partial-syncing.js | 2 +- 13 files changed, 23 insertions(+), 20 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/block-bindings-api/index.php b/lib/experimental/block-bindings-api/index.php index 4ca10b1057483..8b3f4197017d7 100644 --- a/lib/experimental/block-bindings-api/index.php +++ b/lib/experimental/block-bindings-api/index.php @@ -14,7 +14,7 @@ if ( array_key_exists( 'gutenberg-pattern-partial-syncing', $gutenberg_experiments ) ) { require_once __DIR__ . '/sources/pattern.php'; } - if ( array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) ) { + if ( array_key_exists( 'gutenberg-block-bindings', $gutenberg_experiments ) ) { require_once __DIR__ . '/sources/post-meta.php'; require_once __DIR__ . '/sources/post-data.php'; require_once __DIR__ . '/sources/site-data.php'; diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index 70eeee6241fd7..518617e3e622e 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -80,7 +80,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 ) ) ) { 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..282ec880f3176 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -127,14 +127,14 @@ function gutenberg_initialize_experiments_settings() { ); add_settings_field( - 'gutenberg-custom-fields', - __( 'Connections', 'gutenberg' ), + 'gutenberg-block-bindings', + __( '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 8ab816abc7352..531bdffb1566f 100644 --- a/packages/block-editor/src/hooks/custom-fields.js +++ b/packages/block-editor/src/hooks/custom-fields.js @@ -21,7 +21,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, @@ -107,7 +107,7 @@ const withCustomFieldsControls = createHigherOrderComponent( ( BlockEdit ) => { return ( props ) => { const hasCustomFieldsSupport = hasBlockSupport( props.name, - '__experimentalConnections', + '__experimentalBlockBindings', false ); @@ -129,7 +129,7 @@ const withCustomFieldsControls = createHigherOrderComponent( ( BlockEdit ) => { }, 'withCustomFieldsControls' ); if ( - window.__experimentalConnections || + window.__experimentalBlockBindings || window.__experimentalPatternPartialSyncing ) { addFilter( @@ -138,7 +138,7 @@ if ( addAttribute ); } -if ( window.__experimentalConnections ) { +if ( window.__experimentalBlockBindings ) { addFilter( 'editor.BlockEdit', 'core/editor/connections/with-inspector-controls', diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index ead3a6394c216..cf2cbe5f09693 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.metadata?.bindings && Object.values( block.attributes.metadata.bindings ).some( ( binding ) => binding.source.name === 'pattern_attributes' diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json index 85f56f4a838f5..809284b6f7927 100644 --- a/packages/block-library/src/paragraph/block.json +++ b/packages/block-library/src/paragraph/block.json @@ -42,7 +42,6 @@ "text": true } }, - "__experimentalConnections": true, "spacing": { "margin": true, "padding": true, diff --git a/packages/editor/src/components/block-bindings/bindings-ui.js b/packages/editor/src/components/block-bindings/bindings-ui.js index c5ea226d38a26..60baaee61dca2 100644 --- a/packages/editor/src/components/block-bindings/bindings-ui.js +++ b/packages/editor/src/components/block-bindings/bindings-ui.js @@ -238,7 +238,7 @@ const BlockBindingsUI = ( props ) => { ); }; -if ( window.__experimentalConnections ) { +if ( window.__experimentalBlockBindings ) { addFilter( 'blocks.registerBlockType', 'core/block-bindings-ui', diff --git a/packages/editor/src/components/block-bindings/sources/post-data.js b/packages/editor/src/components/block-bindings/sources/post-data.js index 2d27bde25231a..e9d76569904ef 100644 --- a/packages/editor/src/components/block-bindings/sources/post-data.js +++ b/packages/editor/src/components/block-bindings/sources/post-data.js @@ -57,7 +57,7 @@ const PostData = ( props ) => { ); }; -if ( window.__experimentalConnections ) { +if ( window.__experimentalBlockBindings ) { // TODO: Read the context somehow to decide if we should add the source. const withCoreSources = createHigherOrderComponent( diff --git a/packages/editor/src/components/block-bindings/sources/post-meta.js b/packages/editor/src/components/block-bindings/sources/post-meta.js index cd21f241ea896..fab1e352a5465 100644 --- a/packages/editor/src/components/block-bindings/sources/post-meta.js +++ b/packages/editor/src/components/block-bindings/sources/post-meta.js @@ -56,7 +56,7 @@ const PostMeta = ( props ) => { ); }; -if ( window.__experimentalConnections ) { +if ( window.__experimentalBlockBindings ) { // TODO: Read the context somehow to decide if we should add the source. // const data = useSelect( editorStore ); diff --git a/packages/editor/src/components/block-bindings/sources/site-data.js b/packages/editor/src/components/block-bindings/sources/site-data.js index 07f3eb514ab0b..bb9eb7b01c5d4 100644 --- a/packages/editor/src/components/block-bindings/sources/site-data.js +++ b/packages/editor/src/components/block-bindings/sources/site-data.js @@ -47,7 +47,7 @@ const SiteData = ( props ) => { ); }; -if ( window.__experimentalConnections ) { +if ( window.__experimentalBlockBindings ) { const withCoreSources = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const { isSelected } = props; 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 62a8bc10d8baea6eb790c8ad88e7e4ad7c624c3e Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 07:38:57 +0100 Subject: [PATCH 29/30] Remove old UI --- .../block-editor/src/hooks/custom-fields.js | 147 ------------------ packages/block-editor/src/hooks/index.js | 1 - 2 files changed, 148 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 531bdffb1566f..0000000000000 --- a/packages/block-editor/src/hooks/custom-fields.js +++ /dev/null @@ -1,147 +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'; -import { createHigherOrderComponent } from '@wordpress/compose'; - -/** - * 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 CustomFieldsControl( props ) { - 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 ( props.name === 'core/paragraph' ) attributeName = 'content'; - if ( props.name === 'core/image' ) attributeName = 'url'; - - return ( - - - { - if ( nextValue === '' ) { - props.setAttributes( { - connections: undefined, - [ attributeName ]: undefined, - placeholder: undefined, - } ); - } else { - props.setAttributes( { - connections: { - attributes: { - // The attributeName will be either `content` or `url`. - [ attributeName ]: { - // Source will be variable, could be post_meta, user_meta, term_meta, etc. - // Could even be a custom source like a social media attribute. - source: 'meta_fields', - value: nextValue, - }, - }, - }, - [ attributeName ]: undefined, - placeholder: sprintf( - 'This content will be replaced on the frontend by the value of "%s" custom field.', - nextValue - ), - } ); - } - } } - /> - - - ); -} - -/** - * Override the default edit UI to include a new block inspector control for - * assigning a connection to blocks that has support for connections. - * Currently, only the `core/paragraph` block is supported and there is only a relation - * between paragraph content and a custom field. - * - * @param {Component} BlockEdit Original component. - * - * @return {Component} Wrapped component. - */ -const withCustomFieldsControls = createHigherOrderComponent( ( BlockEdit ) => { - return ( props ) => { - const hasCustomFieldsSupport = hasBlockSupport( - props.name, - '__experimentalBlockBindings', - false - ); - - // Check if the current block is a paragraph or image block. - // Currently, only these two blocks are supported. - if ( ! [ 'core/paragraph', 'core/image' ].includes( props.name ) ) { - return ; - } - - return ( - <> - - { hasCustomFieldsSupport && props.isSelected && ( - - ) } - - ); - }; -}, 'withCustomFieldsControls' ); - -if ( - window.__experimentalBlockBindings || - window.__experimentalPatternPartialSyncing -) { - addFilter( - 'blocks.registerBlockType', - 'core/editor/connections/attribute', - addAttribute - ); -} -if ( window.__experimentalBlockBindings ) { - addFilter( - 'editor.BlockEdit', - 'core/editor/connections/with-inspector-controls', - withCustomFieldsControls - ); -} diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index c088216c0645c..62f9dc4619e6c 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -19,7 +19,6 @@ import './position'; import './layout'; import './content-lock-ui'; import './metadata'; -import './custom-fields'; import './block-hooks'; import './block-renaming'; From b95ac5e96931488eb69b1a1c1b4d45b61c0edc68 Mon Sep 17 00:00:00 2001 From: Mario Santos Date: Wed, 20 Dec 2023 15:32:10 +0100 Subject: [PATCH 30/30] Change variables names --- lib/experimental/blocks.php | 20 +- .../components/block-bindings/bindings-ui.js | 214 +++++++++--------- 2 files changed, 119 insertions(+), 115 deletions(-) diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index 518617e3e622e..106fd41d48d1b 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -85,10 +85,10 @@ function wp_enqueue_block_view_script( $block_name, $args ) { ) ) { require_once __DIR__ . '/block-bindings-api/index.php'; - // Whitelist of blocks that support block bindings. + // List of allowed of blocks that support block bindings. // We should 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' ), @@ -126,16 +126,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. @@ -157,8 +157,8 @@ 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 ) { 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 60baaee61dca2..f0dafc80239dd 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' ], @@ -79,114 +79,118 @@ const BlockBindingsUI = ( props ) => { const [ activeSource, setIsActiveSource ] = useState( false ); return ( - { blockBindingsWhitelist[ props.name ].map( ( attribute ) => ( -
- - setIsActiveAttribute( - activeAttribute === attribute - ? false - : attribute - ) - } - className="block-bindings-attribute-picker-button" + { blockBindingsAllowedBlocks[ 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 - ) + return ( + - { - sourceLabel - } - - { isSourceSelected && - fill } - - ); - } - ) } - - ); - } } - - - - - ) } -
- ) ) } + + setIsActiveSource( + isSourceSelected + ? false + : source + ) + } + className="block-bindings-source-picker-button" + > + { + sourceLabel + } + + { isSourceSelected && + fill } + + ); + } + ) } + + ); + } } + + + + + ) } +
+ ) + ) }
); } @@ -243,7 +247,7 @@ if ( window.__experimentalBlockBindings ) { 'blocks.registerBlockType', 'core/block-bindings-ui', ( settings, name ) => { - if ( ! ( name in blockBindingsWhitelist ) ) { + if ( ! ( name in blockBindingsAllowedBlocks ) ) { return settings; }