mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-27 09:04:31 +02:00
fix to greatly improve performance, and void selections in void nodes
This commit is contained in:
@@ -44,6 +44,7 @@
|
|||||||
"no-array-constructor": "error",
|
"no-array-constructor": "error",
|
||||||
"no-class-assign": "error",
|
"no-class-assign": "error",
|
||||||
"no-const-assign": "error",
|
"no-const-assign": "error",
|
||||||
|
"no-console": "warn",
|
||||||
"no-debugger": "warn",
|
"no-debugger": "warn",
|
||||||
"no-dupe-args": "error",
|
"no-dupe-args": "error",
|
||||||
"no-dupe-class-members": "error",
|
"no-dupe-class-members": "error",
|
||||||
|
@@ -129,12 +129,11 @@ class CodeHighlighting extends React.Component {
|
|||||||
* Render decorations on `text` nodes inside code blocks.
|
* Render decorations on `text` nodes inside code blocks.
|
||||||
*
|
*
|
||||||
* @param {Text} text
|
* @param {Text} text
|
||||||
|
* @param {Block} block
|
||||||
* @return {Characters}
|
* @return {Characters}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
renderDecorations = (text, state) => {
|
renderDecorations = (text, block) => {
|
||||||
const { document } = state
|
|
||||||
const block = document.getClosestBlock(text)
|
|
||||||
if (block.type != 'code') return text.characters
|
if (block.type != 'code') return text.characters
|
||||||
|
|
||||||
let characters = text.characters.asMutable()
|
let characters = text.characters.asMutable()
|
||||||
|
@@ -42,6 +42,7 @@ class Content extends React.Component {
|
|||||||
onPaste: React.PropTypes.func.isRequired,
|
onPaste: React.PropTypes.func.isRequired,
|
||||||
onSelect: React.PropTypes.func.isRequired,
|
onSelect: React.PropTypes.func.isRequired,
|
||||||
readOnly: React.PropTypes.bool.isRequired,
|
readOnly: React.PropTypes.bool.isRequired,
|
||||||
|
renderDecorations: React.PropTypes.func.isRequired,
|
||||||
renderMark: React.PropTypes.func.isRequired,
|
renderMark: React.PropTypes.func.isRequired,
|
||||||
renderNode: React.PropTypes.func.isRequired,
|
renderNode: React.PropTypes.func.isRequired,
|
||||||
spellCheck: React.PropTypes.bool.isRequired,
|
spellCheck: React.PropTypes.bool.isRequired,
|
||||||
@@ -114,6 +115,36 @@ class Content extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a point from a native selection's DOM `element` and `offset`.
|
||||||
|
*
|
||||||
|
* @param {Element} element
|
||||||
|
* @param {Number} offset
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
getPoint(element, offset) {
|
||||||
|
const offsetKey = OffsetKey.findKey(element, offset)
|
||||||
|
const ranges = this.getRanges(offsetKey.key)
|
||||||
|
const point = OffsetKey.findPoint(offsetKey, ranges)
|
||||||
|
return point
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ranges for a text node by `key`.
|
||||||
|
*
|
||||||
|
* @param {String} key
|
||||||
|
* @return {List}
|
||||||
|
*/
|
||||||
|
|
||||||
|
getRanges(key) {
|
||||||
|
const { state, renderDecorations } = this.props
|
||||||
|
const node = state.document.getDescendant(key)
|
||||||
|
const block = state.document.getClosestBlock(node)
|
||||||
|
const ranges = node.getDecoratedRanges(block, renderDecorations)
|
||||||
|
return ranges
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On before input, bubble up.
|
* On before input, bubble up.
|
||||||
*
|
*
|
||||||
@@ -333,7 +364,7 @@ class Content extends React.Component {
|
|||||||
if (this.props.readOnly) return
|
if (this.props.readOnly) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
const { state } = this.props
|
const { state, renderDecorations } = this.props
|
||||||
const { selection } = state
|
const { selection } = state
|
||||||
const data = e.nativeEvent.dataTransfer
|
const data = e.nativeEvent.dataTransfer
|
||||||
const drop = {}
|
const drop = {}
|
||||||
@@ -355,7 +386,7 @@ class Content extends React.Component {
|
|||||||
|
|
||||||
const startNode = range.startContainer
|
const startNode = range.startContainer
|
||||||
const startOffset = range.startOffset
|
const startOffset = range.startOffset
|
||||||
const point = OffsetKey.findPoint(startNode, startOffset, state)
|
const point = this.getPoint(startNode, startOffset)
|
||||||
const target = Selection.create({
|
const target = Selection.create({
|
||||||
anchorKey: point.key,
|
anchorKey: point.key,
|
||||||
anchorOffset: point.offset,
|
anchorOffset: point.offset,
|
||||||
@@ -418,16 +449,16 @@ class Content extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
onInput = (e) => {
|
onInput = (e) => {
|
||||||
let { state } = this.props
|
let { state, renderDecorations } = this.props
|
||||||
const { selection } = state
|
const { selection } = state
|
||||||
const native = window.getSelection()
|
const native = window.getSelection()
|
||||||
const { anchorNode, anchorOffset, focusOffset } = native
|
const { anchorNode, anchorOffset, focusOffset } = native
|
||||||
let { textContent } = anchorNode
|
const point = this.getPoint(anchorNode, anchorOffset)
|
||||||
const offsetKey = OffsetKey.findKey(anchorNode)
|
const { key, index, start, end } = point
|
||||||
const { key, index } = OffsetKey.parse(offsetKey)
|
const ranges = this.getRanges(key)
|
||||||
const { start, end } = OffsetKey.findBounds(key, index, state)
|
const range = ranges.get(index)
|
||||||
const range = OffsetKey.findRange(anchorNode, state)
|
|
||||||
const { text, marks } = range
|
const { text, marks } = range
|
||||||
|
let { textContent } = anchorNode
|
||||||
|
|
||||||
// COMPAT: If the DOM text ends in a new line, we will have added one to
|
// COMPAT: If the DOM text ends in a new line, we will have added one to
|
||||||
// account for browsers collapsing a single one, so remove it.
|
// account for browsers collapsing a single one, so remove it.
|
||||||
@@ -564,7 +595,52 @@ class Content extends React.Component {
|
|||||||
if (this.tmp.isCopying) return
|
if (this.tmp.isCopying) return
|
||||||
if (this.tmp.isComposing) return
|
if (this.tmp.isComposing) return
|
||||||
|
|
||||||
this.props.onSelect(e)
|
const { state, renderDecorations } = this.props
|
||||||
|
let { document, selection } = state
|
||||||
|
const native = window.getSelection()
|
||||||
|
const select = {}
|
||||||
|
|
||||||
|
// If there are no ranges, the editor was blurred natively.
|
||||||
|
if (!native.rangeCount) {
|
||||||
|
select.selection = selection.merge({ isFocused: false })
|
||||||
|
select.isNative = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, determine the Slate selection from the native one.
|
||||||
|
else {
|
||||||
|
const { anchorNode, anchorOffset, focusNode, focusOffset } = native
|
||||||
|
const anchor = this.getPoint(anchorNode, anchorOffset)
|
||||||
|
const focus = this.getPoint(focusNode, focusOffset)
|
||||||
|
|
||||||
|
// COMPAT: In Firefox, and potentially other browsers, sometimes a select
|
||||||
|
// event will fire that resolves to the same location as the current
|
||||||
|
// selection, so we can ignore it.
|
||||||
|
if (
|
||||||
|
anchor.key == selection.anchorKey &&
|
||||||
|
anchor.offset == selection.anchorOffset &&
|
||||||
|
focus.key == selection.focusKey &&
|
||||||
|
focus.offset == selection.focusOffset
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the native selection is inside text nodes, we can trust the native
|
||||||
|
// state and not need to re-render.
|
||||||
|
select.isNative = (
|
||||||
|
anchorNode.nodeType == 3 &&
|
||||||
|
focusNode.nodeType == 3
|
||||||
|
)
|
||||||
|
|
||||||
|
select.selection = selection.merge({
|
||||||
|
anchorKey: anchor.key,
|
||||||
|
anchorOffset: anchor.offset,
|
||||||
|
focusKey: focus.key,
|
||||||
|
focusOffset: focus.offset,
|
||||||
|
isFocused: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onSelect(e, select)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -637,15 +713,16 @@ class Content extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
renderNode = (node) => {
|
renderNode = (node) => {
|
||||||
const { editor, renderMark, renderNode, state } = this.props
|
const { editor, renderDecorations, renderMark, renderNode, state } = this.props
|
||||||
return (
|
return (
|
||||||
<Node
|
<Node
|
||||||
key={node.key}
|
key={node.key}
|
||||||
node={node}
|
node={node}
|
||||||
state={state}
|
state={state}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
renderNode={renderNode}
|
renderDecorations={renderDecorations}
|
||||||
renderMark={renderMark}
|
renderMark={renderMark}
|
||||||
|
renderNode={renderNode}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -62,7 +62,7 @@ class Editor extends React.Component {
|
|||||||
this.tmp = {}
|
this.tmp = {}
|
||||||
this.state = {}
|
this.state = {}
|
||||||
this.state.plugins = this.resolvePlugins(props)
|
this.state.plugins = this.resolvePlugins(props)
|
||||||
this.state.state = this.resolveState(props.state)
|
this.state.state = props.state
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,13 +72,11 @@ class Editor extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
componentWillReceiveProps = (props) => {
|
componentWillReceiveProps = (props) => {
|
||||||
|
this.state.state = props.state
|
||||||
|
|
||||||
if (props.plugins != this.props.plugins) {
|
if (props.plugins != this.props.plugins) {
|
||||||
this.setState({ plugins: this.resolvePlugins(props) })
|
this.setState({ plugins: this.resolvePlugins(props) })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.state != this.props.state) {
|
|
||||||
this.setState({ state: this.resolveState(props.state) })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -232,6 +230,7 @@ class Editor extends React.Component {
|
|||||||
onPaste={this.onPaste}
|
onPaste={this.onPaste}
|
||||||
onSelect={this.onSelect}
|
onSelect={this.onSelect}
|
||||||
readOnly={this.props.readOnly}
|
readOnly={this.props.readOnly}
|
||||||
|
renderDecorations={this.renderDecorations}
|
||||||
renderMark={this.renderMark}
|
renderMark={this.renderMark}
|
||||||
renderNode={this.renderNode}
|
renderNode={this.renderNode}
|
||||||
spellCheck={this.props.spellCheck}
|
spellCheck={this.props.spellCheck}
|
||||||
@@ -242,18 +241,21 @@ class Editor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a `node`, cascading through the plugins.
|
* Render the decorations for a `text`, cascading through the plugins.
|
||||||
*
|
*
|
||||||
* @param {Node} node
|
* @param {Block} text
|
||||||
* @return {Element} element
|
* @param {Block} block
|
||||||
|
* @return {Object} style
|
||||||
*/
|
*/
|
||||||
|
|
||||||
renderNode = (node) => {
|
renderDecorations = (text, block) => {
|
||||||
for (const plugin of this.state.plugins) {
|
for (const plugin of this.state.plugins) {
|
||||||
if (!plugin.renderNode) continue
|
if (!plugin.renderDecorations) continue
|
||||||
const component = plugin.renderNode(node, this.state.state, this)
|
const style = plugin.renderDecorations(text, block, this.state.state, this)
|
||||||
if (component) return component
|
if (style) return style
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return text.characters
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -274,6 +276,21 @@ class Editor extends React.Component {
|
|||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a `node`, cascading through the plugins.
|
||||||
|
*
|
||||||
|
* @param {Node} node
|
||||||
|
* @return {Element} element
|
||||||
|
*/
|
||||||
|
|
||||||
|
renderNode = (node) => {
|
||||||
|
for (const plugin of this.state.plugins) {
|
||||||
|
if (!plugin.renderNode) continue
|
||||||
|
const component = plugin.renderNode(node, this.state.state, this)
|
||||||
|
if (component) return component
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the editor's current plugins from `props` when they change.
|
* Resolve the editor's current plugins from `props` when they change.
|
||||||
*
|
*
|
||||||
@@ -298,34 +315,6 @@ class Editor extends React.Component {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve the editor's current state from `props` when they change.
|
|
||||||
*
|
|
||||||
* This is where we handle decorating the text nodes with the decorator
|
|
||||||
* functions, so that they are always accounted for when rendering.
|
|
||||||
*
|
|
||||||
* @param {State} state
|
|
||||||
* @return {State} state
|
|
||||||
*/
|
|
||||||
|
|
||||||
resolveState = (state) => {
|
|
||||||
const { plugins } = this.state
|
|
||||||
let { document } = state
|
|
||||||
|
|
||||||
document = document.decorateTexts((text) => {
|
|
||||||
for (const plugin of plugins) {
|
|
||||||
if (!plugin.renderDecorations) continue
|
|
||||||
const characters = plugin.renderDecorations(text, state, this)
|
|
||||||
if (characters) return characters
|
|
||||||
}
|
|
||||||
|
|
||||||
return text.characters
|
|
||||||
})
|
|
||||||
|
|
||||||
state = state.merge({ document })
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -17,6 +17,7 @@ class Leaf extends React.Component {
|
|||||||
index: React.PropTypes.number.isRequired,
|
index: React.PropTypes.number.isRequired,
|
||||||
marks: React.PropTypes.object.isRequired,
|
marks: React.PropTypes.object.isRequired,
|
||||||
node: React.PropTypes.object.isRequired,
|
node: React.PropTypes.object.isRequired,
|
||||||
|
ranges: React.PropTypes.object.isRequired,
|
||||||
renderMark: React.PropTypes.func.isRequired,
|
renderMark: React.PropTypes.func.isRequired,
|
||||||
state: React.PropTypes.object.isRequired,
|
state: React.PropTypes.object.isRequired,
|
||||||
text: React.PropTypes.string.isRequired
|
text: React.PropTypes.string.isRequired
|
||||||
@@ -54,7 +55,7 @@ class Leaf extends React.Component {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const { start, end } = OffsetKey.findBounds(node.key, index, state)
|
const { start, end } = OffsetKey.findBounds(index, props.ranges)
|
||||||
return selection.hasEdgeBetween(node, start, end)
|
return selection.hasEdgeBetween(node, start, end)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,15 +68,15 @@ class Leaf extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateSelection() {
|
updateSelection() {
|
||||||
const { state } = this.props
|
const { state, ranges } = this.props
|
||||||
const { selection } = state
|
const { selection } = state
|
||||||
|
|
||||||
// If the selection is not focused we have nothing to do.
|
// If the selection is blurred we have nothing to do.
|
||||||
if (!selection.isFocused) return
|
if (selection.isBlurred) return
|
||||||
|
|
||||||
const { anchorOffset, focusOffset } = selection
|
const { anchorOffset, focusOffset } = selection
|
||||||
const { node, index } = this.props
|
const { node, index } = this.props
|
||||||
const { start, end } = OffsetKey.findBounds(node.key, index, state)
|
const { start, end } = OffsetKey.findBounds(index, ranges)
|
||||||
|
|
||||||
// If neither matches, the selection doesn't start or end here, so exit.
|
// If neither matches, the selection doesn't start or end here, so exit.
|
||||||
const hasAnchor = selection.hasAnchorBetween(node, start, end)
|
const hasAnchor = selection.hasAnchorBetween(node, start, end)
|
||||||
|
@@ -15,6 +15,7 @@ class Node extends React.Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
editor: React.PropTypes.object.isRequired,
|
editor: React.PropTypes.object.isRequired,
|
||||||
node: React.PropTypes.object.isRequired,
|
node: React.PropTypes.object.isRequired,
|
||||||
|
renderDecorations: React.PropTypes.func.isRequired,
|
||||||
renderMark: React.PropTypes.func.isRequired,
|
renderMark: React.PropTypes.func.isRequired,
|
||||||
renderNode: React.PropTypes.func.isRequired,
|
renderNode: React.PropTypes.func.isRequired,
|
||||||
state: React.PropTypes.object.isRequired
|
state: React.PropTypes.object.isRequired
|
||||||
@@ -65,15 +66,16 @@ class Node extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
renderNode = (node) => {
|
renderNode = (node) => {
|
||||||
const { editor, renderMark, renderNode, state } = this.props
|
const { editor, renderDecorations, renderMark, renderNode, state } = this.props
|
||||||
return (
|
return (
|
||||||
<Node
|
<Node
|
||||||
key={node.key}
|
key={node.key}
|
||||||
node={node}
|
node={node}
|
||||||
state={state}
|
state={state}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
renderNode={renderNode}
|
renderDecorations={renderDecorations}
|
||||||
renderMark={renderMark}
|
renderMark={renderMark}
|
||||||
|
renderNode={renderNode}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -118,12 +120,13 @@ class Node extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
renderText = () => {
|
renderText = () => {
|
||||||
const { node, editor, renderMark, state } = this.props
|
const { node, editor, renderDecorations, renderMark, state } = this.props
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
key={node.key}
|
key={node.key}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
node={node}
|
node={node}
|
||||||
|
renderDecorations={renderDecorations}
|
||||||
renderMark={renderMark}
|
renderMark={renderMark}
|
||||||
state={state}
|
state={state}
|
||||||
/>
|
/>
|
||||||
|
@@ -16,6 +16,7 @@ class Text extends React.Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
editor: React.PropTypes.object.isRequired,
|
editor: React.PropTypes.object.isRequired,
|
||||||
node: React.PropTypes.object.isRequired,
|
node: React.PropTypes.object.isRequired,
|
||||||
|
renderDecorations: React.PropTypes.func.isRequired,
|
||||||
renderMark: React.PropTypes.func.isRequired,
|
renderMark: React.PropTypes.func.isRequired,
|
||||||
state: React.PropTypes.object.isRequired
|
state: React.PropTypes.object.isRequired
|
||||||
};
|
};
|
||||||
@@ -30,9 +31,8 @@ class Text extends React.Component {
|
|||||||
|
|
||||||
shouldComponentUpdate(props, state) {
|
shouldComponentUpdate(props, state) {
|
||||||
return (
|
return (
|
||||||
props.state.selection.hasEdgeIn(props.node) ||
|
props.node != this.props.node ||
|
||||||
props.node.decorations != this.props.node.decorations ||
|
props.state.selection.hasEdgeIn(props.node)
|
||||||
props.node.characters != this.props.node.characters
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,28 +58,30 @@ class Text extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
renderLeaves() {
|
renderLeaves() {
|
||||||
const { node } = this.props
|
const { node, state, renderDecorations } = this.props
|
||||||
const ranges = node.getDecoratedRanges()
|
const block = state.document.getClosestBlock(node)
|
||||||
|
const ranges = node.getDecoratedRanges(block, renderDecorations)
|
||||||
|
|
||||||
return ranges.map((range, i, original) => {
|
return ranges.map((range, i, original) => {
|
||||||
const previous = original.slice(0, i)
|
const previous = original.slice(0, i)
|
||||||
const offset = previous.size
|
const offset = previous.size
|
||||||
? previous.map(r => r.text).join('').length
|
? previous.map(r => r.text).join('').length
|
||||||
: 0
|
: 0
|
||||||
return this.renderLeaf(range, i, offset)
|
return this.renderLeaf(ranges, range, i, offset)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a single leaf node given a `range` and `offset`.
|
* Render a single leaf node given a `range` and `offset`.
|
||||||
*
|
*
|
||||||
* @param {Object} range
|
* @param {List} ranges
|
||||||
|
* @param {Range} range
|
||||||
* @param {Number} index
|
* @param {Number} index
|
||||||
* @param {Number} offset
|
* @param {Number} offset
|
||||||
* @return {Element} leaf
|
* @return {Element} leaf
|
||||||
*/
|
*/
|
||||||
|
|
||||||
renderLeaf(range, index, offset) {
|
renderLeaf(ranges, range, index, offset) {
|
||||||
const { node, renderMark, state } = this.props
|
const { node, renderMark, state } = this.props
|
||||||
const text = range.text
|
const text = range.text
|
||||||
const marks = range.marks
|
const marks = range.marks
|
||||||
@@ -92,6 +94,7 @@ class Text extends React.Component {
|
|||||||
node={node}
|
node={node}
|
||||||
text={text}
|
text={text}
|
||||||
marks={marks}
|
marks={marks}
|
||||||
|
ranges={ranges}
|
||||||
renderMark={renderMark}
|
renderMark={renderMark}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@@ -8,10 +8,16 @@ import keycode from 'keycode'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Void.
|
* Void.
|
||||||
|
*
|
||||||
|
* @type {Component}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class Void extends React.Component {
|
class Void extends React.Component {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property types.
|
||||||
|
*/
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: React.PropTypes.any.isRequired,
|
children: React.PropTypes.any.isRequired,
|
||||||
className: React.PropTypes.string,
|
className: React.PropTypes.string,
|
||||||
@@ -21,17 +27,53 @@ class Void extends React.Component {
|
|||||||
style: React.PropTypes.object
|
style: React.PropTypes.object
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default properties.
|
||||||
|
*/
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
style: {}
|
style: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate = (props) => {
|
/**
|
||||||
|
* Should the component update?
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Object} state
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
|
||||||
|
shouldComponentUpdate = (props, state) => {
|
||||||
return (
|
return (
|
||||||
props.node != this.props.node ||
|
props.node != this.props.node ||
|
||||||
props.state.selection.hasEdgeIn(props.node)
|
props.state.selection.hasEdgeIn(props.node)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When one of the wrapper elements it clicked, select the void node.
|
||||||
|
*
|
||||||
|
* @param {Event} e
|
||||||
|
*/
|
||||||
|
|
||||||
|
onClick = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const { state, node, editor } = this.props
|
||||||
|
const next = state
|
||||||
|
.transform()
|
||||||
|
.moveToRangeOf(node)
|
||||||
|
.focus()
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
editor.onChange(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render.
|
||||||
|
*
|
||||||
|
* @return {Element}
|
||||||
|
*/
|
||||||
|
|
||||||
render = () => {
|
render = () => {
|
||||||
const { children, node, className, style } = this.props
|
const { children, node, className, style } = this.props
|
||||||
const Tag = node.kind == 'block' ? 'div' : 'span'
|
const Tag = node.kind == 'block' ? 'div' : 'span'
|
||||||
@@ -43,7 +85,7 @@ class Void extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tag contentEditable={false}>
|
<Tag contentEditable={false} onClick={this.onClick}>
|
||||||
<Tag
|
<Tag
|
||||||
contentEditable
|
contentEditable
|
||||||
suppressContentEditableWarning
|
suppressContentEditableWarning
|
||||||
@@ -51,12 +93,21 @@ class Void extends React.Component {
|
|||||||
style={styles}
|
style={styles}
|
||||||
>
|
>
|
||||||
{this.renderSpacer()}
|
{this.renderSpacer()}
|
||||||
<Tag contentEditable={false}>{children}</Tag>
|
<Tag contentEditable={false} onClick={this.onClick}>{children}</Tag>
|
||||||
</Tag>
|
</Tag>
|
||||||
</Tag>
|
</Tag>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a fake spacer leaf, which will catch the cursor when it the void
|
||||||
|
* node is navigated to with the arrow keys. Having this spacer there means
|
||||||
|
* the browser continues to manage the selection natively, so it keeps track
|
||||||
|
* of the right offset when moving across the block.
|
||||||
|
*
|
||||||
|
* @return {Element}
|
||||||
|
*/
|
||||||
|
|
||||||
renderSpacer = () => {
|
renderSpacer = () => {
|
||||||
const style = {
|
const style = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -70,9 +121,16 @@ class Void extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a fake leaf.
|
||||||
|
*
|
||||||
|
* @return {Element}
|
||||||
|
*/
|
||||||
|
|
||||||
renderLeaf = () => {
|
renderLeaf = () => {
|
||||||
const { node, state } = this.props
|
const { node, state } = this.props
|
||||||
const child = node.getTexts().first()
|
const child = node.getTexts().first()
|
||||||
|
const ranges = child.getRanges()
|
||||||
const text = ''
|
const text = ''
|
||||||
const marks = Mark.createSet()
|
const marks = Mark.createSet()
|
||||||
const index = 0
|
const index = 0
|
||||||
@@ -88,6 +146,7 @@ class Void extends React.Component {
|
|||||||
key={offsetKey}
|
key={offsetKey}
|
||||||
state={state}
|
state={state}
|
||||||
node={child}
|
node={child}
|
||||||
|
ranges={ranges}
|
||||||
index={index}
|
index={index}
|
||||||
text={text}
|
text={text}
|
||||||
marks={marks}
|
marks={marks}
|
||||||
@@ -95,14 +154,16 @@ class Void extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a fake leaf mark.
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
renderLeafMark = (mark) => {
|
renderLeafMark = (mark) => {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLeafRefs = (el) => {
|
|
||||||
this.leaf = el
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -903,21 +903,23 @@ const Node = {
|
|||||||
desc = desc.merge({ nodes })
|
desc = desc.merge({ nodes })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (desc.kind == 'text') {
|
if (desc.kind == 'text' && !removals.has(desc.key)) {
|
||||||
let next = node.getNextSibling(desc)
|
let next = node.getNextSibling(desc)
|
||||||
|
|
||||||
// ...that there are no adjacent text nodes.
|
// ...that there are no adjacent text nodes.
|
||||||
while (next && next.kind == 'text') {
|
if (next && next.kind == 'text') {
|
||||||
const characters = desc.characters.concat(next.characters)
|
while (next && next.kind == 'text') {
|
||||||
desc = desc.merge({ characters })
|
const characters = desc.characters.concat(next.characters)
|
||||||
removals = removals.add(next.key)
|
desc = desc.merge({ characters })
|
||||||
next = node.getNextSibling(next)
|
removals = removals.add(next.key)
|
||||||
|
next = node.getNextSibling(next)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ...that there are no extra empty text nodes.
|
// ...that there are no extra empty text nodes.
|
||||||
if (desc.length == 0) {
|
else if (desc.length == 0) {
|
||||||
const parent = node.getParent(desc)
|
const parent = node.getParent(desc)
|
||||||
if (parent.nodes.size != 1) removals = removals.add(desc.key)
|
if (parent.nodes.size > 1) removals = removals.add(desc.key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -102,44 +102,29 @@ class Text extends new Record(DEFAULTS) {
|
|||||||
.join('')
|
.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Decorate the text node's characters with a `decorator` function.
|
|
||||||
*
|
|
||||||
* @param {Function} decorator
|
|
||||||
* @return {Text} text
|
|
||||||
*/
|
|
||||||
|
|
||||||
decorateCharacters(decorator) {
|
|
||||||
let { characters, cache } = this
|
|
||||||
if (characters == cache) return this
|
|
||||||
|
|
||||||
const decorations = this.getDecoratedCharacters(decorator)
|
|
||||||
if (decorations == characters) return this
|
|
||||||
|
|
||||||
return this.merge({
|
|
||||||
cache: characters,
|
|
||||||
decorations,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the decorated characters.
|
* Get the decorated characters.
|
||||||
*
|
*
|
||||||
|
* @param {Block} block
|
||||||
|
* @param {Function} decorator
|
||||||
* @return {List} characters
|
* @return {List} characters
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getDecoratedCharacters(decorator) {
|
getDecoratedCharacters(block, decorator) {
|
||||||
return decorator(this)
|
return decorator(this, block)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the decorated characters grouped by marks.
|
* Get the decorated characters grouped by marks.
|
||||||
*
|
*
|
||||||
|
* @param {Block} block
|
||||||
|
* @param {Function} decorator
|
||||||
* @return {List} ranges
|
* @return {List} ranges
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getDecoratedRanges() {
|
getDecoratedRanges(block, decorator) {
|
||||||
return this.getRangesForCharacters(this.decorations || this.characters)
|
const decorations = this.getDecoratedCharacters(block, decorator)
|
||||||
|
return this.getRangesForCharacters(decorations)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -87,26 +87,47 @@ function Plugin(options = {}) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
onBeforeInput(e, state, editor) {
|
onBeforeInput(e, state, editor) {
|
||||||
const transform = state.transform().insertText(e.data)
|
const { renderDecorations } = editor
|
||||||
const synthetic = transform.apply()
|
const { startOffset, startText, startBlock } = state
|
||||||
const resolved = editor.resolveState(synthetic)
|
|
||||||
|
// Determine what the characters would be if natively inserted.
|
||||||
|
const prev = startText.getDecoratedCharacters(startBlock, renderDecorations)
|
||||||
|
const char = prev.get(startOffset)
|
||||||
|
const chars = prev
|
||||||
|
.slice(0, startOffset)
|
||||||
|
.push(char.merge({ text: e.data }))
|
||||||
|
.concat(prev.slice(startOffset))
|
||||||
|
|
||||||
|
// Determine what the characters should be, if not natively inserted.
|
||||||
|
let next = state
|
||||||
|
.transform()
|
||||||
|
.insertText(e.data)
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
const nextText = next.startText
|
||||||
|
const nextBlock = next.startBlock
|
||||||
|
const nextChars = nextText.getDecoratedCharacters(nextBlock, renderDecorations)
|
||||||
|
|
||||||
// We do not have to re-render if the current selection is collapsed, the
|
// We do not have to re-render if the current selection is collapsed, the
|
||||||
// current node is not empty, there are no marks on the cursor, and the
|
// current node is not empty, there are no marks on the cursor, and the
|
||||||
// new state has the same decorations as the current one.
|
// natively inserted characters would be the same as the non-native.
|
||||||
const isNative = (
|
const isNative = (
|
||||||
state.isCollapsed &&
|
state.isCollapsed &&
|
||||||
state.startText.text != '' &&
|
state.startText.text != '' &&
|
||||||
state.cursorMarks == null &&
|
state.cursorMarks == null &&
|
||||||
resolved.equals(synthetic)
|
chars.equals(nextChars)
|
||||||
)
|
)
|
||||||
|
|
||||||
state = isNative
|
// Add the `isNative` flag directly, so we don't have to re-transform.
|
||||||
? transform.apply({ isNative })
|
if (isNative) {
|
||||||
: synthetic
|
next = next.merge({ isNative })
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not native, prevent default so that the DOM remains untouched.
|
||||||
if (!isNative) e.preventDefault()
|
if (!isNative) e.preventDefault()
|
||||||
return state
|
|
||||||
|
// Return the new state.
|
||||||
|
return next
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -303,56 +324,18 @@ function Plugin(options = {}) {
|
|||||||
* The core `onSelect` handler.
|
* The core `onSelect` handler.
|
||||||
*
|
*
|
||||||
* @param {Event} e
|
* @param {Event} e
|
||||||
|
* @param {Object} select
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
* @param {Editor} editor
|
* @param {Editor} editor
|
||||||
* @return {State or Null}
|
* @return {State or Null}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
onSelect(e, state, editor) {
|
onSelect(e, select, state, editor) {
|
||||||
let { document, selection } = state
|
const { selection, isNative } = select
|
||||||
const native = window.getSelection()
|
|
||||||
|
|
||||||
// If there are no ranges, the editor was blurred natively.
|
|
||||||
if (!native.rangeCount) {
|
|
||||||
return state
|
|
||||||
.transform()
|
|
||||||
.blur()
|
|
||||||
.apply({ isNative: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the Slate-specific selection based on the native one.
|
|
||||||
const { anchorNode, anchorOffset, focusNode, focusOffset } = native
|
|
||||||
const anchor = OffsetKey.findPoint(anchorNode, anchorOffset, state)
|
|
||||||
const focus = OffsetKey.findPoint(focusNode, focusOffset, state)
|
|
||||||
|
|
||||||
// COMPAT: In Firefox, and potentially other browsers, sometimes a select
|
|
||||||
// event will fire that resolves to the same location as the current
|
|
||||||
// selection, so we can ignore it.
|
|
||||||
if (
|
|
||||||
anchor.key == selection.anchorKey &&
|
|
||||||
anchor.offset == selection.anchorOffset &&
|
|
||||||
focus.key == selection.focusKey &&
|
|
||||||
focus.offset == selection.focusOffset
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the native selection is inside text nodes, we can trust the native
|
|
||||||
// state and not need to re-render.
|
|
||||||
const isNative = (
|
|
||||||
anchorNode.nodeType == 3 &&
|
|
||||||
focusNode.nodeType == 3
|
|
||||||
)
|
|
||||||
|
|
||||||
return state
|
return state
|
||||||
.transform()
|
.transform()
|
||||||
|
.moveTo(selection)
|
||||||
.focus()
|
.focus()
|
||||||
.moveTo({
|
|
||||||
anchorKey: anchor.key,
|
|
||||||
anchorOffset: anchor.offset,
|
|
||||||
focusKey: focus.key,
|
|
||||||
focusOffset: focus.offset
|
|
||||||
})
|
|
||||||
.apply({ isNative })
|
.apply({ isNative })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -13,17 +13,14 @@ const ATTRIBUTE = 'data-offset-key'
|
|||||||
const SELECTOR = `[${ATTRIBUTE}]`
|
const SELECTOR = `[${ATTRIBUTE}]`
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the start and end bounds from a node's `key` and `index`.
|
* Find the start and end bounds from an `offsetKey` and `ranges`.
|
||||||
*
|
*
|
||||||
* @param {String} key
|
|
||||||
* @param {Number} index
|
* @param {Number} index
|
||||||
* @param {State} state
|
* @param {List} ranges
|
||||||
* @return {Object}
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function findBounds(key, index, state) {
|
function findBounds(index, ranges) {
|
||||||
const text = state.document.assertDescendant(key)
|
|
||||||
const ranges = text.getDecoratedRanges()
|
|
||||||
const range = ranges.get(index)
|
const range = ranges.get(index)
|
||||||
const start = ranges
|
const start = ranges
|
||||||
.slice(0, index)
|
.slice(0, index)
|
||||||
@@ -41,33 +38,26 @@ function findBounds(key, index, state) {
|
|||||||
* From a `element`, find the closest parent's offset key.
|
* From a `element`, find the closest parent's offset key.
|
||||||
*
|
*
|
||||||
* @param {Element} element
|
* @param {Element} element
|
||||||
* @return {String or Null}
|
* @param {Number} offset
|
||||||
*/
|
|
||||||
|
|
||||||
function findKey(element) {
|
|
||||||
if (element.nodeType == 3) element = element.parentNode
|
|
||||||
const parent = element.closest(SELECTOR)
|
|
||||||
if (!parent) return null
|
|
||||||
return parent.getAttribute(ATTRIBUTE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the selection point from an `element`, `offset`, and `state`.
|
|
||||||
*
|
|
||||||
* @param {Element} element
|
|
||||||
* @param {Offset} offset
|
|
||||||
* @param {State} state
|
|
||||||
* @return {Object}
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function findPoint(element, offset, state) {
|
function findKey(element, offset) {
|
||||||
let offsetKey = findKey(element)
|
if (element.nodeType == 3) element = element.parentNode
|
||||||
|
|
||||||
|
const parent = element.closest(SELECTOR)
|
||||||
|
const children = element.querySelectorAll(SELECTOR)
|
||||||
|
let offsetKey
|
||||||
|
|
||||||
|
// Get the key from a parent if one exists.
|
||||||
|
if (parent) {
|
||||||
|
offsetKey = parent.getAttribute(ATTRIBUTE)
|
||||||
|
}
|
||||||
|
|
||||||
// COMPAT: In Firefox, and potentially other browsers, when performing a
|
// COMPAT: In Firefox, and potentially other browsers, when performing a
|
||||||
// "select all" action, a parent element is selected instead of the text. In
|
// "select all" action, a parent element is selected instead of the text. In
|
||||||
// this case, we need to select the proper inner text nodes. (2016/07/26)
|
// this case, we need to select the proper inner text nodes. (2016/07/26)
|
||||||
if (!offsetKey) {
|
else if (children.length) {
|
||||||
const children = element.querySelectorAll(SELECTOR)
|
|
||||||
let child = children[0]
|
let child = children[0]
|
||||||
|
|
||||||
if (offset != 0) {
|
if (offset != 0) {
|
||||||
@@ -78,8 +68,44 @@ function findPoint(element, offset, state) {
|
|||||||
offsetKey = child.getAttribute(ATTRIBUTE)
|
offsetKey = child.getAttribute(ATTRIBUTE)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { key, index } = parse(offsetKey)
|
// Otherwise, for void node scenarios, a cousin element will be selected, and
|
||||||
const { start, end } = findBounds(key, index, state)
|
// we need to select the first text node cousin we can find.
|
||||||
|
else {
|
||||||
|
while (element = element.parentNode) {
|
||||||
|
const cousin = element.querySelector(SELECTOR)
|
||||||
|
if (!cousin) continue
|
||||||
|
offsetKey = cousin.getAttribute(ATTRIBUTE)
|
||||||
|
offset = cousin.textContent.length
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we still didn't find an offset key, error. This is a bug.
|
||||||
|
if (!offsetKey) {
|
||||||
|
throw new Error(`Unable to find offset key for ${element} with offset "${offset}".`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the offset key.
|
||||||
|
const parsed = parse(offsetKey)
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: parsed.key,
|
||||||
|
index: parsed.index,
|
||||||
|
offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the selection point from an `offsetKey` and `ranges`.
|
||||||
|
*
|
||||||
|
* @param {Object} offsetKey
|
||||||
|
* @param {List} ranges
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function findPoint(offsetKey, ranges) {
|
||||||
|
let { key, index, offset } = offsetKey
|
||||||
|
const { start, end } = findBounds(index, ranges)
|
||||||
|
|
||||||
// Don't let the offset be outside of the start and end bounds.
|
// Don't let the offset be outside of the start and end bounds.
|
||||||
offset = start + offset
|
offset = start + offset
|
||||||
@@ -88,27 +114,13 @@ function findPoint(element, offset, state) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
|
index,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
offset
|
offset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the range from an `element`.
|
|
||||||
*
|
|
||||||
* @param {Element} element
|
|
||||||
* @param {State} state
|
|
||||||
* @return {Range}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function findRange(element, state) {
|
|
||||||
const offsetKey = findKey(element)
|
|
||||||
const { key, index } = parse(offsetKey)
|
|
||||||
const text = state.document.getDescendant(key)
|
|
||||||
const ranges = text.getDecoratedRanges()
|
|
||||||
const range = ranges.get(index)
|
|
||||||
return range
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse an offset key `string`.
|
* Parse an offset key `string`.
|
||||||
*
|
*
|
||||||
@@ -147,7 +159,6 @@ export default {
|
|||||||
findBounds,
|
findBounds,
|
||||||
findKey,
|
findKey,
|
||||||
findPoint,
|
findPoint,
|
||||||
findRange,
|
|
||||||
parse,
|
parse,
|
||||||
stringify
|
stringify
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user