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(