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

Block Bindings API: Add block bindings PHP registration mechanisms and "Post meta" source under the experimental flag #57249

Merged
merged 28 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2fd22b9
Change block bindings experiment name
SantosGuillamot Dec 20, 2023
d98b4e6
Remove old custom fields UI
SantosGuillamot Dec 20, 2023
4d085c5
Add block bindings PHP logic
SantosGuillamot Dec 20, 2023
a88e9dc
Add pattern source
SantosGuillamot Dec 20, 2023
999784f
Add post meta source
SantosGuillamot Dec 20, 2023
9105898
Adapt partially synced patterns experiment
SantosGuillamot Dec 20, 2023
35bd58d
Add domain to the translations in sources
SantosGuillamot Dec 20, 2023
f57d531
Remove unused post_meta attrs
SantosGuillamot Dec 20, 2023
766295e
Change variable name
SantosGuillamot Dec 20, 2023
34e2e40
Remove a couple of comments
SantosGuillamot Dec 22, 2023
bd099d2
Add image alt to permitted attributes
artemiomorales Jan 3, 2024
a1099d6
Rename variables and clean up code
artemiomorales Jan 3, 2024
0d686c9
Fix bug wherein original pattern values were not being displayed
artemiomorales Jan 8, 2024
b3a21d4
fix typo
michalczaplinski Jan 9, 2024
f0b1fda
Format a comment
michalczaplinski Jan 9, 2024
3704637
fix comment indentation
michalczaplinski Jan 9, 2024
e0604df
remove the .vscode/settings.json file that sneaked in
michalczaplinski Jan 9, 2024
25fa668
Sync with added support for multiple attributes in patterns
artemiomorales Jan 9, 2024
7fb646c
Remove block supports requirement for partial syncing
artemiomorales Jan 9, 2024
34e3f29
Remove __experimentalBlockBindings supports from paragraph
artemiomorales Jan 9, 2024
8ff5352
Simplify source callback and update comments
artemiomorales Jan 9, 2024
6a7074d
Remove erroneous comment
artemiomorales Jan 9, 2024
7e8576c
Revert "Remove block supports requirement for partial syncing"
artemiomorales Jan 10, 2024
62adeb8
Revert "Remove __experimentalBlockBindings supports from paragraph"
artemiomorales Jan 10, 2024
5231b1c
Remove block supports dependency
artemiomorales Jan 10, 2024
5a212e6
Update signature and docblock of register function
artemiomorales Jan 10, 2024
b051f17
Empty-Commit
michalczaplinski Jan 11, 2024
23629de
Update the PHPDoc string
michalczaplinski Jan 11, 2024
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
2 changes: 1 addition & 1 deletion lib/block-supports/pattern.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = 'core/paragraph' === $block_type->name ? true : false;

if ( $pattern_support ) {
if ( ! $block_type->uses_context ) {
Copy link
Contributor

@artemiomorales artemiomorales Jan 9, 2024

Choose a reason for hiding this comment

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

If we're removing block supports. we should probably remove this entire file, which exists inside the block-supports/ directory, and put its logic elsewhere. EDIT: Since we may be adding block supports in the future, perhaps removing this file isn't necessary.

Expand Down
110 changes: 110 additions & 0 deletions lib/experimental/block-bindings/html-processing.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php
/**
* Define the mechanism to replace 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.
switch ( $block_type->attributes[ $block_attr ]['source'] ) {
case 'html':
case 'rich-text':
$block_reader = new WP_HTML_Tag_Processor( $block_content );

// TODO: Support for CSS selectors whenever they are ready in the HTML API.
// In the meantime, support comma-separated selectors by exploding them into an array.
$selectors = explode( ',', $block_type->attributes[ $block_attr ]['selector'] );
// Add a bookmark to the first tag to be able to iterate over the selectors.
$block_reader->next_tag();
$block_reader->set_bookmark( 'iterate-selectors' );

// TODO: This shouldn't be needed when the `set_inner_html` function is ready.
// Store the parent tag and its attributes to be able to restore them later in the button.
// The button block has a wrapper while the paragraph and heading blocks don't.
if ( 'core/button' === $block_name ) {
$button_wrapper = $block_reader->get_tag();
$button_wrapper_attribute_names = $block_reader->get_attribute_names_with_prefix( '' );
$button_wrapper_attrs = array();
foreach ( $button_wrapper_attribute_names as $name ) {
$button_wrapper_attrs[ $name ] = $block_reader->get_attribute( $name );
}
}

foreach ( $selectors as $selector ) {
// If the parent tag, or any of its children, matches the selector, replace the HTML.
if ( strcasecmp( $block_reader->get_tag( $selector ), $selector ) === 0 || $block_reader->next_tag(
array(
'tag_name' => $selector,
)
) ) {
$block_reader->release_bookmark( 'iterate-selectors' );

// TODO: Use `set_inner_html` method whenever it's ready in the HTML API.
// Until then, it is hardcoded for the paragraph, heading, and button blocks.
// Store the tag and its attributes to be able to restore them later.
$selector_attribute_names = $block_reader->get_attribute_names_with_prefix( '' );
$selector_attrs = array();
foreach ( $selector_attribute_names as $name ) {
$selector_attrs[ $name ] = $block_reader->get_attribute( $name );
}
$selector_markup = "<$selector>" . esc_html( $source_value ) . "</$selector>";
$amended_content = new WP_HTML_Tag_Processor( $selector_markup );
$amended_content->next_tag();
foreach ( $selector_attrs as $attribute_key => $attribute_value ) {
$amended_content->set_attribute( $attribute_key, $attribute_value );
}
if ( 'core/paragraph' === $block_name || 'core/heading' === $block_name ) {
return $amended_content->get_updated_html();
}
if ( 'core/button' === $block_name ) {
$button_markup = "<$button_wrapper>{$amended_content->get_updated_html()}</$button_wrapper>";
$amended_button = new WP_HTML_Tag_Processor( $button_markup );
$amended_button->next_tag();
foreach ( $button_wrapper_attrs as $attribute_key => $attribute_value ) {
$amended_button->set_attribute( $attribute_key, $attribute_value );
}
return $amended_button->get_updated_html();
}
} else {
$block_reader->seek( 'iterate-selectors' );
}
}
$block_reader->release_bookmark( 'iterate-selectors' );
return $block_content;

case 'attribute':
$amended_content = new WP_HTML_Tag_Processor( $block_content );
if ( ! $amended_content->next_tag(
array(
// TODO: build the query from CSS selector.
'tag_name' => $block_type->attributes[ $block_attr ]['selector'],
)
) ) {
return $block_content;
}
$amended_content->set_attribute( $block_type->attributes[ $block_attr ]['attribute'], esc_attr( $source_value ) );
return $amended_content->get_updated_html();
break;

default:
return $block_content;
break;
}
return;
}
}
20 changes: 20 additions & 0 deletions lib/experimental/block-bindings/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-block-bindings', $gutenberg_experiments ) ) {
require_once __DIR__ . '/sources/post-meta.php';
}
}
32 changes: 32 additions & 0 deletions lib/experimental/block-bindings/sources/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
/**
* Define the mechanism to add new sources available in the block bindings API.
*
* @package gutenberg
*/

global $block_bindings_sources;
$block_bindings_sources = array();
Comment on lines +8 to +9
Copy link
Contributor

Choose a reason for hiding this comment

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

A lot of the registries in WordPress use the approach of having a singleton class, so it could be something to replicate here.

Some examples:

If a class is used, it's worth thinking about how it might be extended by the gutenberg codebase when shipped in core if changes need to be made to block bindings.

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! @michalczaplinski @artemiomorales It'd be great if you can explore this possibility.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks! Agreed that we should begin improving the code as we look to bring this into Core.

In order to keep things moving and start getting more feedback on the feature, and since this is still an experiment, I think we can get this PR merged first then immediately begin another PR to refactor and start moving the logic over to a Block Bindings class 👍

Copy link
Member

Choose a reason for hiding this comment

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

Great feedback, @talldan. That’s the direction I would recommend, too. I agree with @artemiomorales as well that this refactoring might be easier to perform as a focused effort in a follow up PR. All functionality is behind an experiment. Well, another option is to open a new branch targeting this branch if you decide to spend more time polishing this PR.

Some inspiration for how to architect the class and helper functions can be borrowed from the recent work on ES Modules API in WordPress/wordpress-develop#5818 where @felixarntz did an outstanding job explaining several good practices to take into account. That combined with prior work referenced by Dan should give you necessary insights to refactor the code.

if ( ! function_exists( 'register_block_bindings_source' ) ) {
/**
* Function to register a new source.
*
* @param string $source_name The name of the source.
* @param string $label The label of the source.
* @param callable(object, object, string): string $apply - The callback executed when the source is processed during block rendering.
* - object $source_attrs: Object containing source ID used to look up the override value, i.e. {"value": "{ID}"}.
* - object $block_instance: The block instance.
* - string $attribute_name: The name of an attribute used to retrieve an override value from the block context.
* The callable should return a string that will be used to override the block's original value.
*
*
* @return void
*/
function register_block_bindings_source( $source_name, $label, $apply ) {
global $block_bindings_sources;
$block_bindings_sources[ $source_name ] = array(
'label' => $label,
'apply' => $apply,
);
}
}
21 changes: 21 additions & 0 deletions lib/experimental/block-bindings/sources/pattern.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?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_instance, $attribute_name ) {
if ( ! _wp_array_get( $block_instance->attributes, array( 'metadata', 'id' ), false ) ) {
return null;
}
$block_id = $block_instance->attributes['metadata']['id'];
return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id, $attribute_name ), null );
};
register_block_bindings_source(
'pattern_attributes',
__( 'Pattern Attributes', 'gutenberg' ),
$pattern_source_callback
);
}
25 changes: 25 additions & 0 deletions lib/experimental/block-bindings/sources/post-meta.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
/**
* Add the post_meta source to the block bindings API.
*
* @package gutenberg
*/

if ( function_exists( 'register_block_bindings_source' ) ) {
$post_meta_source_callback = function ( $source_attrs ) {
// Use the postId attribute if available
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();
}

return get_post_meta( $post_id, $source_attrs['value'], true );
};
register_block_bindings_source(
'post_meta',
__( 'Post Meta', 'gutenberg' ),
$post_meta_source_callback
);
}
Loading
Loading