From f6de2b9b511fe4125ec3faf4a683c9e383b3a317 Mon Sep 17 00:00:00 2001 From: Nicola Heald Date: Mon, 16 Jul 2018 18:35:51 +0100 Subject: [PATCH] Multi block toolbar controls support --- core-blocks/paragraph/index.js | 11 +- .../alignment-toolbar/multi-block.js | 9 ++ .../block-controls/multi-block-controls.js | 101 ++++++++++++++++++ editor/components/block-edit/context.js | 46 +++++++- editor/components/block-toolbar/index.js | 13 ++- editor/components/index.js | 2 + 6 files changed, 169 insertions(+), 13 deletions(-) create mode 100644 editor/components/alignment-toolbar/multi-block.js create mode 100644 editor/components/block-controls/multi-block-controls.js diff --git a/core-blocks/paragraph/index.js b/core-blocks/paragraph/index.js index acdc498c55faf..82be3fe52d815 100644 --- a/core-blocks/paragraph/index.js +++ b/core-blocks/paragraph/index.js @@ -23,10 +23,10 @@ import { import { getColorClass, withColors, - AlignmentToolbar, - BlockControls, + MultiBlockAlignmentToolbar, ContrastChecker, InspectorControls, + MultiBlockControls, PanelColor, RichText, } from '@wordpress/editor'; @@ -206,17 +206,16 @@ class ParagraphBlock extends Component { } = attributes; const fontSize = this.getFontSize(); - return ( - - + { setAttributes( { align: nextAlign } ); } } /> - + ( + + + { children } + +); + +const MultiBlockControls = isFirstOrOnlyBlockSelected( 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 ) => { + const newProps = { ...props }; + if ( props.multiSelectedBlocks.length > 1 ) { + 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 ].uid, 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/editor/components/block-edit/context.js b/editor/components/block-edit/context.js index 99591d0fdb321..30158cf97ab3f 100644 --- a/editor/components/block-edit/context.js +++ b/editor/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,44 @@ 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 isFirstOrOnlyBlockSelectedHOC = createHigherOrderComponent( ( OriginalComponent ) => { + return ( props ) => { + return ( + + { ( { isSelected, uid } ) => ( isSelected || ( uid === props.firstMultiSelectedBlockUid && props.allSelectedBlocksOfSameType ) ) && ( + + ) } + + ); + }; +}, 'isFirstOrOnlyBlockSelected' ); + +export const isFirstOrOnlyBlockSelected = ( component ) => { + return compose( [ + withSelect( ( select ) => { + const { + getMultiSelectedBlocks, + getFirstMultiSelectedBlockUid, + isMultiSelecting, + } = select( 'core/editor' ); + const allSelectedBlocksOfSameType = uniq( getMultiSelectedBlocks().map( ( { name } ) => name ) ).length === 1; + return { + firstMultiSelectedBlockUid: getFirstMultiSelectedBlockUid(), + isSelecting: isMultiSelecting(), + selectedBlocks: getMultiSelectedBlocks(), + allSelectedBlocksOfSameType, + }; + } ), + ] )( isFirstOrOnlyBlockSelectedHOC( component ) ); +}; diff --git a/editor/components/block-toolbar/index.js b/editor/components/block-toolbar/index.js index f3a4df5e385f2..72ee4c372e298 100644 --- a/editor/components/block-toolbar/index.js +++ b/editor/components/block-toolbar/index.js @@ -8,19 +8,20 @@ import { withSelect } from '@wordpress/data'; */ import './style.scss'; 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'; -function BlockToolbar( { blockUIDs, isValid, mode } ) { - if ( blockUIDs.length > 1 ) { +function BlockToolbar( { blockUIDs, isValid, mode, isSelecting } ) { + if ( blockUIDs.length > 1 && ! isSelecting ) { return (
+
); } - if ( ! isValid || 'visual' !== mode ) { return null; } @@ -28,6 +29,7 @@ function BlockToolbar( { blockUIDs, isValid, mode } ) { return (
+
@@ -35,7 +37,7 @@ function BlockToolbar( { blockUIDs, isValid, mode } ) { } export default withSelect( ( select ) => { - const { getSelectedBlock, getBlockMode, getMultiSelectedBlockUids } = select( 'core/editor' ); + const { getSelectedBlock, getBlockMode, getMultiSelectedBlockUids, isMultiSelecting } = select( 'core/editor' ); const block = getSelectedBlock(); const blockUIDs = block ? [ block.uid ] : getMultiSelectedBlockUids(); @@ -43,5 +45,6 @@ export default withSelect( ( select ) => { blockUIDs, isValid: block ? block.isValid : null, mode: block ? getBlockMode( block.uid ) : null, + isSelecting: isMultiSelecting(), }; } )( BlockToolbar ); diff --git a/editor/components/index.js b/editor/components/index.js index 0ae4fbc914cfb..e9cf8ff26cfac 100644 --- a/editor/components/index.js +++ b/editor/components/index.js @@ -14,6 +14,8 @@ export { default as ContrastChecker } from './contrast-checker'; 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 PanelColor } from './panel-color'; export { default as PlainText } from './plain-text'; export { default as RichText } from './rich-text';