diff --git a/lib/compat/wordpress-6.3/rest-api.php b/lib/compat/wordpress-6.3/rest-api.php index 130da6ed774d8..041398eda24b4 100644 --- a/lib/compat/wordpress-6.3/rest-api.php +++ b/lib/compat/wordpress-6.3/rest-api.php @@ -85,9 +85,9 @@ function add_modified_wp_template_schema() { } add_filter( 'rest_api_init', 'add_modified_wp_template_schema' ); -// If the Auto-inserting Blocks experiment is enabled, we load the block patterns +// 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-auto-inserting-blocks' ) ) { +if ( ! gutenberg_is_experiment_enabled( 'gutenberg-block-hooks' ) ) { /** * Registers the block patterns REST API routes. */ diff --git a/lib/experimental/auto-inserting-blocks.php b/lib/experimental/block-hooks.php similarity index 74% rename from lib/experimental/auto-inserting-blocks.php rename to lib/experimental/block-hooks.php index 4790dc4166fa3..abe85ee3b9ace 100644 --- a/lib/experimental/auto-inserting-blocks.php +++ b/lib/experimental/block-hooks.php @@ -1,110 +1,26 @@ 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; - }; -} - -/** - * Add auto-insertion information to a block type's controller. - * - * @param array $inserted_block_type The type of block to insert. - * @param string $position The position relative to the anchor block. - * Can be 'before', 'after', 'first_child', or 'last_child'. - * @param string $anchor_block_type The auto-inserted block will be inserted next to instances of this block type. - * @return callable A filter for the `rest_prepare_block_type` hook that adds an `auto_insert` field to the network response. - */ -function gutenberg_add_auto_insert_field_to_block_type_controller( $inserted_block_type, $position, $anchor_block_type ) { - return function( $response, $block_type ) use ( $inserted_block_type, $position, $anchor_block_type ) { - if ( $block_type->name !== $inserted_block_type ) { - return $response; - } - - $data = $response->get_data(); - if ( ! isset( $data['auto_insert'] ) ) { - $data['auto_insert'] = array(); - } - $data['auto_insert'][ $anchor_block_type ] = $position; - $response->set_data( $data ); - return $response; - }; -} - -/** - * Register blocks for auto-insertion, based on their block.json metadata. + * Register hooked blocks for automatic 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'] ) ) { +function gutenberg_add_hooked_blocks( $settings, $metadata ) { + if ( ! isset( $metadata['__experimentalBlockHooks'] ) ) { return $settings; } - $auto_insert = $metadata['__experimentalAutoInsert']; + $block_hooks = $metadata['__experimentalBlockHooks']; /** * Map the camelCased position string from block.json to the snake_cased block type position - * used in the auto-inserting block registration function. + * used in the hooked block registration function. * * @var array */ @@ -116,12 +32,12 @@ function gutenberg_register_auto_inserted_blocks( $settings, $metadata ) { ); $inserted_block_name = $metadata['name']; - foreach ( $auto_insert as $anchor_block_name => $position ) { - // Avoid infinite recursion (auto-inserting next to or into self). + foreach ( $block_hooks as $anchor_block_name => $position ) { + // Avoid infinite recursion (hooking to itself). if ( $inserted_block_name === $anchor_block_name ) { _doing_it_wrong( __METHOD__, - __( 'Cannot auto-insert block next to itself.', 'gutenberg' ), + __( 'Cannot hook block to itself.', 'gutenberg' ), '6.4.0' ); continue; @@ -133,9 +49,9 @@ function gutenberg_register_auto_inserted_blocks( $settings, $metadata ) { $mapped_position = $property_mappings[ $position ]; - gutenberg_register_auto_inserted_block( $inserted_block_name, $mapped_position, $anchor_block_name ); + gutenberg_add_hooked_block( $inserted_block_name, $mapped_position, $anchor_block_name ); - $settings['auto_insert'][ $anchor_block_name ] = $mapped_position; + $settings['block_hooks'][ $anchor_block_name ] = $mapped_position; } // Copied from `get_block_editor_server_block_settings()`. @@ -158,12 +74,12 @@ function gutenberg_register_auto_inserted_blocks( $settings, $metadata ) { 'example' => 'example', 'variations' => 'variations', ); - // Add `auto_insert` to the list of fields to pick. - $fields_to_pick['auto_insert'] = 'autoInsert'; + // Add `block_hooks` to the list of fields to pick. + $fields_to_pick['block_hooks'] = 'blockHooks'; $exposed_settings = array_intersect_key( $settings, $fields_to_pick ); - // TODO: Make work for blocks registered via direct call to gutenberg_register_auto_inserted_block(). + // TODO: Make work for blocks registered via direct call to gutenberg_add_hooked_block(). wp_add_inline_script( 'wp-blocks', 'wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( array( $inserted_block_name => $exposed_settings ) ) . ');' @@ -171,49 +87,138 @@ function gutenberg_register_auto_inserted_blocks( $settings, $metadata ) { return $settings; } -add_filter( 'block_type_metadata_settings', 'gutenberg_register_auto_inserted_blocks', 10, 2 ); +add_filter( 'block_type_metadata_settings', 'gutenberg_add_hooked_blocks', 10, 2 ); /** - * Register block for auto-insertion into the frontend and REST API. + * Register a hooked block for automatic insertion into a given block hook. * - * Register a block for auto-insertion into the frontend and into the markup + * A block hook is specified by a block type and a relative position. The hooked block + * will be automatically inserted in the given position next to the "anchor" block + * whenever the latter is encountered. This applies both to the frontend and to 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. + * This is currently done by filtering parsed blocks as obtained from a block template, + * template part, or pattern, and injecting the hooked block where applicable. * - * @todo In the long run, we'd likely want some sort of registry for auto-inserted blocks. + * @todo In the long run, we'd likely want some sort of registry for hooked 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. + * @param string $hooked_block The name of the block to insert. + * @param string $position The desired position of the hooked 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 hooked block next to. * @return void */ -function gutenberg_register_auto_inserted_block( $inserted_block, $position, $anchor_block ) { - $inserted_block_array = array( - 'blockName' => $inserted_block, +function gutenberg_add_hooked_block( $hooked_block, $position, $anchor_block ) { + $hooked_block_array = array( + 'blockName' => $hooked_block, 'attrs' => array(), 'innerHTML' => '', 'innerContent' => array(), 'innerBlocks' => array(), ); - $inserter = gutenberg_auto_insert_block( $inserted_block_array, $position, $anchor_block ); + $inserter = gutenberg_insert_hooked_block( $hooked_block_array, $position, $anchor_block ); add_filter( 'gutenberg_serialize_block', $inserter, 10, 1 ); /* * The block-types REST API controller uses objects of the `WP_Block_Type` class, which are * in turn created upon block type registration. However, that class does not contain - * an `auto_insert` property (and is not easily extensible), so we have to use a different - * mechanism to communicate to the controller which blocks have been registered for - * auto-insertion. We're doing so here (i.e. upon block registration), by adding a filter to + * a `block_hooks` property (and is not easily extensible), so we have to use a different + * mechanism to communicate to the controller which hooked blocks have been registered for + * automatic insertion. We're doing so here (i.e. upon block registration), by adding a filter to * the controller's response. */ - $controller_extender = gutenberg_add_auto_insert_field_to_block_type_controller( $inserted_block, $position, $anchor_block ); + $controller_extender = gutenberg_add_block_hooks_field_to_block_type_controller( $hooked_block, $position, $anchor_block ); add_filter( 'rest_prepare_block_type', $controller_extender, 10, 2 ); } +/** + * Return a function that 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 returned function 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 string $anchor_block_type The automatically inserted block will be inserted next to instances of this block type. + * @return callable A function that accepts a block's content and returns the content with the inserted block. + */ +function gutenberg_insert_hooked_block( $inserted_block, $relative_position, $anchor_block_type ) { + return function( $block ) use ( $inserted_block, $relative_position, $anchor_block_type ) { + if ( $anchor_block_type === $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_type, 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; + }; +} + +/** + * Add block hooks information to a block type's controller. + * + * @param array $inserted_block_type The type of block to insert. + * @param string $position The position relative to the anchor block. + * Can be 'before', 'after', 'first_child', or 'last_child'. + * @param string $anchor_block_type The hooked block will be inserted next to instances of this block type. + * @return callable A filter for the `rest_prepare_block_type` hook that adds a `block_hooks` field to the network response. + */ +function gutenberg_add_block_hooks_field_to_block_type_controller( $inserted_block_type, $position, $anchor_block_type ) { + return function( $response, $block_type ) use ( $inserted_block_type, $position, $anchor_block_type ) { + if ( $block_type->name !== $inserted_block_type ) { + return $response; + } + + $data = $response->get_data(); + if ( ! isset( $data['block_hooks'] ) ) { + $data['block_hooks'] = array(); + } + $data['block_hooks'][ $anchor_block_type ] = $position; + $response->set_data( $data ); + return $response; + }; +} + /** * Parse and reserialize block templates to allow running filters. * @@ -325,14 +330,14 @@ function gutenberg_serialize_blocks( $blocks ) { } /** - * Register the `auto_insert` field for the block-types REST API controller. + * Register the `block_hooks` field for the block-types REST API controller. * * @return void */ -function gutenberg_register_auto_insert_rest_field() { +function gutenberg_register_block_hooks_rest_field() { register_rest_field( 'block-type', - 'auto_insert', + 'block_hooks', array( 'schema' => array( 'description' => __( 'This block is automatically inserted near any occurence of the block types used as keys of this map, into a relative position given by the corresponding value.', 'gutenberg' ), @@ -346,4 +351,4 @@ function gutenberg_register_auto_insert_rest_field() { ) ); } -add_action( 'rest_api_init', 'gutenberg_register_auto_insert_rest_field' ); +add_action( 'rest_api_init', 'gutenberg_register_block_hooks_rest_field' ); diff --git a/lib/experimental/class-gutenberg-rest-block-patterns-controller.php b/lib/experimental/class-gutenberg-rest-block-patterns-controller.php index 1ac567959b146..e4ac5581910e0 100644 --- a/lib/experimental/class-gutenberg-rest-block-patterns-controller.php +++ b/lib/experimental/class-gutenberg-rest-block-patterns-controller.php @@ -26,7 +26,7 @@ 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-auto-inserting-blocks' ) ) { + if ( ! gutenberg_is_experiment_enabled( 'gutenberg-block-hooks' ) ) { return $response; } diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index a9b7b18e75a5f..eb36b5b49b8e9 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -31,8 +31,8 @@ function gutenberg_enable_experiments() { wp_add_inline_script( 'wp-block-library', 'window.__experimentalDisableTinymce = true', 'before' ); } - if ( $gutenberg_experiments && array_key_exists( 'gutenberg-auto-inserting-blocks', $gutenberg_experiments ) ) { - wp_add_inline_script( 'wp-block-editor', 'window.__experimentalAutoInsertingBlocks = true', 'before' ); + if ( $gutenberg_experiments && array_key_exists( 'gutenberg-block-hooks', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalBlockHooks = true', 'before' ); } } diff --git a/lib/experimental/rest-api.php b/lib/experimental/rest-api.php index 8e548600b3875..f8c5a3041d312 100644 --- a/lib/experimental/rest-api.php +++ b/lib/experimental/rest-api.php @@ -10,7 +10,7 @@ die( 'Silence is golden.' ); } -if ( gutenberg_is_experiment_enabled( 'gutenberg-auto-inserting-blocks' ) ) { +if ( gutenberg_is_experiment_enabled( 'gutenberg-block-hooks' ) ) { /** * Registers the block patterns REST API routes. */ diff --git a/lib/experiments-page.php b/lib/experiments-page.php index 74b2ad01672f7..6a022963807e3 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -104,14 +104,14 @@ function gutenberg_initialize_experiments_settings() { ); add_settings_field( - 'gutenberg-auto-inserting-blocks', - __( 'Auto-inserting blocks', 'gutenberg' ), + 'gutenberg-block-hooks', + __( 'Block hooks', 'gutenberg' ), 'gutenberg_display_experiment_field', 'gutenberg-experiments', 'gutenberg_experiments_section', array( - 'label' => __( 'Test Auto-inserting blocks', 'gutenberg' ), - 'id' => 'gutenberg-auto-inserting-blocks', + 'label' => __( 'Block hooks allow automatically inserting a block in a position relative to another.', 'gutenberg' ), + 'id' => 'gutenberg-block-hooks', ) ); diff --git a/lib/load.php b/lib/load.php index ef8da334debe6..51a6500396650 100644 --- a/lib/load.php +++ b/lib/load.php @@ -63,7 +63,7 @@ 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-auto-inserting-blocks' ) ) { + 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'; @@ -110,8 +110,8 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/disable-tinymce.php'; } -if ( gutenberg_is_experiment_enabled( 'gutenberg-auto-inserting-blocks' ) ) { - require __DIR__ . '/experimental/auto-inserting-blocks.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'; diff --git a/packages/block-editor/src/hooks/auto-inserting-blocks.js b/packages/block-editor/src/hooks/auto-inserting-blocks.js deleted file mode 100644 index 5b3adfbdde8b9..0000000000000 --- a/packages/block-editor/src/hooks/auto-inserting-blocks.js +++ /dev/null @@ -1,271 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { addFilter } from '@wordpress/hooks'; -import { Fragment, useMemo } from '@wordpress/element'; -import { - __experimentalHStack as HStack, - PanelBody, - ToggleControl, -} from '@wordpress/components'; -import { createHigherOrderComponent } from '@wordpress/compose'; -import { createBlock, store as blocksStore } from '@wordpress/blocks'; -import { useDispatch, useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { BlockIcon, InspectorControls } from '../components'; -import { store as blockEditorStore } from '../store'; - -const EMPTY_OBJECT = {}; - -function AutoInsertingBlocksControl( props ) { - const blockTypes = useSelect( - ( select ) => select( blocksStore ).getBlockTypes(), - [] - ); - - const autoInsertedBlocksForCurrentBlock = useMemo( - () => - blockTypes?.filter( - ( { autoInsert } ) => - autoInsert && props.blockName in autoInsert - ), - [ blockTypes, props.blockName ] - ); - - const { blockIndex, rootClientId, innerBlocksLength } = useSelect( - ( select ) => { - const { getBlock, getBlockIndex, getBlockRootClientId } = - select( blockEditorStore ); - - return { - blockIndex: getBlockIndex( props.clientId ), - innerBlocksLength: getBlock( props.clientId )?.innerBlocks - ?.length, - rootClientId: getBlockRootClientId( props.clientId ), - }; - }, - [ props.clientId ] - ); - - const autoInsertedBlockClientIds = useSelect( - ( select ) => { - const { getBlock, getGlobalBlockCount } = - select( blockEditorStore ); - - const _autoInsertedBlockClientIds = - autoInsertedBlocksForCurrentBlock.reduce( - ( clientIds, block ) => { - // If the block doesn't exist anywhere in the block tree, - // we know that we have to display the toggle for it, and set - // it to disabled. - if ( getGlobalBlockCount( block.name ) === 0 ) { - return clientIds; - } - - const relativePosition = - block?.autoInsert?.[ props.blockName ]; - let candidates; - - switch ( relativePosition ) { - case 'before': - case 'after': - // Any of the current block's siblings (with the right block type) qualifies - // as an auto-inserted block (inserted `before` or `after` the current one), - // as the block might've been auto-inserted and then moved around a bit by the user. - candidates = - getBlock( rootClientId )?.innerBlocks; - break; - - case 'first_child': - case 'last_child': - // Any of the current block's child blocks (with the right block type) qualifies - // as an auto-inserted first or last child block, as the block might've been - // auto-inserted and then moved around a bit by the user. - candidates = getBlock( - props.clientId - ).innerBlocks; - break; - } - - const autoInsertedBlock = candidates?.find( - ( { name } ) => name === block.name - ); - - // If the block exists in the designated location, we consider it auto-inserted - // and show the toggle as enabled. - if ( autoInsertedBlock ) { - return { - ...clientIds, - [ block.name ]: autoInsertedBlock.clientId, - }; - } - - // If no auto-inserted block was found in any of its designated locations, - // but it exists elsewhere in the block tree, we consider it manually inserted. - // In this case, we take note and will remove the corresponding toggle from the - // block inspector panel. - return { - ...clientIds, - [ block.name ]: false, - }; - }, - {} - ); - - if ( Object.values( _autoInsertedBlockClientIds ).length > 0 ) { - return _autoInsertedBlockClientIds; - } - - return EMPTY_OBJECT; - }, - [ - autoInsertedBlocksForCurrentBlock, - props.blockName, - props.clientId, - rootClientId, - ] - ); - - const { insertBlock, removeBlock } = useDispatch( blockEditorStore ); - - // Remove toggle if block isn't present in the designated location but elsewhere in the block tree. - const autoInsertedBlocksForCurrentBlockIfNotPresentElsewhere = - autoInsertedBlocksForCurrentBlock?.filter( - ( block ) => autoInsertedBlockClientIds?.[ block.name ] !== false - ); - - if ( ! autoInsertedBlocksForCurrentBlockIfNotPresentElsewhere.length ) { - return null; - } - - // Group by block namespace (i.e. prefix before the slash). - const groupedAutoInsertedBlocks = autoInsertedBlocksForCurrentBlock.reduce( - ( groups, block ) => { - const [ namespace ] = block.name.split( '/' ); - if ( ! groups[ namespace ] ) { - groups[ namespace ] = []; - } - groups[ namespace ].push( block ); - return groups; - }, - {} - ); - - const insertBlockIntoDesignatedLocation = ( block, relativePosition ) => { - switch ( relativePosition ) { - case 'before': - case 'after': - insertBlock( - block, - relativePosition === 'after' ? blockIndex + 1 : blockIndex, - rootClientId, // Insert as a child of the current block's parent - false - ); - break; - - case 'first_child': - case 'last_child': - insertBlock( - block, - // TODO: It'd be great if insertBlock() would accept negative indices for insertion. - relativePosition === 'first_child' ? 0 : innerBlocksLength, - props.clientId, // Insert as a child of the current block. - false - ); - break; - } - }; - - return ( - - - { Object.keys( groupedAutoInsertedBlocks ).map( ( vendor ) => { - return ( - -

{ vendor }

- { groupedAutoInsertedBlocks[ vendor ].map( - ( block ) => { - const checked = - block.name in - autoInsertedBlockClientIds; - - return ( - - - { block.title } - - } - onChange={ () => { - if ( ! checked ) { - // Create and insert block. - const relativePosition = - block.autoInsert[ - props.blockName - ]; - insertBlockIntoDesignatedLocation( - createBlock( - block.name - ), - relativePosition - ); - return; - } - - // Remove block. - const clientId = - autoInsertedBlockClientIds[ - block.name - ]; - removeBlock( clientId, false ); - } } - /> - ); - } - ) } -
- ); - } ) } -
-
- ); -} - -export const withAutoInsertingBlocks = createHigherOrderComponent( - ( BlockEdit ) => { - return ( props ) => { - const blockEdit = ; - return ( - <> - { blockEdit } - - - ); - }; - }, - 'withAutoInsertingBlocks' -); - -if ( window?.__experimentalAutoInsertingBlocks ) { - addFilter( - 'editor.BlockEdit', - 'core/auto-inserting-blocks/with-inspector-control', - withAutoInsertingBlocks - ); -} diff --git a/packages/block-editor/src/hooks/block-hooks.js b/packages/block-editor/src/hooks/block-hooks.js new file mode 100644 index 0000000000000..678f8ee06d971 --- /dev/null +++ b/packages/block-editor/src/hooks/block-hooks.js @@ -0,0 +1,259 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { addFilter } from '@wordpress/hooks'; +import { Fragment, useMemo } from '@wordpress/element'; +import { + __experimentalHStack as HStack, + PanelBody, + ToggleControl, +} from '@wordpress/components'; +import { createHigherOrderComponent } from '@wordpress/compose'; +import { createBlock, store as blocksStore } from '@wordpress/blocks'; +import { useDispatch, useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { BlockIcon, InspectorControls } from '../components'; +import { store as blockEditorStore } from '../store'; + +const EMPTY_OBJECT = {}; + +function BlockHooksControl( props ) { + const blockTypes = useSelect( + ( select ) => select( blocksStore ).getBlockTypes(), + [] + ); + + const hookedBlocksForCurrentBlock = useMemo( + () => + blockTypes?.filter( + ( { blockHooks } ) => + blockHooks && props.blockName in blockHooks + ), + [ blockTypes, props.blockName ] + ); + + const { blockIndex, rootClientId, innerBlocksLength } = useSelect( + ( select ) => { + const { getBlock, getBlockIndex, getBlockRootClientId } = + select( blockEditorStore ); + + return { + blockIndex: getBlockIndex( props.clientId ), + innerBlocksLength: getBlock( props.clientId )?.innerBlocks + ?.length, + rootClientId: getBlockRootClientId( props.clientId ), + }; + }, + [ props.clientId ] + ); + + const hookedBlockClientIds = useSelect( + ( select ) => { + const { getBlock, getGlobalBlockCount } = + select( blockEditorStore ); + + const _hookedBlockClientIds = hookedBlocksForCurrentBlock.reduce( + ( clientIds, block ) => { + // If the block doesn't exist anywhere in the block tree, + // we know that we have to display the toggle for it, and set + // it to disabled. + if ( getGlobalBlockCount( block.name ) === 0 ) { + return clientIds; + } + + const relativePosition = + block?.blockHooks?.[ props.blockName ]; + let candidates; + + switch ( relativePosition ) { + case 'before': + case 'after': + // Any of the current block's siblings (with the right block type) qualifies + // as a hooked block (inserted `before` or `after` the current one), as the block + // might've been automatically inserted and then moved around a bit by the user. + candidates = getBlock( rootClientId )?.innerBlocks; + break; + + case 'first_child': + case 'last_child': + // Any of the current block's child blocks (with the right block type) qualifies + // as a hooked first or last child block, as the block might've been automatically + // inserted and then moved around a bit by the user. + candidates = getBlock( props.clientId ).innerBlocks; + break; + } + + const hookedBlock = candidates?.find( + ( { name } ) => name === block.name + ); + + // If the block exists in the designated location, we consider it hooked + // and show the toggle as enabled. + if ( hookedBlock ) { + return { + ...clientIds, + [ block.name ]: hookedBlock.clientId, + }; + } + + // If no hooked block was found in any of its designated locations, + // but it exists elsewhere in the block tree, we consider it manually inserted. + // In this case, we take note and will remove the corresponding toggle from the + // block inspector panel. + return { + ...clientIds, + [ block.name ]: false, + }; + }, + {} + ); + + if ( Object.values( _hookedBlockClientIds ).length > 0 ) { + return _hookedBlockClientIds; + } + + return EMPTY_OBJECT; + }, + [ + hookedBlocksForCurrentBlock, + props.blockName, + props.clientId, + rootClientId, + ] + ); + + const { insertBlock, removeBlock } = useDispatch( blockEditorStore ); + + // Remove toggle if block isn't present in the designated location but elsewhere in the block tree. + const hookedBlocksForCurrentBlockIfNotPresentElsewhere = + hookedBlocksForCurrentBlock?.filter( + ( block ) => hookedBlockClientIds?.[ block.name ] !== false + ); + + if ( ! hookedBlocksForCurrentBlockIfNotPresentElsewhere.length ) { + return null; + } + + // Group by block namespace (i.e. prefix before the slash). + const groupedHookedBlocks = hookedBlocksForCurrentBlock.reduce( + ( groups, block ) => { + const [ namespace ] = block.name.split( '/' ); + if ( ! groups[ namespace ] ) { + groups[ namespace ] = []; + } + groups[ namespace ].push( block ); + return groups; + }, + {} + ); + + const insertBlockIntoDesignatedLocation = ( block, relativePosition ) => { + switch ( relativePosition ) { + case 'before': + case 'after': + insertBlock( + block, + relativePosition === 'after' ? blockIndex + 1 : blockIndex, + rootClientId, // Insert as a child of the current block's parent + false + ); + break; + + case 'first_child': + case 'last_child': + insertBlock( + block, + // TODO: It'd be great if insertBlock() would accept negative indices for insertion. + relativePosition === 'first_child' ? 0 : innerBlocksLength, + props.clientId, // Insert as a child of the current block. + false + ); + break; + } + }; + + return ( + + + { Object.keys( groupedHookedBlocks ).map( ( vendor ) => { + return ( + +

{ vendor }

+ { groupedHookedBlocks[ vendor ].map( ( block ) => { + const checked = + block.name in hookedBlockClientIds; + + return ( + + + { block.title } + + } + onChange={ () => { + if ( ! checked ) { + // Create and insert block. + const relativePosition = + block.blockHooks[ + props.blockName + ]; + insertBlockIntoDesignatedLocation( + createBlock( block.name ), + relativePosition + ); + return; + } + + // Remove block. + const clientId = + hookedBlockClientIds[ + block.name + ]; + removeBlock( clientId, false ); + } } + /> + ); + } ) } +
+ ); + } ) } +
+
+ ); +} + +export const withBlockHooks = createHigherOrderComponent( ( BlockEdit ) => { + return ( props ) => { + const blockEdit = ; + return ( + <> + { blockEdit } + + + ); + }; +}, 'withBlockHooks' ); + +if ( window?.__experimentalBlockHooks ) { + addFilter( + 'editor.BlockEdit', + 'core/block-hooks/with-inspector-control', + withBlockHooks + ); +} diff --git a/packages/block-editor/src/hooks/auto-inserting-blocks.scss b/packages/block-editor/src/hooks/block-hooks.scss similarity index 89% rename from packages/block-editor/src/hooks/auto-inserting-blocks.scss rename to packages/block-editor/src/hooks/block-hooks.scss index 8c43c3673053e..dc05d6e6947bb 100644 --- a/packages/block-editor/src/hooks/auto-inserting-blocks.scss +++ b/packages/block-editor/src/hooks/block-hooks.scss @@ -1,4 +1,4 @@ -.block-editor-hooks__auto-inserting-blocks { +.block-editor-hooks__block-hooks { /** * Since we're displaying the block icon alongside the block name, * we need to right-align the toggle. diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 9907fbed89c52..0f6d52cc2c00d 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -22,7 +22,7 @@ import './metadata'; import './metadata-name'; import './behaviors'; import './custom-fields'; -import './auto-inserting-blocks'; +import './block-hooks'; import './block-rename-ui'; export { useCustomSides } from './dimensions'; diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index e3e65469b4c54..cdb4bb2a07abf 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -46,7 +46,7 @@ @import "./components/url-input/style.scss"; @import "./components/url-popover/style.scss"; @import "./hooks/anchor.scss"; -@import "./hooks/auto-inserting-blocks.scss"; +@import "./hooks/block-hooks.scss"; @import "./hooks/border.scss"; @import "./hooks/color.scss"; @import "./hooks/dimensions.scss"; diff --git a/packages/block-library/src/pattern/index.php b/packages/block-library/src/pattern/index.php index 97a8c3ddc663f..50687220992e2 100644 --- a/packages/block-library/src/pattern/index.php +++ b/packages/block-library/src/pattern/index.php @@ -44,7 +44,7 @@ function render_block_core_pattern( $attributes ) { $content = _inject_theme_attribute_in_block_template_content( $pattern['content'] ); $gutenberg_experiments = get_option( 'gutenberg-experiments' ); - if ( $gutenberg_experiments && ! empty( $gutenberg_experiments['gutenberg-auto-inserting-blocks'] ) ) { + if ( $gutenberg_experiments && ! empty( $gutenberg_experiments['gutenberg-block-hooks'] ) ) { // 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 72c0a30db0205..44776ed7e4992 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', - '__experimentalAutoInsert', + '__experimentalBlockHooks', ]; const settings = Object.fromEntries( diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index a8f114fea79c7..db01f06bb7db6 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -79,18 +79,18 @@ function bootstrappedBlockTypes( state = {}, action ) { }; } - // The `autoInsert` prop is not yet included in the server provided + // The `blockHooks` prop is not yet included in the server provided // definitions and needs to be polyfilled. This can be removed when the // minimum supported WordPress is >= 6.4. if ( - serverDefinition.__experimentalAutoInsert === undefined && - blockType.__experimentalAutoInsert + serverDefinition.__experimentalBlockHooks === undefined && + blockType.__experimentalBlockHooks ) { newDefinition = { ...serverDefinition, ...newDefinition, - __experimentalAutoInsert: - blockType.__experimentalAutoInsert, + __experimentalBlockHooks: + blockType.__experimentalBlockHooks, }; } } else {