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:
@@ -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",
|
||||
|
@@ -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,
|
||||
}
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user