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

Try multi-select inspector for same blocks #3535

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
91 changes: 48 additions & 43 deletions blocks/library/paragraph/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import AlignmentToolbar from '../../alignment-toolbar';
import BlockAlignmentToolbar from '../../block-alignment-toolbar';
import BlockControls from '../../block-controls';
import Editable from '../../editable';
import InspectorControls from '../../inspector-controls';
import ToggleControl from '../../inspector-controls/toggle-control';
import RangeControl from '../../inspector-controls/range-control';
import ColorPalette from '../../color-palette';
Expand Down Expand Up @@ -93,9 +92,55 @@ registerBlockType( 'core/paragraph', {
}
},

inspector( { attributes, setAttributes } ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

A little bit concerned about this. Some times we need local state to be shared beween the inspector, toolbar and the edit. If we separate this, it's not possible anymore.

Do you think it's possible to add some magic to InspectorControls to detect if it's a multiselection and only render for one block only or something?

Copy link
Member Author

Choose a reason for hiding this comment

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

I tried it before, but can't remember it well, so I'll try it again. Could you give an example of local state, just for me to think about solutions for it? :)

Copy link
Contributor

Choose a reason for hiding this comment

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

We don't have an exemple with shared local state between the edit and BlockInspector but we do have between edit and BlockControls (in the audio or block blocks) and I believe these two are similar because in the exact same way we can display the same block toolbar in we multi-select similar blocks.

Copy link
Member Author

Choose a reason for hiding this comment

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

Exactly, what is done here for the inspector should then also be done for the toolbar. The problem with the edit function is that it makes use for focus. We'd need to remove that and figure out how to do that differently. I'll find the example with shared state and think about it.

Copy link
Member Author

Choose a reason for hiding this comment

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

@youknowriad Which block are you referring to with the local state? Is there anything that wouldn't be solvable with block attributes? Ideally, we should stick with those for state.

Copy link
Contributor

Choose a reason for hiding this comment

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

@iseulde block attributes are "state" but they are "state" persisted in post_content or comments or meta. I don't know if it's a good idea to use them for "local state", we may end up serializing useless local state attributes in comments.

Copy link
Contributor

Choose a reason for hiding this comment

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

For example, the audio block triggers an editing local state using a button toolbar. And this local state changes the UI of the block.

Copy link
Member

Choose a reason for hiding this comment

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

I'm looking at this for the block toolbar (so if the selected blocks are of the same type, they get the toolbar and the controls apply changes to all selected blocks.)

It seems to me that having an inspector and toolbar function like this is very close to what we need, but as we need to share state between edit and BlockInspector/Toolbar, we need to reuse those functions to fill the appropriate slots.

The render in edit would look like this:

<BlockControls>
    { this.toolbar( { state, attributes, setAttributes } ) }
</BlockControls>
<InspectorControls>
    { this.inspector( { state, attributes, setAttributes } ) }
</InspectorControls> 

The slots would get filled as normal, single block selection behaviour in unchanged.

Then, for example, in the block inspector where we currently have "Coming Soon", we'd get the block settings for the type of blocks we have selected, and call settings.inspector passing an appropriate state, attributes, and a setAttributes function that would set attributes across all selected blocks. The state and attributes could be reduced from the selected blocks, so that if all blocks had the same value for a control, it would get that value (for example, if all paragraphs were the same color, that color would be selected).

const { dropCap, fontSize, backgroundColor, textColor, width } = attributes;
const toggleDropCap = () => setAttributes( { dropCap: ! dropCap } );

return (
<div>
<BlockDescription>
<p>{ __( 'Text. Great things start here.' ) }</p>
</BlockDescription>
<PanelBody title={ __( 'Text Settings' ) }>
<ToggleControl
label={ __( 'Drop Cap' ) }
checked={ !! dropCap }
onChange={ toggleDropCap }
/>
<RangeControl
label={ __( 'Font Size' ) }
value={ fontSize || '' }
onChange={ ( value ) => setAttributes( { fontSize: value } ) }
min={ 10 }
max={ 200 }
beforeIcon="editor-textcolor"
allowReset
/>
</PanelBody>
<PanelColor title={ __( 'Background Color' ) } colorValue={ backgroundColor } initialOpen={ false }>
<ColorPalette
value={ backgroundColor }
onChange={ ( colorValue ) => setAttributes( { backgroundColor: colorValue } ) }
/>
</PanelColor>
<PanelColor title={ __( 'Text Color' ) } colorValue={ textColor } initialOpen={ false }>
<ColorPalette
value={ textColor }
onChange={ ( colorValue ) => setAttributes( { textColor: colorValue } ) }
/>
</PanelColor>
<PanelBody title={ __( 'Block Alignment' ) }>
<BlockAlignmentToolbar
value={ width }
onChange={ ( nextWidth ) => setAttributes( { width: nextWidth } ) }
/>
</PanelBody>
</div>
);
},

edit( { attributes, setAttributes, insertBlocksAfter, focus, setFocus, mergeBlocks, onReplace } ) {
const { align, content, dropCap, placeholder, fontSize, backgroundColor, textColor, width } = attributes;
const toggleDropCap = () => setAttributes( { dropCap: ! dropCap } );
const className = dropCap ? 'has-drop-cap' : null;

return [
Expand All @@ -109,47 +154,7 @@ registerBlockType( 'core/paragraph', {
/>
</BlockControls>
),
focus && (
<InspectorControls key="inspector">
<BlockDescription>
<p>{ __( 'Text. Great things start here.' ) }</p>
</BlockDescription>
<PanelBody title={ __( 'Text Settings' ) }>
<ToggleControl
label={ __( 'Drop Cap' ) }
checked={ !! dropCap }
onChange={ toggleDropCap }
/>
<RangeControl
label={ __( 'Font Size' ) }
value={ fontSize || '' }
onChange={ ( value ) => setAttributes( { fontSize: value } ) }
min={ 10 }
max={ 200 }
beforeIcon="editor-textcolor"
allowReset
/>
</PanelBody>
<PanelColor title={ __( 'Background Color' ) } colorValue={ backgroundColor } initialOpen={ false }>
<ColorPalette
value={ backgroundColor }
onChange={ ( colorValue ) => setAttributes( { backgroundColor: colorValue } ) }
/>
</PanelColor>
<PanelColor title={ __( 'Text Color' ) } colorValue={ textColor } initialOpen={ false }>
<ColorPalette
value={ textColor }
onChange={ ( colorValue ) => setAttributes( { textColor: colorValue } ) }
/>
</PanelColor>
<PanelBody title={ __( 'Block Alignment' ) }>
<BlockAlignmentToolbar
value={ width }
onChange={ ( nextWidth ) => setAttributes( { width: nextWidth } ) }
/>
</PanelBody>
</InspectorControls>
),

<Autocomplete key="editable" completers={ [
blockAutocompleter( { onReplace } ),
userAutocompleter(),
Expand Down
66 changes: 62 additions & 4 deletions editor/components/block-inspector/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,76 @@
* External dependencies
*/
import { connect } from 'react-redux';
import { uniq, keys, flatten } from 'lodash';

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { Slot } from '@wordpress/components';
import { getBlockType } from '@wordpress/blocks';

/**
* Internal Dependencies
*/
import './style.scss';
import { getSelectedBlock, getSelectedBlockCount } from '../../selectors';
import { getSelectedBlock, getSelectedBlockCount, getMultiSelectedBlocks } from '../../selectors';

const BlockInspector = ( { selectedBlock, count } ) => {
const BlockInspector = ( { selectedBlock, count, multiSelectedBlocks, onChange } ) => {
if ( count > 1 ) {
return <span className="editor-block-inspector__multi-blocks">{ __( 'Coming Soon' ) }</span>;
const names = uniq( multiSelectedBlocks.map( ( { name } ) => name ) );

if ( names.length === 1 ) {
const Inspector = getBlockType( names[ 0 ] ).inspector;

if ( ! Inspector ) {
return null;
}

const attributeArray = multiSelectedBlocks.map( ( block ) => block.attributes );
const attributeKeys = uniq( flatten( attributeArray.map( keys ) ) );
const attributes = attributeKeys.reduce( ( acc, key ) => {
Copy link
Member Author

Choose a reason for hiding this comment

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

Attempted to use lodash's intersection or similar, but not quite sure how to get it to work.

acc[ key ] = attributeArray.reduce( ( accu, attrs ) => {
return accu === attrs[ key ] ? accu : undefined;
}, attributeArray[ 0 ][ key ] );
return acc;
}, {} );

const setAttributes = ( attrs ) => {
multiSelectedBlocks.forEach( ( block ) => {
onChange( block.uid, {
...block.attributes,
...attrs,
} );
} );
};

return (
<Inspector
attributes={ attributes }
setAttributes={ setAttributes }
/>
);
}

return <span className="editor-block-inspector__multi-blocks">{ __( 'Various blocks' ) }</span>;
}

if ( ! selectedBlock ) {
return <span className="editor-block-inspector__no-blocks">{ __( 'No block selected.' ) }</span>;
}

const Inspector = getBlockType( selectedBlock.name ).inspector;

if ( Inspector ) {
return (
<Inspector
attributes={ selectedBlock.attributes }
setAttributes={ ( attrs ) => onChange( selectedBlock.uid, attrs ) }
/>
);
}

return (
<Slot name="Inspector.Controls" />
);
Expand All @@ -34,6 +82,16 @@ export default connect(
return {
selectedBlock: getSelectedBlock( state ),
count: getSelectedBlockCount( state ),
multiSelectedBlocks: getMultiSelectedBlocks( state ),
};
}
},
( dispatch ) => ( {
onChange( uid, attributes ) {
dispatch( {
type: 'UPDATE_BLOCK_ATTRIBUTES',
uid,
attributes,
} );
},
} ),
)( BlockInspector );