diff --git a/lib/experimental/auto-inserting-blocks.php b/lib/experimental/auto-inserting-blocks.php index 90f3bdf15e509d..f4a650e110bf92 100644 --- a/lib/experimental/auto-inserting-blocks.php +++ b/lib/experimental/auto-inserting-blocks.php @@ -64,6 +64,31 @@ function gutenberg_auto_insert_block( $inserted_block, $relative_position, $anch }; } +/** + * 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. * @@ -113,6 +138,37 @@ function gutenberg_register_auto_inserted_blocks( $settings, $metadata ) { $settings['auto_insert'][ $anchor_block_name ] = $mapped_position; } + // Copied from `get_block_editor_server_block_settings()`. + $fields_to_pick = array( + 'api_version' => 'apiVersion', + 'title' => 'title', + 'description' => 'description', + 'icon' => 'icon', + 'attributes' => 'attributes', + 'provides_context' => 'providesContext', + 'uses_context' => 'usesContext', + 'selectors' => 'selectors', + 'supports' => 'supports', + 'category' => 'category', + 'styles' => 'styles', + 'textdomain' => 'textdomain', + 'parent' => 'parent', + 'ancestor' => 'ancestor', + 'keywords' => 'keywords', + 'example' => 'example', + 'variations' => 'variations', + ); + // Add `auto_insert` to the list of fields to pick. + $fields_to_pick['auto_insert'] = 'autoInsert'; + + $exposed_settings = array_intersect_key( $settings, $fields_to_pick ); + + // TODO: Make work for blocks registered via direct call to gutenberg_register_auto_inserted_block(). + wp_add_inline_script( + 'wp-blocks', + 'wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( array( $inserted_block_name => $exposed_settings ) ) . ');' + ); + return $settings; } add_filter( 'block_type_metadata_settings', 'gutenberg_register_auto_inserted_blocks', 10, 2 ); @@ -135,7 +191,7 @@ function gutenberg_register_auto_inserted_blocks( $settings, $metadata ) { * @return void */ function gutenberg_register_auto_inserted_block( $inserted_block, $position, $anchor_block ) { - $inserted_block = array( + $inserted_block_array = array( 'blockName' => $inserted_block, 'attrs' => array(), 'innerHTML' => '', @@ -143,8 +199,19 @@ function gutenberg_register_auto_inserted_block( $inserted_block, $position, $an 'innerBlocks' => array(), ); - $inserter = gutenberg_auto_insert_block( $inserted_block, $position, $anchor_block ); + $inserter = gutenberg_auto_insert_block( $inserted_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 + * the controller's response. + */ + $controller_extender = gutenberg_add_auto_insert_field_to_block_type_controller( $inserted_block, $position, $anchor_block ); + add_filter( 'rest_prepare_block_type', $controller_extender, 10, 2 ); } /** @@ -256,3 +323,27 @@ function gutenberg_serialize_block( $block ) { function gutenberg_serialize_blocks( $blocks ) { return implode( '', array_map( 'gutenberg_serialize_block', $blocks ) ); } + +/** + * Register the `auto_insert` field for the block-types REST API controller. + * + * @return void + */ +function gutenberg_register_auto_insert_rest_field() { + register_rest_field( + 'block-type', + 'auto_insert', + array( + 'schema' => array( + 'description' => __( 'Block types that may be automatically inserted near this block and the associated relative position where they are inserted.', 'gutenberg' ), + 'patternProperties' => array( + '^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$' => array( + 'type' => 'string', + 'enum' => array( 'before', 'after', 'first_child', 'last_child' ), + ), + ), + ), + ) + ); +} +add_action( 'rest_api_init', 'gutenberg_register_auto_insert_rest_field' ); diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index c09b5cde0f16bc..a9b7b18e75a5f1 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -30,6 +30,10 @@ 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-auto-inserting-blocks', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalAutoInsertingBlocks = true', 'before' ); + } } add_action( 'admin_init', 'gutenberg_enable_experiments' ); diff --git a/packages/block-editor/src/hooks/auto-inserting-blocks.js b/packages/block-editor/src/hooks/auto-inserting-blocks.js new file mode 100644 index 00000000000000..266d9dd55fa674 --- /dev/null +++ b/packages/block-editor/src/hooks/auto-inserting-blocks.js @@ -0,0 +1,232 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { addFilter } from '@wordpress/hooks'; +import { Fragment } from '@wordpress/element'; +import { 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 { InspectorControls } from '../components'; +import { store as blockEditorStore } from '../store'; + +function AutoInsertingBlocksControl( props ) { + const { autoInsertedBlocksForCurrentBlock, groupedAutoInsertedBlocks } = + useSelect( + ( select ) => { + const { getBlockTypes } = select( blocksStore ); + const _autoInsertedBlocksForCurrentBlock = + getBlockTypes()?.filter( + ( { autoInsert } ) => + autoInsert && props.blockName in autoInsert + ); + + // 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; + }, + {} + ); + + return { + autoInsertedBlocksForCurrentBlock: + _autoInsertedBlocksForCurrentBlock, + groupedAutoInsertedBlocks: _groupedAutoInsertedBlocks, + }; + }, + [ props.blockName ] + ); + + const { + autoInsertedBlockClientIds, + blockIndex, + rootClientId, + innerBlocksLength, + } = useSelect( + ( select ) => { + const { getBlock, getBlockIndex, getBlockRootClientId } = + select( blockEditorStore ); + const _rootClientId = getBlockRootClientId( props.clientId ); + + const _autoInsertedBlockClientIds = + autoInsertedBlocksForCurrentBlock.reduce( + ( clientIds, block ) => { + 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 ( autoInsertedBlock ) { + clientIds[ block.name ] = + autoInsertedBlock.clientId; + } + + // TOOD: If no auto-inserted block was found in any of its designated locations, + // we want to check if it's present elsewhere in the block tree. + // If it is, we'd consider it manually inserted and would want to remove the + // corresponding toggle from the block inspector panel. + + return clientIds; + }, + {} + ); + + return { + blockIndex: getBlockIndex( props.clientId ), + innerBlocksLength: getBlock( props.clientId )?.innerBlocks + ?.length, + rootClientId: _rootClientId, + autoInsertedBlockClientIds: _autoInsertedBlockClientIds, + }; + }, + [ autoInsertedBlocksForCurrentBlock, props.blockName, props.clientId ] + ); + + const { insertBlock, removeBlock } = useDispatch( blockEditorStore ); + + if ( ! autoInsertedBlocksForCurrentBlock.length ) { + return null; + } + + 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 ) => { + // TODO: Display block icon. + // + + const checked = + block.name in + autoInsertedBlockClientIds; + + return ( + { + 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/index.js b/packages/block-editor/src/hooks/index.js index 6834d859d25453..5e18c6a309d693 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -22,6 +22,7 @@ import './metadata'; import './metadata-name'; import './behaviors'; import './custom-fields'; +import './auto-inserting-blocks'; export { useCustomSides } from './dimensions'; export { useLayoutClasses, useLayoutStyles } from './layout'; diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index bf866b7a2143ba..702582ff6489e8 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -180,6 +180,17 @@ export function unstable__bootstrapServerSideBlockDefinitions( definitions ) { serverSideBlockDefinitions[ blockName ].selectors = definitions[ blockName ].selectors; } + + if ( + serverSideBlockDefinitions[ blockName ] + .__experimentalAutoInsert === undefined && + definitions[ blockName ].__experimentalAutoInsert + ) { + serverSideBlockDefinitions[ + blockName + ].__experimentalAutoInsert = + definitions[ blockName ].__experimentalAutoInsert; + } continue; } @@ -219,6 +230,7 @@ function getBlockSettingsFromMetadata( { textdomain, ...metadata } ) { 'styles', 'example', 'variations', + '__experimentalAutoInsert', ]; const settings = Object.fromEntries(