diff --git a/examples/rich-text/index.js b/examples/rich-text/index.js
index 878118efa..c6660ace2 100644
--- a/examples/rich-text/index.js
+++ b/examples/rich-text/index.js
@@ -1,7 +1,8 @@
-import { Editor, Mark, Raw } from '../..'
+import { Editor, Mark, Raw, Utils } from '../..'
import React from 'react'
import initialState from './state.json'
+import keycode from 'keycode'
/**
* Node renderers.
@@ -55,42 +56,6 @@ class RichText extends React.Component {
state: Raw.deserialize(initialState)
};
- hasMark = (type) => {
- const { state } = this.state
- return state.marks.some(mark => mark.type == type)
- }
-
- hasBlock = (type) => {
- const { state } = this.state
- return state.blocks.some(node => node.type == type)
- }
-
- onClickMark = (e, type) => {
- e.preventDefault()
- const isActive = this.hasMark(type)
- let { state } = this.state
-
- state = state
- .transform()
- [isActive ? 'unmark' : 'mark'](type)
- .apply()
-
- this.setState({ state })
- }
-
- onClickBlock = (e, type) => {
- e.preventDefault()
- const isActive = this.hasBlock(type)
- let { state } = this.state
-
- state = state
- .transform()
- .setBlock(isActive ? 'paragraph' : type)
- .apply()
-
- this.setState({ state })
- }
-
render = () => {
return (
@@ -146,6 +111,7 @@ class RichText extends React.Component {
renderNode={this.renderNode}
renderMark={this.renderMark}
onChange={this.onChange}
+ onKeyDown={this.onKeyDown}
/>
)
@@ -159,6 +125,16 @@ class RichText extends React.Component {
return MARKS[mark.type]
}
+ hasMark = (type) => {
+ const { state } = this.state
+ return state.marks.some(mark => mark.type == type)
+ }
+
+ hasBlock = (type) => {
+ const { state } = this.state
+ return state.blocks.some(node => node.type == type)
+ }
+
onChange = (state) => {
console.groupCollapsed('Change!')
console.log('Document:', state.document.toJS())
@@ -168,6 +144,63 @@ class RichText extends React.Component {
this.setState({ state })
}
+ onKeyDown = (e, state) => {
+ if (!Utils.Key.isCommand(e)) return
+ const key = keycode(e.which)
+ let mark
+
+ switch (key) {
+ case 'b':
+ mark = 'bold'
+ break
+ case 'i':
+ mark = 'italic'
+ break
+ case 'u':
+ mark = 'underlined'
+ break
+ case '`':
+ mark = 'code'
+ break
+ default:
+ return
+ }
+
+ state = state
+ .transform()
+ [this.hasMark(mark) ? 'unmark' : 'mark'](mark)
+ .apply()
+
+ e.preventDefault()
+ return state
+ }
+
+ onClickMark = (e, type) => {
+ e.preventDefault()
+ const isActive = this.hasMark(type)
+ let { state } = this.state
+
+ state = state
+ .transform()
+ [isActive ? 'unmark' : 'mark'](type)
+ .apply()
+
+ this.setState({ state })
+ }
+
+ onClickBlock = (e, type) => {
+ e.preventDefault()
+ const isActive = this.hasBlock(type)
+ let { state } = this.state
+
+ state = state
+ .transform()
+ .setBlock(isActive ? 'paragraph' : type)
+ .apply()
+
+ this.setState({ state })
+ }
+
}
/**
diff --git a/examples/rich-text/state.json b/examples/rich-text/state.json
index 1e3fb93e8..12983598d 100644
--- a/examples/rich-text/state.json
+++ b/examples/rich-text/state.json
@@ -65,7 +65,7 @@
}
]
},{
- "text": ", or add a semanticlly rendered block quote in the middle of the page, like this:"
+ "text": ", or add a semantically rendered block quote in the middle of the page, like this:"
}
]
}
diff --git a/lib/components/content.js b/lib/components/content.js
index 4a524d91d..debf7eafa 100644
--- a/lib/components/content.js
+++ b/lib/components/content.js
@@ -1,11 +1,18 @@
+import Key from '../utils/key'
import OffsetKey from '../utils/offset-key'
+import Raw from '../serializers/raw'
import React from 'react'
import Text from './text'
import Void from './void'
import keycode from 'keycode'
-import { Raw } from '..'
-import { isCommand, isWindowsCommand } from '../utils/event'
+import { IS_FIREFOX } from '../utils/environment'
+
+/**
+ * Noop.
+ */
+
+function noop() {}
/**
* Content.
@@ -63,20 +70,10 @@ class Content extends React.Component {
* @param {Object} props
*/
- componentWillMount = () => {
- this.tmp.isRendering = true
- }
-
componentWillUpdate = (props, state) => {
this.tmp.isRendering = true
}
- componentDidMount = () => {
- setTimeout(() => {
- this.tmp.isRendering = false
- })
- }
-
componentDidUpdate = (props, state) => {
setTimeout(() => {
this.tmp.isRendering = false
@@ -214,10 +211,10 @@ class Content extends React.Component {
(key == 'enter') ||
(key == 'backspace') ||
(key == 'delete') ||
- (key == 'b' && isCommand(e)) ||
- (key == 'i' && isCommand(e)) ||
- (key == 'y' && isWindowsCommand(e)) ||
- (key == 'z' && isCommand(e))
+ (key == 'b' && Key.isCommand(e)) ||
+ (key == 'i' && Key.isCommand(e)) ||
+ (key == 'y' && Key.isWindowsCommand(e)) ||
+ (key == 'z' && Key.isCommand(e))
) {
e.preventDefault()
}
@@ -234,7 +231,7 @@ class Content extends React.Component {
onPaste = (e) => {
e.preventDefault()
const data = e.clipboardData
- const { types } = data
+ const types = Array.from(data.types)
const paste = {}
// Handle files.
@@ -308,13 +305,13 @@ class Content extends React.Component {
state = state
.transform()
+ .focus()
.moveTo({
anchorKey: anchor.key,
anchorOffset: anchor.offset,
focusKey: focus.key,
focusOffset: focus.offset
})
- .focus()
.apply({ isNative: true })
this.onChange(state)
@@ -350,6 +347,7 @@ class Content extends React.Component {
onKeyDown={this.onKeyDown}
onPaste={this.onPaste}
onSelect={this.onSelect}
+ onKeyUp={noop}
>
{children}
diff --git a/lib/components/leaf.js b/lib/components/leaf.js
index a44aaa3be..e4491633c 100644
--- a/lib/components/leaf.js
+++ b/lib/components/leaf.js
@@ -27,17 +27,22 @@ class Leaf extends React.Component {
* Should component update?
*
* @param {Object} props
- * @param {Object} state
* @return {Boolean} shouldUpdate
*/
- shouldComponentUpdate(props, state) {
- return (
+ shouldComponentUpdate(props) {
+ const { start, end, node, state } = props
+ const { selection } = state
+
+ const should = (
+ selection.hasEdgeBetween(node, start, end) ||
props.start != this.props.start ||
props.end != this.props.end ||
props.text != this.props.text ||
props.marks != this.props.marks
)
+
+ return should
}
componentDidMount() {
@@ -55,19 +60,19 @@ class Leaf extends React.Component {
// If the selection is not focused we have nothing to do.
if (!selection.isFocused) return
- const { anchorKey, anchorOffset, focusKey, focusOffset } = selection
+ const { anchorOffset, focusOffset } = selection
const { node, start, end } = this.props
- const { key } = node
// If neither matches, the selection doesn't start or end here, so exit.
- const hasStart = key == anchorKey && start <= anchorOffset && anchorOffset <= end
- const hasEnd = key == focusKey && start <= focusOffset && focusOffset <= end
+ const hasStart = selection.hasStartBetween(node, start, end)
+ const hasEnd = selection.hasEndBetween(node, start, end)
if (!hasStart && !hasEnd) return
// We have a selection to render, so prepare a few things...
const native = window.getSelection()
const el = ReactDOM.findDOMNode(this).firstChild
+
// If both the start and end are here, set the selection all at once.
if (hasStart && hasEnd) {
native.removeAllRanges()
diff --git a/lib/components/text.js b/lib/components/text.js
index 5b66f997d..480359761 100644
--- a/lib/components/text.js
+++ b/lib/components/text.js
@@ -1,6 +1,5 @@
import Leaf from './leaf'
-import OffsetKey from '../utils/offset-key'
import React from 'react'
import groupByMarks from '../utils/group-by-marks'
import { List } from 'immutable'
@@ -32,6 +31,7 @@ class Text extends React.Component {
shouldComponentUpdate(props, state) {
return (
+ props.state.selection.hasEdgeIn(props.node) ||
props.node.decorations != this.props.node.decorations ||
props.node.characters != this.props.node.characters
)
@@ -67,7 +67,7 @@ class Text extends React.Component {
const offset = previous.size
? previous.map(r => r.text).join('').length
: 0
- return this.renderLeaf(range, offset)
+ return this.renderLeaf(range, i, offset)
})
}
@@ -75,25 +75,21 @@ class Text extends React.Component {
* Render a single leaf node given a `range` and `offset`.
*
* @param {Object} range
+ * @param {Number} index
* @param {Number} offset
* @return {Element} leaf
*/
- renderLeaf(range, offset) {
+ renderLeaf(range, index, offset) {
const { node, renderMark, state } = this.props
const text = range.text
const marks = range.marks
const start = offset
const end = offset + text.length
- const offsetKey = OffsetKey.stringify({
- key: node.key,
- start,
- end
- })
return (
n.key == this.anchorKey)
+ }
+
+ /**
+ * Check whether focus point of the selection is at the end of a `node`.
+ *
+ * @param {Node} node
+ * @return {Boolean}
+ */
+
+ hasFocusAtEndOf(node) {
+ const last = node.kind == 'text' ? node : node.getTextNodes().last()
+ return this.focusKey == last.key && this.focusOffset == last.length
+ }
+
+ /**
+ * Check whether focus point of the selection is at the start of a `node`.
+ *
+ * @param {Node} node
+ * @return {Boolean}
+ */
+
+ hasFocusAtStartOf(node) {
+ const first = node.kind == 'text' ? node : node.getTextNodes().first()
+ return this.focusKey == first.key && this.focusOffset == 0
+ }
+
+ /**
+ * Check whether the focus edge of a selection is in a `node` and at an
+ * offset between `start` and `end`.
+ *
+ * @param {Node} node
+ * @param {Number} start
+ * @param {Number} end
+ * @return {Boolean}
+ */
+
+ hasFocusBetween(node, start, end) {
+ return (
+ this.hasFocusIn(node) &&
+ start <= this.focusOffset &&
+ this.focusOffset <= end
+ )
+ }
+
+ /**
+ * Check whether the focus edge of a selection is in a `node`.
+ *
+ * @param {Node} node
+ * @return {Boolean}
+ */
+
+ hasFocusIn(node) {
+ const nodes = node.kind == 'text' ? [node] : node.getTextNodes()
+ return nodes.some(n => n.key == this.focusKey)
+ }
+
/**
* Check whether the selection is at the start of a `node`.
*
@@ -141,38 +268,6 @@ class Selection extends new Record(DEFAULTS) {
return this.isCollapsed && endKey == last.key && endOffset == last.length
}
- /**
- * Check whether the selection has an edge at the start of a `node`.
- *
- * @param {Node} node
- * @return {Boolean} hasEdgeAtStart
- */
-
- hasEdgeAtStartOf(node) {
- const { startKey, startOffset, endKey, endOffset } = this
- const first = node.kind == 'text' ? node : node.getTextNodes().first()
- return (
- (startKey == first.key && startOffset == 0) ||
- (endKey == first.key && endOffset == 0)
- )
- }
-
- /**
- * Check whether the selection has an edge at the end of a `node`.
- *
- * @param {Node} node
- * @return {Boolean} hasEdgeAtEnd
- */
-
- hasEdgeAtEndOf(node) {
- const { startKey, startOffset, endKey, endOffset } = this
- const last = node.kind == 'text' ? node : node.getTextNodes().last()
- return (
- (startKey == last.key && startOffset == last.length) ||
- (endKey == last.key && endOffset == last.length)
- )
- }
-
/**
* Normalize the selection, relative to a `node`, ensuring that the anchor
* and focus nodes of the selection always refer to leaf text nodes.
@@ -221,36 +316,6 @@ class Selection extends new Record(DEFAULTS) {
: anchorIndex > focusIndex
}
- // If the selection is expanded and has an edge on a void block, move it.
- // if (isExpanded) {
- // let anchorBlock = node.getClosestBlock(anchorNode)
- // let focusBlock = node.getClosestBlock(focusNode)
-
- // if (anchorBlock.isVoid) {
- // while (anchorBanchorBlock.isVoid) {
- // anchorBlock = isBackward
- // ? node.getPreviousBlock(anchorBlock)
- // : node.getNextBlock(anchorBlock)
- // }
-
- // anchorNode = isBackward
- // ? anchorBlock.getTextNodes().last()
- // : anchorBlock.getTextNodes().first()
- // anchorOffset = isBackward
- // ? anchorNode.length
- // : 0
- // }
-
- // else if (focusBlock.isVoid) {
- // focusNode = isBackward
- // ? node.getNextBlock(focusBlock).getTextNodes().first()
- // : node.getPreviousBlock(focusBlock).getTextNodes().last()
- // focusOffset = isBackward
- // ? 0
- // : focusNode.length
- // }
- // }
-
// Merge in any updated properties.
return selection.merge({
anchorKey: anchorNode.key,
@@ -285,17 +350,6 @@ class Selection extends new Record(DEFAULTS) {
})
}
- /**
- * Move the selection to a specific anchor and focus point.
- *
- * @param {Object} properties
- * @return {Selection} selection
- */
-
- moveTo(properties) {
- return this.merge(properties)
- }
-
/**
* Move the focus point to the anchor point.
*
@@ -324,46 +378,6 @@ class Selection extends new Record(DEFAULTS) {
})
}
- /**
- * Move the end point to the start point.
- *
- * @return {Selection} selection
- */
-
- moveToStart() {
- return this.isBackward
- ? this.merge({
- anchorKey: this.focusKey,
- anchorOffset: this.focusOffset,
- isBackward: false
- })
- : this.merge({
- focusKey: this.anchorKey,
- focusOffset: this.anchorOffset,
- isBackward: false
- })
- }
-
- /**
- * Move the start point to the end point.
- *
- * @return {Selection} selection
- */
-
- moveToEnd() {
- return this.isBackward
- ? this.merge({
- focusKey: this.anchorKey,
- focusOffset: this.anchorOffset,
- isBackward: false
- })
- : this.merge({
- anchorKey: this.focusKey,
- anchorOffset: this.focusOffset,
- isBackward: false
- })
- }
-
/**
* Move to the start of a `node`.
*
@@ -502,6 +516,41 @@ class Selection extends new Record(DEFAULTS) {
}
+/**
+ * Add start, end and edge convenience methods.
+ */
+
+START_END_METHODS.concat(EDGE_METHODS).forEach((pattern) => {
+ const [ p, s ] = pattern.split('%')
+ const anchor = `${p}Anchor${s}`
+ const edge = `${p}Edge${s}`
+ const end = `${p}End${s}`
+ const focus = `${p}Focus${s}`
+ const start = `${p}Start${s}`
+
+ Selection.prototype[start] = function (...args) {
+ return this.isBackward
+ ? this[focus](...args)
+ : this[anchor](...args)
+ }
+
+ Selection.prototype[end] = function (...args) {
+ return this.isBackward
+ ? this[anchor](...args)
+ : this[focus](...args)
+ }
+
+ if (!EDGE_METHODS.includes(pattern)) return
+
+ Selection.prototype[edge] = function (...args) {
+ return this[anchor](...args) || this[focus](...args)
+ }
+})
+
+/**
+ * Add edge methods.
+ */
+
/**
* Export.
*/
diff --git a/lib/models/state.js b/lib/models/state.js
index eeda0b8ec..531ac29c1 100644
--- a/lib/models/state.js
+++ b/lib/models/state.js
@@ -519,6 +519,31 @@ class State extends new Record(DEFAULTS) {
return state
}
+ /**
+ * Move the selection to a specific anchor and focus point.
+ *
+ * @param {Object} properties
+ * @return {State} state
+ */
+
+ moveTo(properties) {
+ let state = this
+ let { document, selection } = state
+
+ // Pass in properties, and force `isBackward` to be re-resolved.
+ selection = selection.merge({
+ anchorKey: properties.anchorKey,
+ anchorOffset: properties.anchorOffset,
+ focusKey: properties.focusKey,
+ focusOffset: properties.focusOffset,
+ isBackward: null
+ })
+
+ selection = selection.normalize(document)
+ state = state.merge({ selection })
+ return state
+ }
+
/**
* Move the selection to the start of the previous block.
*
diff --git a/lib/models/transform.js b/lib/models/transform.js
index a1c0e164e..f185da26b 100644
--- a/lib/models/transform.js
+++ b/lib/models/transform.js
@@ -57,7 +57,6 @@ const SELECTION_TRANSFORMS = [
'focus',
'moveBackward',
'moveForward',
- 'moveTo',
'moveToAnchor',
'moveToEnd',
'moveToEndOf',
@@ -78,6 +77,7 @@ const STATE_TRANSFORMS = [
'insertFragment',
'insertText',
'mark',
+ 'moveTo',
'moveToStartOfPreviousBlock',
'moveToEndOfPreviousBlock',
'moveToStartOfNextBlock',
diff --git a/lib/plugins/core.js b/lib/plugins/core.js
index af7687985..e0f8520c9 100644
--- a/lib/plugins/core.js
+++ b/lib/plugins/core.js
@@ -1,7 +1,7 @@
+import Key from '../utils/key'
import React from 'react'
import keycode from 'keycode'
-import { isCommand, isCtrl, isWindowsCommand, isWord } from '../utils/event'
import { IS_WINDOWS, IS_MAC } from '../utils/environment'
/**
@@ -93,13 +93,13 @@ export default {
}
case 'backspace': {
- return isWord(e)
+ return Key.isWord(e)
? transform.backspaceWord().apply()
: transform.deleteBackward().apply()
}
case 'delete': {
- return isWord(e)
+ return Key.isWord(e)
? transform.deleteWord().apply()
: transform.deleteForward().apply()
}
@@ -137,13 +137,13 @@ export default {
}
case 'y': {
- if (!isWindowsCommand(e)) return
+ if (!Key.isWindowsCommand(e)) return
return transform.redo()
}
case 'z': {
- if (!isCommand(e)) return
- return IS_MAC && e.shiftKey
+ if (!Key.isCommand(e)) return
+ return IS_MAC && Key.isShift(e)
? transform.redo()
: transform.undo()
}
diff --git a/lib/utils/environment.js b/lib/utils/environment.js
index 3c594a932..22582c593 100644
--- a/lib/utils/environment.js
+++ b/lib/utils/environment.js
@@ -16,3 +16,16 @@ export const IS_MAC = process.browser && new Parser().getOS().name == 'Mac OS'
export const IS_SAFARI = process.browser && browser.name == 'safari'
export const IS_UBUNTU = process.browser && new Parser().getOS().name == 'Ubuntu'
export const IS_WINDOWS = process.browser && new Parser().getOS().name.includes('Windows')
+
+export default {
+ IS_ANDROID,
+ IS_CHROME,
+ IS_EDGE,
+ IS_FIREFOX,
+ IS_IE,
+ IS_IOS,
+ IS_MAC,
+ IS_SAFARI,
+ IS_UBUNTU,
+ IS_WINDOWS
+}
diff --git a/lib/utils/event.js b/lib/utils/key.js
similarity index 69%
rename from lib/utils/event.js
rename to lib/utils/key.js
index 1b9e6906c..7a757ed3d 100644
--- a/lib/utils/event.js
+++ b/lib/utils/key.js
@@ -2,16 +2,27 @@
import { IS_MAC, IS_WINDOWS } from './environment'
/**
- * Does an `e` have the word-level modifier?
+ * Does an `e` have the alt modifier?
*
* @param {Event} e
* @return {Boolean}
*/
-export function isWord(e) {
+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.altKey
- : e.ctrlKey
+ ? e.metaKey && !e.altKey
+ : e.ctrlKey && !e.altKey
}
/**
@@ -21,10 +32,21 @@ export function isWord(e) {
* @return {Boolean}
*/
-export function isCtrl(e) {
+function isCtrl(e) {
return e.ctrlKey && !e.altKey
}
+/**
+ * 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?
*
@@ -32,7 +54,7 @@ export function isCtrl(e) {
* @return {Boolean}
*/
-export function isOption(e) {
+function isOption(e) {
return IS_MAC && e.altKey
}
@@ -43,34 +65,10 @@ export function isOption(e) {
* @return {Boolean}
*/
-export function isShift(e) {
+function isShift(e) {
return e.shiftKey
}
-/**
- * Does an `e` have the command modifier?
- *
- * @param {Event} e
- * @return {Boolean}
- */
-
-export function isCommand(e) {
- return IS_MAC
- ? e.metaKey && !e.altKey
- : e.ctrlKey && !e.altKey
-}
-
-/**
- * Does an `e` have the Mac command modifier?
- *
- * @param {Event} e
- * @return {Boolean}
- */
-
-export function isMacCommand(e) {
- return IS_MAC && isCommand(e)
-}
-
/**
* Does an `e` have the Windows command modifier?
*
@@ -78,6 +76,34 @@ export function isMacCommand(e) {
* @return {Boolean}
*/
-export function isWindowsCommand(e) {
+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,
+ isMacCommand,
+ isOption,
+ isShift,
+ isWindowsCommand,
+ isWord
+}
diff --git a/package.json b/package.json
index 6291d7df2..20759022c 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"license": "MIT",
"repository": "git://github.com/ianstormtaylor/slate.git",
- "main": "./dist/index.js",
+ "main": "./lib/index.js",
"scripts": {
"prepublish": "make dist",
"test": "make check"
@@ -32,6 +32,7 @@
"babelify": "^7.3.0",
"browserify": "^13.0.1",
"component-type": "^1.2.1",
+ "draft-js": "^0.7.0",
"eslint": "^3.0.1",
"eslint-plugin-import": "^1.10.2",
"eslint-plugin-react": "^5.2.2",