diff --git a/editor/components/editor-global-keyboard-shortcuts/index.js b/editor/components/editor-global-keyboard-shortcuts/index.js index f7d23bd410b0e8..63947cc6921840 100644 --- a/editor/components/editor-global-keyboard-shortcuts/index.js +++ b/editor/components/editor-global-keyboard-shortcuts/index.js @@ -64,6 +64,7 @@ class EditorGlobalKeyboardShortcuts extends Component { const { hasMultiSelection, clearSelectedBlock } = this.props; if ( hasMultiSelection ) { clearSelectedBlock(); + window.getSelection().removeAllRanges(); } } diff --git a/editor/components/writing-flow/index.js b/editor/components/writing-flow/index.js index b93ec9ea256e8e..abc3f24f78003a 100644 --- a/editor/components/writing-flow/index.js +++ b/editor/components/writing-flow/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { overEvery, find, findLast, reverse } from 'lodash'; +import { overEvery, find, findLast, reverse, first, last } from 'lodash'; /** * WordPress dependencies @@ -15,6 +15,7 @@ import { isVerticalEdge, placeCaretAtHorizontalEdge, placeCaretAtVerticalEdge, + isEntirelySelected, } from '@wordpress/dom'; import { keycodes } from '@wordpress/utils'; import { withSelect, withDispatch } from '@wordpress/data'; @@ -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 @@ -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; @@ -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; } @@ -278,6 +298,7 @@ export default compose( [ getFirstMultiSelectedBlockUid, getLastMultiSelectedBlockUid, hasMultiSelection, + getBlockOrder, } = select( 'core/editor' ); const selectedBlockUID = getSelectedBlockUID(); @@ -292,6 +313,7 @@ export default compose( [ selectedFirstUid: getFirstMultiSelectedBlockUid(), selectedLastUid: getLastMultiSelectedBlockUid(), hasMultiSelection: hasMultiSelection(), + blocks: getBlockOrder(), }; } ), withDispatch( ( dispatch ) => { diff --git a/packages/dom/src/dom.js b/packages/dom/src/dom.js index f0dbeb5c5ae8f8..40b7c7f08147d9 100644 --- a/packages/dom/src/dom.js +++ b/packages/dom/src/dom.js @@ -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. * diff --git a/utils/keycodes.js b/utils/keycodes.js index 40274e25bfa6ca..a94c824470456f 100644 --- a/utils/keycodes.js +++ b/utils/keycodes.js @@ -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; @@ -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; + }; +} );