1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-04-21 13:51:59 +02:00

refactor onKeyDown to use data object

This commit is contained in:
Ian Storm Taylor 2016-07-27 14:30:09 -07:00
parent fba3fe7a13
commit d20b8511bb
14 changed files with 131 additions and 228 deletions
History.md
examples
auto-markdown
code-highlighting
development/performance-rich
images
links
paste-html
rich-text
tables
lib
components
index.js
plugins
utils
package.json

@ -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

@ -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)

@ -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

@ -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

@ -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)
}
/**

@ -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()
}

@ -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()

@ -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

@ -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)

@ -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('<span data-fragment="')) {
if (data.type == 'html' && ~data.html.indexOf('<span data-fragment="')) {
const regexp = /data-fragment="([^\s]+)"/
const matches = regexp.exec(paste.html)
const matches = regexp.exec(data.html)
const [ full, encoded ] = matches
const fragment = Base64.deserializeNode(encoded)
let { state } = this.props
@ -532,8 +549,7 @@ class Content extends React.Component {
return
}
paste.data = data
this.props.onPaste(e, paste)
this.props.onPaste(e, data)
}
/**
@ -551,12 +567,12 @@ class Content extends React.Component {
const { state, renderDecorations } = this.props
let { document, selection } = state
const native = window.getSelection()
const select = {}
const data = {}
// If there are no ranges, the editor was blurred natively.
if (!native.rangeCount) {
select.selection = selection.merge({ isFocused: false })
select.isNative = true
data.selection = selection.merge({ isFocused: false })
data.isNative = true
}
// Otherwise, determine the Slate selection from the native one.
@ -579,12 +595,12 @@ class Content extends React.Component {
// If the native selection is inside text nodes, we can trust the native
// state and not need to re-render.
select.isNative = (
data.isNative = (
anchorNode.nodeType == 3 &&
focusNode.nodeType == 3
)
select.selection = selection.merge({
data.selection = selection.merge({
anchorKey: anchor.key,
anchorOffset: anchor.offset,
focusKey: focus.key,
@ -593,7 +609,7 @@ class Content extends React.Component {
})
}
this.props.onSelect(e, select)
this.props.onSelect(e, data)
}
/**

@ -33,14 +33,8 @@ import Raw from './serializers/raw'
* Utils.
*/
import Key from './utils/key'
import findDOMNode from './utils/find-dom-node'
const Utils = {
Key,
findDOMNode
}
/**
* Export.
*/
@ -60,8 +54,8 @@ export {
Selection,
State,
Text,
Utils,
Void
Void,
findDOMNode
}
export default {
@ -79,6 +73,6 @@ export default {
Selection,
State,
Text,
Utils,
Void
Void,
findDOMNode
}

@ -1,7 +1,6 @@
import Base64 from '../serializers/base-64'
import Character from '../models/character'
import Key from '../utils/key'
import Placeholder from '../components/placeholder'
import React from 'react'
import String from '../utils/string'
@ -314,17 +313,18 @@ function Plugin(options = {}) {
* On key down.
*
* @param {Event} e
* @param {Object} data
* @param {State} state
* @return {State}
*/
function onKeyDown(e, state) {
switch (keycode(e.which)) {
case 'enter': return onKeyDownEnter(e, state)
case 'backspace': return onKeyDownBackspace(e, state)
case 'delete': return onKeyDownDelete(e, state)
case 'y': return onKeyDownY(e, state)
case 'z': return onKeyDownZ(e, state)
function onKeyDown(e, data, state) {
switch (data.key) {
case 'enter': return onKeyDownEnter(e, data, state)
case 'backspace': return onKeyDownBackspace(e, data, state)
case 'delete': return onKeyDownDelete(e, data, state)
case 'y': return onKeyDownY(e, data, state)
case 'z': return onKeyDownZ(e, data, state)
}
}
@ -332,11 +332,12 @@ function Plugin(options = {}) {
* On `enter` key down, split the current block in half.
*
* @param {Event} e
* @param {Object} data
* @param {State} state
* @return {State}
*/
function onKeyDownEnter(e, state) {
function onKeyDownEnter(e, data, state) {
const { document, startKey, startBlock } = state
// For void blocks, we don't want to split. Instead we just move to the
@ -360,11 +361,12 @@ function Plugin(options = {}) {
* On `backspace` key down, delete backwards.
*
* @param {Event} e
* @param {Object} data
* @param {State} state
* @return {State}
*/
function onKeyDownBackspace(e, state) {
function onKeyDownBackspace(e, data, state) {
// If expanded, delete regularly.
if (state.isExpanded) {
return state
@ -378,11 +380,15 @@ function Plugin(options = {}) {
let n
// Determine how far backwards to delete.
if (Key.isWord(e)) {
if (data.isWord) {
n = String.getWordOffsetBackward(text, startOffset)
} else if (Key.isLine(e)) {
}
else if (data.isLine) {
n = startOffset
} else {
}
else {
n = String.getCharOffsetBackward(text, startOffset)
}
@ -396,11 +402,12 @@ function Plugin(options = {}) {
* On `delete` key down, delete forwards.
*
* @param {Event} e
* @param {Object} data
* @param {State} state
* @return {State}
*/
function onKeyDownDelete(e, state) {
function onKeyDownDelete(e, data, state) {
// If expanded, delete regularly.
if (state.isExpanded) {
return state
@ -414,11 +421,15 @@ function Plugin(options = {}) {
let n
// Determine how far forwards to delete.
if (Key.isWord(e)) {
if (data.isWord) {
n = String.getWordOffsetForward(text, startOffset)
} else if (Key.isLine(e)) {
}
else if (data.isLine) {
n = text.length - startOffset
} else {
}
else {
n = String.getCharOffsetForward(text, startOffset)
}
@ -432,12 +443,13 @@ function Plugin(options = {}) {
* On `y` key down, redo.
*
* @param {Event} e
* @param {Object} data
* @param {State} state
* @return {State}
*/
function onKeyDownY(e, state) {
if (!Key.isWindowsCommand(e)) return
function onKeyDownY(e, data, state) {
if (!data.isMod) return
return state
.transform()
.redo()
@ -447,15 +459,16 @@ function Plugin(options = {}) {
* On `z` key down, undo or redo.
*
* @param {Event} e
* @param {Object} data
* @param {State} state
* @return {State}
*/
function onKeyDownZ(e, state) {
if (!Key.isCommand(e)) return
function onKeyDownZ(e, data, state) {
if (!data.isMod) return
return state
.transform()
[IS_MAC && Key.isShift(e) ? 'redo' : 'undo']()
[data.isShift ? 'redo' : 'undo']()
}
/**

@ -1,123 +0,0 @@
import { IS_MAC, IS_WINDOWS } from './environment'
/**
* Does an `e` have the alt modifier?
*
* @param {Event} e
* @return {Boolean}
*/
function isAlt(e) {
return e.altKey
}
/**
* Does an `e` have the command modifier?
*
* @param {Event} e
* @return {Boolean}
*/
function isCommand(e) {
return IS_MAC
? e.metaKey && !e.altKey
: e.ctrlKey && !e.altKey
}
/**
* Does an `e` have the control modifier?
*
* @param {Event} e
* @return {Boolean}
*/
function isCtrl(e) {
return e.ctrlKey && !e.altKey
}
/**
* Does an `e` have the line-level modifier?
*
* @param {Event} e
* @return {Boolean}
*/
function isLine(e) {
return IS_MAC
? e.metaKey
: false
}
/**
* Does an `e` have the Mac command modifier?
*
* @param {Event} e
* @return {Boolean}
*/
function isMacCommand(e) {
return IS_MAC && isCommand(e)
}
/**
* Does an `e` have the option modifier?
*
* @param {Event} e
* @return {Boolean}
*/
function isOption(e) {
return IS_MAC && e.altKey
}
/**
* Does an `e` have the shift modifier?
*
* @param {Event} e
* @return {Boolean}
*/
function isShift(e) {
return e.shiftKey
}
/**
* Does an `e` have the Windows command modifier?
*
* @param {Event} e
* @return {Boolean}
*/
function isWindowsCommand(e) {
return IS_WINDOWS && isCommand(e)
}
/**
* Does an `e` have the word-level modifier?
*
* @param {Event} e
* @return {Boolean}
*/
function isWord(e) {
return IS_MAC
? e.altKey
: e.ctrlKey
}
/**
* Export.
*/
export default {
isAlt,
isCommand,
isCtrl,
isLine,
isMacCommand,
isOption,
isShift,
isWindowsCommand,
isWord
}

@ -58,7 +58,7 @@
"watchify": "^3.7.0"
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules",
"clean": "rm -rf ./dist ./node_modules ./examples/build.js",
"disc": "npm run dist && npm run disc:build && npm run disc:open",
"disc:build": "mkdir -p ./tmp && browserify ./dist/index.js --full-paths --outfile ./tmp/build.js",
"disc:open": "discify ./tmp/build.js --open",