Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto-inserting blocks: Add block inspector panel #52969

Merged
merged 62 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
44b69f3
Add Auto-inserting blocks panel to block inspector
ockham Jul 26, 2023
31942c0
Try register rest field approach
cbravobernal Jul 26, 2023
549367b
Placate linter
ockham Jul 26, 2023
0bf4253
Remove Gutenberg_REST_Block_Types_Controller
ockham Jul 26, 2023
7cd7732
Add auto_insert field to REST API response
ockham Jul 26, 2023
742c20e
Remove misleading console.log() statements
ockham Jul 26, 2023
a2184a3
Set auto_insert field to correct value
ockham Jul 26, 2023
ebb1da2
Add TODO note
ockham Jul 26, 2023
f248a65
Allow auto-inserting into multiple locations
ockham Jul 27, 2023
7c53cac
Add explanatory comment
ockham Jul 27, 2023
145773b
Formatting
ockham Jul 27, 2023
fc3c472
Add __experimentalAutoInsert to block.json schema
ockham Aug 1, 2023
ea2e4ee
Expose auto_insert field via unstable__bootstrapServerSideBlockDefini…
ockham Aug 1, 2023
cd06950
Filter fields
ockham Aug 1, 2023
dc7e09c
Formatting
ockham Aug 1, 2023
d385c35
Show basic list of auto-inserted block names in panel
ockham Aug 1, 2023
5868df5
Show auto-inserted block title
ockham Aug 1, 2023
012c69f
Simplify
ockham Aug 1, 2023
6d1f6da
Basic list with icons
ockham Aug 1, 2023
42338a4
Group by vendor
ockham Aug 1, 2023
8568c66
Nicer naming in grouping reduce call
ockham Aug 3, 2023
6cdb95d
Implement basic toggle-controlled insertion
ockham Aug 3, 2023
41fa152
Handle 'before' insertion
ockham Aug 3, 2023
e4d32cc
Implement basic 'after' block removal
ockham Aug 8, 2023
db4c7eb
Implement basic 'before' block removal
ockham Aug 8, 2023
feea87f
Fix
ockham Aug 8, 2023
bba74af
Add inline TODO note
ockham Aug 9, 2023
3739395
Implement first/last child insertion
ockham Aug 9, 2023
76d0af4
Simplify
ockham Aug 9, 2023
92442d0
Stop before/after check if we encounter a non-auto-inserted block
ockham Aug 9, 2023
8534c83
Add explanatory comment
ockham Aug 9, 2023
f0ad150
Clarify comment
ockham Aug 9, 2023
6312e2d
Don't show panel if there are no auto-inserting blocks
ockham Aug 9, 2023
2d063f0
Consider other potentially auto-inserted first/last child blocks
ockham Aug 9, 2023
89cdb63
Revert "Add __experimentalAutoInsert to block.json schema"
ockham Aug 10, 2023
2d15887
Remove now-obsolete comment
ockham Aug 10, 2023
ab3a254
Simplify
ockham Aug 10, 2023
085cd85
Extract insertBlockIntoDesignatedLocation() function
ockham Aug 10, 2023
c423941
Clean up a bit
ockham Aug 10, 2023
a938ca0
Accept any sibling blocks as after/before, and any child as first/las…
ockham Aug 10, 2023
406391c
Rename blocks variable to autoInsertedBlockClientIds
ockham Aug 10, 2023
c91ce65
Move code around a bit to make more intuitive
ockham Aug 10, 2023
7ab4cfe
Add small TODO note
ockham Aug 10, 2023
ad8f3f2
Add another TODO note
ockham Aug 10, 2023
965875f
Fix PHP lint errors
ockham Aug 10, 2023
cdd56b2
Add missing PHPDoc, change some variable names
ockham Aug 10, 2023
1514816
Rename accumulator to clientIds
ockham Aug 21, 2023
aa8dc07
Use in operator
ockham Aug 21, 2023
f8c92d4
Improve REST API field description
ockham Aug 21, 2023
550cc9e
REVERT ME: Auto-insert Login/out block after Navigation
ockham Aug 25, 2023
be01f29
Revert "REVERT ME: Auto-insert Login/out block after Navigation"
ockham Aug 28, 2023
311ec1c
Move groupedAutoInsertedBlocks computation into first useSelect
ockham Aug 28, 2023
7dd8200
Rewrite auto-inserted block finding logic
ockham Aug 28, 2023
64c70af
Rewrite block insertion function
ockham Aug 28, 2023
70c3787
Fix PHPDoc
ockham Aug 28, 2023
101455d
Update REST API field description
ockham Aug 29, 2023
769419c
Use 'in' operator
ockham Aug 29, 2023
0204dda
Rename 'prefix' to 'namespace'
ockham Aug 29, 2023
cd33697
Make schema more precise
ockham Aug 29, 2023
1aa48e7
Remove trailing comma from function call
ockham Aug 29, 2023
65c892d
Use enum in schema
ockham Aug 29, 2023
e10c5b3
Formatting
ockham Aug 29, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
ockham marked this conversation as resolved.
Show resolved Hide resolved

$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' ),
),
),
),
ockham marked this conversation as resolved.
Show resolved Hide resolved
)
);
}
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;
},
{}
);
Mamaduka marked this conversation as resolved.
Show resolved Hide resolved

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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you planning on adding this one before merging?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was planning to do that in a follow-up. Would that be okay?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up: #54024

// 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;
}
ockham marked this conversation as resolved.
Show resolved Hide resolved

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