From f69d2c4a121ec628bd839e1519688cb249835eba Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Sun, 15 Oct 2017 19:23:07 -0700 Subject: [PATCH] remove keyboard `data.*` properties (#1235) * update examples and walkthroughs * deprecate data keyboard properties * update examples * add is-hotkey to resources doc * udpate docs * update docs * fix split-block test --- docs/general/resources.md | 3 +- docs/reference/slate-react/plugins.md | 26 ----- docs/walkthroughs/adding-event-handlers.md | 8 +- .../applying-custom-formatting.md | 16 ++- .../defining-custom-block-nodes.md | 20 ++-- docs/walkthroughs/using-plugins.md | 105 ++--------------- examples/check-lists/index.js | 4 +- examples/code-highlighting/index.js | 2 +- examples/huge-document/index.js | 34 ------ examples/markdown-shortcuts/index.js | 8 +- examples/rich-text/index.js | 40 ++++--- examples/rtl/index.js | 2 +- examples/syncing-operations/index.js | 38 ++++--- examples/tables/index.js | 8 +- package.json | 1 + .../slate-react/src/components/content.js | 106 +++++++++++------- packages/slate-react/src/plugins/core.js | 69 +++++++----- .../core/on-key-down/split-empty-block.js | 2 +- yarn.lock | 4 + 19 files changed, 200 insertions(+), 296 deletions(-) diff --git a/docs/general/resources.md b/docs/general/resources.md index e0b186659..4bc295b78 100644 --- a/docs/general/resources.md +++ b/docs/general/resources.md @@ -6,7 +6,8 @@ A few resources that are helpful for building with Slate. ## Libraries -- [`react-broadcast`] works well when you need to have your custom node components re-render based on state that lives outside the `document`. It's the same pattern that `react-router` uses to update `` components. +- [`is-hotkey`](https://github.com/ianstormtaylor/is-hotkey) is a simple way to check whether an `onKeyDown` handler should fire for a given hotkey, handling cross-platform concerns like cmd vs. ctrl keys for you automatically. +- [`react-broadcast`](https://github.com/ReactTraining/react-broadcast) works well when you need to have your custom node components re-render based on state that lives outside the `document`. It's the same pattern that `react-router` uses to update `` components. ## Tooling diff --git a/docs/reference/slate-react/plugins.md b/docs/reference/slate-react/plugins.md index 6b7917296..8872db2c6 100644 --- a/docs/reference/slate-react/plugins.md +++ b/docs/reference/slate-react/plugins.md @@ -115,30 +115,6 @@ If no other plugin handles this event, it will be handled by the [Core plugin](. This handler is called when any key is pressed in the `contenteditable` element, before any action is taken. -The `data` object contains the `key` which is a string name of the key that was pressed, as well as it's `code`. It also contains a series of helpful utility properties for determining hotkey logic. For example, `isCtrl` is true if the `control` key was pressed before. - -```js -{ - key: String, - code: Number, - isAlt: Boolean, - isCmd: Boolean, - isCtrl: Boolean, - isLine: Boolean, - isMeta: Boolean, - isMod: Boolean, - isModAlt: Boolean, - isShift: Boolean, - isWord: Boolean -} -``` - -The `isMod` boolean is `true` if the `control` key was pressed on Windows or the `command` key was pressed on Mac _without_ the `alt/option` key also being pressed. This is a convenience for adding hotkeys like `command+b`. - -The `isModAlt` boolean is `true` if the `control` key was pressed on Windows or the `command` key was pressed on Mac _and_ the `alt/option` key was also being pressed. This is a convenience for secondary hotkeys like `command+option+1`. - -The `isLine` and `isWord` booleans represent whether the "line modifier" or "word modifier" hotkeys are pressed when deleting or moving the cursor. For example, on a Mac `option + right` moves the cursor to the right one word at a time. - Make sure to `event.preventDefault()` if you do not want the default insertion behavior to occur! If no other plugin handles this event, it will be handled by the [Core plugin](./core.md). ### `onKeyUp` @@ -146,8 +122,6 @@ Make sure to `event.preventDefault()` if you do not want the default insertion b This handler is called when any key is released in the `contenteditable` element. -The `data` object contains the same information as the `data` object of `onKeyDown`. - ### `onPaste` `Function onPaste(event: Event, data: Object, change: Change, editor: Editor) => Change || Void` diff --git a/docs/walkthroughs/adding-event-handlers.md b/docs/walkthroughs/adding-event-handlers.md index 7c9662bcf..f1e89cea1 100644 --- a/docs/walkthroughs/adding-event-handlers.md +++ b/docs/walkthroughs/adding-event-handlers.md @@ -49,9 +49,9 @@ class App extends React.Component { this.setState({ state }) } - // Define a new handler which prints the key code that was pressed. + // Define a new handler which prints the key that was pressed. onKeyDown = (event, data, change) => { - console.log(event.which) + console.log(event.key) } render() { @@ -85,8 +85,8 @@ class App extends React.Component { } onKeyDown = (event, data, change) => { - // Return with no changes if it's not the "7" key with shift pressed. - if (event.which != 55 || !event.shiftKey) return + // Return with no changes if it's not the "&" key. + if (event.key != '&') return // Prevent the ampersand character from being inserted. event.preventDefault() diff --git a/docs/walkthroughs/applying-custom-formatting.md b/docs/walkthroughs/applying-custom-formatting.md index f01ee9331..c70dc5255 100644 --- a/docs/walkthroughs/applying-custom-formatting.md +++ b/docs/walkthroughs/applying-custom-formatting.md @@ -28,7 +28,7 @@ class App extends React.Component { } onKeyDown = (event, data, change) => { - if (event.which != 67 || !event.metaKey || !event.altKey) return + if (event.key != '`' || !event.metaKey) return event.preventDefault() const isCode = change.state.blocks.some(block => block.type == 'code') @@ -72,16 +72,15 @@ class App extends React.Component { if (!event.metaKey) return // Decide what to do based on the key code... - switch (event.which) { + switch (event.key) { // When "B" is pressed, add a "bold" mark to the text. - case 66: { + case 'b': { event.preventDefault() change.addMark('bold') return true } // When "`" is pressed, keep our existing code block logic. - case 67: { - if (!event.altKey) return + case '`': { const isCode = change.state.blocks.some(block => block.type == 'code') event.preventDefault() change.setBlock(isCode ? 'paragraph' : 'code') @@ -148,14 +147,13 @@ class App extends React.Component { onKeyDown = (event, data, change) => { if (!event.metaKey) return - switch (event.which) { - case 66: { + switch (event.key) { + case 'b': { event.preventDefault() change.toggleMark('bold') return true } - case 67: { - if (!event.altKey) return + case '`': { const isCode = change.state.blocks.some(block => block.type == 'code') event.preventDefault() state.setBlock(isCode ? 'paragraph' : 'code') diff --git a/docs/walkthroughs/defining-custom-block-nodes.md b/docs/walkthroughs/defining-custom-block-nodes.md index ed3fbbe1c..a1cd9aacd 100644 --- a/docs/walkthroughs/defining-custom-block-nodes.md +++ b/docs/walkthroughs/defining-custom-block-nodes.md @@ -23,10 +23,8 @@ class App extends React.Component { } onKeyDown = (event, data, change) => { - if (event.which != 55 || !event.shiftKey) return - + if (event.key != '&') return event.preventDefault() - change.insertText('and'); return true } @@ -87,10 +85,8 @@ class App extends React.Component { } onKeyDown = (event, data, change) => { - if (event.which != 55 || !event.shiftKey) return - + if (event.key != '&') return event.preventDefault() - change.insertText('and') return true } @@ -110,7 +106,7 @@ class App extends React.Component { } ``` -Okay, but now we'll need a way for the user to actually turn a block into a code block. So let's change our `onKeyDown` function to add a `⌘-Alt-C` shortcut that does just that: +Okay, but now we'll need a way for the user to actually turn a block into a code block. So let's change our `onKeyDown` function to add a `⌘-\`` shortcut that does just that: ```js function CodeNode(props) { @@ -134,7 +130,7 @@ class App extends React.Component { onKeyDown = (event, data, change) => { // Return with no changes if it's not the "`" key with cmd/ctrl pressed. - if (event.which != 67 || !event.metaKey || !event.altKey) return + if (event.key != '`' || !event.metaKey) return // Prevent the "`" from being inserted by default. event.preventDefault() @@ -158,9 +154,9 @@ class App extends React.Component { } ``` -Now, if you press `⌘-Alt-C`, the block your cursor is in should turn into a code block! Magic! +Now, if you press `⌘-\`` the block your cursor is in should turn into a code block! Magic! -But we forgot one thing. When you hit `⌘-Alt-C` again, it should change the code block back into a paragraph. To do that, we'll need to add a bit of logic to change the type we set based on whether any of the currently selected blocks are already a code block: +But we forgot one thing. When you hit `⌘-\`` again, it should change the code block back into a paragraph. To do that, we'll need to add a bit of logic to change the type we set based on whether any of the currently selected blocks are already a code block: ```js function CodeNode(props) { @@ -183,7 +179,7 @@ class App extends React.Component { } onKeyDown = (event, data, change) => { - if (event.which != 67 || !event.metaKey || !event.altKey) return + if (event.key != '`' || !event.metaKey) return event.preventDefault() @@ -209,7 +205,7 @@ class App extends React.Component { } ``` -And there you have it! If you press `⌘-Alt-C` while inside a code block, it should turn back into a paragraph! +And there you have it! If you press `⌘-\`` while inside a code block, it should turn back into a paragraph!

Next:
Applying Custom Formatting

diff --git a/docs/walkthroughs/using-plugins.md b/docs/walkthroughs/using-plugins.md index 5b582a5fa..990d8bdac 100644 --- a/docs/walkthroughs/using-plugins.md +++ b/docs/walkthroughs/using-plugins.md @@ -30,7 +30,7 @@ class App extends React.Component { } onKeyDown = (event, data, change) => { - if (!event.metaKey || event.which != 66) return + if (event.key != 'b' || !event.metaKey) return event.preventDefault() change.toggleMark('bold') return true @@ -50,12 +50,12 @@ class App extends React.Component { } ``` -Let's write a new function, that takes a set of options: the mark `type` to toggle and the key `code` to press. +Let's write a new function, that takes a set of options: the mark `type` to toggle and the `key` to press. ```js function MarkHotkey(options) { // Grab our options from the ones passed in. - const { type, code, isAltKey = false } = options + const { type, key } = options } ``` @@ -67,13 +67,13 @@ In this case our plugin object will have one property: a `onKeyDown` handler, wi ```js function MarkHotkey(options) { - const { type, code, isAltKey = false } = options + const { type, key } = options // Return our "plugin" object, containing the `onKeyDown` handler. return { onKeyDown(event, data, change) { - // Check that the key pressed matches our `code` option. - if (!event.metaKey || event.which != code || event.altKey != isAltKey) return + // Check that the key pressed matches our `key` option. + if (!event.metaKey || event.key != key) return // Prevent the default characters from being inserted. event.preventDefault() @@ -94,7 +94,7 @@ Now that we have our plugin, let's remove the hard-coded logic from our app, and // Initialize our bold-mark-adding plugin. const boldPlugin = MarkHotkey({ type: 'bold', - code: 66 + key: 'b' }) // Create an array of plugins. @@ -139,11 +139,11 @@ Let's add _italic_, `code`, ~~strikethrough~~ and underline marks: ```js // Initialize a plugin for each mark... const plugins = [ - MarkHotkey({ code: 66, type: 'bold' }), - MarkHotkey({ code: 67, type: 'code', isAltKey: true }), - MarkHotkey({ code: 73, type: 'italic' }), - MarkHotkey({ code: 68, type: 'strikethrough' }), - MarkHotkey({ code: 85, type: 'underline' }) + MarkHotkey({ key: 'b', type: 'bold' }), + MarkHotkey({ key: '`', type: 'code' }), + MarkHotkey({ key: 'i', type: 'italic' }), + MarkHotkey({ key: '~', type: 'strikethrough' }), + MarkHotkey({ key: 'u', type: 'underline' }) ] class App extends React.Component { @@ -182,87 +182,6 @@ class App extends React.Component { And there you have it! We just added a ton of functionality to the editor with very little work. And we can keep all of our mark hotkey logic tested and isolated in a single place, making maintaining the code easier. -Of course... now that it's reusable, we could actually make our `MarkHotkey` plugin a little easier to use. What if instead of a `code` argument it took the text of the `key` itself? That would make the calling code a lot clearer, since key codes are really obtuse. - -In fact, unless you have weirdly good keycode knowledge, you probably have no idea what our current hotkeys actually are. - -Let's fix that. - -Using the `keycode` module in npm makes this dead simple. - -First install it: - -``` -npm install keycode -``` - -And then we can add it our plugin: - -```js -// Import the keycode module. -import keycode from `keycode` - -function MarkHotkey(options) { - // Change the options to take a `key`. - const { type, key, isAltKey = false } = options - - return { - onKeyDown(event, data, change) { - // Change the comparison to use the key name. - if (!event.metaKey || keycode(event.which) != key || event.altKey != isAltKey) return - event.preventDefault() - change.toggleMark(type) - return true - } - } -} -``` - -And now we can make our app code much clearer for the next person who reads it: - -```js -// Use the much clearer key names instead of key codes! -const plugins = [ - MarkHotkey({ key: 'b', type: 'bold' }), - MarkHotkey({ key: 'c', type: 'code', isAltKey: true }), - MarkHotkey({ key: 'i', type: 'italic' }), - MarkHotkey({ key: 'd', type: 'strikethrough' }), - MarkHotkey({ key: 'u', type: 'underline' }) -] - -class App extends React.Component { - - state = { - state: initialState, - schema: { - marks: { - bold: props => {props.children}, - code: props => {props.children}, - italic: props => {props.children}, - strikethrough: props => {props.children}, - underline: props => {props.children}, - } - } - } - - onChange = ({ state }) => { - this.setState({ state }) - } - - render() { - return ( - - ) - } - -} -``` - That's why plugins are awesome. They let you get really expressive while also making your codebase easier to manage. And since Slate is built with plugins as a primary consideration, using them is dead simple! diff --git a/examples/check-lists/index.js b/examples/check-lists/index.js index a6426d8e3..c1e1008ad 100644 --- a/examples/check-lists/index.js +++ b/examples/check-lists/index.js @@ -116,7 +116,7 @@ class CheckLists extends React.Component { const { state } = change if ( - data.key == 'enter' && + e.key == 'Enter' && state.startBlock.type == 'check-list-item' ) { return change @@ -125,7 +125,7 @@ class CheckLists extends React.Component { } if ( - data.key == 'backspace' && + e.key == 'Backspace' && state.isCollapsed && state.startBlock.type == 'check-list-item' && state.selection.startOffset == 0 diff --git a/examples/code-highlighting/index.js b/examples/code-highlighting/index.js index f8ef827a8..aff2ce2fb 100644 --- a/examples/code-highlighting/index.js +++ b/examples/code-highlighting/index.js @@ -174,7 +174,7 @@ class CodeHighlighting extends React.Component { onKeyDown = (e, data, change) => { const { state } = change const { startBlock } = state - if (data.key != 'enter') return + if (e.key != 'Enter') return if (startBlock.type != 'code') return if (state.isExpanded) change.delete() return change.insertText('\n') diff --git a/examples/huge-document/index.js b/examples/huge-document/index.js index 635802435..ec5b29437 100644 --- a/examples/huge-document/index.js +++ b/examples/huge-document/index.js @@ -96,40 +96,6 @@ class HugeDocument extends React.Component { this.setState({ state }) } - /** - * On key down, if it's a formatting command toggle a mark. - * - * @param {Event} e - * @param {Object} data - * @param {Change} change - */ - - onKeyDown = (e, data, change) => { - if (!data.isMod) return - let mark - - switch (data.key) { - case 'b': - mark = 'bold' - break - case 'i': - mark = 'italic' - break - case 'u': - mark = 'underlined' - break - case '`': - mark = 'code' - break - default: - return - } - - e.preventDefault() - change.toggleMark(mark) - return true - } - /** * Render the editor. * diff --git a/examples/markdown-shortcuts/index.js b/examples/markdown-shortcuts/index.js index 30a7d2217..fd51a0272 100644 --- a/examples/markdown-shortcuts/index.js +++ b/examples/markdown-shortcuts/index.js @@ -105,10 +105,10 @@ class MarkdownShortcuts extends React.Component { */ onKeyDown = (e, data, change) => { - switch (data.key) { - case 'space': return this.onSpace(e, change) - case 'backspace': return this.onBackspace(e, change) - case 'enter': return this.onEnter(e, change) + switch (e.key) { + case ' ': return this.onSpace(e, change) + case 'Backspace': return this.onBackspace(e, change) + case 'Enter': return this.onEnter(e, change) } } diff --git a/examples/rich-text/index.js b/examples/rich-text/index.js index f1a91710c..c28cd9d6b 100644 --- a/examples/rich-text/index.js +++ b/examples/rich-text/index.js @@ -3,14 +3,28 @@ import { Editor } from 'slate-react' import { State } from 'slate' import React from 'react' +import isHotkey from 'is-hotkey' import initialState from './state.json' /** * Define the default node type. + * + * @type {String} */ const DEFAULT_NODE = 'paragraph' +/** + * Hotkey matchers. + * + * @type {Function} + */ + +const isBoldHotkey = isHotkey('mod+b') +const isItalicHotkey = isHotkey('mod+i') +const isUnderlinedHotkey = isHotkey('mod+u') +const isCodeHotkey = isHotkey('mod+`') + /** * Define a schema. * @@ -107,24 +121,18 @@ class RichTextExample extends React.Component { */ onKeyDown = (e, data, change) => { - if (!data.isMod) return let mark - switch (data.key) { - case 'b': - mark = 'bold' - break - case 'i': - mark = 'italic' - break - case 'u': - mark = 'underlined' - break - case '`': - mark = 'code' - break - default: - return + if (isBoldHotkey(e)) { + mark = 'bold' + } else if (isItalicHotkey(e)) { + mark = 'italic' + } else if (isUnderlinedHotkey(e)) { + mark = 'underlined' + } else if (isCodeHotkey(e)) { + mark = 'code' + } else { + return } e.preventDefault() diff --git a/examples/rtl/index.js b/examples/rtl/index.js index 6d5a38a67..a0e43d4a7 100644 --- a/examples/rtl/index.js +++ b/examples/rtl/index.js @@ -54,7 +54,7 @@ class PlainText extends React.Component { */ onKeyDown = (e, data, change) => { - if (data.key == 'enter' && data.isShift) { + if (e.key == 'Enter' && e.shiftKey) { e.preventDefault() change.insertText('\n') return true diff --git a/examples/syncing-operations/index.js b/examples/syncing-operations/index.js index 04736fc4a..93d4d5f06 100644 --- a/examples/syncing-operations/index.js +++ b/examples/syncing-operations/index.js @@ -3,8 +3,20 @@ import { Editor } from 'slate-react' import { State } from 'slate' import React from 'react' +import isHotkey from 'is-hotkey' import initialState from './state.json' +/** + * Hotkey matchers. + * + * @type {Function} + */ + +const isBoldHotkey = isHotkey('mod+b') +const isItalicHotkey = isHotkey('mod+i') +const isUnderlinedHotkey = isHotkey('mod+u') +const isCodeHotkey = isHotkey('mod+`') + /** * Define a schema. * @@ -100,24 +112,18 @@ class SyncingEditor extends React.Component { */ onKeyDown = (e, data, change) => { - if (!data.isMod) return let mark - switch (data.key) { - case 'b': - mark = 'bold' - break - case 'i': - mark = 'italic' - break - case 'u': - mark = 'underlined' - break - case '`': - mark = 'code' - break - default: - return + if (isBoldHotkey(e)) { + mark = 'bold' + } else if (isItalicHotkey(e)) { + mark = 'italic' + } else if (isUnderlinedHotkey(e)) { + mark = 'underlined' + } else if (isCodeHotkey(e)) { + mark = 'code' + } else { + return } e.preventDefault() diff --git a/examples/tables/index.js b/examples/tables/index.js index 408730b45..9d934c8a4 100644 --- a/examples/tables/index.js +++ b/examples/tables/index.js @@ -118,10 +118,10 @@ class Tables extends React.Component { return } - switch (data.key) { - case 'backspace': return this.onBackspace(e, state) - case 'delete': return this.onDelete(e, state) - case 'enter': return this.onEnter(e, state) + switch (e.key) { + case 'Backspace': return this.onBackspace(e, state) + case 'Delete': return this.onDelete(e, state) + case 'Enter': return this.onEnter(e, state) } } diff --git a/package.json b/package.json index 9a1d0998e..405921851 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "gh-pages": "^0.11.0", "http-server": "^0.9.0", "immutable": "^3.8.1", + "is-hotkey": "^0.0.1", "is-image": "^1.0.1", "is-url": "^1.2.2", "jest": "^17.0.3", diff --git a/packages/slate-react/src/components/content.js b/packages/slate-react/src/components/content.js index 9d2f972d7..35d0a12db 100644 --- a/packages/slate-react/src/components/content.js +++ b/packages/slate-react/src/components/content.js @@ -572,14 +572,17 @@ class Content extends React.Component { if (this.props.readOnly) return if (!this.isInEditor(event.target)) return - const { altKey, ctrlKey, metaKey, shiftKey, which } = event - const key = keycode(which) + const { key, metaKey, ctrlKey } = event const data = {} + const modKey = IS_MAC ? metaKey : ctrlKey + + // COMPAT: add the deprecated keyboard event properties. + addDeprecatedKeyProperties(data, event) // Keep track of an `isShifting` flag, because it's often used to trigger // "Paste and Match Style" commands, but isn't available on the event in a // normal paste event. - if (key == 'shift') { + if (key == 'Shift') { this.tmp.isShifting = true } @@ -588,35 +591,23 @@ class Content extends React.Component { // selection-moving behavior. if ( this.tmp.isComposing && - (key == 'left' || key == 'right' || key == 'up' || key == 'down') + (key == 'ArrowLeft' || key == 'ArrowRight' || key == 'ArrowUp' || key == 'ArrowDown') ) { event.preventDefault() return } - // Add helpful properties for handling hotkeys to the data object. - data.code = which - data.key = key - data.isAlt = altKey - data.isCmd = IS_MAC ? metaKey && !altKey : false - data.isCtrl = ctrlKey && !altKey - data.isLine = IS_MAC ? metaKey : false - data.isMeta = metaKey - data.isMod = IS_MAC ? metaKey && !altKey : ctrlKey && !altKey - data.isModAlt = IS_MAC ? metaKey && altKey : ctrlKey && altKey - data.isShift = shiftKey - data.isWord = IS_MAC ? altKey : ctrlKey - // These key commands have native behavior in contenteditable elements which // will cause our state to be out of sync, so prevent them. if ( - (key == 'enter') || - (key == 'backspace') || - (key == 'delete') || - (key == 'b' && data.isMod) || - (key == 'i' && data.isMod) || - (key == 'y' && data.isMod) || - (key == 'z' && data.isMod) + (key == 'Enter') || + (key == 'Backspace') || + (key == 'Delete') || + (key == 'b' && modKey) || + (key == 'i' && modKey) || + (key == 'y' && modKey) || + (key == 'z' && modKey) || + (key == 'Z' && modKey) ) { event.preventDefault() } @@ -632,27 +623,15 @@ class Content extends React.Component { */ onKeyUp = (event) => { - const { altKey, ctrlKey, metaKey, shiftKey, which } = event - const key = keycode(which) const data = {} - if (key == 'shift') { + // COMPAT: add the deprecated keyboard event properties. + addDeprecatedKeyProperties(data, event) + + if (event.key == 'Shift') { this.tmp.isShifting = false } - // Add helpful properties for handling hotkeys to the data object. - data.code = which - data.key = key - data.isAlt = altKey - data.isCmd = IS_MAC ? metaKey && !altKey : false - data.isCtrl = ctrlKey && !altKey - data.isLine = IS_MAC ? metaKey : false - data.isMeta = metaKey - data.isMod = IS_MAC ? metaKey && !altKey : ctrlKey && !altKey - data.isModAlt = IS_MAC ? metaKey && altKey : ctrlKey && altKey - data.isShift = shiftKey - data.isWord = IS_MAC ? altKey : ctrlKey - debug('onKeyUp', { event, data }) this.props.onKeyUp(event, data) } @@ -669,9 +648,16 @@ class Content extends React.Component { const data = getTransferData(event.clipboardData) - // Attach the `isShift` flag, so that people can use it to trigger "Paste - // and Match Style" logic. - data.isShift = !!this.tmp.isShifting + // COMPAT: Attach the `isShift` flag, so that people can use it to trigger + // "Paste and Match Style" logic. + Object.defineProperty(data, 'isShift', { + enumerable: true, + get() { + logger.deprecate('0.28.0', 'The `data.isShift` property of paste events has been deprecated. If you need this functionality, you\'ll need to keep track of that state with `onKeyDown` and `onKeyUp` events instead') + return !!this.tmp.isShifting + } + }) + debug('onPaste', { event, data }) // COMPAT: In IE 11, only plain text can be retrieved from the event's @@ -897,6 +883,40 @@ class Content extends React.Component { } +/** + * Add deprecated `data` fields from a key `event`. + * + * @param {Object} data + * @param {Object} event + */ + +function addDeprecatedKeyProperties(data, event) { + const { altKey, ctrlKey, metaKey, shiftKey, which } = event + const name = keycode(which) + + function define(key, value) { + Object.defineProperty(data, key, { + enumerable: true, + get() { + logger.deprecate('0.28.0', `The \`data.${key}\` property of keyboard events is deprecated, please use the native \`event\` properties instead.`) + return value + } + }) + } + + define('code', which) + define('key', name) + define('isAlt', altKey) + define('isCmd', IS_MAC ? metaKey && !altKey : false) + define('isCtrl', ctrlKey && !altKey) + define('isLine', IS_MAC ? metaKey : false) + define('isMeta', metaKey) + define('isMod', IS_MAC ? metaKey && !altKey : ctrlKey && !altKey) + define('isModAlt', IS_MAC ? metaKey && altKey : ctrlKey && altKey) + define('isShift', shiftKey) + define('isWord', IS_MAC ? altKey : ctrlKey) +} + /** * Export. * diff --git a/packages/slate-react/src/plugins/core.js b/packages/slate-react/src/plugins/core.js index ca3c4fa9f..b04b09564 100644 --- a/packages/slate-react/src/plugins/core.js +++ b/packages/slate-react/src/plugins/core.js @@ -407,19 +407,20 @@ function Plugin(options = {}) { function onKeyDown(e, data, change) { debug('onKeyDown', { data }) - switch (data.key) { - case 'enter': return onKeyDownEnter(e, data, change) - case 'backspace': return onKeyDownBackspace(e, data, change) - case 'delete': return onKeyDownDelete(e, data, change) - case 'left': return onKeyDownLeft(e, data, change) - case 'right': return onKeyDownRight(e, data, change) - case 'up': return onKeyDownUp(e, data, change) - case 'down': return onKeyDownDown(e, data, change) + switch (e.key) { + case 'Enter': return onKeyDownEnter(e, data, change) + case 'Backspace': return onKeyDownBackspace(e, data, change) + case 'Delete': return onKeyDownDelete(e, data, change) + case 'ArrowLeft': return onKeyDownLeft(e, data, change) + case 'ArrowRight': return onKeyDownRight(e, data, change) + case 'ArrowUp': return onKeyDownUp(e, data, change) + case 'ArrowDown': return onKeyDownDown(e, data, change) case 'd': return onKeyDownD(e, data, change) case 'h': return onKeyDownH(e, data, change) case 'k': return onKeyDownK(e, data, change) case 'y': return onKeyDownY(e, data, change) - case 'z': return onKeyDownZ(e, data, change) + case 'z': + case 'Z': return onKeyDownZ(e, data, change) } } @@ -457,9 +458,13 @@ function Plugin(options = {}) { */ function onKeyDownBackspace(e, data, change) { + const isWord = IS_MAC ? e.altKey : e.ctrlKey + const isLine = IS_MAC ? e.metaKey : false + let boundary = 'Char' - if (data.isWord) boundary = 'Word' - if (data.isLine) boundary = 'Line' + if (isWord) boundary = 'Word' + if (isLine) boundary = 'Line' + change[`delete${boundary}Backward`]() } @@ -472,9 +477,13 @@ function Plugin(options = {}) { */ function onKeyDownDelete(e, data, change) { + const isWord = IS_MAC ? e.altKey : e.ctrlKey + const isLine = IS_MAC ? e.metaKey : false + let boundary = 'Char' - if (data.isWord) boundary = 'Word' - if (data.isLine) boundary = 'Line' + if (isWord) boundary = 'Word' + if (isLine) boundary = 'Line' + change[`delete${boundary}Forward`]() } @@ -496,8 +505,8 @@ function Plugin(options = {}) { function onKeyDownLeft(e, data, change) { const { state } = change - if (data.isCtrl) return - if (data.isAlt) return + if (e.ctrlKey) return + if (e.altKey) return if (state.isExpanded) return const { document, startKey, startText } = state @@ -519,7 +528,7 @@ function Plugin(options = {}) { const previousInline = document.getClosestInline(previous.key) if (previousBlock === startBlock && previousInline && !previousInline.isVoid) { - const extendOrMove = data.isShift ? 'extend' : 'move' + const extendOrMove = e.shiftKey ? 'extend' : 'move' change.collapseToEndOf(previous)[extendOrMove](-1) return } @@ -552,8 +561,8 @@ function Plugin(options = {}) { function onKeyDownRight(e, data, change) { const { state } = change - if (data.isCtrl) return - if (data.isAlt) return + if (e.ctrlKey) return + if (e.altKey) return if (state.isExpanded) return const { document, startKey, startText } = state @@ -581,7 +590,7 @@ function Plugin(options = {}) { const nextInline = document.getClosestInline(next.key) if (nextBlock == startBlock && nextInline) { - const extendOrMove = data.isShift ? 'extend' : 'move' + const extendOrMove = e.shiftKey ? 'extend' : 'move' change.collapseToStartOf(next)[extendOrMove](1) return } @@ -604,11 +613,11 @@ function Plugin(options = {}) { */ function onKeyDownUp(e, data, change) { - if (!IS_MAC || data.isCtrl || !data.isAlt) return + if (!IS_MAC || e.ctrlKey || !e.altKey) return const { state } = change const { selection, document, focusKey, focusBlock } = state - const transform = data.isShift ? 'extendToStartOf' : 'collapseToStartOf' + const transform = e.shiftKey ? 'extendToStartOf' : 'collapseToStartOf' const block = selection.hasFocusAtStartOf(focusBlock) ? document.getPreviousBlock(focusKey) : focusBlock @@ -633,11 +642,11 @@ function Plugin(options = {}) { */ function onKeyDownDown(e, data, change) { - if (!IS_MAC || data.isCtrl || !data.isAlt) return + if (!IS_MAC || e.ctrlKey || !e.altKey) return const { state } = change const { selection, document, focusKey, focusBlock } = state - const transform = data.isShift ? 'extendToEndOf' : 'collapseToEndOf' + const transform = e.shiftKey ? 'extendToEndOf' : 'collapseToEndOf' const block = selection.hasFocusAtEndOf(focusBlock) ? document.getNextBlock(focusKey) : focusBlock @@ -658,7 +667,7 @@ function Plugin(options = {}) { */ function onKeyDownD(e, data, change) { - if (!IS_MAC || !data.isCtrl) return + if (!IS_MAC || !e.ctrlKey || e.altKey) return e.preventDefault() change.deleteCharForward() } @@ -672,7 +681,7 @@ function Plugin(options = {}) { */ function onKeyDownH(e, data, change) { - if (!IS_MAC || !data.isCtrl) return + if (!IS_MAC || !e.ctrlKey || e.altKey) return e.preventDefault() change.deleteCharBackward() } @@ -686,7 +695,7 @@ function Plugin(options = {}) { */ function onKeyDownK(e, data, change) { - if (!IS_MAC || !data.isCtrl) return + if (!IS_MAC || !e.ctrlKey || e.altKey) return e.preventDefault() change.deleteLineForward() } @@ -700,7 +709,8 @@ function Plugin(options = {}) { */ function onKeyDownY(e, data, change) { - if (!data.isMod) return + const modKey = IS_MAC ? e.metaKey : e.ctrlKey + if (!modKey) return change.redo() } @@ -713,8 +723,9 @@ function Plugin(options = {}) { */ function onKeyDownZ(e, data, change) { - if (!data.isMod) return - change[data.isShift ? 'redo' : 'undo']() + const modKey = IS_MAC ? e.metaKey : e.ctrlKey + if (!modKey) return + change[e.shiftKey ? 'redo' : 'undo']() } /** diff --git a/packages/slate-react/test/plugins/core/on-key-down/split-empty-block.js b/packages/slate-react/test/plugins/core/on-key-down/split-empty-block.js index 743241198..dd3aee93f 100644 --- a/packages/slate-react/test/plugins/core/on-key-down/split-empty-block.js +++ b/packages/slate-react/test/plugins/core/on-key-down/split-empty-block.js @@ -3,7 +3,7 @@ import h from '../../../helpers/h' export default function (simulator) { - simulator.keyDown(null, { key: 'enter' }) + simulator.keyDown({ key: 'Enter' }) } export const input = ( diff --git a/yarn.lock b/yarn.lock index a2021c84e..a7fc9e5e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3360,6 +3360,10 @@ 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-image@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-image/-/is-image-1.0.1.tgz#6fd51a752a1a111506d060d952118b0b989b426e"