diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index a33d2f6b98012..dad4ef971d43f 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -100,3 +100,108 @@ function gutenberg_register_metadata_attribute( $args ) { return $args; } add_filter( 'register_block_type_args', 'gutenberg_register_metadata_attribute' ); + +/** + * Return a function that auto-inserts as the first or last inner block of a given block. + * + * @param string $relative_position The position relative to the given block ("first_child" or "last_child"). + * @param array $inserted_block The block to insert. + * @return callable A function that accepts a block's content and returns the content with the inserted block. + */ +function gutenberg_auto_insert_child_block( $relative_position, $inserted_block ) { + return function( $parsed_block ) use ( $relative_position, $inserted_block ) { + if ( 'first_child' === $relative_position ) { + array_unshift( $parsed_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( $parsed_block['innerContent'], null ); + } elseif ( 'last_child' === $relative_position ) { + array_push( $parsed_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( $parsed_block['innerContent'], null ); + } + return $parsed_block; + }; +} + +/** + * Return a function that auto-inserts blocks relative to a given block. + * + * @param string $relative_position The position relative to the given block. + * @param array $inserted_block The block to insert. + * @return callable A function that accepts a block's content and returns the content with the inserted block. + */ +function gutenberg_auto_insert_block( $relative_position, $inserted_block ) { + // Can we avoid infinite loops? + + return function( $block_content ) use ( $relative_position, $inserted_block ) { + $inserted_content = render_block( $inserted_block ); + + if ( 'before' === $relative_position ) { + $block_content = $inserted_content . $block_content; + } elseif ( 'after' === $relative_position ) { + $block_content = $block_content . $inserted_content; + } + return $block_content; + }; +} + +function gutenberg_register_auto_inserted_blocks( $settings, $metadata ) { + if ( ! isset( $metadata['autoInsert'] ) ) { + return $settings; + } + + $property_mappings = array( + 'before' => 'before', + 'after' => 'after', + 'firstChild' => 'first_child', + 'lastChild' => 'last_child', + ); + + $auto_insert = $metadata['autoInsert']; + foreach ( $auto_insert as $block_name => $block_data ) { + $position = $block_data['position']; + if ( ! isset( $property_mappings[ $position ] ) ) { + continue; + } + + $mapped_position = $property_mappings[ $position ]; + + $inserted_block = array( + 'blockName' => $metadata['name'], + 'attrs' => $block_data['attrs'], + ); + // TODO: In the long run, we'd likely want some sort of registry for auto-inserted blocks. + if ( 'before' === $mapped_position || 'after' === $mapped_position ) { + $inserter = gutenberg_auto_insert_block( $mapped_position, $inserted_block ); + add_filter( "render_block_$block_name", $inserter, 10, 2 ); + } elseif ( 'first_child' === $mapped_position || 'last_child' === $mapped_position ) { + $inserter = gutenberg_auto_insert_child_block( $mapped_position, $inserted_block ); + add_filter( "render_block_data_$block_name", $inserter, 10, 2 ); + } + $settings['auto_insert'][ $block_name ] = $mapped_position; + } + + return $settings; +} +add_filter( 'block_type_metadata_settings', 'gutenberg_register_auto_inserted_blocks', 10, 2 ); + +function gutenberg_apply_render_block_data_block_type_filter( $parsed_block, $source_block, $parent_block ) { + $block_name = $parsed_block['blockName']; + /** + * Filters the block being rendered in render_block(), before it's processed. + * + * The dynamic portion of the hook name, `$name`, refers to + * the block name, e.g. "core/paragraph". + * + * @param array $parsed_block The block being rendered. + * @param array $source_block An un-modified copy of $parsed_block, as it appeared in the source content. + * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. + */ + $parsed_block = apply_filters( "render_block_data_$block_name", $parsed_block, $source_block, $parent_block ); + return $parsed_block; +} +add_filter( 'render_block_data', 'gutenberg_apply_render_block_data_block_type_filter', 15, 3 ); diff --git a/packages/block-library/src/avatar/block.json b/packages/block-library/src/avatar/block.json index 3fbb6dd9221ae..902c3300bfec5 100644 --- a/packages/block-library/src/avatar/block.json +++ b/packages/block-library/src/avatar/block.json @@ -50,5 +50,18 @@ } }, "editorStyle": "wp-block-avatar", - "style": "wp-block-avatar" + "style": "wp-block-avatar", + "autoInsert": { + "core/comment-template": { + "position": "lastChild", + "attrs": { + "size": 40, + "style": { + "border": { + "radius": "10px" + } + } + } + } + } } diff --git a/packages/block-library/src/social-link/block.json b/packages/block-library/src/social-link/block.json index 140cc123ec484..3ae1fe41819ba 100644 --- a/packages/block-library/src/social-link/block.json +++ b/packages/block-library/src/social-link/block.json @@ -34,5 +34,14 @@ "reusable": false, "html": false }, - "editorStyle": "wp-block-social-link-editor" + "editorStyle": "wp-block-social-link-editor", + "autoInsert": { + "core/post-content": { + "position": "after", + "attrs": { + "service": "wordpress", + "url": "https://wordpress.org/" + } + } + } } diff --git a/phpunit/blocks/render-comment-template-test.php b/phpunit/blocks/render-comment-template-test.php index c297d1729d0dc..19f453fc18594 100644 --- a/phpunit/blocks/render-comment-template-test.php +++ b/phpunit/blocks/render-comment-template-test.php @@ -142,6 +142,9 @@ public function test_inner_block_inserted_by_render_block_data_is_retained() { return $parsed_block; }; + // Remove auto-insertion filter so it won't collide. + remove_filter( 'render_block_data', 'gutenberg_auto_insert_child_block' ); + add_filter( 'render_block_data', $render_block_data_callback, 10, 1 ); $parsed_blocks = parse_blocks( '' @@ -154,6 +157,8 @@ public function test_inner_block_inserted_by_render_block_data_is_retained() { ); $block->render(); remove_filter( 'render_block_data', $render_block_data_callback ); + // Add back auto-insertion filter. + add_filter( 'render_block_data', 'gutenberg_auto_insert_child_block', 10, 1 ); $this->assertSame( 5, $render_block_callback->get_call_count() ); diff --git a/schemas/json/block.json b/schemas/json/block.json index 5b92a654fbc4a..14a560b4a0070 100644 --- a/schemas/json/block.json +++ b/schemas/json/block.json @@ -737,6 +737,32 @@ "render": { "type": "string", "description": "Template file loaded on the server when rendering a block." + }, + "autoInsert": { + "type": "object", + "description": "Blocks to auto-insert this block next to.", + "patternProperties": { + "[a-zA-Z]": { + "type": "object", + "description": "Position relative to the block to auto-insert this block next to.", + "properties": { + "position": { + "type": "string", + "description": "Position relative to the block to auto-insert this block next to.", + "enum": [ + "before", + "after", + "firstChild", + "lastChild" + ] + }, + "attrs": { + "type": "object", + "description": "Attributes for the auto-inserted block." + } + } + } + } } }, "required": [ "name", "title" ],