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

Allow non-consecutive multi-selection of blocks #16797

Open
swissspidy opened this issue Jul 29, 2019 · 10 comments
Open

Allow non-consecutive multi-selection of blocks #16797

swissspidy opened this issue Jul 29, 2019 · 10 comments
Labels
[Feature] Block Multi Selection The ability to select and manipulate multiple blocks [Package] Block editor /packages/block-editor [Type] Enhancement A suggestion for improvement.

Comments

@swissspidy
Copy link
Member

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

@swissspidy swissspidy added [Type] Enhancement A suggestion for improvement. [Feature] Block Multi Selection The ability to select and manipulate multiple blocks [Package] Block editor /packages/block-editor labels Jul 29, 2019
@talldan
Copy link
Contributor

talldan commented Jul 30, 2019

I've been thinking about a similar problem in the table block, where you also expect to be able to multi-select cells. At the moment individual cells can only be selected. Similar sort of problem here, except blocks already support ranges of selections.

The solution I had in mind is a fairly obvious one, using an array to build up a list of the different selections. The existing 'range' object becomes the data used in an individual element of the array, so it should be possible to reuse some of the logic:

const selections = [
  {
    start: { clientId, ... },
    end: { clientId, ... },
  },
  {
    start: { clientId, ... },
    end: { clientId, ... },
  }
]

The idea then is that you can reduce the array to work out whether something is selected.

There are some little edge cases though. For example, a user might select a range by shift-clicking and then deselect one of the blocks in the middle by command clicking.

As blocks are stored sequentially, some splitting and merging logic when adding selections would possibly be the way to go. Another option is to add an object that represents a 'deselection' to the array.

@swissspidy
Copy link
Member Author

Could another option be to store a list of all the selected client IDs or would that take up too much space?

@talldan
Copy link
Contributor

talldan commented Jul 30, 2019

Yeah, that's also definitely an option. Could do an array slice on state.blocks.order to build up a list for range selections.

@github-actions github-actions bot added the [Status] In Progress Tracking issues with work in progress label Jul 30, 2019
@swissspidy
Copy link
Member Author

FYI there's now a work-in-progress PR at #16811 that uses one big array of client IDs. Seemed the easiest solution to implement as a proof-of-concept. There's only some edge cases to address.

@swissspidy
Copy link
Member Author

There was a good question from today's #core-editor meeting (source):

How would it handle “moving” once you select a bunch of non-consecutive blocks?

One suggestion was:

I think they could collapse before the last selected element, so any non selected element between them will move up the selected list, it makes sense when refactoring sides of the articles to different sections

Also another good thing to keep in mind:

My main thoughs at the moment is around all these places where we just assumed a sequential list of blocks for multiselection. start/end in the state are the obvious one but I feel we have more hidden places with that assumption.

@maheshwaghmare
Copy link
Contributor

I have something similar situation in which I want to manage the styling of similar blocks.

Instead of creating new issue added below comments.

The situation is described as below:

  • Added a icon list block and change colors & other settings.
  • Duplicate the block and update content.

Now, If user want to change the colors & other settings then I'm thinning to provide such option in which user can bulk select the blocks and change selected block (similar selected blocks) to change the colors & other settings.

I think I need to do this by adding some custom solution. Or It is good to add SlotFills in <MultiSelectionInspector>

Screenshot for reference:

Bulk Select Hook

I'm still experimenting things in the Gutenberg. So, Instead of creating new task added above comments here.

@lubieowoce
Copy link

lubieowoce commented Apr 18, 2020

+1, it would be really great to have something like PluginMultiBlockControls and PluginMultiInspectorControls (perhaps behind an __experimental for now) . i'm working on a plugin to edit multiple blocks at once (see #8743) and i have to render my own popover toolbar and do all sorts of ugly stuff because right now the standard contextual toolbar hides all controls during a multiselection (except for the builtin "convert" button).

@talldan
Copy link
Contributor

talldan commented Apr 21, 2020

#16895 has some proposals on how moving non-consecutive blocks might work.

Here are some of the images from that now closed issue:
moving down:
ezgif com-video-to-gif (1)

moving up (fixed version):
moveup

@porg
Copy link

porg commented Jul 15, 2024

👆 my linked ticket was a duplicate.

  • It only dealt with the non-consecutive multi selection. But it has some screen recording of the status quo with the lack of the feature plus clear reproduction steps of what's desired, which might be useful for UX evaluation, once a prototype of this feature is available for testing.
  • Glad to see that @senadir and @talldan took that user scenario even further and also thought about how the move process would work then.
  • Last progress 2021... what's the status on this now?

@swissspidy swissspidy removed their assignment Jul 15, 2024
@swissspidy
Copy link
Member Author

IDK why I was still assigned here. This isn't a focus for me anymore.

Someone else might want to pick up the ideas from #16811 and attempt this again, but a lot has changed in the last few years, so... 🤷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Block Multi Selection The ability to select and manipulate multiple blocks [Package] Block editor /packages/block-editor [Type] Enhancement A suggestion for improvement.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants