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 a toolbar that affects multiple selected blocks #7635

Closed
wants to merge 2 commits 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Internal dependencies
*/
import { withMultiBlockSupport } from '../block-controls/multi-block-controls';
import AlignmentToolbar from './';

const MultiBlockAlignmentToolbar = withMultiBlockSupport( AlignmentToolbar, 'align' );

export default MultiBlockAlignmentToolbar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* WordPress dependencies
*/
import { createSlotFill, Toolbar } from '@wordpress/components';
import { createHigherOrderComponent, compose } from '@wordpress/compose';
import { withSelect, withDispatch } from '@wordpress/data';

/**
* Internal dependencies
*/
import { withFirstOrOnlyBlockSelected } from '../block-edit/context';

const { Fill, Slot } = createSlotFill( 'MultiBlockControls' );

const MultiBlockControlsFill = ( { controls, children } ) => (
<Fill>
<Toolbar controls={ controls } />
{ children }
</Fill>
);

const MultiBlockControls = withFirstOrOnlyBlockSelected( MultiBlockControlsFill );

MultiBlockControls.Slot = Slot;

export default MultiBlockControls;

/**
* Reduces blocks to a single attribute's value, taking the first in the list as
* a default, returning `undefined` if all blocks are not the same value.
*
* @param {Array} multiSelectedBlocks Array of selected blocks.
* @param {string} attributeName Attribute name.
*
* @return {*} Reduced value of attribute.
*/
function reduceAttribute( multiSelectedBlocks, attributeName ) {
let attribute;
// Reduce the selected block's attributes, so if they all have the
// same value for an attribute, we get it in the multi toolbar attributes.
for ( let i = 0; i < multiSelectedBlocks.length; i++ ) {
const block = multiSelectedBlocks[ i ];
if ( block.attributes[ attributeName ] === attribute || 0 === i ) {
attribute = block.attributes[ attributeName ];
} else {
attribute = undefined;
}
}
return attribute;
}

/**
* Adds multi block support to a block control. If the control is used when there is a
* multi block selection, the `onChange` and `value` props are intercepted, and uses
* `reduceAttribute` to get a single value for the control from all selected blocks,
* and changes all selected blocks with the new value.
*
* This requires that multi block controls have `value` and `onChange` props, and
* set attributes on blocks with no other side effects (other than those handled
* when the edit component receives new props)
*
* @param {Component} component Component to make multi block selection aware.
* @param {string} attributeName Attribute name the component controls.
*
* @return {Component} Component that can handle multple selected blocks.
*/
export const withMultiBlockSupport = ( component, attributeName ) => createHigherOrderComponent( ( OriginalComponent ) => {
const multSelectComponent = ( props ) => {
let newProps = props;
if ( props.multiSelectedBlocks.length > 1 ) {
newProps = { ...props };
newProps.value = reduceAttribute( props.multiSelectedBlocks, attributeName );
newProps.onChange = ( newValue ) => {
const newAttributes = {
[ attributeName ]: newValue,
};
for ( let i = 0; i < props.multiSelectedBlocks.length; i++ ) {
newProps.onMultiBlockChange( props.multiSelectedBlocks[ i ].clientId, newAttributes );
}
};
}
return (
<OriginalComponent { ...newProps } />
);
};
return compose( [
withSelect( ( select ) => {
const { getMultiSelectedBlocks } = select( 'core/editor' );
return {
multiSelectedBlocks: getMultiSelectedBlocks(),
};
} ),
withDispatch( ( dispatch ) => {
const { updateBlockAttributes } = dispatch( 'core/editor' );
return {
onMultiBlockChange( uid, attributes ) {
updateBlockAttributes( uid, attributes );
},
};
} ),
] )( multSelectComponent );
}, 'withMultiBlockSupport' )( component );
48 changes: 46 additions & 2 deletions packages/block-editor/src/components/block-edit/context.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
/**
* External dependencies
*/
import { noop } from 'lodash';
import { noop, uniq } from 'lodash';

/**
* WordPress dependencies
*/
import { createContext } from '@wordpress/element';
import { createHigherOrderComponent } from '@wordpress/compose';
import { createHigherOrderComponent, compose } from '@wordpress/compose';
import { withSelect } from '@wordpress/data';

const { Consumer, Provider } = createContext( {
name: '',
Expand Down Expand Up @@ -59,3 +60,46 @@ export const ifBlockEditSelected = createHigherOrderComponent( ( OriginalCompone
</Consumer>
);
}, 'ifBlockEditSelected' );

/**
* A Higher Order Component used to render conditionally the wrapped
* component only when the BlockEdit has selected state set or it is
* the first block in a multi selection of all one type of block..
*
* @param {Component} OriginalComponent Component to wrap.
*
* @return {Component} Component which renders only when the BlockEdit is selected or it is the first block in a multi selection.
*/
const isFirstOrOnlyBlockSelected = createHigherOrderComponent( ( OriginalComponent ) => {
return ( props ) => {
return (
<Consumer>
{ ( { isSelected, clientId } ) => ( isSelected || ( clientId === props.getFirstMultiSelectedBlockClientId && props.allSelectedBlocksOfSameType ) ) && (
<OriginalComponent { ...props } />
) }
</Consumer>
);
};
}, 'isFirstOrOnlyBlockSelected' );

export const withFirstOrOnlyBlockSelected = ( component ) => {
return compose( [
withSelect( ( select ) => {
const {
getMultiSelectedBlocks,
getFirstMultiSelectedBlockClientId,
isMultiSelecting,
} = select( 'core/editor' );
const allSelectedBlocksOfSameType = uniq(
getMultiSelectedBlocks().map( ( { name } ) => name )
).length === 1;
return {
getFirstMultiSelectedBlockClientId: getFirstMultiSelectedBlockClientId(),
isSelecting: isMultiSelecting(),
selectedBlocks: getMultiSelectedBlocks(),
allSelectedBlocksOfSameType,
};
} ),
isFirstOrOnlyBlockSelected,
] )( component );
};
5 changes: 4 additions & 1 deletion packages/block-editor/src/components/block-toolbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import { Fragment } from '@wordpress/element';
* Internal dependencies
*/
import BlockSwitcher from '../block-switcher';
import MultiBlocksSwitcher from '../block-switcher/multi-blocks-switcher';
import BlockControls from '../block-controls';
import MultiBlockControls from '../block-controls/multi-block-controls';
import MultiBlocksSwitcher from '../block-switcher/multi-blocks-switcher';
import BlockFormatControls from '../block-format-controls';
import BlockSettingsMenu from '../block-settings-menu';

Expand All @@ -22,6 +23,7 @@ function BlockToolbar( { blockClientIds, isValid, mode } ) {
return (
<div className="editor-block-toolbar block-editor-block-toolbar">
<MultiBlocksSwitcher />
<MultiBlockControls.Slot />
<BlockSettingsMenu clientIds={ blockClientIds } />
</div>
);
Expand All @@ -32,6 +34,7 @@ function BlockToolbar( { blockClientIds, isValid, mode } ) {
{ mode === 'visual' && isValid && (
<Fragment>
<BlockSwitcher clientIds={ blockClientIds } />
<MultiBlockControls.Slot />
<BlockControls.Slot />
<BlockFormatControls.Slot />
</Fragment>
Expand Down
2 changes: 2 additions & 0 deletions packages/block-editor/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export * from './font-sizes';
export { default as InnerBlocks } from './inner-blocks';
export { default as InspectorAdvancedControls } from './inspector-advanced-controls';
export { default as InspectorControls } from './inspector-controls';
export { default as MultiBlockAlignmentToolbar } from './alignment-toolbar/multi-block';
export { default as MultiBlockControls } from './block-controls/multi-block-controls';
export { default as PanelColorSettings } from './panel-color-settings';
export { default as PlainText } from './plain-text';
export {
Expand Down
10 changes: 5 additions & 5 deletions packages/block-library/src/paragraph/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import {
} from '@wordpress/components';
import {
withColors,
AlignmentToolbar,
BlockControls,
MultiBlockAlignmentToolbar,
MultiBlockControls,
ContrastChecker,
FontSizePicker,
InspectorControls,
Expand Down Expand Up @@ -152,8 +152,8 @@ class ParagraphBlock extends Component {

return (
<Fragment>
<BlockControls>
<AlignmentToolbar
<MultiBlockControls>
<MultiBlockAlignmentToolbar
value={ align }
onChange={ ( nextAlign ) => {
setAttributes( { align: nextAlign } );
Expand All @@ -176,7 +176,7 @@ class ParagraphBlock extends Component {
] }
/>
) }
</BlockControls>
</MultiBlockControls>
<InspectorControls>
<PanelBody title={ __( 'Text Settings' ) } className="blocks-font-size">
<FontSizePicker
Expand Down