-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Allow non-consecutive multi-selection of blocks #16797
Description
Is your feature request related to a problem? Please describe.
We are currently facing a situation where we need to be able to select multiple non-consecutive blocks to then perform a bulk action on them (e.g. remove, copy, move up/down, etc.).
However, in Gutenberg multi-selection is really designed for selecting a range of blocks from start to end, not a loose list of blocks that could be all over the place.
Examples in the code:
gutenberg/packages/block-editor/src/store/selectors.js
Lines 538 to 598 in 1b5fc6a
| /** | |
| * Returns the current selection set of block client IDs (multiselection or single selection). | |
| * | |
| * @param {Object} state Editor state. | |
| * | |
| * @return {Array} Multi-selected block client IDs. | |
| */ | |
| export const getSelectedBlockClientIds = createSelector( | |
| ( state ) => { | |
| const { start, end } = state.blockSelection; | |
| if ( start.clientId === undefined || end.clientId === undefined ) { | |
| return EMPTY_ARRAY; | |
| } | |
| if ( start.clientId === end.clientId ) { | |
| return [ start.clientId ]; | |
| } | |
| // Retrieve root client ID to aid in retrieving relevant nested block | |
| // order, being careful to allow the falsey empty string top-level root | |
| // by explicitly testing against null. | |
| const rootClientId = getBlockRootClientId( state, start.clientId ); | |
| if ( rootClientId === null ) { | |
| return EMPTY_ARRAY; | |
| } | |
| const blockOrder = getBlockOrder( state, rootClientId ); | |
| const startIndex = blockOrder.indexOf( start.clientId ); | |
| const endIndex = blockOrder.indexOf( end.clientId ); | |
| if ( startIndex > endIndex ) { | |
| return blockOrder.slice( endIndex, startIndex + 1 ); | |
| } | |
| return blockOrder.slice( startIndex, endIndex + 1 ); | |
| }, | |
| ( state ) => [ | |
| state.blocks.order, | |
| state.blockSelection.start.clientId, | |
| state.blockSelection.end.clientId, | |
| ], | |
| ); | |
| /** | |
| * Returns the current multi-selection set of block client IDs, or an empty | |
| * array if there is no multi-selection. | |
| * | |
| * @param {Object} state Editor state. | |
| * | |
| * @return {Array} Multi-selected block client IDs. | |
| */ | |
| export function getMultiSelectedBlockClientIds( state ) { | |
| const { start, end } = state.blockSelection; | |
| if ( start.clientId === end.clientId ) { | |
| return EMPTY_ARRAY; | |
| } | |
| return getSelectedBlockClientIds( state ); | |
| } |
gutenberg/packages/block-editor/src/store/selectors.js
Lines 313 to 337 in 1b5fc6a
| /** | |
| * Returns the current block selection start. This value may be null, and it | |
| * may represent either a singular block selection or multi-selection start. | |
| * A selection is singular if its start and end match. | |
| * | |
| * @param {Object} state Global application state. | |
| * | |
| * @return {?string} Client ID of block selection start. | |
| */ | |
| export function getBlockSelectionStart( state ) { | |
| return state.blockSelection.start.clientId; | |
| } | |
| /** | |
| * Returns the current block selection end. This value may be null, and it | |
| * may represent either a singular block selection or multi-selection end. | |
| * A selection is singular if its start and end match. | |
| * | |
| * @param {Object} state Global application state. | |
| * | |
| * @return {?string} Client ID of block selection end. | |
| */ | |
| export function getBlockSelectionEnd( state ) { | |
| return state.blockSelection.end.clientId; | |
| } |
gutenberg/packages/block-editor/src/store/actions.js
Lines 157 to 193 in 1b5fc6a
| /** | |
| * Returns an action object used in signalling that a block multi-selection has started. | |
| * | |
| * @return {Object} Action object. | |
| */ | |
| export function startMultiSelect() { | |
| return { | |
| type: 'START_MULTI_SELECT', | |
| }; | |
| } | |
| /** | |
| * Returns an action object used in signalling that block multi-selection stopped. | |
| * | |
| * @return {Object} Action object. | |
| */ | |
| export function stopMultiSelect() { | |
| return { | |
| type: 'STOP_MULTI_SELECT', | |
| }; | |
| } | |
| /** | |
| * Returns an action object used in signalling that block multi-selection changed. | |
| * | |
| * @param {string} start First block of the multi selection. | |
| * @param {string} end Last block of the multiselection. | |
| * | |
| * @return {Object} Action object. | |
| */ | |
| export function multiSelect( start, end ) { | |
| return { | |
| type: 'MULTI_SELECT', | |
| start, | |
| end, | |
| }; | |
| } |
Describe the solution you'd like
I think the resolvers under the hood could be re-written in a way that multi-selection would store the actual list of items in an array, and not start and end values. This would allow plugins to perform this non-consecutive multi-selection on their own.
I think this could be done in a backward-compatible way.
In a next step, we could think about how to expose this feature in the Gutenberg UI itself. For example, in our plugin we were thinking about allowing CMD+clicking on individual blocks to mark them as selected.
Describe alternatives you've considered
I looked into rolling our own implementation for this in our plugin, but it's not really doable. The multi-selection block toolbar and sidebar all rely on the built-in selectors like getMultiSelectedBlocks(), getMultiSelectedBlockClientIds(), and getSelectedBlockClientIds().