diff --git a/examples/rich-text/index.js b/examples/rich-text/index.js index 102e7af4b..86d1e8c27 100644 --- a/examples/rich-text/index.js +++ b/examples/rich-text/index.js @@ -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. diff --git a/examples/syncing-operations/index.js b/examples/syncing-operations/index.js index aa3f2faf3..bc68becf9 100644 --- a/examples/syncing-operations/index.js +++ b/examples/syncing-operations/index.js @@ -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. diff --git a/package.json b/package.json index eae761b5e..14e6fe3ce 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/slate-react/package.json b/packages/slate-react/package.json index b839bc628..8c3875a22 100644 --- a/packages/slate-react/package.json +++ b/packages/slate-react/package.json @@ -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", diff --git a/packages/slate-react/src/constants/hotkeys.js b/packages/slate-react/src/constants/hotkeys.js index 675c0706c..1d4349530 100644 --- a/packages/slate-react/src/constants/hotkeys.js +++ b/packages/slate-react/src/constants/hotkeys.js @@ -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, } diff --git a/packages/slate-react/src/plugins/after.js b/packages/slate-react/src/plugins/after.js index c2d7b52f4..bf02e1cd4 100644 --- a/packages/slate-react/src/plugins/after.js +++ b/packages/slate-react/src/plugins/after.js @@ -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() + } + } } /** diff --git a/packages/slate/src/changes/on-selection.js b/packages/slate/src/changes/on-selection.js index 2a169ed4e..d68fb80df 100644 --- a/packages/slate/src/changes/on-selection.js +++ b/packages/slate/src/changes/on-selection.js @@ -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. */ diff --git a/packages/slate/src/models/state.js b/packages/slate/src/models/state.js index 4b689bb52..59b314c37 100644 --- a/packages/slate/src/models/state.js +++ b/packages/slate/src/models/state.js @@ -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. * diff --git a/yarn.lock b/yarn.lock index e2938e471..2b93a384d 100644 --- a/yarn.lock +++ b/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"