Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add block bindings API basis and metadata source #56867

Closed
Closed
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4100933
Add the basis of the Block Bindings API
SantosGuillamot Nov 29, 2023
edee28a
Add the first PHP logic of the `metadata` source
SantosGuillamot Nov 29, 2023
01189c7
Update metadata folder structure
SantosGuillamot Nov 30, 2023
5e38246
Add initial version of the block bindings editor UI
SantosGuillamot Nov 30, 2023
35d03dd
Filter to add the bindings attribute automatically
SantosGuillamot Nov 30, 2023
a19009c
Logic to handle different attributes in the editor
SantosGuillamot Nov 30, 2023
87e333d
Fetch metadata from REST API correctly
SantosGuillamot Nov 30, 2023
081ed5c
Use const instead of let
SantosGuillamot Nov 30, 2023
ecd1377
Move bindings inside metadata attribute
SantosGuillamot Dec 1, 2023
7543606
Use getEntityRecord to fetch the REST API
SantosGuillamot Dec 1, 2023
1017d4d
Merge branch `trunk` into `add/block-bindings-api-basis-and-metadata-…
SantosGuillamot Dec 1, 2023
8096402
Add helper to update bindings in the editor
SantosGuillamot Dec 1, 2023
5d5c68c
Use editor helper in metadata source
SantosGuillamot Dec 1, 2023
24ff0d2
Adapt pattern source PHP side
SantosGuillamot Dec 1, 2023
6c7fe63
Adapt pattern source editor side
SantosGuillamot Dec 1, 2023
6176f15
Change bindings format to be an object
SantosGuillamot Dec 3, 2023
feaf22d
Fix selected metadata attribute
SantosGuillamot Dec 3, 2023
4689e76
Slightly modify the bindings property syntax
SantosGuillamot Dec 3, 2023
f6bc2c0
Add support for heading and button blocks
SantosGuillamot Dec 4, 2023
3590330
Add post and site data
SantosGuillamot Dec 4, 2023
5b5546f
Support for button and heading selectors
SantosGuillamot Dec 18, 2023
01d834b
Change bindings syntax to use name and attributes
SantosGuillamot Dec 18, 2023
d215035
Add extensibility to the Block Bindings UI in the editor
SantosGuillamot Dec 18, 2023
159cb0a
Add post-data and site-data sources in the editor
SantosGuillamot Dec 18, 2023
1178ba8
Add core sources in PHP
SantosGuillamot Dec 18, 2023
006872f
Abstract block bindings UI
SantosGuillamot Dec 19, 2023
cc59e1b
Remove extra dependency
SantosGuillamot Dec 19, 2023
4f651c2
Modify `register_block_bindings_source` syntax
SantosGuillamot Dec 20, 2023
7dd4368
Change block bindings experiment name
SantosGuillamot Dec 20, 2023
62a8bc1
Remove old UI
SantosGuillamot Dec 20, 2023
b95ac5e
Change variables names
SantosGuillamot Dec 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions lib/experimental/block-bindings-api/html-processing.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php
/**
* Define the mechanism to replpace the HTML depending on the block attributes.
*
* @package gutenberg
*/

if ( ! function_exists( 'block_bindings_replace_html' ) ) {
/**
* Depending on the block attributes, replace the proper HTML based on the value returned by the source.
*
* @param string $block_content Block Content.
* @param string $block_name The name of the block to process.
* @param string $block_attr The attribute of the block we want to process.
* @param string $source_value The value used to replace the HTML.
*/
function block_bindings_replace_html( $block_content, $block_name, $block_attr, $source_value ) {
$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name );
if ( null === $block_type ) {
return;
}

// Depending on the attribute source, the processing will be different.
// TODO: Get the type from the block attribute definition and modify/validate the value returned by the source if needed.
switch ( $block_type->attributes[ $block_attr ]['source'] ) {
case 'html':
$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'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on #56704, this might not work for the heading block, but you could borrow from @kevin940726's approach in that PR of exploding the selector, at least until the HTML tag processor supports CSS selectors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for sharing! As part of this commit, I'm using that approach for supporting the different selectors we can have for the heading and the button.

)
) ) {
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 ) . "</$tag_name>";
$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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mentioned in #56704 (comment) that this unfortunately disregards any markup before or after the found tag, so it won't work for button which is a <button> inside a <div> wrapper. It strips the <div> wrapper.

I realize we need $p->set_inner_html to support it properly, but in the meantime it might be worth removing button block support.

Copy link
Contributor Author

@SantosGuillamot SantosGuillamot Dec 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're totally right. I just made a commit that tries to make it work with heading and button structures: link. Whenever the set_inner_html function is ready we can change the logic. But as of right now, we only plan to support a few core blocks. I don't see a problem having it hardcoded because we know their HTML structure.

Let me know if that works for you 🙂

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;
}
}
20 changes: 20 additions & 0 deletions lib/experimental/block-bindings-api/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php
/**
* Require the necessary files.
*
* @package gutenberg
*/

require_once __DIR__ . '/sources/index.php';
require_once __DIR__ . '/html-processing.php';

// Register the sources.
$gutenberg_experiments = get_option( 'gutenberg-experiments' );
if ( $gutenberg_experiments ) {
if ( array_key_exists( 'gutenberg-pattern-partial-syncing', $gutenberg_experiments ) ) {
require_once __DIR__ . '/sources/pattern.php';
}
if ( array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) ) {
require_once __DIR__ . '/sources/metadata.php';
}
}
23 changes: 23 additions & 0 deletions lib/experimental/block-bindings-api/sources/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
/**
* Define the mechanism to add new sources available in the block bindings API.
*
* @package gutenberg
*/

global $block_bindings_sources;
$block_bindings_sources = array();
if ( ! function_exists( 'register_block_bindings_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.
global $block_bindings_sources;
$block_bindings_sources[ $source_name ] = array( 'apply_source' => $source_callback );
}
}
29 changes: 29 additions & 0 deletions lib/experimental/block-bindings-api/sources/metadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
/**
* Add the metadata source to the block bindings API.
*
* @package gutenberg
*/

if ( function_exists( 'register_block_bindings_source' ) ) {
$metadata_source_callback = function ( $source_attrs, $block_content, $block, $block_instance ) {
// Use the postId attribute if available, otherwise use the context.
if ( isset( $source_attrs['postId'] ) ) {
$post_id = $source_attrs['postId'];
} else {
// I tried using $block_instance->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 );
}
17 changes: 17 additions & 0 deletions lib/experimental/block-bindings-api/sources/pattern.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
/**
* Add the metadata source to the block bindings API.
*
* @package gutenberg
*/

if ( function_exists( 'register_block_bindings_source' ) ) {
$pattern_source_callback = function ( $source_attrs, $block_content, $block, $block_instance ) {
if ( ! _wp_array_get( $block_instance->attributes, array( 'metadata', 'id' ), false ) ) {
return;
}
$block_id = $block_instance->attributes['metadata']['id'];
return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id ), false );
};
register_block_bindings_source( 'pattern_attributes', $pattern_source_callback );
}
164 changes: 63 additions & 101 deletions lib/experimental/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,123 +78,85 @@ 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-pattern-partial-syncing', $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;
}

// Skip if the source value is not "meta_fields" or "pattern_attributes".
if ( 'meta_fields' !== $attribute_value['source'] && 'pattern_attributes' !== $attribute_value['source'] ) {
continue;
}

// If the attribute does not have a source, skip it.
if ( ! isset( $block_type->attributes[ $attribute_name ]['source'] ) ) {
continue;
require_once __DIR__ . '/block-bindings-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/heading' => array( 'content' ),
'core/image' => array( 'url', 'title' ),
'core/button' => array( 'url', 'text' ),
);
if ( ! function_exists( 'process_block_bindings' ) ) {
/**
* Process the block bindings attribute.
*
* @param string $block_content Block Content.
* @param array $block Block attributes.
* @param WP_Block $block_instance The block instance.
*/
function process_block_bindings( $block_content, $block, $block_instance ) {
// If the block doesn't have the bindings property, return.
if ( ! isset( $block['attrs']['metadata']['bindings'] ) ) {
return $block_content;
}

if ( 'pattern_attributes' === $attribute_value['source'] ) {
if ( ! _wp_array_get( $block_instance->attributes, array( 'metadata', 'id' ), false ) ) {
// TODO: Review the bindings syntax.
// Assuming the following format for the bindings property of the "metadata" attribute:
//
// "bindings": {
// "title": {
// "source": {
// "id": "metadata",
// "params": { "value": "text_custom_field" }
// }
// },
// "url": {
// "source": {
// "id": "metadata",
// "params": { "value": "text_custom_field" }
// }
// }
// },
// .
global $block_bindings_whitelist;
global $block_bindings_sources;
$modified_block_content = $block_content;
foreach ( $block['attrs']['metadata']['bindings'] as $binding_attribute => $binding_source ) {
if ( ! isset( $block_bindings_whitelist[ $block['blockName'] ] ) ) {
continue;
}

$custom_value = $connection_sources[ $attribute_value['source'] ]( $block_instance );
} else {
// If the attribute does not specify the name of the custom field, skip it.
if ( ! isset( $attribute_value['value'] ) ) {
if ( ! in_array( $binding_attribute, $block_bindings_whitelist[ $block['blockName'] ], true ) ) {
continue;
}

// Get the content from the connection source.
$custom_value = $connection_sources[ $attribute_value['source'] ](
$block_instance,
$attribute_value['value']
);
}

if ( false === $custom_value ) {
continue;
}

$tags = new WP_HTML_Tag_Processor( $block_content );
$found = $tags->next_tag(
array(
// TODO: In the future, when blocks other than Paragraph and Image are
// supported, we should build the full query from CSS selector.
'tag_name' => $block_type->attributes[ $attribute_name ]['selector'],
)
);
if ( ! $found ) {
return $block_content;
}
$tag_name = $tags->get_tag();
$markup = "<$tag_name>$custom_value</$tag_name>";
$updated_tags = new WP_HTML_Tag_Processor( $markup );
$updated_tags->next_tag();
// 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']['id'] ]['apply_source'] ) ) {
return $block_content;
}
$source_value = $block_bindings_sources[ $binding_source['source']['id'] ]['apply_source']( $binding_source['source']['params'], $block_content, $block, $block_instance );
if ( false === $source_value ) {
return $block_content;
}

// 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 ) );
// 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 );
}
19 changes: 0 additions & 19 deletions lib/experimental/connection-sources/index.php

This file was deleted.

12 changes: 12 additions & 0 deletions packages/block-editor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,18 @@ _Properties_

Ensures that the text selection keeps the same vertical distance from the viewport during keyboard events within this component. The vertical distance can vary. It is the last clicked or scrolled to position.

### updateBlockBindingsAttribute

Helper to update the bindings attribute used by the Block Bindings API.

_Parameters_

- _blockAttributes_ `Object`: - The original block attributes.
- _setAttributes_ `Function`: - setAttributes function to modify the bindings property.
- _updatingAttribute_ `string`: - The attribute in the bindings object to update.
- _sourceName_ `string`: - The source name added to the bindings property.
- _sourceParams_ `string`: - The source params added to the bindings property.

### URLInput

_Related_
Expand Down
1 change: 1 addition & 0 deletions packages/block-editor/src/utils/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as transformStyles } from './transform-styles';
export * from './block-variation-transforms';
export { default as getPxFromCssUnit } from './parse-css-unit-to-px';
export * from './update-bindings';
Loading
Loading