diff --git a/docs/reference-guides/block-api/block-metadata.md b/docs/reference-guides/block-api/block-metadata.md index ba24ff0f58401..22d9d66b4f462 100644 --- a/docs/reference-guides/block-api/block-metadata.md +++ b/docs/reference-guides/block-api/block-metadata.md @@ -464,7 +464,7 @@ Plugins and Themes can also register [custom block style](/docs/reference-guides It provides structured example data for the block. This data is used to construct a preview for the block to be shown in the Inspector Help Panel when the user mouses over the block. -See the [the example documentation](/docs/reference-guides/block-api/block-registration.md#example-optional) for more details. +See the [Example documentation](/docs/reference-guides/block-api/block-registration.md#example-optional) for more details. ### Variations @@ -497,6 +497,25 @@ _Note: In JavaScript you can provide a function for the `isActive` property, and See the [the variations documentation](/docs/reference-guides/block-api/block-variations.md) for more details. +### Block Hooks + +- Type: `object` +- Optional +- Property: `blockHooks` +- Since: `WordPress 6.4.0` + +```json +{ + "blockHooks": { + "my-plugin/banner": "after" + } +} +``` + +Block Hooks is an API that allows a block to automatically insert itself next to all instances of a given block type, in a relative position also specified by the "hooked" block. That is, a block can opt to be inserted before or after a given block type, or as its first or last child (i.e. to be prepended or appended to the list of its child blocks, respectively). Hooked blocks will appear both on the frontend and in the editor (to allow for customization by the user). + +The key is the name of the block (`string`) to hook into, and the value is the position to hook into (`string`). Take a look at the [Block Hooks documentation](/docs/reference-guides/block-api/block-registration.md#block-hooks-optional) for more info about available configurations. + ### Editor Script - Type: `WPDefinedAsset`|`WPDefinedAsset[]` ([learn more](#wpdefinedasset)) diff --git a/docs/reference-guides/block-api/block-registration.md b/docs/reference-guides/block-api/block-registration.md index cecdb663e3d60..0fcaaa510b81a 100644 --- a/docs/reference-guides/block-api/block-registration.md +++ b/docs/reference-guides/block-api/block-registration.md @@ -234,6 +234,7 @@ example: { #### variations (optional) - **Type:** `Object[]` +- **Since**: `WordPress 5.9.0` Similarly to how the block's styles can be declared, a block type can define block variations that the user can pick from. The difference is that, rather than changing only the visual appearance, this field provides a way to apply initial custom attributes and inner blocks at the time when a block is inserted. See the [Block Variations API](/docs/reference-guides/block-api/block-variations.md) for more details. @@ -265,6 +266,7 @@ parent: [ 'core/columns' ], #### ancestor (optional) - **Type:** `Array` +- **Since**: `WordPress 6.0.0` The `ancestor` property makes a block available inside the specified block types at any position of the ancestor block subtree. That allows, for example, to place a 'Comment Content' block inside a 'Column' block, as long as 'Column' is somewhere within a 'Comment Template' block. In comparison to the `parent` property blocks that specify their `ancestor` can be placed anywhere in the subtree whilst blocks with a specified `parent` need to be direct children. @@ -273,6 +275,35 @@ The `ancestor` property makes a block available inside the specified block types ancestor: [ 'core/columns' ], ``` +#### Block Hooks (optional) + +- **Type:** `Object` +- **Since**: `WordPress 6.4.0` + +Block Hooks is an API that allows a block to automatically insert itself next to all instances of a given block type, in a relative position also specified by the "hooked" block. That is, a block can opt to be inserted before or after a given block type, or as its first or last child (i.e. to be prepended or appended to the list of its child blocks, respectively). Hooked blocks will appear both on the frontend and in the editor (to allow for customization by the user). + +The key is the name of the block (`string`) to hook into, and the value is the position to hook into (`string`). Allowed target values are: + +- `before` – inject before the target block. +- `after` - inject after the target block. +- `firstChild` - inject before the first inner block of the target container block. +- `lastChild` - inject after the last inner block of the target container block. + +```js +{ + blockHooks: { + 'core/verse': 'before' + 'core/spacer': 'after', + 'core/column': 'firstChild', + 'core/group': 'lastChild', + } +} +``` + +It’s crucial to emphasize that the Block Hooks feature is only designed to work with _static_ block-based templates, template parts, and patterns. For patterns, this includes those provided by the theme, from [Block Pattern Directory](https://wordpress.org/patterns/), or from calls to [`register_block_pattern`](https://developer.wordpress.org/reference/functions/register_block_pattern/). + +Block Hooks will not work with post content or patterns crafted by the user, such as synced patterns, or theme templates and template parts that have been modified by the user. + ## Block Collections ## `registerBlockCollection` diff --git a/lib/compat/wordpress-6.3/rest-api.php b/lib/compat/wordpress-6.3/rest-api.php index db90a2575d9ee..fc0c295bdaaf8 100644 --- a/lib/compat/wordpress-6.3/rest-api.php +++ b/lib/compat/wordpress-6.3/rest-api.php @@ -85,19 +85,6 @@ function add_modified_wp_template_schema() { } add_filter( 'rest_api_init', 'add_modified_wp_template_schema' ); -// If the Block Hooks experiment is enabled, we load the block patterns -// controller in lib/experimental/rest-api.php instead. -if ( ! gutenberg_is_experiment_enabled( 'gutenberg-block-hooks' ) ) { - /** - * Registers the block patterns REST API routes. - */ - function gutenberg_register_rest_block_patterns() { - $block_patterns = new Gutenberg_REST_Block_Patterns_Controller_6_3(); - $block_patterns->register_routes(); - } - add_action( 'rest_api_init', 'gutenberg_register_rest_block_patterns' ); -} - /** * Registers the Navigation Fallbacks REST API routes. */ diff --git a/lib/experimental/block-hooks.php b/lib/compat/wordpress-6.4/block-hooks.php similarity index 99% rename from lib/experimental/block-hooks.php rename to lib/compat/wordpress-6.4/block-hooks.php index 0586b98960813..cb663122f0e0c 100644 --- a/lib/experimental/block-hooks.php +++ b/lib/compat/wordpress-6.4/block-hooks.php @@ -13,10 +13,10 @@ * @return array Updated settings array. */ function gutenberg_add_hooked_blocks( $settings, $metadata ) { - if ( ! isset( $metadata['__experimentalBlockHooks'] ) ) { + if ( ! isset( $metadata['blockHooks'] ) ) { return $settings; } - $block_hooks = $metadata['__experimentalBlockHooks']; + $block_hooks = $metadata['blockHooks']; /** * Map the camelCased position string from block.json to the snake_cased block type position diff --git a/lib/experimental/class-gutenberg-rest-block-patterns-controller.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-block-patterns-controller.php similarity index 94% rename from lib/experimental/class-gutenberg-rest-block-patterns-controller.php rename to lib/compat/wordpress-6.4/class-gutenberg-rest-block-patterns-controller.php index e4ac5581910e0..8128934b9b011 100644 --- a/lib/experimental/class-gutenberg-rest-block-patterns-controller.php +++ b/lib/compat/wordpress-6.4/class-gutenberg-rest-block-patterns-controller.php @@ -26,12 +26,13 @@ class Gutenberg_REST_Block_Patterns_Controller extends Gutenberg_REST_Block_Patt */ public function prepare_item_for_response( $item, $request ) { $response = parent::prepare_item_for_response( $item, $request ); - if ( ! gutenberg_is_experiment_enabled( 'gutenberg-block-hooks' ) ) { - return $response; - } $data = $response->get_data(); + if ( empty( $data['content'] ) ) { + return $response; + } + $blocks = parse_blocks( $data['content'] ); $data['content'] = gutenberg_serialize_blocks( $blocks ); // Serialize or render? diff --git a/lib/compat/wordpress-6.4/rest-api.php b/lib/compat/wordpress-6.4/rest-api.php index 903a2795f2089..7c81a6a274c03 100644 --- a/lib/compat/wordpress-6.4/rest-api.php +++ b/lib/compat/wordpress-6.4/rest-api.php @@ -10,6 +10,15 @@ die( 'Silence is golden.' ); } +/** + * Registers the block patterns REST API routes. + */ +function gutenberg_register_rest_block_patterns_routes() { + $block_patterns = new Gutenberg_REST_Block_Patterns_Controller(); + $block_patterns->register_routes(); +} +add_action( 'rest_api_init', 'gutenberg_register_rest_block_patterns_routes' ); + /** * Registers the Global Styles Revisions REST API routes. */ diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index eb36b5b49b8e9..c09b5cde0f16b 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -30,10 +30,6 @@ function gutenberg_enable_experiments() { if ( gutenberg_is_experiment_enabled( 'gutenberg-no-tinymce' ) ) { wp_add_inline_script( 'wp-block-library', 'window.__experimentalDisableTinymce = true', 'before' ); } - - if ( $gutenberg_experiments && array_key_exists( 'gutenberg-block-hooks', $gutenberg_experiments ) ) { - wp_add_inline_script( 'wp-block-editor', 'window.__experimentalBlockHooks = true', 'before' ); - } } add_action( 'admin_init', 'gutenberg_enable_experiments' ); diff --git a/lib/experimental/rest-api.php b/lib/experimental/rest-api.php index f8c5a3041d312..7c6a9bf74d739 100644 --- a/lib/experimental/rest-api.php +++ b/lib/experimental/rest-api.php @@ -10,17 +10,6 @@ die( 'Silence is golden.' ); } -if ( gutenberg_is_experiment_enabled( 'gutenberg-block-hooks' ) ) { - /** - * Registers the block patterns REST API routes. - */ - function gutenberg_register_rest_block_patterns() { - $block_patterns = new Gutenberg_REST_Block_Patterns_Controller(); - $block_patterns->register_routes(); - } - add_action( 'rest_api_init', 'gutenberg_register_rest_block_patterns' ); -} - /** * Registers the customizer nonces REST API routes. */ diff --git a/lib/experiments-page.php b/lib/experiments-page.php index 6a022963807e3..133d968ba2cb7 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -103,18 +103,6 @@ function gutenberg_initialize_experiments_settings() { ) ); - add_settings_field( - 'gutenberg-block-hooks', - __( 'Block hooks', 'gutenberg' ), - 'gutenberg_display_experiment_field', - 'gutenberg-experiments', - 'gutenberg_experiments_section', - array( - 'label' => __( 'Block hooks allow automatically inserting a block in a position relative to another.', 'gutenberg' ), - 'id' => 'gutenberg-block-hooks', - ) - ); - add_settings_field( 'gutenberg-custom-fields', __( 'Connections', 'gutenberg' ), diff --git a/lib/load.php b/lib/load.php index 95cf413da6853..c0b69e2ab2ca0 100644 --- a/lib/load.php +++ b/lib/load.php @@ -52,6 +52,7 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.4 compat. require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php'; + require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-block-patterns-controller.php'; require_once __DIR__ . '/compat/wordpress-6.4/rest-api.php'; require_once __DIR__ . '/compat/wordpress-6.4/theme-previews.php'; @@ -64,9 +65,6 @@ function gutenberg_is_experiment_enabled( $name ) { require_once __DIR__ . '/experimental/class-wp-rest-customizer-nonces.php'; } require_once __DIR__ . '/experimental/class-gutenberg-rest-template-revision-count.php'; - if ( gutenberg_is_experiment_enabled( 'gutenberg-block-hooks' ) ) { - require_once __DIR__ . '/experimental/class-gutenberg-rest-block-patterns-controller.php'; - } require_once __DIR__ . '/experimental/rest-api.php'; } @@ -96,6 +94,7 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.4 compat. require __DIR__ . '/compat/wordpress-6.4/blocks.php'; +require __DIR__ . '/compat/wordpress-6.4/block-hooks.php'; require __DIR__ . '/compat/wordpress-6.4/block-patterns.php'; require __DIR__ . '/compat/wordpress-6.4/script-loader.php'; @@ -111,9 +110,6 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/disable-tinymce.php'; } -if ( gutenberg_is_experiment_enabled( 'gutenberg-block-hooks' ) ) { - require __DIR__ . '/experimental/block-hooks.php'; -} require __DIR__ . '/experimental/interactivity-api/class-wp-interactivity-store.php'; require __DIR__ . '/experimental/interactivity-api/store.php'; require __DIR__ . '/experimental/interactivity-api/scripts.php'; diff --git a/packages/block-editor/src/hooks/block-hooks.js b/packages/block-editor/src/hooks/block-hooks.js index 678f8ee06d971..3285854db6c79 100644 --- a/packages/block-editor/src/hooks/block-hooks.js +++ b/packages/block-editor/src/hooks/block-hooks.js @@ -250,10 +250,8 @@ export const withBlockHooks = createHigherOrderComponent( ( BlockEdit ) => { }; }, 'withBlockHooks' ); -if ( window?.__experimentalBlockHooks ) { - addFilter( - 'editor.BlockEdit', - 'core/block-hooks/with-inspector-control', - withBlockHooks - ); -} +addFilter( + 'editor.BlockEdit', + 'core/block-hooks/with-inspector-control', + withBlockHooks +); diff --git a/packages/block-library/src/pattern/index.php b/packages/block-library/src/pattern/index.php index 50687220992e2..b117e31e125cf 100644 --- a/packages/block-library/src/pattern/index.php +++ b/packages/block-library/src/pattern/index.php @@ -43,8 +43,8 @@ function render_block_core_pattern( $attributes ) { $pattern = $registry->get_registered( $slug ); $content = _inject_theme_attribute_in_block_template_content( $pattern['content'] ); - $gutenberg_experiments = get_option( 'gutenberg-experiments' ); - if ( $gutenberg_experiments && ! empty( $gutenberg_experiments['gutenberg-block-hooks'] ) ) { + // This can be removed when the minimum supported WordPress is >= 6.4. + if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { // 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. $blocks = parse_blocks( $content ); diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 44776ed7e4992..818a26fdb4e5c 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -167,7 +167,7 @@ function getBlockSettingsFromMetadata( { textdomain, ...metadata } ) { 'styles', 'example', 'variations', - '__experimentalBlockHooks', + 'blockHooks', ]; const settings = Object.fromEntries( diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index 877c9fdc4a038..6347912c0402c 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -133,6 +133,7 @@ describe( 'blocks', () => { supports: {}, styles: [], variations: [], + blockHooks: {}, save: noop, category: 'text', title: 'block title', @@ -280,6 +281,7 @@ describe( 'blocks', () => { supports: {}, styles: [], variations: [], + blockHooks: {}, save: expect.any( Function ), } ); } ); @@ -317,6 +319,7 @@ describe( 'blocks', () => { supports: {}, styles: [], variations: [], + blockHooks: {}, } ); } ); @@ -350,6 +353,7 @@ describe( 'blocks', () => { supports: {}, styles: [], variations: [], + blockHooks: {}, } ); } ); @@ -362,6 +366,9 @@ describe( 'blocks', () => { fontSize: 'fontSize', }, uses_context: [ 'textColor' ], + block_hooks: { + 'tests/my-block': 'after', + }, }, } ); @@ -385,6 +392,9 @@ describe( 'blocks', () => { supports: {}, styles: [], variations: [], + blockHooks: { + 'tests/my-block': 'after', + }, } ); } ); @@ -421,6 +431,49 @@ describe( 'blocks', () => { supports: {}, styles: [], variations: [], + blockHooks: {}, + } ); + } ); + + // This test can be removed once the polyfill for blockHooks gets removed. + it( 'should polyfill blockHooks using metadata on the client when not set on the server', () => { + const blockName = 'tests/hooked-block'; + unstable__bootstrapServerSideBlockDefinitions( { + [ blockName ]: { + category: 'widgets', + }, + } ); + + const blockType = { + title: 'block title', + }; + registerBlockType( + { + name: blockName, + blockHooks: { + 'tests/block': 'firstChild', + }, + category: 'ignored', + }, + blockType + ); + expect( getBlockType( blockName ) ).toEqual( { + name: blockName, + save: expect.any( Function ), + title: 'block title', + category: 'widgets', + icon: { src: BLOCK_ICON_DEFAULT }, + attributes: {}, + providesContext: {}, + usesContext: [], + keywords: [], + selectors: {}, + supports: {}, + styles: [], + variations: [], + blockHooks: { + 'tests/block': 'firstChild', + }, } ); } ); @@ -490,6 +543,7 @@ describe( 'blocks', () => { supports: {}, styles: [], variations: [], + blockHooks: {}, } ); } ); @@ -522,6 +576,7 @@ describe( 'blocks', () => { supports: {}, styles: [], variations: [], + blockHooks: {}, } ); } ); @@ -568,6 +623,7 @@ describe( 'blocks', () => { supports: {}, styles: [], variations: [], + blockHooks: {}, } ); } ); @@ -628,6 +684,7 @@ describe( 'blocks', () => { supports: {}, styles: [], variations: [], + blockHooks: {}, } ); } ); @@ -655,6 +712,7 @@ describe( 'blocks', () => { supports: {}, styles: [], variations: [], + blockHooks: {}, } ); } ); @@ -742,6 +800,7 @@ describe( 'blocks', () => { supports: {}, styles: [], variations: [], + blockHooks: {}, save: () => null, ...blockSettingsWithDeprecations, }, @@ -916,6 +975,7 @@ describe( 'blocks', () => { keywords: [ 'variation' ], }, ], + blockHooks: {}, edit: Edit, save: noop, } ); @@ -988,6 +1048,7 @@ describe( 'blocks', () => { keywords: [ 'variation (translated)' ], }, ], + blockHooks: {}, edit: Edit, save: noop, } ); @@ -1042,6 +1103,7 @@ describe( 'blocks', () => { supports: {}, styles: [], variations: [], + blockHooks: {}, }, ] ); const oldBlock = unregisterBlockType( 'core/test-block' ); @@ -1060,6 +1122,7 @@ describe( 'blocks', () => { supports: {}, styles: [], variations: [], + blockHooks: {}, } ); expect( getBlockTypes() ).toEqual( [] ); } ); @@ -1140,6 +1203,7 @@ describe( 'blocks', () => { supports: {}, styles: [], variations: [], + blockHooks: {}, } ); } ); @@ -1166,6 +1230,7 @@ describe( 'blocks', () => { supports: {}, styles: [], variations: [], + blockHooks: {}, } ); } ); } ); @@ -1199,6 +1264,7 @@ describe( 'blocks', () => { supports: {}, styles: [], variations: [], + blockHooks: {}, }, { name: 'core/test-block-with-settings', @@ -1215,6 +1281,7 @@ describe( 'blocks', () => { supports: {}, styles: [], variations: [], + blockHooks: {}, }, ] ); } ); diff --git a/packages/blocks/src/store/process-block-type.js b/packages/blocks/src/store/process-block-type.js index aab198af6c66f..d69f9f8e5810f 100644 --- a/packages/blocks/src/store/process-block-type.js +++ b/packages/blocks/src/store/process-block-type.js @@ -55,6 +55,7 @@ export const processBlockType = supports: {}, styles: [], variations: [], + blockHooks: {}, save: () => null, ...select.getBootstrappedBlockType( name ), ...blockSettings, diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index db01f06bb7db6..c16c1a6087efb 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -83,14 +83,13 @@ function bootstrappedBlockTypes( state = {}, action ) { // definitions and needs to be polyfilled. This can be removed when the // minimum supported WordPress is >= 6.4. if ( - serverDefinition.__experimentalBlockHooks === undefined && - blockType.__experimentalBlockHooks + serverDefinition.blockHooks === undefined && + blockType.blockHooks ) { newDefinition = { ...serverDefinition, ...newDefinition, - __experimentalBlockHooks: - blockType.__experimentalBlockHooks, + blockHooks: blockType.blockHooks, }; } } else { diff --git a/schemas/json/block.json b/schemas/json/block.json index b181bf2d9ab40..44ce570912767 100644 --- a/schemas/json/block.json +++ b/schemas/json/block.json @@ -706,6 +706,16 @@ } } }, + "blockHooks": { + "description": "Block Hooks allow a block to automatically insert itself next to all instances of a given block type.\n\nSee the Block Hooks documentation at https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/#block-hooks-optional for more details.", + "type": "object", + "patternProperties": { + "^[a-z][a-z0-9-]*/[a-z][a-z0-9-]*$": { + "enum": [ "before", "after", "firstChild", "lastChild" ] + } + }, + "additionalProperties": false + }, "editorScript": { "description": "Block type editor script definition. It will only be enqueued in the context of the editor.", "oneOf": [