Skip to content

Commit

Permalink
Multi block toolbar controls support
Browse files Browse the repository at this point in the history
  • Loading branch information
notnownikki committed Jul 17, 2018
1 parent 3343e8c commit f6de2b9
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 13 deletions.
11 changes: 5 additions & 6 deletions core-blocks/paragraph/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ import {
import {
getColorClass,
withColors,
AlignmentToolbar,
BlockControls,
MultiBlockAlignmentToolbar,
ContrastChecker,
InspectorControls,
MultiBlockControls,
PanelColor,
RichText,
} from '@wordpress/editor';
Expand Down Expand Up @@ -206,17 +206,16 @@ class ParagraphBlock extends Component {
} = attributes;

const fontSize = this.getFontSize();

return (
<Fragment>
<BlockControls>
<AlignmentToolbar
<MultiBlockControls>
<MultiBlockAlignmentToolbar
value={ align }
onChange={ ( nextAlign ) => {
setAttributes( { align: nextAlign } );
} }
/>
</BlockControls>
</MultiBlockControls>
<InspectorControls>
<PanelBody title={ __( 'Text Settings' ) } className="blocks-font-size">
<FontSizePicker
Expand Down
9 changes: 9 additions & 0 deletions editor/components/alignment-toolbar/multi-block.js
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;
101 changes: 101 additions & 0 deletions editor/components/block-controls/multi-block-controls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* WordPress dependencies
*/
import { createSlotFill, Toolbar } from '@wordpress/components';
import { createHigherOrderComponent, compose } from '@wordpress/compose';
import { withSelect, withDispatch } from '@wordpress/data';

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

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

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

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 (
<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 );
46 changes: 44 additions & 2 deletions editor/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,44 @@ 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 isFirstOrOnlyBlockSelectedHOC = createHigherOrderComponent( ( OriginalComponent ) => {
return ( props ) => {
return (
<Consumer>
{ ( { isSelected, uid } ) => ( isSelected || ( uid === props.firstMultiSelectedBlockUid && props.allSelectedBlocksOfSameType ) ) && (
<OriginalComponent { ...props } />
) }
</Consumer>
);
};
}, '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 ) );
};
13 changes: 8 additions & 5 deletions editor/components/block-toolbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,43 @@ 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 (
<div className="editor-block-toolbar">
<MultiBlocksSwitcher />
<MultiBlockControls.Slot />
</div>
);
}

if ( ! isValid || 'visual' !== mode ) {
return null;
}

return (
<div className="editor-block-toolbar">
<BlockSwitcher uids={ blockUIDs } />
<MultiBlockControls.Slot />
<BlockControls.Slot />
<BlockFormatControls.Slot />
</div>
);
}

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();

return {
blockUIDs,
isValid: block ? block.isValid : null,
mode: block ? getBlockMode( block.uid ) : null,
isSelecting: isMultiSelecting(),
};
} )( BlockToolbar );
2 changes: 2 additions & 0 deletions editor/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit f6de2b9

Please sign in to comment.