-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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 Hooks (a.k.a. Auto-inserting Blocks) #5158
Changes from all commits
b926b86
f7465b9
7fda6f5
3ad1fd7
cdcfbcc
c897d86
5013547
c8115dc
37496a0
600e55d
b1a82fe
25b1ed4
ea1f53f
8fc29bc
5cd2ded
0267cc9
67e6a34
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -513,6 +513,44 @@ function _inject_theme_attribute_in_block_template_content( $template_content ) | |||||
return $template_content; | ||||||
} | ||||||
|
||||||
/** | ||||||
* Returns a function that... | ||||||
* | ||||||
* @since 6.4.0 | ||||||
* @access private | ||||||
* | ||||||
* @param WP_Block_Template $block_template a block template. | ||||||
* @return callable A function that returns a block. | ||||||
*/ | ||||||
function _parsed_block_visitor( $block_template ) { | ||||||
return function( $block ) use ( $block_template ) { | ||||||
if ( | ||||||
'core/template-part' === $block['blockName'] && | ||||||
! isset( $block['attrs']['theme'] ) | ||||||
) { | ||||||
$block['attrs']['theme'] = get_stylesheet(); | ||||||
} | ||||||
|
||||||
$hooked_blocks = get_hooked_blocks( $block['blockName'] ); | ||||||
foreach ( $hooked_blocks as $hooked_block_type => $relative_position ) { | ||||||
$hooked_block = array( | ||||||
'blockName' => $hooked_block_type, | ||||||
'attrs' => array(), | ||||||
'innerHTML' => '', | ||||||
'innerContent' => array(), | ||||||
'innerBlocks' => array(), | ||||||
); | ||||||
// Need to pass full current block, (potentially its parent -- for sibiling insertion), relative position, and hooked_block. | ||||||
$block = insert_hooked_block( $hooked_block, $relative_position, $block ); | ||||||
/** | ||||||
* This filter allows modifiying the auto-inserting behavior... | ||||||
*/ | ||||||
$block = apply_filters( 'insert_hooked_block', $block, $hooked_blocks[ $hooked_block_type ], $block_template ); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might use separate filters for template and pattern insertion
Suggested change
|
||||||
} | ||||||
return $block; | ||||||
}; | ||||||
} | ||||||
|
||||||
/** | ||||||
* Parses a block template and removes the theme attribute from each template part. | ||||||
* | ||||||
|
@@ -565,7 +603,6 @@ function _build_block_template_result_from_file( $template_file, $template_type | |||||
$template = new WP_Block_Template(); | ||||||
$template->id = $theme . '//' . $template_file['slug']; | ||||||
$template->theme = $theme; | ||||||
$template->content = _inject_theme_attribute_in_block_template_content( $template_content ); | ||||||
$template->slug = $template_file['slug']; | ||||||
$template->source = 'theme'; | ||||||
$template->type = $template_type; | ||||||
|
@@ -589,6 +626,10 @@ function _build_block_template_result_from_file( $template_file, $template_type | |||||
$template->area = $template_file['area']; | ||||||
} | ||||||
|
||||||
$blocks = parse_blocks( $template_content ); | ||||||
$visitor = _parsed_block_visitor( $template ); | ||||||
$template->content = serialize_blocks( $blocks, $visitor ); | ||||||
|
||||||
return $template; | ||||||
} | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -513,6 +513,39 @@ function register_block_type_from_metadata( $file_or_folder, $args = array() ) { | |
} | ||
} | ||
|
||
if ( ! empty( $metadata['blockHooks'] ) ) { | ||
/** | ||
* Map camelCased position string (from block.json) to snake_cased block type position. | ||
* | ||
* @var array | ||
*/ | ||
$position_mappings = array( | ||
'before' => 'before', | ||
'after' => 'after', | ||
'firstChild' => 'first_child', | ||
'lastChild' => 'last_child', | ||
); | ||
|
||
$settings['block_hooks'] = array(); | ||
foreach ( $metadata['blockHooks'] as $anchor_block_name => $position ) { | ||
// Avoid infinite recursion (hooking to itself). | ||
if ( $metadata['name'] === $anchor_block_name ) { | ||
_doing_it_wrong( | ||
__METHOD__, | ||
__( 'Cannot hook block to itself.', 'gutenberg' ), | ||
'6.4.0' | ||
); | ||
continue; | ||
} | ||
|
||
if ( ! isset( $position_mappings[ $position ] ) ) { | ||
continue; | ||
} | ||
|
||
$settings['block_hooks'][ $anchor_block_name ] = $position_mappings[ $position ]; | ||
} | ||
} | ||
|
||
if ( ! empty( $metadata['render'] ) ) { | ||
$template_path = wp_normalize_path( | ||
realpath( | ||
|
@@ -794,16 +827,22 @@ function get_comment_delimited_block_content( $block_name, $block_attributes, $b | |
* instead preserve the markup as parsed. | ||
* | ||
* @since 5.3.1 | ||
* @since 6.4.0 The `$callback` parameter was added. | ||
* | ||
* @param array $block A representative array of a single parsed block object. See WP_Block_Parser_Block. | ||
* @param array $block A representative array of a single parsed block object. See WP_Block_Parser_Block. | ||
* @param string $callback Optional. Callback to run on the block before serialization. Default null. | ||
* @return string String of rendered HTML. | ||
*/ | ||
function serialize_block( $block ) { | ||
function serialize_block( $block, $callback = null ) { | ||
if ( is_callable( $callback ) ) { | ||
$block = call_user_func( $callback, $block ); | ||
} | ||
|
||
$block_content = ''; | ||
|
||
$index = 0; | ||
foreach ( $block['innerContent'] as $chunk ) { | ||
$block_content .= is_string( $chunk ) ? $chunk : serialize_block( $block['innerBlocks'][ $index++ ] ); | ||
$block_content .= is_string( $chunk ) ? $chunk : serialize_block( $block['innerBlocks'][ $index++ ], $callback ); | ||
} | ||
|
||
if ( ! is_array( $block['attrs'] ) ) { | ||
|
@@ -822,12 +861,18 @@ function serialize_block( $block ) { | |
* parsed blocks. | ||
* | ||
* @since 5.3.1 | ||
* @since 6.4.0 The `$callback` parameter was added. | ||
* | ||
* @param array[] $blocks An array of representative arrays of parsed block objects. See serialize_block(). | ||
* @param array[] $blocks An array of representative arrays of parsed block objects. See serialize_block(). | ||
* @param string $callback Optional. Callback to run on the blocks before serialization. Default null. | ||
* @return string String of rendered HTML. | ||
*/ | ||
function serialize_blocks( $blocks ) { | ||
return implode( '', array_map( 'serialize_block', $blocks ) ); | ||
function serialize_blocks( $blocks, $callback = null ) { | ||
$result = ''; | ||
foreach ( $blocks as $block ) { | ||
$result .= serialize_block( $block, $callback ); | ||
}; | ||
return $result; | ||
} | ||
|
||
/** | ||
|
@@ -1620,3 +1665,102 @@ function get_comments_pagination_arrow( $block, $pagination_type = 'next' ) { | |
} | ||
return null; | ||
} | ||
|
||
/** | ||
* Retrieves block types (and positions) hooked into the given block. | ||
* | ||
* @since 6.4.0 | ||
* | ||
* @param string $name Block type name including namespace. | ||
* @return array Associative array of `$block_type_name => $position` pairs. | ||
*/ | ||
function get_hooked_blocks( $name ) { | ||
$block_types = WP_Block_Type_Registry::get_instance()->get_all_registered(); | ||
$hooked_blocks = array(); | ||
foreach ( $block_types as $block_type ) { | ||
foreach ( $block_type->block_hooks as $anchor_block_type => $relative_position ) { | ||
if ( $anchor_block_type === $name ) { | ||
$hooked_blocks[ $block_type->name ] = $relative_position; | ||
} | ||
} | ||
} | ||
return $hooked_blocks; | ||
} | ||
Comment on lines
+1677
to
+1688
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gziolo and I discussed adding a cache for this to (We might want to implement the cache in a separate class for better encapsulation and call the relevant class methods from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. First version is ready with #5239. Let's integrate it with the auto-insertion logic first, because it might impact how it's going to be optimized. |
||
|
||
function insert_hooked_blocks( $block ) { | ||
$hooked_blocks = get_hooked_blocks( $block['blockName'] ); | ||
foreach ( $hooked_blocks as $hooked_block_type => $relative_position ) { | ||
$hooked_block = array( | ||
'blockName' => $hooked_block_type, | ||
'attrs' => array(), | ||
'innerHTML' => '', | ||
'innerContent' => array(), | ||
'innerBlocks' => array(), | ||
); | ||
// Need to pass full current block, (possibly its parent), relative position, and hooked_block. | ||
$block = insert_hooked_block( $hooked_block, $relative_position, $block ); | ||
Comment on lines
+1691
to
+1701
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One way to implement sibling insertion could be by changing this code by treating the two cases separately (child vs sibling insertion) -- a bit more similar to what we're doing in GB: $hooked_blocks = get_hooked_blocks( $block['blockName'] );
$sibling_hooked_blocks = array_filter( $hooked_blocks, ... ); // position is 'before' or 'after'
$child_hooked_blocks = array_filter( $hooked_blocks , ... ); // position is 'first_child' or 'last_child'
foreach ( $child_hooked_blocks as $hooked_block_type => $relative_position ) {
/* Same implementation as before */
}
foreach ( $sibling_hooked_blocks as $hooked_block_type => $relative_position ) {
$anchor_block_index = array_search( $hooked_block_type, array_column( $block['innerBlocks'], 'blockName' ), true );
if ( false !== $anchor_block_index && ( 'after' === $relative_position || 'before' === $relative_position ) )
{
/* See GB's gutenberg_insert_hooked_block */
} (Rough draft; we might want to move some of that logic into There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The crucial conceptual difference is that for child insertion, the anchor block is the one that needs to be modified (by inserting the first or last child into its inner blocks), whereas for sibling insertion, it isn't; instead, it is the anchor block's parent -- which is an additional piece of information that we need in that case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They are definitely different, the case for I still don't have a clear picture about the sibling insertion so whatever works, should be implemented initially. We can discuss further optimization later, but I can't tell based on the abstract code as it's a very complex use case. I'm still impressed how you managed to integrate it using only existing filters in the plugin. |
||
} | ||
return $block; | ||
} | ||
|
||
/** | ||
* Auto-inserts a block next to a given "anchor" block. | ||
* | ||
* This is a helper function used in the implementation of block hooks. | ||
* It is not meant for public use. | ||
* | ||
* The auto-inserted block can be inserted before or after the anchor block, | ||
* or as the first or last child of the anchor block. | ||
* | ||
* Note that the this mutates the automatically inserted block's | ||
* designated parent block by inserting into the parent's `innerBlocks` array, | ||
* and by updating the parent's `innerContent` array accordingly. | ||
* | ||
* @param array $inserted_block The block to insert. | ||
* @param string $relative_position The position relative to the given block. | ||
* Can be 'before', 'after', 'first_child', or 'last_child'. | ||
* @param array $anchor_block The automatically inserted block will be inserted next to instances of this block type. | ||
* @return array The modified anchor block. | ||
*/ | ||
function insert_hooked_block( $inserted_block, $relative_position, $anchor_block ) { | ||
if ( 'first_child' === $relative_position ) { | ||
array_unshift( $anchor_block['innerBlocks'], $inserted_block ); | ||
/* | ||
* Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`) | ||
* when rendering blocks, we also need to prepend a value (`null`, to mark a block | ||
* location) to that array. | ||
*/ | ||
array_unshift( $anchor_block['innerContent'], null ); | ||
} elseif ( 'last_child' === $relative_position ) { | ||
array_push( $anchor_block['innerBlocks'], $inserted_block ); | ||
// Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`) | ||
// when rendering blocks, we also need to prepend a value (`null`, to mark a block | ||
// location) to that array. | ||
array_push( $anchor_block['innerContent'], null ); | ||
} | ||
Comment on lines
+1726
to
+1740
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to update based on WordPress/gutenberg#54349! |
||
return $anchor_block; | ||
|
||
// TODO: Implement sibling insertion. We might need to pass the anchor's parent block for this to work. | ||
// | ||
// $anchor_block_index = array_search( $anchor_block, $anchor_block_parent['innerBlocks'], true ); // Will this work? | ||
// if ( false !== $anchor_block_index && ( 'after' === $relative_position || 'before' === $relative_position ) ) { | ||
// if ( 'after' === $relative_position ) { | ||
// $anchor_block_index++; | ||
// } | ||
// array_splice( $anchor_block_parent['innerBlocks'], $anchor_block_index, 0, array( $inserted_block ) ); | ||
|
||
// // Find matching `innerContent` chunk index. | ||
// $chunk_index = 0; | ||
// while ( $anchor_block_index > 0 ) { | ||
// if ( ! is_string( $anchor_block_parent['innerContent'][ $chunk_index ] ) ) { | ||
// $anchor_block_index--; | ||
// } | ||
// $chunk_index++; | ||
// } | ||
// // Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`) | ||
// // when rendering blocks, we also need to insert a value (`null`, to mark a block | ||
// // location) into that array. | ||
// array_splice( $anchor_block_parent['innerContent'], $chunk_index, 0, array( null ) ); | ||
// } | ||
// return $anchor_block; | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -165,7 +165,11 @@ public function get_registered( $pattern_name ) { | |||||
return null; | ||||||
} | ||||||
|
||||||
return $this->registered_patterns[ $pattern_name ]; | ||||||
$pattern = $this->registered_patterns[ $pattern_name ]; | ||||||
$blocks = parse_blocks( $pattern['content'] ); | ||||||
$visitor = _parsed_block_visitor( $pattern ); // TODO: Should we use different functions for template vs pattern? | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably something like
Suggested change
In that function, apply a |
||||||
$pattern['content'] = serialize_blocks( $blocks, $visitor ); | ||||||
Comment on lines
+168
to
+171
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO (unrelated to Block Hooks): We probably want some kind of filter here to allow modifying the pattern content -- otherwise, there's no way for plugins (and GB) to modify patterns. |
||||||
return $pattern; | ||||||
} | ||||||
|
||||||
/** | ||||||
|
@@ -178,11 +182,19 @@ public function get_registered( $pattern_name ) { | |||||
* and per style. | ||||||
*/ | ||||||
public function get_all_registered( $outside_init_only = false ) { | ||||||
return array_values( | ||||||
$patterns = array_values( | ||||||
$outside_init_only | ||||||
? $this->registered_patterns_outside_init | ||||||
: $this->registered_patterns | ||||||
); | ||||||
|
||||||
foreach ( $patterns as $index => $pattern ) { | ||||||
$blocks = parse_blocks( $pattern['content'] ); | ||||||
$visitor = _parsed_block_visitor( $pattern ); // TODO: Should we use different functions for template vs pattern? | ||||||
$patterns[ $index ]['content'] = serialize_blocks( $blocks, $visitor ); | ||||||
} | ||||||
Comment on lines
+192
to
+195
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO: Reuse logic from |
||||||
|
||||||
return $patterns; | ||||||
} | ||||||
|
||||||
/** | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.