Skip to content

Commit

Permalink
Auto-inserting blocks: Add block inspector panel (#52969)
Browse files Browse the repository at this point in the history
Add a new panel (tentatively named "Plugins") to the block inspector that lists blocks that are slated for auto-insertion next to that block, and provide toggles to insert (or remove) them.

Co-authored-by: Carlos Bravo <[email protected]>
  • Loading branch information
ockham and cbravobernal authored Aug 29, 2023
1 parent 1231506 commit df40f46
Show file tree
Hide file tree
Showing 5 changed files with 342 additions and 2 deletions.
95 changes: 93 additions & 2 deletions lib/experimental/auto-inserting-blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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 );
Expand All @@ -135,16 +191,27 @@ 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' => '',
'innerContent' => array(),
'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 );
}

/**
Expand Down Expand Up @@ -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' );
4 changes: 4 additions & 0 deletions lib/experimental/editor-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' );
232 changes: 232 additions & 0 deletions packages/block-editor/src/hooks/auto-inserting-blocks.js
Original file line number Diff line number Diff line change
@@ -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 (
<InspectorControls>
<PanelBody title={ __( 'Plugins' ) } initialOpen={ true }>
{ Object.keys( groupedAutoInsertedBlocks ).map( ( vendor ) => {
return (
<Fragment key={ vendor }>
<h3>{ vendor }</h3>
{ groupedAutoInsertedBlocks[ vendor ].map(
( block ) => {
// TODO: Display block icon.
// <BlockIcon icon={ block.icon } />

const checked =
block.name in
autoInsertedBlockClientIds;

return (
<ToggleControl
checked={ checked }
key={ block.title }
label={ 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 );
} }
/>
);
}
) }
</Fragment>
);
} ) }
</PanelBody>
</InspectorControls>
);
}

export const withAutoInsertingBlocks = createHigherOrderComponent(
( BlockEdit ) => {
return ( props ) => {
const blockEdit = <BlockEdit key="edit" { ...props } />;
return (
<>
{ blockEdit }
<AutoInsertingBlocksControl
blockName={ props.name }
clientId={ props.clientId }
/>
</>
);
};
},
'withAutoInsertingBlocks'
);

if ( window?.__experimentalAutoInsertingBlocks ) {
addFilter(
'editor.BlockEdit',
'core/auto-inserting-blocks/with-inspector-control',
withAutoInsertingBlocks
);
}
1 change: 1 addition & 0 deletions packages/block-editor/src/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading

0 comments on commit df40f46

Please sign in to comment.