diff --git a/packages/block-editor/src/components/alignment-toolbar/multi-block.js b/packages/block-editor/src/components/alignment-toolbar/multi-block.js new file mode 100644 index 0000000000000..dee613fb0348f --- /dev/null +++ b/packages/block-editor/src/components/alignment-toolbar/multi-block.js @@ -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; diff --git a/packages/block-editor/src/components/block-controls/multi-block-controls.js b/packages/block-editor/src/components/block-controls/multi-block-controls.js new file mode 100644 index 0000000000000..24ce3534792ad --- /dev/null +++ b/packages/block-editor/src/components/block-controls/multi-block-controls.js @@ -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 } ) => ( + + + { children } + +); + +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 ( + + ); + }; + 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 ); diff --git a/packages/block-editor/src/components/block-edit/context.js b/packages/block-editor/src/components/block-edit/context.js index 863cdc3e5d6fa..9ab62211dcf59 100644 --- a/packages/block-editor/src/components/block-edit/context.js +++ b/packages/block-editor/src/components/block-edit/context.js @@ -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: '', @@ -59,3 +60,46 @@ export const ifBlockEditSelected = createHigherOrderComponent( ( OriginalCompone ); }, '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 ( + + { ( { isSelected, clientId } ) => ( isSelected || ( clientId === props.getFirstMultiSelectedBlockClientId && props.allSelectedBlocksOfSameType ) ) && ( + + ) } + + ); + }; +}, '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 ); +}; diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index c5bc11d920f95..d8b023be8485b 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -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'; @@ -22,6 +23,7 @@ function BlockToolbar( { blockClientIds, isValid, mode } ) { return (
+
); @@ -32,6 +34,7 @@ function BlockToolbar( { blockClientIds, isValid, mode } ) { { mode === 'visual' && isValid && ( + diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index c9a454c253d27..f64ef8d08881d 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -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 { diff --git a/packages/block-library/src/paragraph/edit.js b/packages/block-library/src/paragraph/edit.js index 0ed9c5c91b582..5c91c7e973522 100644 --- a/packages/block-library/src/paragraph/edit.js +++ b/packages/block-library/src/paragraph/edit.js @@ -19,8 +19,8 @@ import { } from '@wordpress/components'; import { withColors, - AlignmentToolbar, - BlockControls, + MultiBlockAlignmentToolbar, + MultiBlockControls, ContrastChecker, FontSizePicker, InspectorControls, @@ -152,8 +152,8 @@ class ParagraphBlock extends Component { return ( - - + { setAttributes( { align: nextAlign } ); @@ -176,7 +176,7 @@ class ParagraphBlock extends Component { ] } /> ) } - +