Skip to content

Commit

Permalink
Expand selection on consecutive Meta+A presses (#4600)
Browse files Browse the repository at this point in the history
* Expand selection on consecutive Meta+A presses

* Restore global primary+a shortcut

* Language

* is => isKeyboardEvent

* Fix doc
  • Loading branch information
ellatrix authored May 22, 2018
1 parent deabbc3 commit 75d3c35
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class EditorGlobalKeyboardShortcuts extends Component {
const { hasMultiSelection, clearSelectedBlock } = this.props;
if ( hasMultiSelection ) {
clearSelectedBlock();
window.getSelection().removeAllRanges();
}
}

Expand Down
28 changes: 25 additions & 3 deletions editor/components/writing-flow/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { overEvery, find, findLast, reverse } from 'lodash';
import { overEvery, find, findLast, reverse, first, last } from 'lodash';

/**
* WordPress dependencies
Expand All @@ -15,6 +15,7 @@ import {
isVerticalEdge,
placeCaretAtHorizontalEdge,
placeCaretAtVerticalEdge,
isEntirelySelected,
} from '@wordpress/dom';
import { keycodes } from '@wordpress/utils';
import { withSelect, withDispatch } from '@wordpress/data';
Expand All @@ -32,7 +33,7 @@ import {
* Module Constants
*/

const { UP, DOWN, LEFT, RIGHT } = keycodes;
const { UP, DOWN, LEFT, RIGHT, isKeyboardEvent } = keycodes;

/**
* Given an element, returns true if the element is a tabbable text field, or
Expand Down Expand Up @@ -179,7 +180,7 @@ class WritingFlow extends Component {
}

onKeyDown( event ) {
const { hasMultiSelection } = this.props;
const { hasMultiSelection, onMultiSelect, blocks } = this.props;

const { keyCode, target } = event;
const isUp = keyCode === UP;
Expand All @@ -201,6 +202,25 @@ class WritingFlow extends Component {
}

if ( ! isNav ) {
// Set immediately before the meta+a combination can be pressed.
if ( isKeyboardEvent.primary( event ) ) {
this.isEntirelySelected = isEntirelySelected( target );
}

if ( isKeyboardEvent.primary( event, 'a' ) ) {
// When the target is contentEditable, selection will already
// have been set by TinyMCE earlier in this call stack. We need
// check the previous result, otherwise all blocks will be
// selected right away.
if ( target.isContentEditable ? this.isEntirelySelected : isEntirelySelected( target ) ) {
onMultiSelect( first( blocks ), last( blocks ) );
event.preventDefault();
}

// Set in case the meta key doesn't get released.
this.isEntirelySelected = isEntirelySelected( target );
}

return;
}

Expand Down Expand Up @@ -278,6 +298,7 @@ export default compose( [
getFirstMultiSelectedBlockUid,
getLastMultiSelectedBlockUid,
hasMultiSelection,
getBlockOrder,
} = select( 'core/editor' );

const selectedBlockUID = getSelectedBlockUID();
Expand All @@ -292,6 +313,7 @@ export default compose( [
selectedFirstUid: getFirstMultiSelectedBlockUid(),
selectedLastUid: getLastMultiSelectedBlockUid(),
hasMultiSelection: hasMultiSelection(),
blocks: getBlockOrder(),
};
} ),
withDispatch( ( dispatch ) => {
Expand Down
34 changes: 34 additions & 0 deletions packages/dom/src/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,40 @@ export function documentHasSelection() {
return range && ! range.collapsed;
}

/**
* Check whether the contents of the element have been entirely selected.
* Returns true if there is no possibility of selection.
*
* @param {Element} element The element to check.
*
* @return {boolean} True if entirely selected, false if not.
*/
export function isEntirelySelected( element ) {
if ( includes( [ 'INPUT', 'TEXTAREA' ], element.nodeName ) ) {
return element.selectionStart === 0 && element.value.length === element.selectionEnd;
}

if ( ! element.isContentEditable ) {
return true;
}

const selection = window.getSelection();
const range = selection.rangeCount ? selection.getRangeAt( 0 ) : null;

if ( ! range ) {
return true;
}

const { startContainer, endContainer, startOffset, endOffset } = range;

return (
startContainer === element &&
endContainer === element &&
startOffset === 0 &&
endOffset === element.childNodes.length
);
}

/**
* Given a DOM node, finds the closest scrollable container node.
*
Expand Down
26 changes: 25 additions & 1 deletion utils/keycodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
/**
* External dependencies
*/
import { get, mapValues } from 'lodash';
import { get, mapValues, includes } from 'lodash';

export const BACKSPACE = 8;
export const TAB = 9;
Expand Down Expand Up @@ -90,3 +90,27 @@ export const displayShortcut = mapValues( modifiers, ( modifier ) => {
return shortcut.replace( /⌘\+([A-Z0-9])$/g, '⌘$1' );
};
} );

/**
* An object that contains functions to check if a keyboard event matches a
* predefined shortcut combination.
* E.g. isKeyboardEvent.primary( event, 'm' ) will return true if the event
* signals pressing ⌘M.
*
* @type {Object} Keyed map of functions to match events.
*/
export const isKeyboardEvent = mapValues( modifiers, ( getModifiers ) => {
return ( event, character, _isMac = isMacOS ) => {
const mods = getModifiers( _isMac );

if ( ! mods.every( ( key ) => event[ `${ key }Key` ] ) ) {
return false;
}

if ( ! character ) {
return includes( mods, event.key.toLowerCase() );
}

return event.key === character;
};
} );

0 comments on commit 75d3c35

Please sign in to comment.