diff --git a/editor/actions.js b/editor/actions.js index 072232bc7d2f71..47b09d0f338988 100644 --- a/editor/actions.js +++ b/editor/actions.js @@ -108,6 +108,18 @@ export function selectBlock( uid ) { }; } +export function startMultiSelect() { + return { + type: 'START_MULTI_SELECT', + }; +} + +export function stopMultiSelect() { + return { + type: 'STOP_MULTI_SELECT', + }; +} + export function multiSelect( start, end ) { return { type: 'MULTI_SELECT', diff --git a/editor/block-mover/index.js b/editor/block-mover/index.js index d127eea6b03baa..f86da5bcc86ee4 100644 --- a/editor/block-mover/index.js +++ b/editor/block-mover/index.js @@ -17,6 +17,7 @@ import { getBlockType } from '@wordpress/blocks'; import './style.scss'; import { isFirstBlock, isLastBlock, getBlockIndex, getBlock } from '../selectors'; import { getBlockMoverLabel } from './mover-label'; +import { selectBlock } from '../actions'; function BlockMover( { onMoveUp, onMoveDown, isFirst, isLast, uids, blockType, firstIndex } ) { // We emulate a disabled state because forcefully applying the `disabled` @@ -68,12 +69,20 @@ export default connect( } ), ( dispatch, ownProps ) => ( { onMoveDown() { + if ( ownProps.uids.length === 1 ) { + dispatch( selectBlock( first( ownProps.uids ) ) ); + } + dispatch( { type: 'MOVE_BLOCKS_DOWN', uids: ownProps.uids, } ); }, onMoveUp() { + if ( ownProps.uids.length === 1 ) { + dispatch( selectBlock( first( ownProps.uids ) ) ); + } + dispatch( { type: 'MOVE_BLOCKS_UP', uids: ownProps.uids, diff --git a/editor/block-settings-menu/content.js b/editor/block-settings-menu/content.js index e5d278a4447fd8..c6298e9d779f38 100644 --- a/editor/block-settings-menu/content.js +++ b/editor/block-settings-menu/content.js @@ -6,18 +6,18 @@ import { connect } from 'react-redux'; /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, sprintf, _n } from '@wordpress/i18n'; import { IconButton } from '@wordpress/components'; /** * Internal dependencies */ import { isEditorSidebarOpened } from '../selectors'; -import { selectBlock, removeBlock, toggleSidebar, setActivePanel, toggleBlockMode } from '../actions'; +import { removeBlocks, toggleSidebar, setActivePanel, toggleBlockMode } from '../actions'; -function BlockSettingsMenuContent( { onDelete, onSelect, isSidebarOpened, onToggleSidebar, onShowInspector, onToggleMode } ) { +function BlockSettingsMenuContent( { onDelete, isSidebarOpened, onToggleSidebar, onShowInspector, onToggleMode, uids } ) { + const count = uids.length; const toggleInspector = () => { - onSelect(); onShowInspector(); if ( ! isSidebarOpened ) { onToggleSidebar(); @@ -36,14 +36,14 @@ function BlockSettingsMenuContent( { onDelete, onSelect, isSidebarOpened, onTogg className="editor-block-settings-menu__control" onClick={ onDelete } icon="trash" - label={ __( 'Delete the block' ) } + label={ sprintf( _n( 'Delete the block', 'Delete the %d blocks', count ), count ) } /> - + /> } ); } @@ -54,10 +54,7 @@ export default connect( } ), ( dispatch, ownProps ) => ( { onDelete() { - dispatch( removeBlock( ownProps.uid ) ); - }, - onSelect() { - dispatch( selectBlock( ownProps.uid ) ); + dispatch( removeBlocks( ownProps.uids ) ); }, onShowInspector() { dispatch( setActivePanel( 'block' ) ); @@ -66,7 +63,7 @@ export default connect( dispatch( toggleSidebar() ); }, onToggleMode() { - dispatch( toggleBlockMode( ownProps.uid ) ); + dispatch( toggleBlockMode( ownProps.uids[ 0 ] ) ); }, } ) )( BlockSettingsMenuContent ); diff --git a/editor/block-settings-menu/index.js b/editor/block-settings-menu/index.js index c4071727d8d404..e79f1d2599ac66 100644 --- a/editor/block-settings-menu/index.js +++ b/editor/block-settings-menu/index.js @@ -28,7 +28,11 @@ class BlockSettingsMenu extends Component { } toggleMenu() { - this.props.onSelect(); + // Block could be hovered, not selected. + if ( this.props.uids.length === 1 ) { + this.props.onSelect( this.props.uids[ 0 ] ); + } + this.setState( ( state ) => ( { opened: ! state.opened, } ) ); @@ -36,7 +40,7 @@ class BlockSettingsMenu extends Component { render() { const { opened } = this.state; - const { uid } = this.props; + const { uids, focus } = this.props; const toggleClassname = classnames( 'editor-block-settings-menu__toggle', 'editor-block-settings-menu__control', { 'is-opened': opened, } ); @@ -48,9 +52,10 @@ class BlockSettingsMenu extends Component { onClick={ this.toggleMenu } icon="ellipsis" label={ opened ? __( 'Close Settings Menu' ) : __( 'Open Settings Menu' ) } + focus={ focus } /> - { opened && } + { opened && } ); } @@ -58,9 +63,9 @@ class BlockSettingsMenu extends Component { export default connect( undefined, - ( dispatch, ownProps ) => ( { - onSelect() { - dispatch( selectBlock( ownProps.uid ) ); + ( dispatch ) => ( { + onSelect( uid ) { + dispatch( selectBlock( uid ) ); }, } ) )( BlockSettingsMenu ); diff --git a/editor/header/index.js b/editor/header/index.js index 0d487693196554..9c922f4f72599e 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,10 @@ 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, toggleSidebar, removeBlocks } from '../actions'; +import { hasEditorUndo, hasEditorRedo, isEditorSidebarOpened } from '../selectors'; +import { toggleSidebar } from '../actions'; function Header( { - multiSelectedBlockUids, - onRemove, - onDeselect, undo, redo, hasRedo, @@ -32,39 +29,6 @@ function Header( { onToggleSidebar, 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( removeBlocks( uids ) ), undo: () => dispatch( { type: 'UNDO' } ), redo: () => dispatch( { type: 'REDO' } ), onToggleSidebar: () => dispatch( toggleSidebar() ), 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-list.js b/editor/modes/visual-editor/block-list.js index 8ae044dc085679..5b3e3f1b4f92e3 100644 --- a/editor/modes/visual-editor/block-list.js +++ b/editor/modes/visual-editor/block-list.js @@ -25,7 +25,7 @@ import { getMultiSelectedBlocks, getMultiSelectedBlockUids, } from '../../selectors'; -import { insertBlock, multiSelect } from '../../actions'; +import { insertBlock, startMultiSelect, stopMultiSelect, multiSelect } from '../../actions'; const INSERTION_POINT_PLACEHOLDER = '[[insertion-point]]'; @@ -138,6 +138,8 @@ class VisualEditorBlockList extends Component { // Capture scroll on all elements. window.addEventListener( 'scroll', this.onScroll, true ); window.addEventListener( 'mouseup', this.onSelectionEnd ); + + this.props.onStartMultiSelect(); } onSelectionChange( uid ) { @@ -169,6 +171,8 @@ class VisualEditorBlockList extends Component { window.removeEventListener( 'mousemove', this.onPointerMove ); window.removeEventListener( 'scroll', this.onScroll, true ); window.removeEventListener( 'mouseup', this.onSelectionEnd ); + + this.props.onStopMultiSelect(); } appendDefaultBlock() { @@ -244,6 +248,12 @@ export default connect( onInsertBlock( block ) { dispatch( insertBlock( block ) ); }, + onStartMultiSelect() { + dispatch( startMultiSelect() ); + }, + onStopMultiSelect() { + dispatch( stopMultiSelect() ); + }, onMultiSelect( start, end ) { dispatch( multiSelect( start, end ) ); }, diff --git a/editor/modes/visual-editor/block.js b/editor/modes/visual-editor/block.js index e8c41c96ae32ca..e6f6d98e2da7b7 100644 --- a/editor/modes/visual-editor/block.js +++ b/editor/modes/visual-editor/block.js @@ -40,6 +40,7 @@ import { import { getBlock, getBlockFocus, + isMultiSelecting, getBlockIndex, getEditedPostAttribute, getMultiSelectedBlockUids, @@ -136,7 +137,6 @@ class VisualEditorBlock extends Component { bindBlockNode( node ) { this.node = node; - this.props.blockRef( node ); } setAttributes( attributes ) { @@ -256,8 +256,9 @@ class VisualEditorBlock extends Component { } onPointerDown( event ) { - // Not the main button (usually the left button on pointer device). - if ( event.buttons !== 1 ) { + // Not the main button. + // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button + if ( event.button !== 0 ) { return; } @@ -306,12 +307,13 @@ class VisualEditorBlock extends Component { // Generate the wrapper class names handling the different states of the block. const { isHovered, isSelected, isMultiSelected, isFirstMultiSelected, focus } = this.props; const showUI = isSelected && ( ! this.props.isTyping || focus.collapsed === false ); + const isProperlyHovered = isHovered && ! this.props.isSelecting; const { error } = this.state; const wrapperClassname = classnames( 'editor-visual-editor__block', { 'has-warning': ! isValid || !! error, 'is-selected': showUI, 'is-multi-selected': isMultiSelected, - 'is-hovered': isHovered, + 'is-hovered': isProperlyHovered, } ); const { onMouseLeave, onFocus, onReplace } = this.props; @@ -330,31 +332,37 @@ class VisualEditorBlock extends Component { /* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ return (
- { ( showUI || isHovered ) && } - { ( showUI || isHovered ) && } + { ( showUI || isProperlyHovered ) && } + { ( showUI || isProperlyHovered ) && } { showUI && isValid && mode === 'visual' && } - - { isFirstMultiSelected && ( + { isFirstMultiSelected && ! this.props.isSelecting && - ) } + } + { isFirstMultiSelected && ! this.props.isSelecting && + + }
event.preventDefault() } onMouseDown={ this.onPointerDown } + onKeyDown={ this.onKeyDown } + onFocus={ this.onFocus } className="editor-visual-editor__block-edit" + tabIndex="0" + aria-label={ blockLabel } > { isValid && mode === 'visual' && ( @@ -404,6 +412,7 @@ export default connect( isFirstMultiSelected: isFirstMultiSelectedBlock( state, ownProps.uid ), isHovered: isBlockHovered( state, ownProps.uid ), focus: getBlockFocus( state, ownProps.uid ), + isSelecting: isMultiSelecting( state ), isTyping: isTyping( state ), order: getBlockIndex( state, ownProps.uid ), multiSelectedBlockUids: getMultiSelectedBlockUids( state ), diff --git a/editor/reducer.js b/editor/reducer.js index df8a45f1015ff4..c5b91422ce1464 100644 --- a/editor/reducer.js +++ b/editor/reducer.js @@ -314,11 +314,18 @@ export function blockSelection( state = { start: null, end: null, focus: null }, end: null, focus: null, }; + case 'START_MULTI_SELECT': + return { + ...state, + isMultiSelecting: true, + }; + case 'STOP_MULTI_SELECT': + return omit( state, 'isMultiSelecting' ); case 'MULTI_SELECT': return { start: action.start, end: action.end, - focus: null, + focus: state.focus, }; case 'SELECT_BLOCK': if ( action.uid === state.start && action.uid === state.end ) { @@ -350,17 +357,6 @@ export function blockSelection( state = { start: null, end: null, focus: null }, end: action.blocks[ 0 ].uid, focus: {}, }; - case 'MOVE_BLOCKS_UP': - case 'MOVE_BLOCKS_DOWN': { - const firstUid = first( action.uids ); - return firstUid === state.start - ? state - : { - start: firstUid, - end: firstUid, - focus: {}, - }; - } } return state; diff --git a/editor/selectors.js b/editor/selectors.js index 09df4ed4185b84..c89b667ac0aae4 100644 --- a/editor/selectors.js +++ b/editor/selectors.js @@ -451,12 +451,28 @@ export const getBlocks = createSelector( * Returns the number of blocks currently present in the post. * * @param {Object} state Global application state - * @return {Object} Number of blocks in the post + * @return {Number} Number of blocks in the post */ 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 {Number} 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. * @@ -705,12 +721,24 @@ export function isBlockHovered( state, uid ) { * @return {Object} Block focus state */ export function getBlockFocus( state, uid ) { - if ( ! isBlockSelected( state, uid ) ) { + // If there is multi-selection, keep returning the focus object for the start block. + if ( ! isBlockSelected( state, uid ) && state.blockSelection.start !== uid ) { return null; } return state.blockSelection.focus; } + +/** + * Whether in the process of multi-selecting or not. + * + * @param {Object} state Global application state + * @return {Boolean} True if multi-selecting, false if not. + */ +export function isMultiSelecting( state ) { + return !! state.blockSelection.isMultiSelecting; +} + /** * Returns thee block's editing mode * diff --git a/editor/sidebar/block-inspector/index.js b/editor/sidebar/block-inspector/index.js index 0024e89b430122..bbcfdac10a4587 100644 --- a/editor/sidebar/block-inspector/index.js +++ b/editor/sidebar/block-inspector/index.js @@ -15,9 +15,13 @@ import { Panel, PanelBody } from '@wordpress/components'; */ import './style.scss'; import BlockInspectorAdvancedControls from './advanced-controls'; -import { getSelectedBlock } from '../../selectors'; +import { getSelectedBlock, getSelectedBlockCount } from '../../selectors'; + +const BlockInspector = ( { selectedBlock, count } ) => { + if ( count > 1 ) { + return { __( 'Coming Soon' ) }; + } -const BlockInspector = ( { selectedBlock } ) => { if ( ! selectedBlock ) { return { __( 'No block selected.' ) }; } @@ -36,6 +40,7 @@ export default connect( ( state ) => { return { selectedBlock: getSelectedBlock( state ), + count: getSelectedBlockCount( state ), }; } )( BlockInspector ); diff --git a/editor/sidebar/block-inspector/style.scss b/editor/sidebar/block-inspector/style.scss index a0772bfe573d57..9b702667def7d6 100644 --- a/editor/sidebar/block-inspector/style.scss +++ b/editor/sidebar/block-inspector/style.scss @@ -8,7 +8,8 @@ } } -.editor-block-inspector__no-blocks { +.editor-block-inspector__no-blocks, +.editor-block-inspector__multi-blocks { display: block; font-size: $default-font-size; background: $white; diff --git a/editor/sidebar/header.js b/editor/sidebar/header.js index 3970128ef4061b..d2788cf4a1f7db 100644 --- a/editor/sidebar/header.js +++ b/editor/sidebar/header.js @@ -6,16 +6,19 @@ import { connect } from 'react-redux'; /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, _n, sprintf } from '@wordpress/i18n'; import { IconButton } from '@wordpress/components'; /** * Internal Dependencies */ -import { getActivePanel } from '../selectors'; +import { getActivePanel, getSelectedBlockCount } from '../selectors'; import { toggleSidebar, setActivePanel } from '../actions'; -const SidebarHeader = ( { panel, onSetPanel, onToggleSidebar } ) => { +const SidebarHeader = ( { panel, onSetPanel, onToggleSidebar, count } ) => { + // Do not display "0 Blocks". + count = count === 0 ? 1 : count; + return (
{ export default connect( ( state ) => ( { panel: getActivePanel( state ), + count: getSelectedBlockCount( state ), } ), ( dispatch ) => ( { onSetPanel: ( panel ) => dispatch( setActivePanel( panel ) ), diff --git a/editor/test/reducer.js b/editor/test/reducer.js index 453b8b73af2372..b4da8e1f6ff21f 100644 --- a/editor/test/reducer.js +++ b/editor/test/reducer.js @@ -693,13 +693,14 @@ describe( 'state', () => { } ); it( 'should set multi selection', () => { - const state = blockSelection( undefined, { + const original = deepFreeze( { focus: { editable: 'citation' } } ); + const state = blockSelection( original, { type: 'MULTI_SELECT', start: 'ribs', end: 'chicken', } ); - expect( state ).toEqual( { start: 'ribs', end: 'chicken', focus: null } ); + expect( state ).toEqual( { start: 'ribs', end: 'chicken', focus: { editable: 'citation' } } ); } ); it( 'should not update the state if the block is already selected', () => { @@ -733,24 +734,6 @@ describe( 'state', () => { expect( state3 ).toEqual( { start: 'ribs', end: 'ribs', focus: {} } ); } ); - it( 'should return with block moved up', () => { - const state = blockSelection( undefined, { - type: 'MOVE_BLOCKS_UP', - uids: [ 'ribs' ], - } ); - - expect( state ).toEqual( { start: 'ribs', end: 'ribs', focus: {} } ); - } ); - - it( 'should return with block moved down', () => { - const state = blockSelection( undefined, { - type: 'MOVE_BLOCKS_DOWN', - uids: [ 'chicken' ], - } ); - - expect( state ).toEqual( { start: 'chicken', end: 'chicken', focus: {} } ); - } ); - it( 'should not update the state if the block moved is already selected', () => { const original = deepFreeze( { start: 'ribs', end: 'ribs', focus: {} } ); const state = blockSelection( original, { diff --git a/editor/test/selectors.js b/editor/test/selectors.js index 5f8f3987a4be1c..89aa057145c99d 100644 --- a/editor/test/selectors.js +++ b/editor/test/selectors.js @@ -1493,6 +1493,30 @@ describe( 'selectors', () => { expect( getBlockFocus( state, 123 ) ).toEqual( { editable: 'cite' } ); } ); + it( 'should return the block focus for the start if the block is multi-selected', () => { + const state = { + blockSelection: { + start: 123, + end: 124, + focus: { editable: 'cite' }, + }, + }; + + expect( getBlockFocus( state, 123 ) ).toEqual( { editable: 'cite' } ); + } ); + + it( 'should return null for the end if the block is multi-selected', () => { + const state = { + blockSelection: { + start: 123, + end: 124, + focus: { editable: 'cite' }, + }, + }; + + expect( getBlockFocus( state, 124 ) ).toEqual( null ); + } ); + it( 'should return null if the block is not selected', () => { const state = { blockSelection: { diff --git a/editor/writing-flow/index.js b/editor/writing-flow/index.js index fd9b5080760576..d3d2239d3f752f 100644 --- a/editor/writing-flow/index.js +++ b/editor/writing-flow/index.js @@ -36,7 +36,7 @@ class WritingFlow extends Component { node.nodeName === 'INPUT' || node.nodeName === 'TEXTAREA' || node.contentEditable === 'true' || - node.classList.contains( 'editor-visual-editor__block' ) + node.classList.contains( 'editor-visual-editor__block-edit' ) ) ); }