diff --git a/blocks/library/paragraph/index.js b/blocks/library/paragraph/index.js index b3550f18d26f69..9b04e609dc635c 100644 --- a/blocks/library/paragraph/index.js +++ b/blocks/library/paragraph/index.js @@ -94,6 +94,22 @@ registerBlockType( 'core/paragraph', { } }, + multiSelectedControls( { attributes, setAttributes } ) { + const value = attributes.reduce( ( acc, { align } ) => { + return acc === align ? acc : null; + }, attributes[ 0 ].align ); + + return [ + { + setAttributes( { align: nextAlign } ); + } } + />, + ]; + }, + edit( { attributes, setAttributes, insertBlocksAfter, focus, setFocus, mergeBlocks, onReplace } ) { const { align, content, dropCap, placeholder, fontSize, backgroundColor, textColor, width } = attributes; const toggleDropCap = () => setAttributes( { dropCap: ! dropCap } ); diff --git a/editor/block-settings-menu/index.js b/editor/block-settings-menu/index.js index d6121c7c290b4b..918ba17dc3ad2d 100644 --- a/editor/block-settings-menu/index.js +++ b/editor/block-settings-menu/index.js @@ -51,11 +51,13 @@ export default connect( onDelete() { dispatch( { type: 'REMOVE_BLOCKS', - uids: [ ownProps.uid ], + uids: ownProps.uids, } ); }, onSelect() { - dispatch( selectBlock( ownProps.uid ) ); + if ( ownProps.uids.length === 1 ) { + dispatch( selectBlock( ownProps.uids[ 0 ] ) ); + } }, setActivePanel() { dispatch( { diff --git a/editor/block-switcher/index.js b/editor/block-switcher/index.js index bc38d3f59dc569..df4cecedfe745c 100644 --- a/editor/block-switcher/index.js +++ b/editor/block-switcher/index.js @@ -2,7 +2,7 @@ * External dependencies */ import { connect } from 'react-redux'; -import { uniq, get, reduce, find } from 'lodash'; +import { uniq, get, reduce, find, first, last } from 'lodash'; import clickOutside from 'react-click-outside'; /** @@ -17,8 +17,8 @@ import { getBlockType, getBlockTypes, switchToBlockType } from '@wordpress/block * Internal dependencies */ import './style.scss'; -import { replaceBlocks } from '../actions'; -import { getBlock } from '../selectors'; +import { replaceBlocks, multiSelect } from '../actions'; +import { getBlocksByUid } from '../selectors'; class BlockSwitcher extends Component { constructor() { @@ -48,15 +48,23 @@ class BlockSwitcher extends Component { this.setState( { open: false, } ); - this.props.onTransform( this.props.block, name ); + this.props.onTransform( this.props.blocks, name ); }; } render() { - const blockType = getBlockType( this.props.block.name ); + const names = uniq( this.props.blocks.map( ( block ) => block.name ) ); + + // Blocks do not share the same name. + if ( names.length !== 1 ) { + return null; + } + + const blockName = first( names ); + const blockType = getBlockType( blockName ); const blocksToBeTransformedFrom = reduce( getBlockTypes(), ( memo, block ) => { const transformFrom = get( block, 'transforms.from', [] ); - const transformation = find( transformFrom, t => t.type === 'block' && t.blocks.indexOf( this.props.block.name ) !== -1 ); + const transformation = find( transformFrom, t => t.type === 'block' && t.blocks.indexOf( blockName ) !== -1 ); return transformation ? memo.concat( [ block.name ] ) : memo; }, [] ); const blocksToBeTransformedTo = get( blockType, 'transforms.to', [] ) @@ -115,14 +123,20 @@ class BlockSwitcher extends Component { export default connect( ( state, ownProps ) => ( { - block: getBlock( state, ownProps.uid ), + blocks: getBlocksByUid( state, ownProps.uids ), } ), ( dispatch, ownProps ) => ( { - onTransform( block, name ) { + onTransform( blocks, name ) { dispatch( replaceBlocks( - [ ownProps.uid ], - switchToBlockType( block, name ) + ownProps.uids, + blocks.reduce( ( acc, block ) => { + return [ ...acc, ...switchToBlockType( block, name ) ]; + }, [] ), ) ); + + if ( blocks.length > 1 ) { + dispatch( multiSelect( first( blocks ).uid, last( blocks ).uid ) ); + } }, } ) )( clickOutside( BlockSwitcher ) ); diff --git a/editor/block-toolbar/index.js b/editor/block-toolbar/index.js index 2639aed32a2086..605bed3ec26866 100644 --- a/editor/block-toolbar/index.js +++ b/editor/block-toolbar/index.js @@ -42,7 +42,7 @@ class BlockToolbar extends Component { render() { const { showMobileControls } = this.state; - const { uid } = this.props; + const { uids } = this.props; const toolbarClassname = classnames( 'editor-block-toolbar', { 'is-showing-mobile-controls': showMobileControls, @@ -60,7 +60,7 @@ class BlockToolbar extends Component {
{ ! showMobileControls && [ - , + , , ] } @@ -74,8 +74,8 @@ class BlockToolbar extends Component { { showMobileControls &&
- - + +
}
diff --git a/editor/header/index.js b/editor/header/index.js index aa0f6a0e0f8b76..cd7cf4e43e05e9 100644 --- a/editor/header/index.js +++ b/editor/header/index.js @@ -6,7 +6,7 @@ import { connect } from 'react-redux'; /** * WordPress dependencies */ -import { sprintf, _n, __ } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import { IconButton } from '@wordpress/components'; /** @@ -18,13 +18,9 @@ import PublishButton from './publish-button'; import PreviewButton from './preview-button'; import ModeSwitcher from './mode-switcher'; import Inserter from '../inserter'; -import { getMultiSelectedBlockUids, hasEditorUndo, hasEditorRedo, isEditorSidebarOpened } from '../selectors'; -import { clearSelectedBlock } from '../actions'; +import { hasEditorUndo, hasEditorRedo, isEditorSidebarOpened } from '../selectors'; function Header( { - multiSelectedBlockUids, - onRemove, - onDeselect, undo, redo, hasRedo, @@ -32,39 +28,6 @@ function Header( { toggleSidebar, isSidebarOpened, } ) { - const count = multiSelectedBlockUids.length; - - if ( count ) { - return ( -
-
- { sprintf( _n( '%d block selected', '%d blocks selected', count ), count ) } -
-
- onRemove( multiSelectedBlockUids ) } - focus={ true } - > - { __( 'Delete' ) } - -
-
- onDeselect() } - /> -
-
- ); - } - return (
( { - multiSelectedBlockUids: getMultiSelectedBlockUids( state ), hasUndo: hasEditorUndo( state ), hasRedo: hasEditorRedo( state ), isSidebarOpened: isEditorSidebarOpened( state ), } ), ( dispatch ) => ( { - onDeselect: () => dispatch( clearSelectedBlock() ), - onRemove: ( uids ) => dispatch( { - type: 'REMOVE_BLOCKS', - uids, - } ), undo: () => dispatch( { type: 'UNDO' } ), redo: () => dispatch( { type: 'REDO' } ), toggleSidebar: () => dispatch( { type: 'TOGGLE_SIDEBAR' } ), diff --git a/editor/header/style.scss b/editor/header/style.scss index 37f4da4e05cc5f..63237df326f551 100644 --- a/editor/header/style.scss +++ b/editor/header/style.scss @@ -73,21 +73,6 @@ right: $admin-sidebar-width-big; } -.editor-header-multi-select { - background: $blue-medium-100; - border-bottom: 1px solid $blue-medium-200; -} - -.editor-selected-count { - padding-right: $item-spacing; - color: $dark-gray-500; - border-right: 1px solid $light-gray-500; -} - -.editor-selected-clear { - margin: 0 0 0 auto; -} - // hide all action buttons except the inserter on mobile .editor-header__content-tools > .components-button { display: none; diff --git a/editor/modes/visual-editor/block.js b/editor/modes/visual-editor/block.js index 773efc9bdb439c..34aa1112bfe3ab 100644 --- a/editor/modes/visual-editor/block.js +++ b/editor/modes/visual-editor/block.js @@ -340,12 +340,11 @@ class VisualEditorBlock extends Component { > { ( showUI || isHovered ) && } - { ( showUI || isHovered ) && } - { showUI && isValid && } - - { isFirstMultiSelected && ( - - ) } + { ( showUI || isHovered ) && } + { showUI && isValid && } + { isFirstMultiSelected && } + { isFirstMultiSelected && } + { isFirstMultiSelected && isValid && }
event.preventDefault() } diff --git a/editor/selectors.js b/editor/selectors.js index bed57fdcd9efc6..d9bf610ffe5524 100644 --- a/editor/selectors.js +++ b/editor/selectors.js @@ -440,6 +440,15 @@ export const getBlocks = createSelector( ] ); +export const getBlocksByUid = createSelector( + ( state, uids ) => { + return uids.map( ( uid ) => getBlock( state, uid ) ); + }, + ( state ) => [ + state.editor.blocksByUid, + ] +); + /** * Returns the number of blocks currently present in the post. * @@ -450,6 +459,22 @@ export function getBlockCount( state ) { return getBlockUids( state ).length; } +/** + * Returns the number of blocks currently selected in the post. + * + * @param {Object} state Global application state + * @return {Object} Number of blocks selected in the post + */ +export function getSelectedBlockCount( state ) { + const multiSelectedBlockCount = getMultiSelectedBlockUids( state ).length; + + if ( multiSelectedBlockCount ) { + return multiSelectedBlockCount; + } + + return state.blockSelection.start ? 1 : 0; +} + /** * Returns the currently selected block, or null if there is no selected block. * diff --git a/editor/sidebar/header.js b/editor/sidebar/header.js index 8f589fe84b0bae..cb62ac7587fd05 100644 --- a/editor/sidebar/header.js +++ b/editor/sidebar/header.js @@ -6,7 +6,7 @@ import { connect } from 'react-redux'; /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, _n, sprintf } from '@wordpress/i18n'; import { IconButton } from '@wordpress/components'; /** @@ -14,7 +14,7 @@ import { IconButton } from '@wordpress/components'; */ import { getActivePanel } from '../selectors'; -const SidebarHeader = ( { panel, onSetPanel, toggleSidebar } ) => { +const SidebarHeader = ( { panel, onSetPanel, toggleSidebar, count } ) => { return (
{ +const Sidebar = ( { panel, selectedBlockCount } ) => { return (
-
+
{ panel === 'document' && } - { panel === 'block' && } + { panel === 'block' && selectedBlockCount === 1 && } + { panel === 'block' && selectedBlockCount > 1 && }
); }; @@ -32,6 +34,7 @@ export default connect( ( state ) => { return { panel: getActivePanel( state ), + selectedBlockCount: getSelectedBlockCount( state ), }; } )( withFocusReturn( Sidebar ) ); diff --git a/editor/sidebar/multi-block-inspector/index.js b/editor/sidebar/multi-block-inspector/index.js new file mode 100644 index 00000000000000..024b4901b79f98 --- /dev/null +++ b/editor/sidebar/multi-block-inspector/index.js @@ -0,0 +1,87 @@ +/** + * External dependencies + */ +import { connect } from 'react-redux'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Panel, PanelBody } from 'components'; +import { getBlockType } from 'blocks'; + +/** + * Internal Dependencies + */ +import { getMultiSelectedBlocks } from '../../selectors'; + +const MultiBlockInspector = ( { selectedBlocks, onChange } ) => { + if ( ! selectedBlocks.length ) { + return null; + } + + const types = selectedBlocks.reduce( ( acc, block ) => { + if ( acc.indexOf( block.name ) === -1 ) { + acc.push( block.name ); + } + + return acc; + }, [] ).map( name => getBlockType( name ) ); + + const components = []; + + if ( types.length === 1 ) { + if ( types[ 0 ].multiSelectedControls ) { + components.push( types[ 0 ].multiSelectedControls ); + } + } + + const setAttributes = ( attributes ) => { + selectedBlocks.forEach( ( block ) => { + onChange( block.uid, { + ...block.attributes, + ...attributes, + } ); + } ); + }; + + const attributes = selectedBlocks.map( block => block.attributes ); + + return ( + + +

+ { types.length === 1 ? types[ 0 ].title : __( 'Mixed' ) } + { ' ' } + { __( 'Blocks' ) } +

+ { components.map( ( Component, index ) => { + return ( + + ); + } ) } +
+
+ ); +}; + +export default connect( + ( state ) => { + return { + selectedBlocks: getMultiSelectedBlocks( state ), + }; + }, + ( dispatch ) => ( { + onChange( uid, attributes ) { + dispatch( { + type: 'UPDATE_BLOCK_ATTRIBUTES', + uid, + attributes, + } ); + }, + } ), +)( MultiBlockInspector );