Skip to content

Allow non-consecutive multi-selection of blocks #16797

@swissspidy

Description

@swissspidy

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:

/**
* 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 );
}

/**
* 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;
}

/**
* 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().

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions