From d20b8511bb15801a4cd43775652581abf89ed5dc Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Wed, 27 Jul 2016 14:30:09 -0700 Subject: [PATCH] refactor onKeyDown to use data object --- History.md | 7 + examples/auto-markdown/index.js | 7 +- examples/code-highlighting/index.js | 7 +- .../development/performance-rich/index.js | 11 +- examples/images/index.js | 24 ++-- examples/links/index.js | 10 +- examples/paste-html/index.js | 9 +- examples/rich-text/index.js | 11 +- examples/tables/index.js | 5 +- lib/components/content.js | 72 ++++++---- lib/index.js | 14 +- lib/plugins/core.js | 57 ++++---- lib/utils/key.js | 123 ------------------ package.json | 2 +- 14 files changed, 131 insertions(+), 228 deletions(-) delete mode 100644 lib/utils/key.js diff --git a/History.md b/History.md index 89631323b..fd9b7ab2a 100644 --- a/History.md +++ b/History.md @@ -5,6 +5,13 @@ This document maintains a list of changes to Slate with each new version. Until --- +### `0.8.0` — _July 27, 2016_ + +- **The `onKeyDown` and `onBeforeInput` handlers signatures have changed!** Previously, some Slate handlers had a signature of `(e, state, editor)` and others had a signature of `(e, data, state, editor)`. Now all handlers will be passed a data object, even if it is empty. This is helpful for future compatibility where we might need to add data to a handler that previously didn't have any, and is nicer for consistency. The `onKeyDown` handler's new `data` object contains the `key` name and a series of `is*` properties to make handling hotkeys easier. The `onBeforeInput` handler's new `data` object is empty. + +- **The `Utils` export has been removed.** Previously, a `Key` utility and the `findDOMNode` utility were exposed under the `Utils` object. The `Key` has been removed in favor of the `data` object passed to `onKeyDown`. And then `findDOMNode` utility has been upgraded to a top-level named export, so you'll now need to access it via `import { findDOMNode } from 'slate'`. + + ### `0.7.0` — _July 24, 2016_ #### BREAKING CHANGES diff --git a/examples/auto-markdown/index.js b/examples/auto-markdown/index.js index d50dae896..ac141c868 100644 --- a/examples/auto-markdown/index.js +++ b/examples/auto-markdown/index.js @@ -1,7 +1,6 @@ import { Editor, Raw } from '../..' import React from 'react' -import keycode from 'keycode' import initialState from './state.json' /** @@ -108,13 +107,13 @@ class AutoMarkdown extends React.Component { * On key down, check for our specific key shortcuts. * * @param {Event} e + * @param {Data} data * @param {State} state * @return {State or Null} state */ - onKeyDown = (e, state) => { - const key = keycode(e.which) - switch (key) { + onKeyDown = (e, data, state) => { + switch (data.key) { case 'space': return this.onSpace(e, state) case 'backspace': return this.onBackspace(e, state) case 'enter': return this.onEnter(e, state) diff --git a/examples/code-highlighting/index.js b/examples/code-highlighting/index.js index 00eed5659..90c014bb8 100644 --- a/examples/code-highlighting/index.js +++ b/examples/code-highlighting/index.js @@ -2,7 +2,6 @@ import { Editor, Mark, Raw, Selection } from '../..' import Prism from 'prismjs' import React from 'react' -import keycode from 'keycode' import initialState from './state.json' /** @@ -65,13 +64,13 @@ class CodeHighlighting extends React.Component { * On key down inside code blocks, insert soft new lines. * * @param {Event} e + * @param {Object} data * @param {State} state * @return {State} */ - onKeyDown = (e, state) => { - const key = keycode(e.which) - if (key != 'enter') return + onKeyDown = (e, data, state) => { + if (data.key != 'enter') return const { startBlock } = state if (startBlock.type != 'code') return diff --git a/examples/development/performance-rich/index.js b/examples/development/performance-rich/index.js index bd3e95ab5..2445197be 100644 --- a/examples/development/performance-rich/index.js +++ b/examples/development/performance-rich/index.js @@ -1,8 +1,7 @@ -import { Editor, Mark, Raw, Utils } from '../../..' +import { Editor, Mark, Raw } from '../../..' import React from 'react' import initialState from './state.json' -import keycode from 'keycode' /** * Define the default node type. @@ -105,16 +104,16 @@ class RichText extends React.Component { * On key down, if it's a formatting command toggle a mark. * * @param {Event} e + * @param {Object} data * @param {State} state * @return {State} */ - onKeyDown = (e, state) => { - if (!Utils.Key.isCommand(e)) return - const key = keycode(e.which) + onKeyDown = (e, data, state) => { + if (!data.isMod) return let mark - switch (key) { + switch (data.key) { case 'b': mark = 'bold' break diff --git a/examples/images/index.js b/examples/images/index.js index e11372364..21f0eee6e 100644 --- a/examples/images/index.js +++ b/examples/images/index.js @@ -162,18 +162,18 @@ class Images extends React.Component { * On drop, insert the image wherever it is dropped. * * @param {Event} e - * @param {Object} drop + * @param {Object} data * @param {State} state * @return {State} */ - onDrop = (e, drop, state) => { - if (drop.type != 'node') return + onDrop = (e, data, state) => { + if (data.type != 'node') return return state .transform() - .removeNodeByKey(drop.node.key) - .moveTo(drop.target) - .insertBlock(drop.node) + .removeNodeByKey(data.node.key) + .moveTo(data.target) + .insertBlock(data.node) .apply() } @@ -181,16 +181,16 @@ class Images extends React.Component { * On paste, if the pasted content is an image URL, insert it. * * @param {Event} e - * @param {Object} paste + * @param {Object} data * @param {State} state * @return {State} */ - onPaste = (e, paste, state) => { - if (paste.type != 'text') return - if (!isUrl(paste.text)) return - if (!isImage(paste.text)) return - return this.insertImage(state, paste.text) + onPaste = (e, data, state) => { + if (data.type != 'text') return + if (!isUrl(data.text)) return + if (!isImage(data.text)) return + return this.insertImage(state, data.text) } /** diff --git a/examples/links/index.js b/examples/links/index.js index 6e251f4c9..55ed701a1 100644 --- a/examples/links/index.js +++ b/examples/links/index.js @@ -106,14 +106,14 @@ class Links extends React.Component { * On paste, if the text is a link, wrap the selection in a link. * * @param {Event} e - * @param {Object} paste + * @param {Object} data * @param {State} state */ - onPaste = (e, paste, state) => { + onPaste = (e, data, state) => { if (state.isCollapsed) return - if (paste.type != 'text' && paste.type != 'html') return - if (!isUrl(paste.text)) return + if (data.type != 'text' && data.type != 'html') return + if (!isUrl(data.text)) return let transform = state.transform() @@ -122,7 +122,7 @@ class Links extends React.Component { } return transform - .wrapInline('link', { href: paste.text }) + .wrapInline('link', { href: data.text }) .collapseToEnd() .apply() } diff --git a/examples/paste-html/index.js b/examples/paste-html/index.js index 8b97fda29..70f640546 100644 --- a/examples/paste-html/index.js +++ b/examples/paste-html/index.js @@ -188,14 +188,13 @@ class PasteHtml extends React.Component { * On paste, deserialize the HTML and then insert the fragment. * * @param {Event} e - * @param {Object} paste + * @param {Object} data * @param {State} state */ - onPaste = (e, paste, state) => { - if (paste.type != 'html') return - const { html } = paste - const { document } = serializer.deserialize(html) + onPaste = (e, data, state) => { + if (data.type != 'html') return + const { document } = serializer.deserialize(data.html) return state .transform() diff --git a/examples/rich-text/index.js b/examples/rich-text/index.js index 1a7783d1e..55a1ec590 100644 --- a/examples/rich-text/index.js +++ b/examples/rich-text/index.js @@ -1,8 +1,7 @@ -import { Editor, Mark, Raw, Utils } from '../..' +import { Editor, Mark, Raw } from '../..' import React from 'react' import initialState from './state.json' -import keycode from 'keycode' /** * Define the default node type. @@ -105,16 +104,16 @@ class RichText extends React.Component { * On key down, if it's a formatting command toggle a mark. * * @param {Event} e + * @param {Object} data * @param {State} state * @return {State} */ - onKeyDown = (e, state) => { - if (!Utils.Key.isCommand(e)) return - const key = keycode(e.which) + onKeyDown = (e, data, state) => { + if (!data.isMod) return let mark - switch (key) { + switch (data.key) { case 'b': mark = 'bold' break diff --git a/examples/tables/index.js b/examples/tables/index.js index 96d08f996..817119281 100644 --- a/examples/tables/index.js +++ b/examples/tables/index.js @@ -101,13 +101,14 @@ class Tables extends React.Component { * On key down, check for our specific key shortcuts. * * @param {Event} e + * @param {Object} data * @param {State} state * @return {State or Null} state */ - onKeyDown = (e, state) => { + onKeyDown = (e, data, state) => { if (state.startBlock.type != 'table-cell') return - switch (keycode(e.which)) { + switch (data.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/lib/components/content.js b/lib/components/content.js index 5915a17a1..fbb3d8e41 100644 --- a/lib/components/content.js +++ b/lib/components/content.js @@ -1,6 +1,5 @@ import Base64 from '../serializers/base-64' -import Key from '../utils/key' import Node from './node' import OffsetKey from '../utils/offset-key' import Raw from '../serializers/raw' @@ -9,7 +8,7 @@ import Selection from '../models/selection' import TYPES from '../utils/types' import includes from 'lodash/includes' import keycode from 'keycode' -import { IS_FIREFOX } from '../utils/environment' +import { IS_FIREFOX, IS_MAC } from '../utils/environment' /** * Noop. @@ -454,7 +453,23 @@ class Content extends React.Component { onKeyDown = (e) => { if (this.props.readOnly) return const key = keycode(e.which) + const data = {} + // Add helpful properties for handling hotkeys to the data object. + data.code = e.which + data.key = key + data.isAlt = e.altKey + data.isCmd = IS_MAC ? e.metaKey && !e.altKey : false + data.isCtrl = e.ctrlKey && !e.altKey + data.isLine = IS_MAC ? e.metaKey : false + data.isMeta = e.metaKey + data.isMod = IS_MAC ? e.metaKey && !e.altKey : e.ctrlKey && !e.altKey + data.isShift = e.shiftKey + data.isWord = IS_MAC ? e.altKey : e.ctrlKey + + // When composing, these characters commit the composition but also move the + // selection before we're able to handle it, so prevent their default, + // selection-moving behavior. if ( this.tmp.isComposing && (key == 'left' || key == 'right' || key == 'up' || key == 'down') @@ -463,19 +478,21 @@ class Content extends React.Component { return } + // 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' && Key.isCommand(e)) || - (key == 'i' && Key.isCommand(e)) || - (key == 'y' && Key.isWindowsCommand(e)) || - (key == 'z' && Key.isCommand(e)) + (key == 'b' && data.isMod) || + (key == 'i' && data.isMod) || + (key == 'y' && data.isMod) || + (key == 'z' && data.isMod) ) { e.preventDefault() } - this.props.onKeyDown(e) + this.props.onKeyDown(e, data) } /** @@ -488,37 +505,37 @@ class Content extends React.Component { if (this.props.readOnly) return e.preventDefault() - const data = e.clipboardData - const paste = {} + const { clipboardData } = e + const data = {} // COMPAT: In Firefox, `types` is array-like. (2016/06/21) - const types = Array.from(data.types) + const types = Array.from(clipboardData.types) // Handle files. - if (data.files.length) { - paste.type = 'files' - paste.files = data.files + if (clipboardData.files.length) { + data.type = 'files' + data.files = clipboardData.files } // Treat it as rich text if there is HTML content. else if (includes(types, TYPES.HTML)) { - paste.type = 'html' - paste.text = data.getData(TYPES.TEXT) - paste.html = data.getData(TYPES.HTML) + data.type = 'html' + data.text = clipboardData.getData(TYPES.TEXT) + data.html = clipboardData.getData(TYPES.HTML) } // Treat everything else as plain text. else { - paste.type = 'text' - paste.text = data.getData(TYPES.TEXT) + data.type = 'text' + data.text = clipboardData.getData(TYPES.TEXT) } // If html, and the html includes a `data-fragment` attribute, it's actually // a raw-serialized JSON fragment from a previous cut/copy, so deserialize // it and insert it normally. - if (paste.type == 'html' && ~paste.html.indexOf('