-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Experiment: Auto-inserting blocks on the frontend and in the editor (via REST API) #51449
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
Changes from all commits
6e3e49a
c73a190
919a2fe
fbb1c6d
da643e8
2f78d81
7f7faee
4d21f5c
5e3bfd0
954eeed
52efdf0
7d7f70a
0f564f5
f3e566b
aa65ac3
bfe97c2
edffdc1
b610c37
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 |
---|---|---|
@@ -0,0 +1,246 @@ | ||
<?php | ||
/** | ||
* Auto-inserting blocks. | ||
* | ||
* @package gutenberg | ||
*/ | ||
|
||
/** | ||
* Return a function that auto-inserts blocks relative to a given block. | ||
dmsnell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* | ||
* @param array $inserted_block The block to insert. | ||
* @param string $relative_position The position relative to the given block. | ||
ockham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* @param string $anchor_block The block to insert relative to. | ||
dmsnell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* @return callable A function that accepts a block's content and returns the content with the inserted block. | ||
*/ | ||
function gutenberg_auto_insert_block( $inserted_block, $relative_position, $anchor_block ) { | ||
return function( $block ) use ( $inserted_block, $relative_position, $anchor_block ) { | ||
if ( $anchor_block === $block['blockName'] ) { | ||
if ( 'first_child' === $relative_position ) { | ||
array_unshift( $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( $block['innerContent'], null ); | ||
} elseif ( 'last_child' === $relative_position ) { | ||
array_push( $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( $block['innerContent'], null ); | ||
} | ||
return $block; | ||
} | ||
|
||
$anchor_block_index = array_search( $anchor_block, array_column( $block['innerBlocks'], 'blockName' ), true ); | ||
if ( false !== $anchor_block_index && ( 'after' === $relative_position || 'before' === $relative_position ) ) { | ||
if ( 'after' === $relative_position ) { | ||
$anchor_block_index++; | ||
} | ||
array_splice( $block['innerBlocks'], $anchor_block_index, 0, array( $inserted_block ) ); | ||
|
||
// Find matching `innerContent` chunk index. | ||
$chunk_index = 0; | ||
while ( $anchor_block_index > 0 ) { | ||
if ( ! is_string( $block['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( $block['innerContent'], $chunk_index, 0, array( null ) ); | ||
} | ||
return $block; | ||
}; | ||
} | ||
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. how often do we know in advance the block we want to insert and then need to create a function to insert that? what if we instead made a function that actually inserts the block and skipped the anonymous function creation? if we know we want to always insert the same block we can create our callback with a static value or an enclosed value. also is this function different than hooking into the existing filters? I wonder if there's reason to insert after more than one block type, in which case we start duplicating code or adding new interfaces, but we already have the ability to do this if we use something like 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.
Not sure I'm reading you correctly, but the point of this API is to allow 3rd parties to have their blocks auto-inserted next to pretty much any other block, so we can't really know in advance what blocks will need to be inserted.
It's definitely possible to insert a block after (or next to) more than one block type, e.g. a like button after Post Content, or as Comment Template's last child. An earlier version of this (#51294) used That aside, I'm not sure how a callback for 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.
this was kind of what led me to ask in the first place, because this function signature implies that we have to know in advance the block we want to insert plus the block type we want to anchor it to.
I wasn't commenting on the mechanism to do this, but the interface we present, which immediately creates an anonymous function and hides the ability to make decisions about insertion based on the current block. it seems like if we want to auto-insert after two block types that we have to call I'm wondering if we were to invert this so that the consumer passed in the logic for where to insert if it could be less constrained and less complicated. we could pass in an anonymous function which returns a relative position and block to insert, if one ought to be inserted, or a filter that does the same. add_filter( 'block_auto_insert', function ( $block ) {
if ( $block['attrs']['show_thing'] ) {
return array( 'where' => 'after', 'block' => array( … );
}
} ); add_filter( 'gutenberg_serialize_block', function ( $block ) {
$auto_insert = apply_filter( 'block_auto_insert', $block );
if ( null === $auto_insert ) {
return $block;
}
list( 'where' => $where, 'block' => $inserted_block ) = $auto_insert;
switch ( $where ) {
case 'before':
…
…
}
} ); |
||
|
||
/** | ||
* Register blocks for auto-insertion, based on their block.json metadata. | ||
* | ||
* @param array $settings Array of determined settings for registering a block type. | ||
* @param array $metadata Metadata provided for registering a block type. | ||
* @return array Updated settings array. | ||
*/ | ||
function gutenberg_register_auto_inserted_blocks( $settings, $metadata ) { | ||
if ( ! isset( $metadata['__experimentalAutoInsert'] ) ) { | ||
return $settings; | ||
} | ||
$auto_insert = $metadata['__experimentalAutoInsert']; | ||
|
||
/** | ||
* Map the camelCased position string from block.json to the snake_cased block type position | ||
* used in the auto-inserting block registration function. | ||
* | ||
* @var array | ||
*/ | ||
$property_mappings = array( | ||
'before' => 'before', | ||
'after' => 'after', | ||
'firstChild' => 'first_child', | ||
'lastChild' => 'last_child', | ||
); | ||
|
||
$inserted_block_name = $metadata['name']; | ||
foreach ( $auto_insert as $anchor_block_name => $position ) { | ||
// Avoid infinite recursion (auto-inserting next to or into self). | ||
if ( $inserted_block_name === $anchor_block_name ) { | ||
_doing_it_wrong( | ||
__METHOD__, | ||
__( 'Cannot auto-insert block next to itself.', 'gutenberg' ), | ||
'6.4.0' | ||
); | ||
continue; | ||
} | ||
|
||
if ( ! isset( $property_mappings[ $position ] ) ) { | ||
continue; | ||
} | ||
|
||
$mapped_position = $property_mappings[ $position ]; | ||
|
||
gutenberg_register_auto_inserted_block( $inserted_block_name, $mapped_position, $anchor_block_name ); | ||
|
||
$settings['auto_insert'][ $anchor_block_name ] = $mapped_position; | ||
} | ||
|
||
return $settings; | ||
} | ||
add_filter( 'block_type_metadata_settings', 'gutenberg_register_auto_inserted_blocks', 10, 2 ); | ||
|
||
/** | ||
* Register block for auto-insertion into the frontend and REST API. | ||
* | ||
* Register a block for auto-insertion into the frontend and into the markup | ||
* returned by the templates and patterns REST API endpoints. | ||
* | ||
* This is currently done by filtering parsed blocks as obtained from a block template | ||
* template part, or pattern and injecting the auto-inserted block where applicable. | ||
* | ||
* @todo In the long run, we'd likely want some sort of registry for auto-inserted blocks. | ||
* | ||
* @param string $inserted_block The name of the block to insert. | ||
* @param string $position The desired position of the auto-inserted block, relative to its anchor block. | ||
* Can be 'before', 'after', 'first_child', or 'last_child'. | ||
* @param string $anchor_block The name of the block to insert the auto-inserted block next to. | ||
* @return void | ||
*/ | ||
function gutenberg_register_auto_inserted_block( $inserted_block, $position, $anchor_block ) { | ||
$inserted_block = array( | ||
'blockName' => $inserted_block, | ||
'attrs' => array(), | ||
'innerHTML' => '', | ||
'innerContent' => array(), | ||
'innerBlocks' => array(), | ||
); | ||
|
||
$inserter = gutenberg_auto_insert_block( $inserted_block, $position, $anchor_block ); | ||
add_filter( 'gutenberg_serialize_block', $inserter, 10, 1 ); | ||
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. it seems here like this is an example of where we can skip the function-creating-function and implement the behavior directly. we don't need an anonymous function in order to pass this empty block around. 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. I might not be seeing the forest for the trees, but how would we do that (without changing the |
||
} | ||
|
||
/** | ||
* Parse and reserialize block templates to allow running filters. | ||
* | ||
* By parsing a block template's content and then reserializing it | ||
* via `gutenberg_serialize_blocks()`, we are able to run filters | ||
* on the parsed blocks. | ||
dmsnell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* | ||
* @param WP_Block_Template[] $query_result Array of found block templates. | ||
* @return WP_Block_Template[] Updated array of found block templates. | ||
*/ | ||
function gutenberg_parse_and_serialize_block_templates( $query_result ) { | ||
foreach ( $query_result as $block_template ) { | ||
if ( 'custom' === $block_template->source ) { | ||
continue; | ||
} | ||
$blocks = parse_blocks( $block_template->content ); | ||
$block_template->content = gutenberg_serialize_blocks( $blocks ); | ||
} | ||
|
||
return $query_result; | ||
} | ||
add_filter( 'get_block_templates', 'gutenberg_parse_and_serialize_block_templates', 10, 1 ); | ||
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. Could we change this priority from WooCommerce BLocks also adds templates through the 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. Hmm, identical values for priority shouldn't really cause either of the filters not to be run 🤔 (Instead, the one that was added first in terms of code execution flow should be run first.) To clarify, you're saying that the auto-insertion filter was run, while the WooCommerce Blocks one wasn't? |
||
|
||
/** | ||
* Filters the block template object after it has been (potentially) fetched from the theme file. | ||
* | ||
* By parsing a block template's content and then reserializing it | ||
* via `gutenberg_serialize_blocks()`, we are able to run filters | ||
* on the parsed blocks. | ||
dmsnell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* | ||
* @param WP_Block_Template|null $block_template The found block template, or null if there is none. | ||
*/ | ||
function gutenberg_parse_and_serialize_blocks( $block_template ) { | ||
|
||
$blocks = parse_blocks( $block_template->content ); | ||
$block_template->content = gutenberg_serialize_blocks( $blocks ); | ||
|
||
return $block_template; | ||
} | ||
add_filter( 'get_block_file_template', 'gutenberg_parse_and_serialize_blocks', 10, 1 ); | ||
|
||
// Helper functions. | ||
// ----------------- | ||
// The sole purpose of the following two functions (`gutenberg_serialize_block` | ||
// and `gutenberg_serialize_blocks`), which are otherwise copies of their unprefixed | ||
// counterparts (`serialize_block` and `serialize_blocks`) is to apply a filter | ||
// (also called `gutenberg_serialize_block`) as an entry point for modifications | ||
// to the parsed blocks. | ||
|
||
/** | ||
* Filterable version of `serialize_block()`. | ||
* | ||
* This function is identical to `serialize_block()`, except that it applies | ||
* the `gutenberg_serialize_block` filter to each block before it is serialized. | ||
* | ||
* @param array $block The block to be serialized. | ||
* @return string The serialized block. | ||
* | ||
* @see serialize_block() | ||
*/ | ||
function gutenberg_serialize_block( $block ) { | ||
$block_content = ''; | ||
|
||
/** | ||
* Filters a parsed block before it is serialized. | ||
* | ||
* @param array $block The block to be serialized. | ||
*/ | ||
$block = apply_filters( 'gutenberg_serialize_block', $block ); | ||
|
||
$index = 0; | ||
foreach ( $block['innerContent'] as $chunk ) { | ||
if ( is_string( $chunk ) ) { | ||
$block_content .= $chunk; | ||
} else { // Compare to WP_Block::render(). | ||
$inner_block = $block['innerBlocks'][ $index++ ]; | ||
$block_content .= gutenberg_serialize_block( $inner_block ); | ||
} | ||
} | ||
|
||
if ( ! is_array( $block['attrs'] ) ) { | ||
$block['attrs'] = array(); | ||
} | ||
|
||
return get_comment_delimited_block_content( | ||
$block['blockName'], | ||
$block['attrs'], | ||
$block_content | ||
); | ||
} | ||
dmsnell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* Filterable version of `serialize_blocks()`. | ||
* | ||
* This function is identical to `serialize_blocks()`, except that it applies | ||
* the `gutenberg_serialize_block` filter to each block before it is serialized. | ||
* | ||
* @param array $blocks The blocks to be serialized. | ||
* @return string[] The serialized blocks. | ||
* | ||
* @see serialize_blocks() | ||
*/ | ||
function gutenberg_serialize_blocks( $blocks ) { | ||
return implode( '', array_map( 'gutenberg_serialize_block', $blocks ) ); | ||
dmsnell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
<?php | ||
/** | ||
* REST API: Gutenberg_REST_Block_Patterns_Controller class | ||
* | ||
* @package Gutenberg | ||
* @subpackage REST_API | ||
*/ | ||
|
||
/** | ||
* Core class used to access block patterns via the REST API. | ||
* | ||
* @since 6.4.0 | ||
* | ||
* @see WP_REST_Controller | ||
*/ | ||
class Gutenberg_REST_Block_Patterns_Controller extends Gutenberg_REST_Block_Patterns_Controller_6_3 { | ||
/** | ||
* Prepare a raw block pattern before it gets output in a REST API response. | ||
* | ||
* @todo In the long run, we'd likely want to have a filter in the `WP_Block_Patterns_Registry` class | ||
* instead to allow us plugging in code like this. | ||
* | ||
* @param array $item Raw pattern as registered, before any changes. | ||
* @param WP_REST_Request $request Request object. | ||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. | ||
*/ | ||
public function prepare_item_for_response( $item, $request ) { | ||
$response = parent::prepare_item_for_response( $item, $request ); | ||
if ( ! gutenberg_is_experiment_enabled( 'gutenberg-auto-inserting-blocks' ) ) { | ||
return $response; | ||
} | ||
|
||
$data = $response->get_data(); | ||
|
||
$blocks = parse_blocks( $data['content'] ); | ||
$data['content'] = gutenberg_serialize_blocks( $blocks ); // Serialize or render? | ||
|
||
return rest_ensure_response( $data ); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.