1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-17 20:51:20 +02:00

refactor hotkeys to constants, and add transforms (#1251)

* refactor hotkeys to constants, and add transforms

* update hotkey helper
This commit is contained in:
Ian Storm Taylor
2017-10-17 18:18:27 -07:00
committed by GitHub
parent b8693eb9ba
commit b375660aa9
9 changed files with 469 additions and 351 deletions

View File

@@ -8,7 +8,7 @@
"dependencies": {
"debug": "^2.3.2",
"get-window": "^1.1.1",
"is-hotkey": "^0.0.3",
"is-hotkey": "^0.1.1",
"is-in-browser": "^1.1.3",
"is-window": "^1.0.2",
"keycode": "^2.1.2",

View File

@@ -1,5 +1,5 @@
import isHotkey from 'is-hotkey'
import { isKeyHotkey } from 'is-hotkey'
import { IS_MAC } from './environment'
@@ -9,32 +9,73 @@ import { IS_MAC } from './environment'
* @type {Function}
*/
const BOLD = isHotkey('mod+b')
const ITALIC = isHotkey('mod+i')
const BOLD = isKeyHotkey('mod+b')
const ITALIC = isKeyHotkey('mod+i')
const UNDO = isHotkey('mod+z')
const REDO_MAC = isHotkey('mod+shift+z')
const REDO_OTHER = isHotkey('mod+y')
const REDO = e => IS_MAC ? REDO_MAC(e) : REDO_OTHER(e)
const ENTER = isKeyHotkey('enter')
const SHIFT_ENTER = isKeyHotkey('shift+enter')
const SPLIT_BLOCK = e => ENTER(e) || SHIFT_ENTER(e)
const DELETE_CHAR_BACKWARD_MAC = isHotkey('ctrl+h')
const DELETE_CHAR_FORWARD_MAC = isHotkey('ctrl+d')
const DELETE_LINE_FORWARD_MAC = isHotkey('ctrl+k')
const DELETE_CHAR_BACKWARD = e => IS_MAC ? DELETE_CHAR_BACKWARD_MAC(e) : false
const DELETE_CHAR_FORWARD = e => IS_MAC ? DELETE_CHAR_FORWARD_MAC(e) : false
const DELETE_LINE_FORWARD = e => IS_MAC ? DELETE_LINE_FORWARD_MAC(e) : false
const BACKSPACE = isKeyHotkey('backspace')
const SHIFT_BACKSPACE = isKeyHotkey('shift+backspace')
const DELETE = isKeyHotkey('delete')
const SHIFT_DELETE = isKeyHotkey('shift+delete')
const DELETE_BACKWARD = e => BACKSPACE(e) || SHIFT_BACKSPACE(e)
const DELETE_FORWARD = e => DELETE(e) || SHIFT_DELETE(e)
const DELETE_CHAR_BACKWARD_MAC = isKeyHotkey('ctrl+h')
const DELETE_CHAR_FORWARD_MAC = isKeyHotkey('ctrl+d')
const DELETE_CHAR_BACKWARD = e => DELETE_BACKWARD(e) || (IS_MAC && DELETE_CHAR_BACKWARD_MAC(e))
const DELETE_CHAR_FORWARD = e => DELETE_FORWARD(e) || (IS_MAC && DELETE_CHAR_FORWARD_MAC(e))
const DELETE_LINE_BACKWARD_MAC = isKeyHotkey('cmd+backspace')
const DELETE_LINE_FORWARD_MAC = isKeyHotkey('ctrl+k')
const DELETE_LINE_BACKWARD = e => IS_MAC && DELETE_LINE_BACKWARD_MAC(e)
const DELETE_LINE_FORWARD = e => IS_MAC && DELETE_LINE_FORWARD_MAC(e)
const DELETE_WORD_BACKWARD_MAC = isKeyHotkey('option+backspace')
const DELETE_WORD_BACKWARD_PC = isKeyHotkey('ctrl+backspace')
const DELETE_WORD_FORWARD_MAC = isKeyHotkey('option+delete')
const DELETE_WORD_FORWARD_PC = isKeyHotkey('ctrl+delete')
const DELETE_WORD_BACKWARD = e => IS_MAC ? DELETE_WORD_BACKWARD_MAC(e) : DELETE_WORD_BACKWARD_PC(e)
const DELETE_WORD_FORWARD = e => IS_MAC ? DELETE_WORD_FORWARD_MAC(e) : DELETE_WORD_FORWARD_PC(e)
const COLLAPSE_CHAR_FORWARD = isKeyHotkey('right')
const COLLAPSE_CHAR_BACKWARD = isKeyHotkey('left')
const COLLAPSE_LINE_BACKWARD_MAC = isKeyHotkey('option+up')
const COLLAPSE_LINE_FORWARD_MAC = isKeyHotkey('option+down')
const COLLAPSE_LINE_BACKWARD = e => IS_MAC && COLLAPSE_LINE_BACKWARD_MAC(e)
const COLLAPSE_LINE_FORWARD = e => IS_MAC && COLLAPSE_LINE_FORWARD_MAC(e)
const EXTEND_CHAR_FORWARD = isKeyHotkey('shift+right')
const EXTEND_CHAR_BACKWARD = isKeyHotkey('shift+left')
const EXTEND_LINE_BACKWARD_MAC = isKeyHotkey('option+shift+up')
const EXTEND_LINE_FORWARD_MAC = isKeyHotkey('option+shift+down')
const EXTEND_LINE_BACKWARD = e => IS_MAC && EXTEND_LINE_BACKWARD_MAC(e)
const EXTEND_LINE_FORWARD = e => IS_MAC && EXTEND_LINE_FORWARD_MAC(e)
const UNDO = isKeyHotkey('mod+z')
const REDO_MAC = isKeyHotkey('mod+shift+z')
const REDO_PC = isKeyHotkey('mod+y')
const REDO = e => IS_MAC ? REDO_MAC(e) : REDO_PC(e)
const TRANSPOSE_CHARACTER_MAC = isKeyHotkey('ctrl+t')
const TRANSPOSE_CHARACTER = e => IS_MAC && TRANSPOSE_CHARACTER_MAC(e)
const CONTENTEDITABLE = e => (
e.key == 'Backspace' ||
e.key == 'Delete' ||
e.key == 'Enter' ||
e.key == 'Insert' ||
BOLD(e) ||
DELETE_CHAR_BACKWARD(e) ||
DELETE_CHAR_FORWARD(e) ||
DELETE_LINE_BACKWARD(e) ||
DELETE_LINE_FORWARD(e) ||
DELETE_WORD_BACKWARD(e) ||
DELETE_WORD_FORWARD(e) ||
ITALIC(e) ||
REDO(e) ||
SPLIT_BLOCK(e) ||
TRANSPOSE_CHARACTER(e) ||
UNDO(e)
)
@@ -53,12 +94,24 @@ const COMPOSING = e => (
export default {
BOLD,
COLLAPSE_LINE_BACKWARD,
COLLAPSE_LINE_FORWARD,
COLLAPSE_CHAR_FORWARD,
COLLAPSE_CHAR_BACKWARD,
COMPOSING,
CONTENTEDITABLE,
DELETE_CHAR_BACKWARD,
DELETE_CHAR_FORWARD,
DELETE_LINE_BACKWARD,
DELETE_LINE_FORWARD,
DELETE_WORD_BACKWARD,
DELETE_WORD_FORWARD,
EXTEND_LINE_BACKWARD,
EXTEND_LINE_FORWARD,
EXTEND_CHAR_FORWARD,
EXTEND_CHAR_BACKWARD,
ITALIC,
REDO,
SPLIT_BLOCK,
UNDO,
}

View File

@@ -17,7 +17,7 @@ import findRange from '../utils/find-range'
import getEventRange from '../utils/get-event-range'
import getEventTransfer from '../utils/get-event-transfer'
import setEventTransfer from '../utils/set-event-transfer'
import { IS_CHROME, IS_MAC, IS_SAFARI } from '../constants/environment'
import { IS_CHROME, IS_SAFARI } from '../constants/environment'
/**
* Debug.
@@ -479,269 +479,107 @@ function AfterPlugin(options = {}) {
function onKeyDown(event, change, editor) {
debug('onKeyDown', { event })
switch (event.key) {
case 'Enter': return onKeyDownEnter(event, change)
case 'Backspace': return onKeyDownBackspace(event, change)
case 'Delete': return onKeyDownDelete(event, change)
case 'ArrowLeft': return onKeyDownLeft(event, change)
case 'ArrowRight': return onKeyDownRight(event, change)
case 'ArrowUp': return onKeyDownUp(event, change)
case 'ArrowDown': return onKeyDownDown(event, change)
const { state } = change
if (HOTKEYS.SPLIT_BLOCK(event)) {
return state.isInVoid
? change.collapseToStartOfNextText()
: change.splitBlock()
}
if (HOTKEYS.DELETE_CHAR_BACKWARD(event)) {
change.deleteCharBackward()
return change.deleteCharBackward()
}
if (HOTKEYS.DELETE_CHAR_FORWARD(event)) {
change.deleteCharForward()
return change.deleteCharForward()
}
if (HOTKEYS.DELETE_LINE_BACKWARD(event)) {
return change.deleteLineBackward()
}
if (HOTKEYS.DELETE_LINE_FORWARD(event)) {
change.deleteLineForward()
return change.deleteLineForward()
}
if (HOTKEYS.DELETE_WORD_BACKWARD(event)) {
return change.deleteWordBackward()
}
if (HOTKEYS.DELETE_WORD_FORWARD(event)) {
return change.deleteWordForward()
}
if (HOTKEYS.REDO(event)) {
change.redo()
return change.redo()
}
if (HOTKEYS.UNDO(event)) {
change.undo()
}
}
/**
* On `enter` key down, split the current block in half.
*
* @param {Event} event
* @param {Change} change
* @param {Editor} editor
*/
function onKeyDownEnter(event, change, editor) {
const { state } = change
const { document, startKey } = state
const hasVoidParent = document.hasVoidParent(startKey)
// For void nodes, we don't want to split. Instead we just move to the start
// of the next text node if one exists.
if (hasVoidParent) {
const text = document.getNextText(startKey)
if (!text) return
change.collapseToStartOf(text)
return
return change.undo()
}
change.splitBlock()
}
/**
* On `backspace` key down, delete backwards.
*
* @param {Event} event
* @param {Change} change
* @param {Editor} editor
*/
function onKeyDownBackspace(event, change, editor) {
const isWord = IS_MAC ? event.altKey : event.ctrlKey
const isLine = IS_MAC ? event.metaKey : false
let boundary = 'Char'
if (isWord) boundary = 'Word'
if (isLine) boundary = 'Line'
change[`delete${boundary}Backward`]()
}
/**
* On `delete` key down, delete forwards.
*
* @param {Event} event
* @param {Change} change
* @param {Editor} editor
*/
function onKeyDownDelete(event, change, editor) {
const isWord = IS_MAC ? event.altKey : event.ctrlKey
const isLine = IS_MAC ? event.metaKey : false
let boundary = 'Char'
if (isWord) boundary = 'Word'
if (isLine) boundary = 'Line'
change[`delete${boundary}Forward`]()
}
/**
* On `left` key down, move backward.
*
* COMPAT: This is required to make navigating with the left arrow work when
* a void node is selected.
*
* COMPAT: This is also required to solve for the case where an inline node is
* surrounded by empty text nodes with zero-width spaces in them. Without this
* the zero-width spaces will cause two arrow keys to jump to the next text.
*
* @param {Event} event
* @param {Change} change
* @param {Editor} editor
*/
function onKeyDownLeft(event, change, editor) {
const { state } = change
if (event.ctrlKey) return
if (event.altKey) return
if (state.isExpanded) return
const { document, startKey, startText } = state
const hasVoidParent = document.hasVoidParent(startKey)
// If the current text node is empty, or we're inside a void parent, we're
// going to need to handle the selection behavior.
if (startText.text == '' || hasVoidParent) {
// COMPAT: Certain browsers don't handle the selection updates properly. In
// Chrome, the selection isn't properly extended. And in Firefox, the
// selection isn't properly collapsed. (2017/10/17)
if (HOTKEYS.COLLAPSE_LINE_BACKWARD(event)) {
event.preventDefault()
const previous = document.getPreviousText(startKey)
// If there's no previous text node in the document, abort.
if (!previous) return
// If the previous text is in the current block, and inside a non-void
// inline node, move one character into the inline node.
const { startBlock } = state
const previousBlock = document.getClosestBlock(previous.key)
const previousInline = document.getClosestInline(previous.key)
if (previousBlock === startBlock && previousInline && !previousInline.isVoid) {
const extendOrMove = event.shiftKey ? 'extend' : 'move'
change.collapseToEndOf(previous)[extendOrMove](-1)
return
}
// Otherwise, move to the end of the previous node.
change.collapseToEndOf(previous)
return change.collapseLineBackward()
}
}
/**
* On `right` key down, move forward.
*
* COMPAT: This is required to make navigating with the right arrow work when
* a void node is selected.
*
* COMPAT: This is also required to solve for the case where an inline node is
* surrounded by empty text nodes with zero-width spaces in them. Without this
* the zero-width spaces will cause two arrow keys to jump to the next text.
*
* COMPAT: In Chrome & Safari, selections that are at the zero offset of
* an inline node will be automatically replaced to be at the last offset
* of a previous inline node, which screws us up, so we never want to set the
* selection to the very start of an inline node here. (2016/11/29)
*
* @param {Event} event
* @param {Change} change
* @param {Editor} editor
*/
function onKeyDownRight(event, change, editor) {
const { state } = change
if (event.ctrlKey) return
if (event.altKey) return
if (state.isExpanded) return
const { document, startKey, startText } = state
const hasVoidParent = document.hasVoidParent(startKey)
// If the current text node is empty, or we're inside a void parent, we're
// going to need to handle the selection behavior.
if (startText.text == '' || hasVoidParent) {
if (HOTKEYS.COLLAPSE_LINE_FORWARD(event)) {
event.preventDefault()
const next = document.getNextText(startKey)
// If there's no next text node in the document, abort.
if (!next) return
// If the next text is inside a void node, move to the end of it.
if (document.hasVoidParent(next.key)) {
change.collapseToEndOf(next)
return
}
// If the next text is in the current block, and inside an inline node,
// move one character into the inline node.
const { startBlock } = state
const nextBlock = document.getClosestBlock(next.key)
const nextInline = document.getClosestInline(next.key)
if (nextBlock == startBlock && nextInline) {
const extendOrMove = event.shiftKey ? 'extend' : 'move'
change.collapseToStartOf(next)[extendOrMove](1)
return
}
// Otherwise, move to the start of the next text node.
change.collapseToStartOf(next)
return change.collapseLineForward()
}
}
/**
* On `up` key down, for Macs, move the selection to start of the block.
*
* COMPAT: Certain browsers don't handle the selection updates properly. In
* Chrome, option-shift-up doesn't properly extend the selection. And in
* Firefox, option-up doesn't properly move the selection.
*
* @param {Event} event
* @param {Change} change
* @param {Editor} editor
*/
if (HOTKEYS.EXTEND_LINE_BACKWARD(event)) {
event.preventDefault()
return change.extendLineBackward()
}
function onKeyDownUp(event, change, editor) {
if (!IS_MAC || event.ctrlKey || !event.altKey) return
if (HOTKEYS.EXTEND_LINE_FORWARD(event)) {
event.preventDefault()
return change.extendLineForward()
}
const { state } = change
const { selection, document, focusKey, focusBlock } = state
const transform = event.shiftKey ? 'extendToStartOf' : 'collapseToStartOf'
const block = selection.hasFocusAtStartOf(focusBlock)
? document.getPreviousBlock(focusKey)
: focusBlock
// COMPAT: If a void node is selected, or a zero-width text node adjacent to
// an inline is selected, we need to handle these hotkeys manually because
// browsers won't know what to do.
if (HOTKEYS.COLLAPSE_CHAR_BACKWARD(event)) {
const { isInVoid, previousText, document } = state
const isPreviousInVoid = previousText && document.hasVoidParent(previousText.key)
if (isInVoid || isPreviousInVoid) {
event.preventDefault()
return change.collapseCharBackward()
}
}
if (!block) return
const text = block.getFirstText()
if (HOTKEYS.COLLAPSE_CHAR_FORWARD(event)) {
const { isInVoid, nextText, document } = state
const isNextInVoid = nextText && document.hasVoidParent(nextText.key)
if (isInVoid || isNextInVoid) {
event.preventDefault()
return change.collapseCharForward()
}
}
event.preventDefault()
change[transform](text)
}
if (HOTKEYS.EXTEND_CHAR_BACKWARD(event)) {
const { isInVoid, previousText, document } = state
const isPreviousInVoid = previousText && document.hasVoidParent(previousText.key)
if (isInVoid || isPreviousInVoid) {
event.preventDefault()
return change.extendCharBackward()
}
}
/**
* On `down` key down, for Macs, move the selection to end of the block.
*
* COMPAT: Certain browsers don't handle the selection updates properly. In
* Chrome, option-shift-down doesn't properly extend the selection. And in
* Firefox, option-down doesn't properly move the selection.
*
* @param {Event} event
* @param {Change} change
* @param {Editor} editor
*/
function onKeyDownDown(event, change, editor) {
if (!IS_MAC || event.ctrlKey || !event.altKey) return
const { state } = change
const { selection, document, focusKey, focusBlock } = state
const transform = event.shiftKey ? 'extendToEndOf' : 'collapseToEndOf'
const block = selection.hasFocusAtEndOf(focusBlock)
? document.getNextBlock(focusKey)
: focusBlock
if (!block) return
const text = block.getLastText()
event.preventDefault()
change[transform](text)
if (HOTKEYS.EXTEND_CHAR_FORWARD(event)) {
const { isInVoid, nextText, document } = state
const isNextInVoid = nextText && document.hasVoidParent(nextText.key)
if (isInVoid || isNextInVoid) {
event.preventDefault()
return change.extendCharForward()
}
}
}
/**