1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-29 18:09:49 +02:00

Extract hotkey and environment detection into separate packages (#1760)

* Extract hotkey and environment detection into separate packages

Overriding default behavior in plugins can be hard, because you have
to match all of the keybindings that Slate uses. By exporting hotkeys
as its own package, both core Slate plugins and custom plugins can use
the same key detection logic.

* Rename Hotkeys.* to Hotkeys.is*
This commit is contained in:
Justin Weiss
2018-04-27 13:19:39 -07:00
committed by Ian Storm Taylor
parent 1cd1bcb3e2
commit d4c630c05a
19 changed files with 245 additions and 168 deletions

View File

@@ -0,0 +1,9 @@
# Changelog
This document maintains a list of changes to the `slate-dev-environment` package with each new version. Until `1.0.0` is released, breaking changes will be added as minor version bumps, and smaller changes won't be accounted for since the library is moving quickly.
---
### `0.1.0` — April 5, 2018
:tada:

View File

@@ -0,0 +1 @@
This package can be used within core Slate packages to detect browser and OS environments.

View File

@@ -0,0 +1,24 @@
{
"name": "slate-dev-environment",
"description": "Detect browser and OS environments in core Slate packages",
"version": "0.1.0",
"license": "MIT",
"repository": "git://github.com/ianstormtaylor/slate.git",
"main": "lib/slate-dev-environment.js",
"module": "lib/slate-dev-environment.es.js",
"umd": "dist/slate-dev-environment.js",
"umdMin": "dist/slate-dev-environment.min.js",
"files": [
"dist/",
"lib/"
],
"scripts": {
"clean": "rm -rf ./dist ./lib ./node_modules"
},
"keywords": [
"slate"
],
"dependencies": {
"is-in-browser": "^1.1.3"
}
}

View File

@@ -0,0 +1,9 @@
# Changelog
This document maintains a list of changes to the `slate-hotkeys` package with each new version. Until `1.0.0` is released, breaking changes will be added as minor version bumps, and smaller changes won't be accounted for since the library is moving quickly.
---
### `0.1.0` — April 5, 2018
:tada:

View File

@@ -0,0 +1 @@
This package contains functions that detect common keypresses in a platform-agnostic way.

View File

@@ -0,0 +1,30 @@
{
"name": "slate-hotkeys",
"description": "A set of function to detect common keypresses in a platform-agnostic way",
"version": "0.1.0",
"license": "MIT",
"repository": "git://github.com/ianstormtaylor/slate.git",
"main": "lib/slate-hotkeys.js",
"module": "lib/slate-hotkeys.es.js",
"umd": "dist/slate-hotkeys.js",
"umdMin": "dist/slate-hotkeys.min.js",
"files": [
"dist/",
"lib/"
],
"dependencies": {
"is-hotkey": "^0.1.1",
"slate-dev-environment": "^0.1.0"
},
"scripts": {
"clean": "rm -rf ./dist ./lib ./node_modules"
},
"keywords": [
"contenteditable",
"editor",
"keybinding",
"keypress",
"slate",
"text"
]
}

View File

@@ -0,0 +1,132 @@
import { isKeyHotkey } from 'is-hotkey'
import { IS_IOS, IS_MAC } from 'slate-dev-environment'
/**
* Is Apple?
*
* @type {Boolean}
*/
const IS_APPLE = IS_IOS || IS_MAC
/**
* Hotkeys.
*
* @type {Function}
*/
const isBold = isKeyHotkey('mod+b')
const isItalic = isKeyHotkey('mod+i')
const isEnter = isKeyHotkey('enter')
const isShiftEnter = isKeyHotkey('shift+enter')
const isSplitBlock = e => isEnter(e) || isShiftEnter(e)
const isBackspace = isKeyHotkey('backspace')
const isShiftBackspace = isKeyHotkey('shift+backspace')
const isDelete = isKeyHotkey('delete')
const isShiftDelete = isKeyHotkey('shift+delete')
const isDeleteBackward = e => isBackspace(e) || isShiftBackspace(e)
const isDeleteForward = e => isDelete(e) || isShiftDelete(e)
const isDeleteCharBackwardMac = isKeyHotkey('ctrl+h')
const isDeleteCharForwardMac = isKeyHotkey('ctrl+d')
const isDeleteCharBackward = e =>
isDeleteBackward(e) || (IS_APPLE && isDeleteCharBackwardMac(e))
const isDeleteCharForward = e =>
isDeleteForward(e) || (IS_APPLE && isDeleteCharForwardMac(e))
const isDeleteLineBackwardMac = e =>
isKeyHotkey('cmd+shift+backspace', e) || isKeyHotkey('cmd+backspace', e)
const isDeleteLineForwardMac = isKeyHotkey('ctrl+k')
const isDeleteLineBackward = e => IS_APPLE && isDeleteLineBackwardMac(e)
const isDeleteLineForward = e => IS_APPLE && isDeleteLineForwardMac(e)
const isDeleteWordBackwardMac = e =>
isKeyHotkey('shift+option+backspace', e) || isKeyHotkey('option+backspace', e)
const isDeleteWordBackwardPC = isKeyHotkey('ctrl+backspace')
const isDeleteWordForwardMac = e =>
isKeyHotkey('shift+option+delete', e) || isKeyHotkey('option+delete', e)
const isDeleteWordForwardPC = isKeyHotkey('ctrl+delete')
const isDeleteWordBackward = e =>
IS_APPLE ? isDeleteWordBackwardMac(e) : isDeleteWordBackwardPC(e)
const isDeleteWordForward = e =>
IS_APPLE ? isDeleteWordForwardMac(e) : isDeleteWordForwardPC(e)
const isExtendCharForward = isKeyHotkey('shift+right')
const isExtendCharBackward = isKeyHotkey('shift+left')
const isRightArrow = isKeyHotkey('right')
const isLeftArrow = isKeyHotkey('left')
const isCollapseCharForward = e => isRightArrow(e) && !isExtendCharForward(e)
const isCollapseCharBackward = e => isLeftArrow(e) && !isExtendCharBackward(e)
const isCollapseLineBackwardMac = isKeyHotkey('option+up')
const isCollapseLineForwardMac = isKeyHotkey('option+down')
const isCollapseLineBackward = e => IS_APPLE && isCollapseLineBackwardMac(e)
const isCollapseLineForward = e => IS_APPLE && isCollapseLineForwardMac(e)
const isExtendLineBackwardMac = isKeyHotkey('option+shift+up')
const isExtendLineForwardMac = isKeyHotkey('option+shift+down')
const isExtendLineBackward = e => IS_APPLE && isExtendLineBackwardMac(e)
const isExtendLineForward = e => IS_APPLE && isExtendLineForwardMac(e)
const isUndo = isKeyHotkey('mod+z')
const isRedoMac = isKeyHotkey('mod+shift+z')
const isRedoPC = isKeyHotkey('mod+y')
const isRedo = e => (IS_APPLE ? isRedoMac(e) : isRedoPC(e))
const isTransposeCharacterMac = isKeyHotkey('ctrl+t')
const isTransposeCharacter = e => IS_APPLE && isTransposeCharacterMac(e)
const isContentEditable = e =>
isBold(e) ||
isDeleteCharBackward(e) ||
isDeleteCharForward(e) ||
isDeleteLineBackward(e) ||
isDeleteLineForward(e) ||
isDeleteWordBackward(e) ||
isDeleteWordForward(e) ||
isItalic(e) ||
isRedo(e) ||
isSplitBlock(e) ||
isTransposeCharacter(e) ||
isUndo(e)
const isComposing = e =>
e.key == 'ArrowDown' ||
e.key == 'ArrowLeft' ||
e.key == 'ArrowRight' ||
e.key == 'ArrowUp' ||
e.key == 'Backspace' ||
e.key == 'Enter'
/**
* Export.
*
* @type {Object}
*/
export default {
isBold,
isCollapseCharBackward,
isCollapseCharForward,
isCollapseLineBackward,
isCollapseLineForward,
isComposing,
isContentEditable,
isDeleteCharBackward,
isDeleteCharForward,
isDeleteLineBackward,
isDeleteLineForward,
isDeleteWordBackward,
isDeleteWordForward,
isExtendCharBackward,
isExtendCharForward,
isExtendLineBackward,
isExtendLineForward,
isItalic,
isRedo,
isSplitBlock,
isUndo,
}

View File

View File

@@ -15,8 +15,6 @@
"dependencies": {
"debug": "^2.3.2",
"get-window": "^1.1.1",
"is-hotkey": "^0.1.1",
"is-in-browser": "^1.1.3",
"is-window": "^1.0.2",
"keycode": "^2.1.2",
"lodash": "^4.1.1",
@@ -25,7 +23,9 @@
"react-portal": "^3.1.0",
"selection-is-backward": "^1.0.0",
"slate-base64-serializer": "^0.2.29",
"slate-dev-environment": "^0.1.0",
"slate-dev-logger": "^0.1.39",
"slate-hotkeys": "^0.1.0",
"slate-plain-serializer": "^0.5.10",
"slate-prop-types": "^0.4.27"
},

View File

@@ -2,6 +2,12 @@ import Debug from 'debug'
import React from 'react'
import Types from 'prop-types'
import getWindow from 'get-window'
import {
IS_FIREFOX,
IS_IOS,
IS_ANDROID,
SUPPORTED_EVENTS,
} from 'slate-dev-environment'
import logger from 'slate-dev-logger'
import throttle from 'lodash/throttle'
@@ -10,12 +16,6 @@ import Node from './node'
import findDOMRange from '../utils/find-dom-range'
import findRange from '../utils/find-range'
import scrollToSelection from '../utils/scroll-to-selection'
import {
IS_FIREFOX,
IS_IOS,
IS_ANDROID,
SUPPORTED_EVENTS,
} from '../constants/environment'
/**
* Debug.

View File

@@ -1,133 +0,0 @@
import { isKeyHotkey } from 'is-hotkey'
import { IS_IOS, IS_MAC } from './environment'
/**
* Is Apple?
*
* @type {Boolean}
*/
const IS_APPLE = IS_IOS || IS_MAC
/**
* Hotkeys.
*
* @type {Function}
*/
const BOLD = isKeyHotkey('mod+b')
const ITALIC = isKeyHotkey('mod+i')
const ENTER = isKeyHotkey('enter')
const SHIFT_ENTER = isKeyHotkey('shift+enter')
const SPLIT_BLOCK = e => ENTER(e) || SHIFT_ENTER(e)
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_APPLE && DELETE_CHAR_BACKWARD_MAC(e))
const DELETE_CHAR_FORWARD = e =>
DELETE_FORWARD(e) || (IS_APPLE && DELETE_CHAR_FORWARD_MAC(e))
const DELETE_LINE_BACKWARD_MAC = e =>
isKeyHotkey('cmd+shift+backspace', e) || isKeyHotkey('cmd+backspace', e)
const DELETE_LINE_FORWARD_MAC = isKeyHotkey('ctrl+k')
const DELETE_LINE_BACKWARD = e => IS_APPLE && DELETE_LINE_BACKWARD_MAC(e)
const DELETE_LINE_FORWARD = e => IS_APPLE && DELETE_LINE_FORWARD_MAC(e)
const DELETE_WORD_BACKWARD_MAC = e =>
isKeyHotkey('shift+option+backspace', e) || isKeyHotkey('option+backspace', e)
const DELETE_WORD_BACKWARD_PC = isKeyHotkey('ctrl+backspace')
const DELETE_WORD_FORWARD_MAC = e =>
isKeyHotkey('shift+option+delete', e) || isKeyHotkey('option+delete', e)
const DELETE_WORD_FORWARD_PC = isKeyHotkey('ctrl+delete')
const DELETE_WORD_BACKWARD = e =>
IS_APPLE ? DELETE_WORD_BACKWARD_MAC(e) : DELETE_WORD_BACKWARD_PC(e)
const DELETE_WORD_FORWARD = e =>
IS_APPLE ? DELETE_WORD_FORWARD_MAC(e) : DELETE_WORD_FORWARD_PC(e)
const RIGHT_ARROW = isKeyHotkey('right')
const LEFT_ARROW = isKeyHotkey('left')
const COLLAPSE_CHAR_FORWARD = e => RIGHT_ARROW(e) && !EXTEND_CHAR_FORWARD(e)
const COLLAPSE_CHAR_BACKWARD = e => LEFT_ARROW(e) && !EXTEND_CHAR_BACKWARD(e)
const COLLAPSE_LINE_BACKWARD_MAC = isKeyHotkey('option+up')
const COLLAPSE_LINE_FORWARD_MAC = isKeyHotkey('option+down')
const COLLAPSE_LINE_BACKWARD = e => IS_APPLE && COLLAPSE_LINE_BACKWARD_MAC(e)
const COLLAPSE_LINE_FORWARD = e => IS_APPLE && 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_APPLE && EXTEND_LINE_BACKWARD_MAC(e)
const EXTEND_LINE_FORWARD = e => IS_APPLE && 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_APPLE ? REDO_MAC(e) : REDO_PC(e))
const TRANSPOSE_CHARACTER_MAC = isKeyHotkey('ctrl+t')
const TRANSPOSE_CHARACTER = e => IS_APPLE && TRANSPOSE_CHARACTER_MAC(e)
const CONTENTEDITABLE = e =>
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)
const COMPOSING = e =>
e.key == 'ArrowDown' ||
e.key == 'ArrowLeft' ||
e.key == 'ArrowRight' ||
e.key == 'ArrowUp' ||
e.key == 'Backspace' ||
e.key == 'Enter'
/**
* Export.
*
* @type {Object}
*/
export default {
BOLD,
COLLAPSE_LINE_BACKWARD,
COLLAPSE_LINE_FORWARD,
COLLAPSE_CHAR_FORWARD,
COLLAPSE_CHAR_BACKWARD,
COMPOSING,
CONTENTEDITABLE,
DELETE_CHAR_BACKWARD,
DELETE_CHAR_FORWARD,
DELETE_LINE_BACKWARD,
DELETE_LINE_FORWARD,
DELETE_WORD_BACKWARD,
DELETE_WORD_FORWARD,
EXTEND_LINE_BACKWARD,
EXTEND_LINE_FORWARD,
EXTEND_CHAR_FORWARD,
EXTEND_CHAR_BACKWARD,
ITALIC,
REDO,
SPLIT_BLOCK,
UNDO,
}

View File

@@ -1,13 +1,13 @@
import Base64 from 'slate-base64-serializer'
import Debug from 'debug'
import Plain from 'slate-plain-serializer'
import { IS_IOS } from 'slate-dev-environment'
import React from 'react'
import getWindow from 'get-window'
import { Block, Inline, Text } from 'slate'
import Hotkeys from 'slate-hotkeys'
import { IS_IOS } from '../constants/environment'
import EVENT_HANDLERS from '../constants/event-handlers'
import HOTKEYS from '../constants/hotkeys'
import Content from '../components/content'
import cloneFragment from '../utils/clone-fragment'
import findDOMNode from '../utils/find-dom-node'
@@ -370,63 +370,63 @@ function AfterPlugin() {
// COMPAT: In iOS, some of these hotkeys are handled in the
// `onNativeBeforeInput` handler of the `<Content>` component in order to
// preserve native autocorrect behavior, so they shouldn't be handled here.
if (HOTKEYS.SPLIT_BLOCK(event) && !IS_IOS) {
if (Hotkeys.isSplitBlock(event) && !IS_IOS) {
return value.isInVoid
? change.collapseToStartOfNextText()
: change.splitBlock()
}
if (HOTKEYS.DELETE_CHAR_BACKWARD(event) && !IS_IOS) {
if (Hotkeys.isDeleteCharBackward(event) && !IS_IOS) {
return change.deleteCharBackward()
}
if (HOTKEYS.DELETE_CHAR_FORWARD(event) && !IS_IOS) {
if (Hotkeys.isDeleteCharForward(event) && !IS_IOS) {
return change.deleteCharForward()
}
if (HOTKEYS.DELETE_LINE_BACKWARD(event)) {
if (Hotkeys.isDeleteLineBackward(event)) {
return change.deleteLineBackward()
}
if (HOTKEYS.DELETE_LINE_FORWARD(event)) {
if (Hotkeys.isDeleteLineForward(event)) {
return change.deleteLineForward()
}
if (HOTKEYS.DELETE_WORD_BACKWARD(event)) {
if (Hotkeys.isDeleteWordBackward(event)) {
return change.deleteWordBackward()
}
if (HOTKEYS.DELETE_WORD_FORWARD(event)) {
if (Hotkeys.isDeleteWordForward(event)) {
return change.deleteWordForward()
}
if (HOTKEYS.REDO(event)) {
if (Hotkeys.isRedo(event)) {
return change.redo()
}
if (HOTKEYS.UNDO(event)) {
if (Hotkeys.isUndo(event)) {
return change.undo()
}
// 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)) {
if (Hotkeys.isCollapseLineBackward(event)) {
event.preventDefault()
return change.collapseLineBackward()
}
if (HOTKEYS.COLLAPSE_LINE_FORWARD(event)) {
if (Hotkeys.isCollapseLineForward(event)) {
event.preventDefault()
return change.collapseLineForward()
}
if (HOTKEYS.EXTEND_LINE_BACKWARD(event)) {
if (Hotkeys.isExtendLineBackward(event)) {
event.preventDefault()
return change.extendLineBackward()
}
if (HOTKEYS.EXTEND_LINE_FORWARD(event)) {
if (Hotkeys.isExtendLineForward(event)) {
event.preventDefault()
return change.extendLineForward()
}
@@ -434,7 +434,7 @@ function AfterPlugin() {
// 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)) {
if (Hotkeys.isCollapseCharBackward(event)) {
const { document, isInVoid, previousText, startText } = value
const isPreviousInVoid =
previousText && document.hasVoidParent(previousText.key)
@@ -444,7 +444,7 @@ function AfterPlugin() {
}
}
if (HOTKEYS.COLLAPSE_CHAR_FORWARD(event)) {
if (Hotkeys.isCollapseCharForward(event)) {
const { document, isInVoid, nextText, startText } = value
const isNextInVoid = nextText && document.hasVoidParent(nextText.key)
if (isInVoid || isNextInVoid || startText.text == '') {
@@ -453,7 +453,7 @@ function AfterPlugin() {
}
}
if (HOTKEYS.EXTEND_CHAR_BACKWARD(event)) {
if (Hotkeys.isExtendCharBackward(event)) {
const { document, isInVoid, previousText, startText } = value
const isPreviousInVoid =
previousText && document.hasVoidParent(previousText.key)
@@ -463,7 +463,7 @@ function AfterPlugin() {
}
}
if (HOTKEYS.EXTEND_CHAR_FORWARD(event)) {
if (Hotkeys.isExtendCharForward(event)) {
const { document, isInVoid, nextText, startText } = value
const isNextInVoid = nextText && document.hasVoidParent(nextText.key)
if (isInVoid || isNextInVoid || startText.text == '') {

View File

@@ -1,14 +1,14 @@
import Debug from 'debug'
import getWindow from 'get-window'
import { findDOMNode } from 'react-dom'
import HOTKEYS from '../constants/hotkeys'
import Hotkeys from 'slate-hotkeys'
import {
IS_FIREFOX,
IS_IOS,
IS_ANDROID,
SUPPORTED_EVENTS,
} from '../constants/environment'
} from 'slate-dev-environment'
import findNode from '../utils/find-node'
/**
@@ -369,13 +369,13 @@ function BeforePlugin() {
// typing. However, certain characters also move the selection before
// we're able to handle it, so prevent their default behavior.
if (isComposing) {
if (HOTKEYS.COMPOSING(event)) event.preventDefault()
if (Hotkeys.isComposing(event)) event.preventDefault()
return true
}
// Certain hotkeys have native behavior in contenteditable elements which
// will cause our value to be out of sync, so prevent them.
if (HOTKEYS.CONTENTEDITABLE(event) && !IS_IOS) {
if (Hotkeys.isContentEditable(event) && !IS_IOS) {
event.preventDefault()
}

View File

@@ -1,9 +1,9 @@
import Base64 from 'slate-base64-serializer'
import { IS_CHROME, IS_SAFARI } from 'slate-dev-environment'
import getWindow from 'get-window'
import findDOMNode from './find-dom-node'
import { ZERO_WIDTH_SELECTOR, ZERO_WIDTH_ATTRIBUTE } from './find-point'
import { IS_CHROME, IS_SAFARI } from '../constants/environment'
/**
* Prepares a Slate document fragment to be copied to the clipboard.

View File

@@ -1,10 +1,10 @@
import getWindow from 'get-window'
import isBackward from 'selection-is-backward'
import { Range } from 'slate'
import { IS_IE, IS_EDGE } from 'slate-dev-environment'
import findPoint from './find-point'
import findDOMPoint from './find-dom-point'
import { IS_IE, IS_EDGE } from '../constants/environment'
/**
* Find a Slate range from a DOM `native` selection.

View File

@@ -1,6 +1,6 @@
import getWindow from 'get-window'
import isBackward from 'selection-is-backward'
import { IS_SAFARI, IS_IOS } from '../constants/environment'
import { IS_SAFARI, IS_IOS } from 'slate-dev-environment'
/**
* CSS overflow values that would cause scrolling.

View File

@@ -1,7 +1,9 @@
import factory from './factory'
import slate from '../../packages/slate/package.json'
import slateBase64Serializer from '../../packages/slate-base64-serializer/package.json'
import slateDevEnvironment from '../../packages/slate-dev-environment/package.json'
import slateDevLogger from '../../packages/slate-dev-logger/package.json'
import slateHotkeys from '../../packages/slate-hotkeys/package.json'
import slateHtmlSerializer from '../../packages/slate-html-serializer/package.json'
import slateHyperscript from '../../packages/slate-hyperscript/package.json'
import slatePlainSerializer from '../../packages/slate-plain-serializer/package.json'
@@ -13,7 +15,9 @@ import slateSimulator from '../../packages/slate-simulator/package.json'
const configurations = [
...factory(slate),
...factory(slateBase64Serializer),
...factory(slateDevEnvironment),
...factory(slateDevLogger),
...factory(slateHotkeys),
...factory(slateHtmlSerializer),
...factory(slateHyperscript),
...factory(slatePlainSerializer),