Skip to content

Commit

Permalink
Merge pull request #3038 from WordPress/try/shift-arrow-block-selection
Browse files Browse the repository at this point in the history
Managing multi-block selection with the keyboard
  • Loading branch information
mcsf authored Oct 27, 2017
2 parents 3b601b5 + 80a7e02 commit 2eea42c
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 14 deletions.
16 changes: 16 additions & 0 deletions editor/modes/visual-editor/block-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import {
findLast,
flatMap,
invert,
isEqual,
mapValues,
noop,
throttle,
} from 'lodash';
import scrollIntoView from 'dom-scroll-into-view';
import 'element-closest';

/**
* WordPress dependencies
Expand Down Expand Up @@ -68,6 +71,19 @@ class VisualEditorBlockList extends Component {
window.removeEventListener( 'mousemove', this.setLastClientY );
}

componentWillReceiveProps( nextProps ) {
if ( isEqual( this.props.multiSelectedBlockUids, nextProps.multiSelectedBlockUids ) ) {
return;
}

if ( nextProps.multiSelectedBlockUids && nextProps.multiSelectedBlockUids.length > 0 ) {
const extent = this.nodes[ nextProps.selectionEnd ];
scrollIntoView( extent, extent.closest( '.editor-layout__editor' ), {
onlyScrollIfNeeded: true,
} );
}
}

setLastClientY( { clientY } ) {
this.lastClientY = clientY;
}
Expand Down
33 changes: 23 additions & 10 deletions editor/utils/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ const { TEXT_NODE } = window.Node;
/**
* Check whether the caret is horizontally at the edge of the container.
*
* @param {Element} container Focusable element.
* @param {Boolean} isReverse Set to true to check left, false for right.
* @return {Boolean} True if at the edge, false if not.
* @param {Element} container Focusable element.
* @param {Boolean} isReverse Set to true to check left, false for right.
* @param {Boolean} collapseRanges Whether or not to collapse the selection range before the check
* @return {Boolean} True if at the horizontal edge, false if not.
*/
export function isHorizontalEdge( container, isReverse ) {
export function isHorizontalEdge( container, isReverse, collapseRanges = false ) {
if ( includes( [ 'INPUT', 'TEXTAREA' ], container.tagName ) ) {
if ( container.selectionStart !== container.selectionEnd ) {
return false;
Expand All @@ -34,7 +35,11 @@ export function isHorizontalEdge( container, isReverse ) {
}

const selection = window.getSelection();
const range = selection.rangeCount ? selection.getRangeAt( 0 ) : null;
let range = selection.rangeCount ? selection.getRangeAt( 0 ) : null;
if ( collapseRanges ) {
range = range.cloneRange();
range.collapse( isReverse );
}

if ( ! range || ! range.collapsed ) {
return false;
Expand Down Expand Up @@ -70,11 +75,12 @@ export function isHorizontalEdge( container, isReverse ) {
/**
* Check whether the caret is vertically at the edge of the container.
*
* @param {Element} container Focusable element.
* @param {Boolean} isReverse Set to true to check top, false for bottom.
* @return {Boolean} True if at the edge, false if not.
* @param {Element} container Focusable element.
* @param {Boolean} isReverse Set to true to check top, false for bottom.
* @param {Boolean} collapseRanges Whether or not to collapse the selection range before the check
* @return {Boolean} True if at the edge, false if not.
*/
export function isVerticalEdge( container, isReverse ) {
export function isVerticalEdge( container, isReverse, collapseRanges = false ) {
if ( includes( [ 'INPUT', 'TEXTAREA' ], container.tagName ) ) {
return isHorizontalEdge( container, isReverse );
}
Expand All @@ -84,7 +90,14 @@ export function isVerticalEdge( container, isReverse ) {
}

const selection = window.getSelection();
const range = selection.rangeCount ? selection.getRangeAt( 0 ) : null;
let range = selection.rangeCount ? selection.getRangeAt( 0 ) : null;
if ( collapseRanges && ! range.collapsed ) {
const newRange = document.createRange();
// Get the end point of the selection (see focusNode vs. anchorNode)
newRange.setStart( selection.focusNode, selection.focusOffset );
newRange.collapse( true );
range = newRange;
}

if ( ! range || ! range.collapsed ) {
return false;
Expand Down
69 changes: 65 additions & 4 deletions editor/writing-flow/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/**
* External dependencies
*/
import { connect } from 'react-redux';
import 'element-closest';

/**
* WordPress dependencies
*/
Expand All @@ -14,6 +20,15 @@ import {
placeCaretAtHorizontalEdge,
placeCaretAtVerticalEdge,
} from '../utils/dom';
import {
getBlockUids,
getMultiSelectedBlocksStartUid,
getMultiSelectedBlocksEndUid,
getMultiSelectedBlocks,
getSelectedBlock,
} from '../selectors';

import { multiSelect } from '../actions';

/**
* Module Constants
Expand All @@ -27,7 +42,6 @@ class WritingFlow extends Component {
this.onKeyDown = this.onKeyDown.bind( this );
this.bindContainer = this.bindContainer.bind( this );
this.clearVerticalRect = this.clearVerticalRect.bind( this );

this.verticalRect = null;
}

Expand All @@ -39,6 +53,16 @@ class WritingFlow extends Component {
this.verticalRect = null;
}

getEditables( target ) {
const outer = target.closest( '.editor-visual-editor__block-edit' );
if ( ! outer || target === outer ) {
return [ target ];
}

const elements = outer.querySelectorAll( '[contenteditable="true"]' );
return [ ...elements ];
}

getVisibleTabbables() {
return focus.tabbable
.find( this.container )
Expand Down Expand Up @@ -75,7 +99,22 @@ class WritingFlow extends Component {
} );
}

expandSelection( blocks, currentStartUid, currentEndUid, delta ) {
const lastIndex = blocks.indexOf( currentEndUid );
const nextIndex = Math.max( 0, Math.min( blocks.length - 1, lastIndex + delta ) );
this.props.onMultiSelect( currentStartUid, blocks[ nextIndex ] );
}

isEditableEdge( moveUp, target ) {
const editables = this.getEditables( target );
const index = editables.indexOf( target );
const edgeIndex = moveUp ? 0 : editables.length - 1;
return editables.length > 0 && index === edgeIndex;
}

onKeyDown( event ) {
const { selectedBlock, selectionStart, selectionEnd, blocks, hasMultiSelection } = this.props;

const { keyCode, target } = event;
const isUp = keyCode === UP;
const isDown = keyCode === DOWN;
Expand All @@ -84,18 +123,27 @@ class WritingFlow extends Component {
const isReverse = isUp || isLeft;
const isHorizontal = isLeft || isRight;
const isVertical = isUp || isDown;
const isShift = event.shiftKey;

if ( ! isVertical ) {
this.verticalRect = null;
} else if ( ! this.verticalRect ) {
this.verticalRect = computeCaretRect( target );
}

if ( isVertical && isVerticalEdge( target, isReverse ) ) {
if ( isVertical && isShift && hasMultiSelection ) {
// Shift key is down and existing block selection
event.preventDefault();
this.expandSelection( blocks, selectionStart, selectionEnd, isReverse ? -1 : +1 );
} else if ( isVertical && isShift && this.isEditableEdge( isReverse, target ) && isVerticalEdge( target, isReverse, true ) ) {
// Shift key is down, but no existing block selection
event.preventDefault();
this.expandSelection( blocks, selectedBlock.uid, selectedBlock.uid, isReverse ? -1 : +1 );
} else if ( isVertical && isVerticalEdge( target, isReverse, isShift ) ) {
const closestTabbable = this.getClosestTabbable( target, isReverse );
placeCaretAtVerticalEdge( closestTabbable, isReverse, this.verticalRect );
event.preventDefault();
} else if ( isHorizontal && isHorizontalEdge( target, isReverse ) ) {
} else if ( isHorizontal && isHorizontalEdge( target, isReverse, isShift ) ) {
const closestTabbable = this.getClosestTabbable( target, isReverse );
placeCaretAtHorizontalEdge( closestTabbable, isReverse );
event.preventDefault();
Expand All @@ -121,4 +169,17 @@ class WritingFlow extends Component {
}
}

export default WritingFlow;
export default connect(
( state ) => ( {
blocks: getBlockUids( state ),
selectionStart: getMultiSelectedBlocksStartUid( state ),
selectionEnd: getMultiSelectedBlocksEndUid( state ),
hasMultiSelection: getMultiSelectedBlocks( state ).length > 1,
selectedBlock: getSelectedBlock( state ),
} ),
( dispatch ) => ( {
onMultiSelect( start, end ) {
dispatch( multiSelect( start, end ) );
},
} )
)( WritingFlow );

0 comments on commit 2eea42c

Please sign in to comment.