mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-18 21:21:21 +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:
@@ -3,8 +3,8 @@ import { Editor } from 'slate-react'
|
||||
import { State } from 'slate'
|
||||
|
||||
import React from 'react'
|
||||
import isHotkey from 'is-hotkey'
|
||||
import initialState from './state.json'
|
||||
import { isKeyHotkey } from 'is-hotkey'
|
||||
|
||||
/**
|
||||
* Define the default node type.
|
||||
@@ -20,10 +20,10 @@ const DEFAULT_NODE = 'paragraph'
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
const isBoldHotkey = isHotkey('mod+b')
|
||||
const isItalicHotkey = isHotkey('mod+i')
|
||||
const isUnderlinedHotkey = isHotkey('mod+u')
|
||||
const isCodeHotkey = isHotkey('mod+`')
|
||||
const isBoldHotkey = isKeyHotkey('mod+b')
|
||||
const isItalicHotkey = isKeyHotkey('mod+i')
|
||||
const isUnderlinedHotkey = isKeyHotkey('mod+u')
|
||||
const isCodeHotkey = isKeyHotkey('mod+`')
|
||||
|
||||
/**
|
||||
* Define a schema.
|
||||
|
@@ -3,8 +3,8 @@ import { Editor } from 'slate-react'
|
||||
import { State } from 'slate'
|
||||
|
||||
import React from 'react'
|
||||
import isHotkey from 'is-hotkey'
|
||||
import initialState from './state.json'
|
||||
import { isKeyHotkey } from 'is-hotkey'
|
||||
|
||||
/**
|
||||
* Hotkey matchers.
|
||||
@@ -12,10 +12,10 @@ import initialState from './state.json'
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
const isBoldHotkey = isHotkey('mod+b')
|
||||
const isItalicHotkey = isHotkey('mod+i')
|
||||
const isUnderlinedHotkey = isHotkey('mod+u')
|
||||
const isCodeHotkey = isHotkey('mod+`')
|
||||
const isBoldHotkey = isKeyHotkey('mod+b')
|
||||
const isItalicHotkey = isKeyHotkey('mod+i')
|
||||
const isUnderlinedHotkey = isKeyHotkey('mod+u')
|
||||
const isCodeHotkey = isKeyHotkey('mod+`')
|
||||
|
||||
/**
|
||||
* Define a schema.
|
||||
|
@@ -27,7 +27,7 @@
|
||||
"gh-pages": "^0.11.0",
|
||||
"http-server": "^0.9.0",
|
||||
"immutable": "^3.8.1",
|
||||
"is-hotkey": "^0.0.1",
|
||||
"is-hotkey": "^0.1.1",
|
||||
"is-image": "^1.0.1",
|
||||
"is-url": "^1.2.2",
|
||||
"jest": "^17.0.3",
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -108,46 +108,185 @@ Changes.snapshotSelection = (change) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set `properties` on the selection.
|
||||
* Move the anchor point backward, accounting for being at the start of a block.
|
||||
*
|
||||
* @param {Mixed} ...args
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
Changes.moveTo = (change, properties) => {
|
||||
logger.deprecate('0.17.0', 'The `moveTo()` change is deprecated, please use `select()` instead.')
|
||||
change.select(properties)
|
||||
Changes.moveAnchorCharBackward = (change) => {
|
||||
const { state } = change
|
||||
const { document, selection, anchorText, anchorBlock } = state
|
||||
const { anchorOffset } = selection
|
||||
const previousText = document.getPreviousText(anchorText.key)
|
||||
const isInVoid = document.hasVoidParent(anchorText.key)
|
||||
const isPreviousInVoid = previousText && document.hasVoidParent(previousText.key)
|
||||
|
||||
if (!isInVoid && anchorOffset > 0) {
|
||||
change.moveAnchor(-1)
|
||||
return
|
||||
}
|
||||
|
||||
if (!previousText) {
|
||||
return
|
||||
}
|
||||
|
||||
change.moveAnchorToEndOf(previousText)
|
||||
|
||||
if (!isInVoid && !isPreviousInVoid && anchorBlock.hasNode(previousText.key)) {
|
||||
change.moveAnchor(-1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset the selection's marks.
|
||||
* Move the anchor point forward, accounting for being at the end of a block.
|
||||
*
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
Changes.unsetMarks = (change) => {
|
||||
logger.deprecate('0.17.0', 'The `unsetMarks()` change is deprecated.')
|
||||
change.select({ marks: null })
|
||||
Changes.moveAnchorCharForward = (change) => {
|
||||
const { state } = change
|
||||
const { document, selection, anchorText, anchorBlock } = state
|
||||
const { anchorOffset } = selection
|
||||
const nextText = document.getNextText(anchorText.key)
|
||||
const isInVoid = document.hasVoidParent(anchorText.key)
|
||||
const isNextInVoid = nextText && document.hasVoidParent(nextText.key)
|
||||
|
||||
if (!isInVoid && anchorOffset < anchorText.text.length) {
|
||||
change.moveAnchor(1)
|
||||
return
|
||||
}
|
||||
|
||||
if (!nextText) {
|
||||
return
|
||||
}
|
||||
|
||||
change.moveAnchorToStartOf(nextText)
|
||||
|
||||
if (!isInVoid && !isNextInVoid && anchorBlock.hasNode(nextText.key)) {
|
||||
change.moveAnchor(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset the selection, removing an association to a node.
|
||||
* Move the focus point backward, accounting for being at the start of a block.
|
||||
*
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
Changes.unsetSelection = (change) => {
|
||||
logger.deprecate('0.17.0', 'The `unsetSelection()` change is deprecated, please use `deselect()` instead.')
|
||||
change.select({
|
||||
anchorKey: null,
|
||||
anchorOffset: 0,
|
||||
focusKey: null,
|
||||
focusOffset: 0,
|
||||
isFocused: false,
|
||||
isBackward: false
|
||||
})
|
||||
Changes.moveFocusCharBackward = (change) => {
|
||||
const { state } = change
|
||||
const { document, selection, focusText, focusBlock } = state
|
||||
const { focusOffset } = selection
|
||||
const previousText = document.getPreviousText(focusText.key)
|
||||
const isInVoid = document.hasVoidParent(focusText.key)
|
||||
const isPreviousInVoid = previousText && document.hasVoidParent(previousText.key)
|
||||
|
||||
if (!isInVoid && focusOffset > 0) {
|
||||
change.moveFocus(-1)
|
||||
return
|
||||
}
|
||||
|
||||
if (!previousText) {
|
||||
return
|
||||
}
|
||||
|
||||
change.moveFocusToEndOf(previousText)
|
||||
|
||||
if (!isInVoid && !isPreviousInVoid && focusBlock.hasNode(previousText.key)) {
|
||||
change.moveFocus(-1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the focus point forward, accounting for being at the end of a block.
|
||||
*
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
Changes.moveFocusCharForward = (change) => {
|
||||
const { state } = change
|
||||
const { document, selection, focusText, focusBlock } = state
|
||||
const { focusOffset } = selection
|
||||
const nextText = document.getNextText(focusText.key)
|
||||
const isInVoid = document.hasVoidParent(focusText.key)
|
||||
const isNextInVoid = nextText && document.hasVoidParent(nextText.key)
|
||||
|
||||
if (!isInVoid && focusOffset < focusText.text.length) {
|
||||
change.moveFocus(1)
|
||||
return
|
||||
}
|
||||
|
||||
if (!nextText) {
|
||||
return
|
||||
}
|
||||
|
||||
change.moveFocusToStartOf(nextText)
|
||||
|
||||
if (!isInVoid && !isNextInVoid && focusBlock.hasNode(nextText.key)) {
|
||||
change.moveFocus(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mix in move methods.
|
||||
*/
|
||||
|
||||
const MOVE_DIRECTIONS = [
|
||||
'Forward',
|
||||
'Backward',
|
||||
]
|
||||
|
||||
MOVE_DIRECTIONS.forEach((direction) => {
|
||||
const anchor = `moveAnchorChar${direction}`
|
||||
const focus = `moveFocusChar${direction}`
|
||||
|
||||
Changes[`moveChar${direction}`] = (change) => {
|
||||
change[anchor]()[focus]()
|
||||
}
|
||||
|
||||
Changes[`moveStartChar${direction}`] = (change) => {
|
||||
if (change.state.isBackward) {
|
||||
change[focus]()
|
||||
} else {
|
||||
change[anchor]()
|
||||
}
|
||||
}
|
||||
|
||||
Changes[`moveEndChar${direction}`] = (change) => {
|
||||
if (change.state.isBackward) {
|
||||
change[anchor]()
|
||||
} else {
|
||||
change[focus]()
|
||||
}
|
||||
}
|
||||
|
||||
Changes[`extendChar${direction}`] = (change) => {
|
||||
change[`moveFocusChar${direction}`]()
|
||||
}
|
||||
|
||||
Changes[`collapseChar${direction}`] = (change) => {
|
||||
const collapse = direction == 'Forward' ? 'collapseToEnd' : 'collapseToStart'
|
||||
change[collapse]()[`moveChar${direction}`]()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Mix in alias methods.
|
||||
*/
|
||||
|
||||
const ALIAS_METHODS = [
|
||||
['collapseLineBackward', 'collapseToStartOfBlock'],
|
||||
['collapseLineForward', 'collapseToEndOfBlock'],
|
||||
['extendLineBackward', 'extendToStartOfBlock'],
|
||||
['extendLineForward', 'extendToEndOfBlock'],
|
||||
]
|
||||
|
||||
ALIAS_METHODS.forEach(([ alias, method ]) => {
|
||||
Changes[alias] = function (change, ...args) {
|
||||
change[method](change, ...args)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Mix in selection changes that are just a proxy for the selection method.
|
||||
*/
|
||||
@@ -211,6 +350,10 @@ PROXY_TRANSFORMS.forEach((method) => {
|
||||
|
||||
const PREFIXES = [
|
||||
'moveTo',
|
||||
'moveAnchorTo',
|
||||
'moveFocusTo',
|
||||
'moveStartTo',
|
||||
'moveEndTo',
|
||||
'collapseTo',
|
||||
'extendTo',
|
||||
]
|
||||
@@ -237,29 +380,78 @@ PREFIXES.forEach((prefix) => {
|
||||
}
|
||||
|
||||
edges.forEach((edge) => {
|
||||
DIRECTIONS.forEach((direction) => {
|
||||
KINDS.forEach((kind) => {
|
||||
const get = `get${direction}${kind}`
|
||||
const getAtRange = `get${kind}sAtRange`
|
||||
const index = direction == 'Next' ? 'last' : 'first'
|
||||
const method = `${prefix}${edge}Of`
|
||||
const name = `${method}${direction}${kind}`
|
||||
const method = `${prefix}${edge}Of`
|
||||
|
||||
Changes[name] = (change) => {
|
||||
KINDS.forEach((kind) => {
|
||||
const getNode = kind == 'Text' ? 'getNode' : `getClosest${kind}`
|
||||
|
||||
Changes[`${method}${kind}`] = (change) => {
|
||||
const { state } = change
|
||||
const { document, selection } = state
|
||||
const node = document[getNode](selection.startKey)
|
||||
if (!node) return
|
||||
change[method](node)
|
||||
}
|
||||
|
||||
DIRECTIONS.forEach((direction) => {
|
||||
const getDirectionNode = `get${direction}${kind}`
|
||||
const directionKey = direction == 'Next' ? 'startKey' : 'endKey'
|
||||
|
||||
Changes[`${method}${direction}${kind}`] = (change) => {
|
||||
const { state } = change
|
||||
const { document, selection } = state
|
||||
const nodes = document[getAtRange](selection)
|
||||
const node = nodes[index]()
|
||||
const target = document[get](node.key)
|
||||
const node = document[getNode](selection[directionKey])
|
||||
if (!node) return
|
||||
const target = document[getDirectionNode](node.key)
|
||||
if (!target) return
|
||||
const next = selection[method](target)
|
||||
change.select(next)
|
||||
change[method](target)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Set `properties` on the selection.
|
||||
*
|
||||
* @param {Mixed} ...args
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
Changes.moveTo = (change, properties) => {
|
||||
logger.deprecate('0.17.0', 'The `moveTo()` change is deprecated, please use `select()` instead.')
|
||||
change.select(properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset the selection's marks.
|
||||
*
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
Changes.unsetMarks = (change) => {
|
||||
logger.deprecate('0.17.0', 'The `unsetMarks()` change is deprecated.')
|
||||
change.select({ marks: null })
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset the selection, removing an association to a node.
|
||||
*
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
Changes.unsetSelection = (change) => {
|
||||
logger.deprecate('0.17.0', 'The `unsetSelection()` change is deprecated, please use `deselect()` instead.')
|
||||
change.select({
|
||||
anchorKey: null,
|
||||
anchorOffset: 0,
|
||||
focusKey: null,
|
||||
focusOffset: 0,
|
||||
isFocused: false,
|
||||
isBackward: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Mix in deprecated changes with a warning.
|
||||
*/
|
||||
|
@@ -326,9 +326,7 @@ class State extends Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
get startBlock() {
|
||||
return this.selection.isUnset
|
||||
? null
|
||||
: this.document.getClosestBlock(this.selection.startKey)
|
||||
return this.startKey && this.document.getClosestBlock(this.startKey)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -338,9 +336,7 @@ class State extends Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
get endBlock() {
|
||||
return this.selection.isUnset
|
||||
? null
|
||||
: this.document.getClosestBlock(this.selection.endKey)
|
||||
return this.endKey && this.document.getClosestBlock(this.endKey)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,9 +346,7 @@ class State extends Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
get anchorBlock() {
|
||||
return this.selection.isUnset
|
||||
? null
|
||||
: this.document.getClosestBlock(this.selection.anchorKey)
|
||||
return this.anchorKey && this.document.getClosestBlock(this.anchorKey)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -362,9 +356,7 @@ class State extends Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
get focusBlock() {
|
||||
return this.selection.isUnset
|
||||
? null
|
||||
: this.document.getClosestBlock(this.selection.focusKey)
|
||||
return this.focusKey && this.document.getClosestBlock(this.focusKey)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -374,9 +366,7 @@ class State extends Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
get startInline() {
|
||||
return this.selection.isUnset
|
||||
? null
|
||||
: this.document.getClosestInline(this.selection.startKey)
|
||||
return this.startKey && this.document.getClosestInline(this.startKey)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -386,9 +376,7 @@ class State extends Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
get endInline() {
|
||||
return this.selection.isUnset
|
||||
? null
|
||||
: this.document.getClosestInline(this.selection.endKey)
|
||||
return this.endKey && this.document.getClosestInline(this.endKey)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -398,9 +386,7 @@ class State extends Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
get anchorInline() {
|
||||
return this.selection.isUnset
|
||||
? null
|
||||
: this.document.getClosestInline(this.selection.anchorKey)
|
||||
return this.anchorKey && this.document.getClosestInline(this.anchorKey)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -410,9 +396,7 @@ class State extends Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
get focusInline() {
|
||||
return this.selection.isUnset
|
||||
? null
|
||||
: this.document.getClosestInline(this.selection.focusKey)
|
||||
return this.focusKey && this.document.getClosestInline(this.focusKey)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -422,9 +406,7 @@ class State extends Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
get startText() {
|
||||
return this.selection.isUnset
|
||||
? null
|
||||
: this.document.getDescendant(this.selection.startKey)
|
||||
return this.startKey && this.document.getDescendant(this.startKey)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -434,9 +416,7 @@ class State extends Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
get endText() {
|
||||
return this.selection.isUnset
|
||||
? null
|
||||
: this.document.getDescendant(this.selection.endKey)
|
||||
return this.endKey && this.document.getDescendant(this.endKey)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -446,9 +426,7 @@ class State extends Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
get anchorText() {
|
||||
return this.selection.isUnset
|
||||
? null
|
||||
: this.document.getDescendant(this.selection.anchorKey)
|
||||
return this.anchorKey && this.document.getDescendant(this.anchorKey)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -458,9 +436,67 @@ class State extends Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
get focusText() {
|
||||
return this.selection.isUnset
|
||||
? null
|
||||
: this.document.getDescendant(this.selection.focusKey)
|
||||
return this.focusKey && this.document.getDescendant(this.focusKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next block node.
|
||||
*
|
||||
* @return {Block}
|
||||
*/
|
||||
|
||||
get nextBlock() {
|
||||
return this.endKey && this.document.getNextBlock(this.endKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the previous block node.
|
||||
*
|
||||
* @return {Block}
|
||||
*/
|
||||
|
||||
get previousBlock() {
|
||||
return this.startKey && this.document.getPreviousBlock(this.startKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next inline node.
|
||||
*
|
||||
* @return {Inline}
|
||||
*/
|
||||
|
||||
get nextInline() {
|
||||
return this.endKey && this.document.getNextInline(this.endKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the previous inline node.
|
||||
*
|
||||
* @return {Inline}
|
||||
*/
|
||||
|
||||
get previousInline() {
|
||||
return this.startKey && this.document.getPreviousInline(this.startKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next text node.
|
||||
*
|
||||
* @return {Text}
|
||||
*/
|
||||
|
||||
get nextText() {
|
||||
return this.endKey && this.document.getNextText(this.endKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the previous text node.
|
||||
*
|
||||
* @return {Text}
|
||||
*/
|
||||
|
||||
get previousText() {
|
||||
return this.startKey && this.document.getPreviousText(this.startKey)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -554,19 +590,22 @@ class State extends Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
get isEmpty() {
|
||||
const { startOffset, endOffset } = this
|
||||
|
||||
if (this.isCollapsed) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (endOffset != 0 && startOffset != 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.isCollapsed) return true
|
||||
if (this.endOffset != 0 && this.startOffset != 0) return false
|
||||
return this.fragment.text.length == 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the selection is collapsed in a void node.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
get isInVoid() {
|
||||
if (this.isExpanded) return false
|
||||
return this.document.hasVoidParent(this.startKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new `Change` with the current state as a starting point.
|
||||
*
|
||||
|
10
yarn.lock
10
yarn.lock
@@ -3352,13 +3352,9 @@ is-glob@^3.1.0:
|
||||
dependencies:
|
||||
is-extglob "^2.1.0"
|
||||
|
||||
is-hotkey@^0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-hotkey/-/is-hotkey-0.0.1.tgz#d8d817209b34292551a85357e65cdbfcfa763443"
|
||||
|
||||
is-hotkey@^0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-hotkey/-/is-hotkey-0.0.3.tgz#3713fea135f86528c87cf39810b3934e45151390"
|
||||
is-hotkey@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-hotkey/-/is-hotkey-0.1.1.tgz#b279a2fd108391be9aa93c6cb317f50357da549a"
|
||||
|
||||
is-image@^1.0.1:
|
||||
version "1.0.1"
|
||||
|
Reference in New Issue
Block a user