diff --git a/editor/actions.js b/editor/actions.js index 19b9def04399f..2ced37de83456 100644 --- a/editor/actions.js +++ b/editor/actions.js @@ -14,6 +14,14 @@ export function deselectBlock( uid ) { }; } +export function multiSelect( start, end ) { + return { + type: 'MULTI_SELECT', + start, + end, + }; +} + export function clearSelectedBlock() { return { type: 'CLEAR_SELECTED_BLOCK', diff --git a/editor/modes/visual-editor/block-list.js b/editor/modes/visual-editor/block-list.js index b38f186b1e1d2..e4ec7bb2f6f97 100644 --- a/editor/modes/visual-editor/block-list.js +++ b/editor/modes/visual-editor/block-list.js @@ -23,7 +23,7 @@ import { getMultiSelectedBlocks, getMultiSelectedBlockUids, } from '../../selectors'; -import { insertBlock } from '../../actions'; +import { insertBlock, multiSelect } from '../../actions'; const INSERTION_POINT_PLACEHOLDER = '[[insertion-point]]'; @@ -136,11 +136,11 @@ class VisualEditorBlockList extends wp.element.Component { } if ( isAtStart && selectionStart ) { - onMultiSelect( { start: null, end: null } ); + onMultiSelect( null, null ); } if ( ! isAtStart && selectionEnd !== uid ) { - onMultiSelect( { start: selectionAtStart, end: uid } ); + onMultiSelect( selectionAtStart, uid ); } } @@ -220,8 +220,8 @@ export default connect( onInsertBlock( block ) { dispatch( insertBlock( block ) ); }, - onMultiSelect( { start, end } ) { - dispatch( { type: 'MULTI_SELECT', start, end } ); + onMultiSelect( start, end ) { + dispatch( multiSelect( start, end ) ); }, onRemove( uids ) { dispatch( { type: 'REMOVE_BLOCKS', uids } ); diff --git a/editor/modes/visual-editor/index.js b/editor/modes/visual-editor/index.js index e6374591a9dd7..3ec6b502b45d7 100644 --- a/editor/modes/visual-editor/index.js +++ b/editor/modes/visual-editor/index.js @@ -2,12 +2,14 @@ * External dependencies */ import { connect } from 'react-redux'; +import { first, last } from 'lodash'; /** * WordPress dependencies */ import { __ } from 'i18n'; import { Component, findDOMNode } from 'element'; +import { CHAR_A } from 'utils/keycodes'; /** * Internal dependencies @@ -16,7 +18,9 @@ import './style.scss'; import Inserter from '../../inserter'; import VisualEditorBlockList from './block-list'; import PostTitle from '../../post-title'; -import { clearSelectedBlock } from '../../actions'; +import { getBlockUids } from '../../selectors'; +import { clearSelectedBlock, multiSelect } from '../../actions'; +import { isEditableElement } from '../../utils/dom'; class VisualEditor extends Component { constructor() { @@ -24,6 +28,15 @@ class VisualEditor extends Component { this.bindContainer = this.bindContainer.bind( this ); this.bindBlocksContainer = this.bindBlocksContainer.bind( this ); this.onClick = this.onClick.bind( this ); + this.onKeyDown = this.onKeyDown.bind( this ); + } + + componentDidMount() { + document.addEventListener( 'keydown', this.onKeyDown ); + } + + componentWillUnmount() { + document.removeEventListener( 'keydown', this.onKeyDown ); } bindContainer( ref ) { @@ -40,6 +53,18 @@ class VisualEditor extends Component { } } + onKeyDown( event ) { + const { uids } = this.props; + if ( + ! isEditableElement( document.activeElement ) && + ( event.ctrlKey || event.metaKey ) && + event.keyCode === CHAR_A + ) { + event.preventDefault(); + this.props.multiSelect( first( uids ), last( uids ) ); + } + } + render() { // Disable reason: Clicking the canvas should clear the selection /* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ @@ -50,6 +75,7 @@ class VisualEditor extends Component { className="editor-visual-editor" onMouseDown={ this.onClick } onTouchStart={ this.onClick } + onKeyDown={ this.onKeyDown } ref={ this.bindContainer } > @@ -62,6 +88,13 @@ class VisualEditor extends Component { } export default connect( - undefined, - { clearSelectedBlock } + ( state ) => { + return { + uids: getBlockUids( state ), + }; + }, + { + clearSelectedBlock, + multiSelect, + } )( VisualEditor ); diff --git a/editor/utils/dom.js b/editor/utils/dom.js new file mode 100644 index 0000000000000..ec3feb0f17ba7 --- /dev/null +++ b/editor/utils/dom.js @@ -0,0 +1,13 @@ + +/** + * Utility function to check whether the domElement provided is editable or not + * An editable element means we can type in it to edit its content + * This includes inputs and contenteditables + * + * @param {DomElement} domElement DOM Element + * @return {Boolean} Whether the DOM Element is editable or not + */ +export function isEditableElement( domElement ) { + return [ 'textarea', 'input', 'select' ].indexOf( domElement.tagName.toLowerCase() ) !== -1 + || !! domElement.isContentEditable; +} diff --git a/editor/utils/test/dom.js b/editor/utils/test/dom.js new file mode 100644 index 0000000000000..0b88a9d85e155 --- /dev/null +++ b/editor/utils/test/dom.js @@ -0,0 +1,35 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; + +/** + * Internal dependencies + */ +import { isEditableElement } from '../dom'; + +describe.only( 'isEditableElement', () => { + it( 'should return false for non editable nodes', () => { + const div = document.createElement( 'div' ); + + expect( isEditableElement( div ) ).to.be.false(); + } ); + + it( 'should return true for inputs', () => { + const input = document.createElement( 'input' ); + + expect( isEditableElement( input ) ).to.be.true(); + } ); + + it( 'should return true for textareas', () => { + const textarea = document.createElement( 'textarea' ); + + expect( isEditableElement( textarea ) ).to.be.true(); + } ); + + it( 'should return true for selects', () => { + const select = document.createElement( 'select' ); + + expect( isEditableElement( select ) ).to.be.true(); + } ); +} ); diff --git a/utils/keycodes.js b/utils/keycodes.js index f7f3b6cfe2db8..3363e4f1ade46 100644 --- a/utils/keycodes.js +++ b/utils/keycodes.js @@ -7,3 +7,4 @@ export const UP = 38; export const RIGHT = 39; export const DOWN = 40; export const DELETE = 46; +export const CHAR_A = 'A'.charCodeAt( 0 );