1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-18 05:01:17 +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

@@ -3,8 +3,8 @@ import { Editor } from 'slate-react'
import { State } from 'slate' import { State } from 'slate'
import React from 'react' import React from 'react'
import isHotkey from 'is-hotkey'
import initialState from './state.json' import initialState from './state.json'
import { isKeyHotkey } from 'is-hotkey'
/** /**
* Define the default node type. * Define the default node type.
@@ -20,10 +20,10 @@ const DEFAULT_NODE = 'paragraph'
* @type {Function} * @type {Function}
*/ */
const isBoldHotkey = isHotkey('mod+b') const isBoldHotkey = isKeyHotkey('mod+b')
const isItalicHotkey = isHotkey('mod+i') const isItalicHotkey = isKeyHotkey('mod+i')
const isUnderlinedHotkey = isHotkey('mod+u') const isUnderlinedHotkey = isKeyHotkey('mod+u')
const isCodeHotkey = isHotkey('mod+`') const isCodeHotkey = isKeyHotkey('mod+`')
/** /**
* Define a schema. * Define a schema.

View File

@@ -3,8 +3,8 @@ import { Editor } from 'slate-react'
import { State } from 'slate' import { State } from 'slate'
import React from 'react' import React from 'react'
import isHotkey from 'is-hotkey'
import initialState from './state.json' import initialState from './state.json'
import { isKeyHotkey } from 'is-hotkey'
/** /**
* Hotkey matchers. * Hotkey matchers.
@@ -12,10 +12,10 @@ import initialState from './state.json'
* @type {Function} * @type {Function}
*/ */
const isBoldHotkey = isHotkey('mod+b') const isBoldHotkey = isKeyHotkey('mod+b')
const isItalicHotkey = isHotkey('mod+i') const isItalicHotkey = isKeyHotkey('mod+i')
const isUnderlinedHotkey = isHotkey('mod+u') const isUnderlinedHotkey = isKeyHotkey('mod+u')
const isCodeHotkey = isHotkey('mod+`') const isCodeHotkey = isKeyHotkey('mod+`')
/** /**
* Define a schema. * Define a schema.

View File

@@ -27,7 +27,7 @@
"gh-pages": "^0.11.0", "gh-pages": "^0.11.0",
"http-server": "^0.9.0", "http-server": "^0.9.0",
"immutable": "^3.8.1", "immutable": "^3.8.1",
"is-hotkey": "^0.0.1", "is-hotkey": "^0.1.1",
"is-image": "^1.0.1", "is-image": "^1.0.1",
"is-url": "^1.2.2", "is-url": "^1.2.2",
"jest": "^17.0.3", "jest": "^17.0.3",

View File

@@ -8,7 +8,7 @@
"dependencies": { "dependencies": {
"debug": "^2.3.2", "debug": "^2.3.2",
"get-window": "^1.1.1", "get-window": "^1.1.1",
"is-hotkey": "^0.0.3", "is-hotkey": "^0.1.1",
"is-in-browser": "^1.1.3", "is-in-browser": "^1.1.3",
"is-window": "^1.0.2", "is-window": "^1.0.2",
"keycode": "^2.1.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' import { IS_MAC } from './environment'
@@ -9,32 +9,73 @@ import { IS_MAC } from './environment'
* @type {Function} * @type {Function}
*/ */
const BOLD = isHotkey('mod+b') const BOLD = isKeyHotkey('mod+b')
const ITALIC = isHotkey('mod+i') const ITALIC = isKeyHotkey('mod+i')
const UNDO = isHotkey('mod+z') const ENTER = isKeyHotkey('enter')
const REDO_MAC = isHotkey('mod+shift+z') const SHIFT_ENTER = isKeyHotkey('shift+enter')
const REDO_OTHER = isHotkey('mod+y') const SPLIT_BLOCK = e => ENTER(e) || SHIFT_ENTER(e)
const REDO = e => IS_MAC ? REDO_MAC(e) : REDO_OTHER(e)
const DELETE_CHAR_BACKWARD_MAC = isHotkey('ctrl+h') const BACKSPACE = isKeyHotkey('backspace')
const DELETE_CHAR_FORWARD_MAC = isHotkey('ctrl+d') const SHIFT_BACKSPACE = isKeyHotkey('shift+backspace')
const DELETE_LINE_FORWARD_MAC = isHotkey('ctrl+k') const DELETE = isKeyHotkey('delete')
const DELETE_CHAR_BACKWARD = e => IS_MAC ? DELETE_CHAR_BACKWARD_MAC(e) : false const SHIFT_DELETE = isKeyHotkey('shift+delete')
const DELETE_CHAR_FORWARD = e => IS_MAC ? DELETE_CHAR_FORWARD_MAC(e) : false const DELETE_BACKWARD = e => BACKSPACE(e) || SHIFT_BACKSPACE(e)
const DELETE_LINE_FORWARD = e => IS_MAC ? DELETE_LINE_FORWARD_MAC(e) : false 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 => ( const CONTENTEDITABLE = e => (
e.key == 'Backspace' ||
e.key == 'Delete' ||
e.key == 'Enter' ||
e.key == 'Insert' ||
BOLD(e) || BOLD(e) ||
DELETE_CHAR_BACKWARD(e) || DELETE_CHAR_BACKWARD(e) ||
DELETE_CHAR_FORWARD(e) || DELETE_CHAR_FORWARD(e) ||
DELETE_LINE_BACKWARD(e) ||
DELETE_LINE_FORWARD(e) || DELETE_LINE_FORWARD(e) ||
DELETE_WORD_BACKWARD(e) ||
DELETE_WORD_FORWARD(e) ||
ITALIC(e) || ITALIC(e) ||
REDO(e) || REDO(e) ||
SPLIT_BLOCK(e) ||
TRANSPOSE_CHARACTER(e) ||
UNDO(e) UNDO(e)
) )
@@ -53,12 +94,24 @@ const COMPOSING = e => (
export default { export default {
BOLD, BOLD,
COLLAPSE_LINE_BACKWARD,
COLLAPSE_LINE_FORWARD,
COLLAPSE_CHAR_FORWARD,
COLLAPSE_CHAR_BACKWARD,
COMPOSING, COMPOSING,
CONTENTEDITABLE, CONTENTEDITABLE,
DELETE_CHAR_BACKWARD, DELETE_CHAR_BACKWARD,
DELETE_CHAR_FORWARD, DELETE_CHAR_FORWARD,
DELETE_LINE_BACKWARD,
DELETE_LINE_FORWARD, DELETE_LINE_FORWARD,
DELETE_WORD_BACKWARD,
DELETE_WORD_FORWARD,
EXTEND_LINE_BACKWARD,
EXTEND_LINE_FORWARD,
EXTEND_CHAR_FORWARD,
EXTEND_CHAR_BACKWARD,
ITALIC, ITALIC,
REDO, REDO,
SPLIT_BLOCK,
UNDO, UNDO,
} }

View File

@@ -17,7 +17,7 @@ import findRange from '../utils/find-range'
import getEventRange from '../utils/get-event-range' import getEventRange from '../utils/get-event-range'
import getEventTransfer from '../utils/get-event-transfer' import getEventTransfer from '../utils/get-event-transfer'
import setEventTransfer from '../utils/set-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. * Debug.
@@ -479,269 +479,107 @@ function AfterPlugin(options = {}) {
function onKeyDown(event, change, editor) { function onKeyDown(event, change, editor) {
debug('onKeyDown', { event }) debug('onKeyDown', { event })
switch (event.key) { const { state } = change
case 'Enter': return onKeyDownEnter(event, change)
case 'Backspace': return onKeyDownBackspace(event, change) if (HOTKEYS.SPLIT_BLOCK(event)) {
case 'Delete': return onKeyDownDelete(event, change) return state.isInVoid
case 'ArrowLeft': return onKeyDownLeft(event, change) ? change.collapseToStartOfNextText()
case 'ArrowRight': return onKeyDownRight(event, change) : change.splitBlock()
case 'ArrowUp': return onKeyDownUp(event, change)
case 'ArrowDown': return onKeyDownDown(event, change)
} }
if (HOTKEYS.DELETE_CHAR_BACKWARD(event)) { if (HOTKEYS.DELETE_CHAR_BACKWARD(event)) {
change.deleteCharBackward() return change.deleteCharBackward()
} }
if (HOTKEYS.DELETE_CHAR_FORWARD(event)) { 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)) { 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)) { if (HOTKEYS.REDO(event)) {
change.redo() return change.redo()
} }
if (HOTKEYS.UNDO(event)) { if (HOTKEYS.UNDO(event)) {
change.undo() return 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
} }
change.splitBlock() // 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)) {
* 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) {
event.preventDefault() event.preventDefault()
const previous = document.getPreviousText(startKey) return change.collapseLineBackward()
// 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)
} }
}
/** if (HOTKEYS.COLLAPSE_LINE_FORWARD(event)) {
* 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) {
event.preventDefault() event.preventDefault()
const next = document.getNextText(startKey) return change.collapseLineForward()
// 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)
} }
}
/** if (HOTKEYS.EXTEND_LINE_BACKWARD(event)) {
* On `up` key down, for Macs, move the selection to start of the block. event.preventDefault()
* return change.extendLineBackward()
* 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
*/
function onKeyDownUp(event, change, editor) { if (HOTKEYS.EXTEND_LINE_FORWARD(event)) {
if (!IS_MAC || event.ctrlKey || !event.altKey) return event.preventDefault()
return change.extendLineForward()
}
const { state } = change // COMPAT: If a void node is selected, or a zero-width text node adjacent to
const { selection, document, focusKey, focusBlock } = state // an inline is selected, we need to handle these hotkeys manually because
const transform = event.shiftKey ? 'extendToStartOf' : 'collapseToStartOf' // browsers won't know what to do.
const block = selection.hasFocusAtStartOf(focusBlock) if (HOTKEYS.COLLAPSE_CHAR_BACKWARD(event)) {
? document.getPreviousBlock(focusKey) const { isInVoid, previousText, document } = state
: focusBlock const isPreviousInVoid = previousText && document.hasVoidParent(previousText.key)
if (isInVoid || isPreviousInVoid) {
event.preventDefault()
return change.collapseCharBackward()
}
}
if (!block) return if (HOTKEYS.COLLAPSE_CHAR_FORWARD(event)) {
const text = block.getFirstText() const { isInVoid, nextText, document } = state
const isNextInVoid = nextText && document.hasVoidParent(nextText.key)
if (isInVoid || isNextInVoid) {
event.preventDefault()
return change.collapseCharForward()
}
}
event.preventDefault() if (HOTKEYS.EXTEND_CHAR_BACKWARD(event)) {
change[transform](text) const { isInVoid, previousText, document } = state
} const isPreviousInVoid = previousText && document.hasVoidParent(previousText.key)
if (isInVoid || isPreviousInVoid) {
event.preventDefault()
return change.extendCharBackward()
}
}
/** if (HOTKEYS.EXTEND_CHAR_FORWARD(event)) {
* On `down` key down, for Macs, move the selection to end of the block. const { isInVoid, nextText, document } = state
* const isNextInVoid = nextText && document.hasVoidParent(nextText.key)
* COMPAT: Certain browsers don't handle the selection updates properly. In if (isInVoid || isNextInVoid) {
* Chrome, option-shift-down doesn't properly extend the selection. And in event.preventDefault()
* Firefox, option-down doesn't properly move the selection. return change.extendCharForward()
* }
* @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)
} }
/** /**

View File

@@ -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 * @param {Change} change
*/ */
Changes.moveTo = (change, properties) => { Changes.moveAnchorCharBackward = (change) => {
logger.deprecate('0.17.0', 'The `moveTo()` change is deprecated, please use `select()` instead.') const { state } = change
change.select(properties) 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 * @param {Change} change
*/ */
Changes.unsetMarks = (change) => { Changes.moveAnchorCharForward = (change) => {
logger.deprecate('0.17.0', 'The `unsetMarks()` change is deprecated.') const { state } = change
change.select({ marks: null }) 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 * @param {Change} change
*/ */
Changes.unsetSelection = (change) => { Changes.moveFocusCharBackward = (change) => {
logger.deprecate('0.17.0', 'The `unsetSelection()` change is deprecated, please use `deselect()` instead.') const { state } = change
change.select({ const { document, selection, focusText, focusBlock } = state
anchorKey: null, const { focusOffset } = selection
anchorOffset: 0, const previousText = document.getPreviousText(focusText.key)
focusKey: null, const isInVoid = document.hasVoidParent(focusText.key)
focusOffset: 0, const isPreviousInVoid = previousText && document.hasVoidParent(previousText.key)
isFocused: false,
isBackward: false 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. * Mix in selection changes that are just a proxy for the selection method.
*/ */
@@ -211,6 +350,10 @@ PROXY_TRANSFORMS.forEach((method) => {
const PREFIXES = [ const PREFIXES = [
'moveTo', 'moveTo',
'moveAnchorTo',
'moveFocusTo',
'moveStartTo',
'moveEndTo',
'collapseTo', 'collapseTo',
'extendTo', 'extendTo',
] ]
@@ -237,29 +380,78 @@ PREFIXES.forEach((prefix) => {
} }
edges.forEach((edge) => { edges.forEach((edge) => {
DIRECTIONS.forEach((direction) => { const method = `${prefix}${edge}Of`
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}`
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 { state } = change
const { document, selection } = state const { document, selection } = state
const nodes = document[getAtRange](selection) const node = document[getNode](selection[directionKey])
const node = nodes[index]() if (!node) return
const target = document[get](node.key) const target = document[getDirectionNode](node.key)
if (!target) return if (!target) return
const next = selection[method](target) change[method](target)
change.select(next)
} }
}) })
}) })
}) })
}) })
/**
* 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. * Mix in deprecated changes with a warning.
*/ */

View File

@@ -326,9 +326,7 @@ class State extends Record(DEFAULTS) {
*/ */
get startBlock() { get startBlock() {
return this.selection.isUnset return this.startKey && this.document.getClosestBlock(this.startKey)
? null
: this.document.getClosestBlock(this.selection.startKey)
} }
/** /**
@@ -338,9 +336,7 @@ class State extends Record(DEFAULTS) {
*/ */
get endBlock() { get endBlock() {
return this.selection.isUnset return this.endKey && this.document.getClosestBlock(this.endKey)
? null
: this.document.getClosestBlock(this.selection.endKey)
} }
/** /**
@@ -350,9 +346,7 @@ class State extends Record(DEFAULTS) {
*/ */
get anchorBlock() { get anchorBlock() {
return this.selection.isUnset return this.anchorKey && this.document.getClosestBlock(this.anchorKey)
? null
: this.document.getClosestBlock(this.selection.anchorKey)
} }
/** /**
@@ -362,9 +356,7 @@ class State extends Record(DEFAULTS) {
*/ */
get focusBlock() { get focusBlock() {
return this.selection.isUnset return this.focusKey && this.document.getClosestBlock(this.focusKey)
? null
: this.document.getClosestBlock(this.selection.focusKey)
} }
/** /**
@@ -374,9 +366,7 @@ class State extends Record(DEFAULTS) {
*/ */
get startInline() { get startInline() {
return this.selection.isUnset return this.startKey && this.document.getClosestInline(this.startKey)
? null
: this.document.getClosestInline(this.selection.startKey)
} }
/** /**
@@ -386,9 +376,7 @@ class State extends Record(DEFAULTS) {
*/ */
get endInline() { get endInline() {
return this.selection.isUnset return this.endKey && this.document.getClosestInline(this.endKey)
? null
: this.document.getClosestInline(this.selection.endKey)
} }
/** /**
@@ -398,9 +386,7 @@ class State extends Record(DEFAULTS) {
*/ */
get anchorInline() { get anchorInline() {
return this.selection.isUnset return this.anchorKey && this.document.getClosestInline(this.anchorKey)
? null
: this.document.getClosestInline(this.selection.anchorKey)
} }
/** /**
@@ -410,9 +396,7 @@ class State extends Record(DEFAULTS) {
*/ */
get focusInline() { get focusInline() {
return this.selection.isUnset return this.focusKey && this.document.getClosestInline(this.focusKey)
? null
: this.document.getClosestInline(this.selection.focusKey)
} }
/** /**
@@ -422,9 +406,7 @@ class State extends Record(DEFAULTS) {
*/ */
get startText() { get startText() {
return this.selection.isUnset return this.startKey && this.document.getDescendant(this.startKey)
? null
: this.document.getDescendant(this.selection.startKey)
} }
/** /**
@@ -434,9 +416,7 @@ class State extends Record(DEFAULTS) {
*/ */
get endText() { get endText() {
return this.selection.isUnset return this.endKey && this.document.getDescendant(this.endKey)
? null
: this.document.getDescendant(this.selection.endKey)
} }
/** /**
@@ -446,9 +426,7 @@ class State extends Record(DEFAULTS) {
*/ */
get anchorText() { get anchorText() {
return this.selection.isUnset return this.anchorKey && this.document.getDescendant(this.anchorKey)
? null
: this.document.getDescendant(this.selection.anchorKey)
} }
/** /**
@@ -458,9 +436,67 @@ class State extends Record(DEFAULTS) {
*/ */
get focusText() { get focusText() {
return this.selection.isUnset return this.focusKey && this.document.getDescendant(this.focusKey)
? null }
: this.document.getDescendant(this.selection.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() { get isEmpty() {
const { startOffset, endOffset } = this if (this.isCollapsed) return true
if (this.endOffset != 0 && this.startOffset != 0) return false
if (this.isCollapsed) {
return true
}
if (endOffset != 0 && startOffset != 0) {
return false
}
return this.fragment.text.length == 0 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. * Create a new `Change` with the current state as a starting point.
* *

View File

@@ -3352,13 +3352,9 @@ is-glob@^3.1.0:
dependencies: dependencies:
is-extglob "^2.1.0" is-extglob "^2.1.0"
is-hotkey@^0.0.1: is-hotkey@^0.1.1:
version "0.0.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/is-hotkey/-/is-hotkey-0.0.1.tgz#d8d817209b34292551a85357e65cdbfcfa763443" resolved "https://registry.yarnpkg.com/is-hotkey/-/is-hotkey-0.1.1.tgz#b279a2fd108391be9aa93c6cb317f50357da549a"
is-hotkey@^0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/is-hotkey/-/is-hotkey-0.0.3.tgz#3713fea135f86528c87cf39810b3934e45151390"
is-image@^1.0.1: is-image@^1.0.1:
version "1.0.1" version "1.0.1"