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

Enable multi select block attribute controls #22470

Merged
merged 3 commits into from
Jun 9, 2020

Conversation

ellatrix
Copy link
Member

@ellatrix ellatrix commented May 19, 2020

Description

Fixes #8743.

Currently multi selections are... not so useful. You can delete and move blocks and that's about it. This PR allows block attribute controls to be enabled for multi selection.

Not only does this work for blocks of the same type, but also controls that are used for the same attribute in multiple block types. For example, if you select a heading and paragraphs, the color control can be used for all of these blocks.

multi-select-controls

Implementation

The approach I take here is displaying controls of the first selected block if all selected blocks are of the same type and modifying setAttributes to set attributes of all selected blocks.

I've added an experimental prop to allow controls for any mix of selections, which is useful for e.g. alignment and colours. For example, you may have a selection of both paragraphs and headings and want to adjust the colour for all. If there's a block in the selection that doesn't support the control/attribute, it will just be skipped. I'm not super sure about this feature. It may be better to leave it out for now. I just wanted to highlight that there's a possibility to do this. Removed for now to simplify the PR.

How has this been tested?

Screenshots

Types of changes

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • My code has proper inline documentation.
  • I've included developer documentation if appropriate.
  • I've updated all React Native files affected by any refactorings/renamings in this PR.

@ellatrix ellatrix added [Type] Enhancement A suggestion for improvement. [Feature] Block Multi Selection The ability to select and manipulate multiple blocks labels May 19, 2020
@github-actions
Copy link

github-actions bot commented May 19, 2020

Size Change: +1.04 kB (0%)

Total Size: 1.13 MB

Filename Size Change
build/a11y/index.js 1.14 kB -1 B
build/block-editor/index.js 106 kB -137 B (0%)
build/block-editor/style-rtl.css 11.8 kB +374 B (3%)
build/block-editor/style.css 11.8 kB +369 B (3%)
build/block-library/index.js 127 kB +18 B (0%)
build/block-serialization-default-parser/index.js 1.88 kB +1 B
build/blocks/index.js 48.1 kB +1 B
build/components/index.js 195 kB +415 B (0%)
build/components/style-rtl.css 19.5 kB +9 B (0%)
build/components/style.css 19.5 kB +7 B (0%)
build/core-data/index.js 11.4 kB -3 B (0%)
build/data-controls/index.js 1.29 kB -2 B (0%)
build/data/index.js 8.45 kB -2 B (0%)
build/date/index.js 5.47 kB -3 B (0%)
build/deprecated/index.js 772 B +1 B
build/dom/index.js 3.17 kB -1 B
build/edit-post/index.js 303 kB -2 B (0%)
build/edit-site/index.js 15.5 kB +1 B
build/edit-widgets/index.js 9.34 kB +1 B
build/editor/index.js 44.8 kB -3 B (0%)
build/element/index.js 4.64 kB -1 B
build/is-shallow-equal/index.js 710 B -1 B
build/keyboard-shortcuts/index.js 2.52 kB +2 B (0%)
build/media-utils/index.js 5.3 kB -1 B
build/plugins/index.js 2.56 kB -1 B
build/rich-text/index.js 14.8 kB -1 B
build/token-list/index.js 1.28 kB +1 B
build/url/index.js 4.06 kB +2 B (0%)
ℹ️ View Unchanged
Filename Size Change
build/annotations/index.js 3.62 kB 0 B
build/api-fetch/index.js 3.4 kB 0 B
build/autop/index.js 2.83 kB 0 B
build/blob/index.js 620 B 0 B
build/block-directory/index.js 6.77 kB 0 B
build/block-directory/style-rtl.css 892 B 0 B
build/block-directory/style.css 892 B 0 B
build/block-library/editor-rtl.css 7.88 kB 0 B
build/block-library/editor.css 7.89 kB 0 B
build/block-library/style-rtl.css 7.72 kB 0 B
build/block-library/style.css 7.72 kB 0 B
build/block-library/theme-rtl.css 684 B 0 B
build/block-library/theme.css 686 B 0 B
build/block-serialization-spec-parser/index.js 3.1 kB 0 B
build/compose/index.js 9.31 kB 0 B
build/dom-ready/index.js 569 B 0 B
build/edit-navigation/index.js 8.25 kB 0 B
build/edit-navigation/style-rtl.css 918 B 0 B
build/edit-navigation/style.css 919 B 0 B
build/edit-post/style-rtl.css 5.6 kB 0 B
build/edit-post/style.css 5.6 kB 0 B
build/edit-site/style-rtl.css 2.96 kB 0 B
build/edit-site/style.css 2.96 kB 0 B
build/edit-widgets/style-rtl.css 2.4 kB 0 B
build/edit-widgets/style.css 2.4 kB 0 B
build/editor/editor-styles-rtl.css 425 B 0 B
build/editor/editor-styles.css 428 B 0 B
build/editor/style-rtl.css 4.26 kB 0 B
build/editor/style.css 4.27 kB 0 B
build/escape-html/index.js 733 B 0 B
build/format-library/index.js 7.72 kB 0 B
build/format-library/style-rtl.css 502 B 0 B
build/format-library/style.css 502 B 0 B
build/hooks/index.js 2.13 kB 0 B
build/html-entities/index.js 621 B 0 B
build/i18n/index.js 3.56 kB 0 B
build/keycodes/index.js 1.94 kB 0 B
build/list-reusable-blocks/index.js 3.12 kB 0 B
build/list-reusable-blocks/style-rtl.css 226 B 0 B
build/list-reusable-blocks/style.css 226 B 0 B
build/notices/index.js 1.79 kB 0 B
build/nux/index.js 3.41 kB 0 B
build/nux/style-rtl.css 616 B 0 B
build/nux/style.css 613 B 0 B
build/primitives/index.js 1.5 kB 0 B
build/priority-queue/index.js 789 B 0 B
build/redux-routine/index.js 2.85 kB 0 B
build/server-side-render/index.js 2.68 kB 0 B
build/shortcode/index.js 1.7 kB 0 B
build/viewport/index.js 1.85 kB 0 B
build/warning/index.js 1.14 kB 0 B
build/wordcount/index.js 1.17 kB 0 B

compressed-size-action

@ellatrix
Copy link
Member Author

Just need to find a way to disable controls if some blocks in the selection don't support them... not clear at first how it can be done. Even if it's not possible, the value of this feature outweighs this downside imho.

@gziolo
Copy link
Member

gziolo commented May 19, 2020

This looks great, I like how clean this approach is and how easy it is to opt into this new block editor feature 👏

@ellatrix ellatrix force-pushed the try/multi-select-controls branch from ed2b74b to e947daf Compare May 21, 2020 14:51
@ellatrix ellatrix requested a review from gziolo May 21, 2020 14:59
@ellatrix
Copy link
Member Author

I think this is ready for review/opinions.

@ellatrix ellatrix force-pushed the try/multi-select-controls branch 2 times, most recently from 512c3d7 to fdb5b84 Compare May 27, 2020 14:41
@ellatrix ellatrix requested a review from ZebulanStanphill May 27, 2020 14:43
@ellatrix
Copy link
Member Author

Would be nice to have this in the next release.

const next = action.clientIds.reduce(
( accumulator, id ) => ( {
...accumulator,
[ id ]: reduce(
Copy link
Member

Choose a reason for hiding this comment

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

We could use the vanilla [].reduce here; we're already using it a few lines above.

Copy link
Member Author

Choose a reason for hiding this comment

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

But it's not an array that we reduce.

@ZebulanStanphill
Copy link
Member

I can't comment much on the technical side of this, but functionality-wise, this is fantastic; it seems to work as expected. The only thing I find a little weird is having the controls show the value of the first block in the selection. I think ideally, it would work a bit more like the font size control in Microsoft Word works when you select text of varying font size, indicating somehow that the selection as a whole does not have a consistent value. I don't think that should be a blocker for getting this merged, though.

Copy link
Contributor

@youknowriad youknowriad left a comment

Choose a reason for hiding this comment

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

Do you think it would be possible to add documentation to the Block API docs? I think having these docs in place will allow us to have a clear idea how it works...

@ellatrix
Copy link
Member Author

@ZebulanStanphill We can change the value to be undefined if not all values are the same.

@ellatrix
Copy link
Member Author

@youknowriad Sure, I'll find a place for it.

@ellatrix ellatrix force-pushed the try/multi-select-controls branch from fdb5b84 to 40d2ba7 Compare June 2, 2020 11:08
@gziolo
Copy link
Member

gziolo commented Jun 5, 2020

I think it needs to be rebased now after ifBlockEditSelected was removed in #22905.

Copy link
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

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

This looks really, good. It should be tested after rebasing into master and we could land it just after the next plugin release is cut to give it some time for proper testing in master.

@ellatrix ellatrix force-pushed the try/multi-select-controls branch from 40d2ba7 to 89421b8 Compare June 8, 2020 22:00
@ellatrix ellatrix requested review from nerrad and ntwb as code owners June 9, 2020 12:26
@ellatrix
Copy link
Member Author

ellatrix commented Jun 9, 2020

Added some tests. Let's merge now so it has some exposure before the next release.

@ellatrix ellatrix merged commit f9d22b3 into master Jun 9, 2020
@ellatrix ellatrix deleted the try/multi-select-controls branch June 9, 2020 15:35
@github-actions github-actions bot added this to the Gutenberg 8.4 milestone Jun 9, 2020
@youknowriad
Copy link
Contributor

The approach I take here is displaying controls of the first selected block if all selected blocks are of the same type and modifying setAttributes to set attributes of all selected blocks.

What "value" do you show on the controls if the blocks have different ones?

@lubieowoce
Copy link

lubieowoce commented Jun 12, 2020

@youknowriad in my experience using undefined for the "inconsistent" attribute seems to work okay – it just lets the block display whatever default/fallback it wants (though perhaps if the attribute has a specified default value, we should use that).

i'm slightly concerned about blocks like core/image – they have attributes that are content, not styling, and "averaging" them like this could break something; not sure if that's a real-world problem though.

a simple example that would break:

// attributes
{ has_val: boolean, val: number | undefined }
// either { has_val: false }
// or     { has_val: true, val: N }

a === { has_val: true, val: 3 }
b === { has_val: true, val: 5 }
average(a, b) === { has_val: true, val: undefined } // uh oh

@youknowriad
Copy link
Contributor

Yes, I'm wondering if it's better to make this more opt-in

@lubieowoce
Copy link

i've been thinking about the options we have available, it got a bit lengthy :)

let's consider an example block, an extension of my "problematic" example above:

registerBlockType( 'example/maybe-number', {
  attributes: {
    
    // "content"
    hasVal: { type: 'boolean', default: false },
    val:    { type: 'number'  },
    
    // "styling"
    decimalSeparator: {
      type: 'string',
      default: '.',
    },
    thousandsSpaces: {
      type: 'boolean',
      default: true,
    }
  },
  
  edit: ...,
})

we would like to be able to multi-edit the "styling" attributes, leaving "content" alone. we'd like the styling attributes to be "averaged" across the selected blocks, like in Word or Illustrator (show the common value if present, and "indeterminate"/default otherwise), and avoid the "averaging problem" outlined above.

here's the solutions i came up with:

a block-level multiEditable: true

this setting would enable showing the block-type's controls in multi-edit mode. this would leave it to the dev to opt-in if safe, and thus preserve correctness, but obviously suboptimal

a per-attribute multiEditable: true

could work with this PR's solution – show the first block's controls, but "average" the attributes marked as multiEditable and keep the rest unchanged. suboptimal UX for e.g. core/image – the controls shown would be a mix, some applying to the whole selection, some to the first block only.

add new controls api?

combined with per-attribute multiEditable, this could be ideal: allow rendering the controls for the "styling" subset of attributes independently from edit:

  attributes: {
    
    // "content"
    hasVal: { type: 'boolean', default: false },
    val:    { type: 'number'  },
    
    // "styling"
    decimalSeparator: {
      type: 'string',
      default: '.',
      multiEditable: true, 
    },
    thousandsSpaces: {
      type: 'boolean',
      default: true,
      multiEditable: true,
    }
  },
  
  // only gets the `multiEditable` subset of attributes
  controls: ({ decimalSeparator, thousandsSpaces }, setAttributes) => {
    // ...
  },
  
  edit: ({attributes: { hasVal, val }, setAttributes}) => {
    // ...
  },

this would free us from the "first block's controls" hack – we could just compute the averaged values and render controls for the whole selection. unfortunately this would require every existing block definition to be modified, but at least it's opt-in and backwards compatible. another possible downside – the split would make interactions with React state, hooks etc troublesome; not sure if that's a problem

importantly, the "styling" attributes must either be independent of each other, or always updated in tandem. if we also had e.g.

  attributes: {
    // ...
    
    // "styling”
    // ...
    hasCurrency: { type: boolean },
    currency:    { type: string  },
  }

a multi-edit setAttributes({ currency: '$' }) in controls could put some of the selected blocks in an inconsistent state, like { ..., hasCurrency: false, currency: '$' }. granted, it wouldn't be a big deal in this case, but i imagine cases where it might be.

note:hasCurrency is only intended to illustrate problems with attributes that "depend" on other attributes; in this case, you could get by with currency being a string|null, but any sum-type-ish thing would have the same issues. consider something with multiple "states":{ status: 'EMPTY' } | { status: 'LOADING', someId: string } | { status: 'LOADED', someId: string, fetchedData: string }.

so we would either have to leave it to the dev to always update interdependent attributes together (setAttributes({ hasCurrency: true, currency: '$' })) or somehow make controls more fine-grained:

  // array of <attribute names + render function>
  controls: [
    [['thousandsSeparator'], (thousandsSeparator) => (
      <Dropdown ...>
    )],
    [['hasCurrency', 'currency'], (hasCurrency, currency) => (
      <>
        <Checkbox ...>
        <Dropdown ...>
      </>
    )],
  ]

which would enforce correctness (independent controls have independent render functions), but is pretty ugly. however, the fine-grainedness could be useful if we decide to allow multi-edit in a mixed-block-type selection

alternatives?

maybe we could make block devs put more info in the attributes definition, specifying interdependent attributes or enumerating states ala sum types. but i don't have a concrete idea for this, and i'm not sure if making attributes some complicated DSL would work well.

Summary

these are the options i could think of – there's probably other choices. however, i think something like separating controls from edit is key to a complete solution.

there's also the issue of RichText controls - ideally, those would be multi-editable as well, but then a block would have to specify which of its attributes should be treated as RichText. i think that one could be solved by introducing a new attribute type 'rich-text', distinct from 'string'.

This was referenced Jun 24, 2020
@ellatrix
Copy link
Member Author

@lubieowoce Perhaps we should make it opt in. Could you create a new issue? Comments on closed PR get a bit lost.

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 [Type] Enhancement A suggestion for improvement.
Projects
None yet
5 participants