mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-27 00:54:22 +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-class-assign": "error",
|
||||
"no-const-assign": "error",
|
||||
"no-console": "warn",
|
||||
"no-debugger": "warn",
|
||||
"no-dupe-args": "error",
|
||||
"no-dupe-class-members": "error",
|
||||
|
@@ -129,12 +129,11 @@ class CodeHighlighting extends React.Component {
|
||||
* Render decorations on `text` nodes inside code blocks.
|
||||
*
|
||||
* @param {Text} text
|
||||
* @param {Block} block
|
||||
* @return {Characters}
|
||||
*/
|
||||
|
||||
renderDecorations = (text, state) => {
|
||||
const { document } = state
|
||||
const block = document.getClosestBlock(text)
|
||||
renderDecorations = (text, block) => {
|
||||
if (block.type != 'code') return text.characters
|
||||
|
||||
let characters = text.characters.asMutable()
|
||||
|
@@ -42,6 +42,7 @@ class Content extends React.Component {
|
||||
onPaste: React.PropTypes.func.isRequired,
|
||||
onSelect: React.PropTypes.func.isRequired,
|
||||
readOnly: React.PropTypes.bool.isRequired,
|
||||
renderDecorations: React.PropTypes.func.isRequired,
|
||||
renderMark: React.PropTypes.func.isRequired,
|
||||
renderNode: React.PropTypes.func.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.
|
||||
*
|
||||
@@ -333,7 +364,7 @@ class Content extends React.Component {
|
||||
if (this.props.readOnly) return
|
||||
e.preventDefault()
|
||||
|
||||
const { state } = this.props
|
||||
const { state, renderDecorations } = this.props
|
||||
const { selection } = state
|
||||
const data = e.nativeEvent.dataTransfer
|
||||
const drop = {}
|
||||
@@ -355,7 +386,7 @@ class Content extends React.Component {
|
||||
|
||||
const startNode = range.startContainer
|
||||
const startOffset = range.startOffset
|
||||
const point = OffsetKey.findPoint(startNode, startOffset, state)
|
||||
const point = this.getPoint(startNode, startOffset)
|
||||
const target = Selection.create({
|
||||
anchorKey: point.key,
|
||||
anchorOffset: point.offset,
|
||||
@@ -418,16 +449,16 @@ class Content extends React.Component {
|
||||
*/
|
||||
|
||||
onInput = (e) => {
|
||||
let { state } = this.props
|
||||
let { state, renderDecorations } = this.props
|
||||
const { selection } = state
|
||||
const native = window.getSelection()
|
||||
const { anchorNode, anchorOffset, focusOffset } = native
|
||||
let { textContent } = anchorNode
|
||||
const offsetKey = OffsetKey.findKey(anchorNode)
|
||||
const { key, index } = OffsetKey.parse(offsetKey)
|
||||
const { start, end } = OffsetKey.findBounds(key, index, state)
|
||||
const range = OffsetKey.findRange(anchorNode, state)
|
||||
const point = this.getPoint(anchorNode, anchorOffset)
|
||||
const { key, index, start, end } = point
|
||||
const ranges = this.getRanges(key)
|
||||
const range = ranges.get(index)
|
||||
const { text, marks } = range
|
||||
let { textContent } = anchorNode
|
||||
|
||||
// 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.
|
||||
@@ -564,7 +595,52 @@ class Content extends React.Component {
|
||||
if (this.tmp.isCopying) 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) => {
|
||||
const { editor, renderMark, renderNode, state } = this.props
|
||||
const { editor, renderDecorations, renderMark, renderNode, state } = this.props
|
||||
return (
|
||||
<Node
|
||||
key={node.key}
|
||||
node={node}
|
||||
state={state}
|
||||
editor={editor}
|
||||
renderNode={renderNode}
|
||||
renderDecorations={renderDecorations}
|
||||
renderMark={renderMark}
|
||||
renderNode={renderNode}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@@ -62,7 +62,7 @@ class Editor extends React.Component {
|
||||
this.tmp = {}
|
||||
this.state = {}
|
||||
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) => {
|
||||
this.state.state = props.state
|
||||
|
||||
if (props.plugins != this.props.plugins) {
|
||||
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}
|
||||
onSelect={this.onSelect}
|
||||
readOnly={this.props.readOnly}
|
||||
renderDecorations={this.renderDecorations}
|
||||
renderMark={this.renderMark}
|
||||
renderNode={this.renderNode}
|
||||
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
|
||||
* @return {Element} element
|
||||
* @param {Block} text
|
||||
* @param {Block} block
|
||||
* @return {Object} style
|
||||
*/
|
||||
|
||||
renderNode = (node) => {
|
||||
renderDecorations = (text, block) => {
|
||||
for (const plugin of this.state.plugins) {
|
||||
if (!plugin.renderNode) continue
|
||||
const component = plugin.renderNode(node, this.state.state, this)
|
||||
if (component) return component
|
||||
if (!plugin.renderDecorations) continue
|
||||
const style = plugin.renderDecorations(text, block, this.state.state, this)
|
||||
if (style) return style
|
||||
}
|
||||
|
||||
return text.characters
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,6 +276,21 @@ class Editor extends React.Component {
|
||||
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.
|
||||
*
|
||||
@@ -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,
|
||||
marks: React.PropTypes.object.isRequired,
|
||||
node: React.PropTypes.object.isRequired,
|
||||
ranges: React.PropTypes.object.isRequired,
|
||||
renderMark: React.PropTypes.func.isRequired,
|
||||
state: React.PropTypes.object.isRequired,
|
||||
text: React.PropTypes.string.isRequired
|
||||
@@ -54,7 +55,7 @@ class Leaf extends React.Component {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -67,15 +68,15 @@ class Leaf extends React.Component {
|
||||
}
|
||||
|
||||
updateSelection() {
|
||||
const { state } = this.props
|
||||
const { state, ranges } = this.props
|
||||
const { selection } = state
|
||||
|
||||
// If the selection is not focused we have nothing to do.
|
||||
if (!selection.isFocused) return
|
||||
// If the selection is blurred we have nothing to do.
|
||||
if (selection.isBlurred) return
|
||||
|
||||
const { anchorOffset, focusOffset } = selection
|
||||
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.
|
||||
const hasAnchor = selection.hasAnchorBetween(node, start, end)
|
||||
|
@@ -15,6 +15,7 @@ class Node extends React.Component {
|
||||
static propTypes = {
|
||||
editor: React.PropTypes.object.isRequired,
|
||||
node: React.PropTypes.object.isRequired,
|
||||
renderDecorations: React.PropTypes.func.isRequired,
|
||||
renderMark: React.PropTypes.func.isRequired,
|
||||
renderNode: React.PropTypes.func.isRequired,
|
||||
state: React.PropTypes.object.isRequired
|
||||
@@ -65,15 +66,16 @@ class Node extends React.Component {
|
||||
*/
|
||||
|
||||
renderNode = (node) => {
|
||||
const { editor, renderMark, renderNode, state } = this.props
|
||||
const { editor, renderDecorations, renderMark, renderNode, state } = this.props
|
||||
return (
|
||||
<Node
|
||||
key={node.key}
|
||||
node={node}
|
||||
state={state}
|
||||
editor={editor}
|
||||
renderNode={renderNode}
|
||||
renderDecorations={renderDecorations}
|
||||
renderMark={renderMark}
|
||||
renderNode={renderNode}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -118,12 +120,13 @@ class Node extends React.Component {
|
||||
*/
|
||||
|
||||
renderText = () => {
|
||||
const { node, editor, renderMark, state } = this.props
|
||||
const { node, editor, renderDecorations, renderMark, state } = this.props
|
||||
return (
|
||||
<Text
|
||||
key={node.key}
|
||||
editor={editor}
|
||||
node={node}
|
||||
renderDecorations={renderDecorations}
|
||||
renderMark={renderMark}
|
||||
state={state}
|
||||
/>
|
||||
|
@@ -16,6 +16,7 @@ class Text extends React.Component {
|
||||
static propTypes = {
|
||||
editor: React.PropTypes.object.isRequired,
|
||||
node: React.PropTypes.object.isRequired,
|
||||
renderDecorations: React.PropTypes.func.isRequired,
|
||||
renderMark: React.PropTypes.func.isRequired,
|
||||
state: React.PropTypes.object.isRequired
|
||||
};
|
||||
@@ -30,9 +31,8 @@ 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
|
||||
props.node != this.props.node ||
|
||||
props.state.selection.hasEdgeIn(props.node)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -58,28 +58,30 @@ class Text extends React.Component {
|
||||
*/
|
||||
|
||||
renderLeaves() {
|
||||
const { node } = this.props
|
||||
const ranges = node.getDecoratedRanges()
|
||||
const { node, state, renderDecorations } = this.props
|
||||
const block = state.document.getClosestBlock(node)
|
||||
const ranges = node.getDecoratedRanges(block, renderDecorations)
|
||||
|
||||
return ranges.map((range, i, original) => {
|
||||
const previous = original.slice(0, i)
|
||||
const offset = previous.size
|
||||
? previous.map(r => r.text).join('').length
|
||||
: 0
|
||||
return this.renderLeaf(range, i, offset)
|
||||
return this.renderLeaf(ranges, range, i, 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} offset
|
||||
* @return {Element} leaf
|
||||
*/
|
||||
|
||||
renderLeaf(range, index, offset) {
|
||||
renderLeaf(ranges, range, index, offset) {
|
||||
const { node, renderMark, state } = this.props
|
||||
const text = range.text
|
||||
const marks = range.marks
|
||||
@@ -92,6 +94,7 @@ class Text extends React.Component {
|
||||
node={node}
|
||||
text={text}
|
||||
marks={marks}
|
||||
ranges={ranges}
|
||||
renderMark={renderMark}
|
||||
/>
|
||||
)
|
||||
|
@@ -8,10 +8,16 @@ import keycode from 'keycode'
|
||||
|
||||
/**
|
||||
* Void.
|
||||
*
|
||||
* @type {Component}
|
||||
*/
|
||||
|
||||
class Void extends React.Component {
|
||||
|
||||
/**
|
||||
* Property types.
|
||||
*/
|
||||
|
||||
static propTypes = {
|
||||
children: React.PropTypes.any.isRequired,
|
||||
className: React.PropTypes.string,
|
||||
@@ -21,17 +27,53 @@ class Void extends React.Component {
|
||||
style: React.PropTypes.object
|
||||
};
|
||||
|
||||
/**
|
||||
* Default properties.
|
||||
*/
|
||||
|
||||
static defaultProps = {
|
||||
style: {}
|
||||
}
|
||||
|
||||
shouldComponentUpdate = (props) => {
|
||||
/**
|
||||
* Should the component update?
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Object} state
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
shouldComponentUpdate = (props, state) => {
|
||||
return (
|
||||
props.node != this.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 = () => {
|
||||
const { children, node, className, style } = this.props
|
||||
const Tag = node.kind == 'block' ? 'div' : 'span'
|
||||
@@ -43,7 +85,7 @@ class Void extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<Tag contentEditable={false}>
|
||||
<Tag contentEditable={false} onClick={this.onClick}>
|
||||
<Tag
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
@@ -51,12 +93,21 @@ class Void extends React.Component {
|
||||
style={styles}
|
||||
>
|
||||
{this.renderSpacer()}
|
||||
<Tag contentEditable={false}>{children}</Tag>
|
||||
<Tag contentEditable={false} onClick={this.onClick}>{children}</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 = () => {
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
@@ -70,9 +121,16 @@ class Void extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a fake leaf.
|
||||
*
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
renderLeaf = () => {
|
||||
const { node, state } = this.props
|
||||
const child = node.getTexts().first()
|
||||
const ranges = child.getRanges()
|
||||
const text = ''
|
||||
const marks = Mark.createSet()
|
||||
const index = 0
|
||||
@@ -88,6 +146,7 @@ class Void extends React.Component {
|
||||
key={offsetKey}
|
||||
state={state}
|
||||
node={child}
|
||||
ranges={ranges}
|
||||
index={index}
|
||||
text={text}
|
||||
marks={marks}
|
||||
@@ -95,14 +154,16 @@ class Void extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a fake leaf mark.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
renderLeafMark = (mark) => {
|
||||
return {}
|
||||
}
|
||||
|
||||
renderLeafRefs = (el) => {
|
||||
this.leaf = el
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -903,21 +903,23 @@ const Node = {
|
||||
desc = desc.merge({ nodes })
|
||||
}
|
||||
|
||||
if (desc.kind == 'text') {
|
||||
if (desc.kind == 'text' && !removals.has(desc.key)) {
|
||||
let next = node.getNextSibling(desc)
|
||||
|
||||
// ...that there are no adjacent text nodes.
|
||||
while (next && next.kind == 'text') {
|
||||
const characters = desc.characters.concat(next.characters)
|
||||
desc = desc.merge({ characters })
|
||||
removals = removals.add(next.key)
|
||||
next = node.getNextSibling(next)
|
||||
if (next && next.kind == 'text') {
|
||||
while (next && next.kind == 'text') {
|
||||
const characters = desc.characters.concat(next.characters)
|
||||
desc = desc.merge({ characters })
|
||||
removals = removals.add(next.key)
|
||||
next = node.getNextSibling(next)
|
||||
}
|
||||
}
|
||||
|
||||
// ...that there are no extra empty text nodes.
|
||||
if (desc.length == 0) {
|
||||
else if (desc.length == 0) {
|
||||
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('')
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param {Block} block
|
||||
* @param {Function} decorator
|
||||
* @return {List} characters
|
||||
*/
|
||||
|
||||
getDecoratedCharacters(decorator) {
|
||||
return decorator(this)
|
||||
getDecoratedCharacters(block, decorator) {
|
||||
return decorator(this, block)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the decorated characters grouped by marks.
|
||||
*
|
||||
* @param {Block} block
|
||||
* @param {Function} decorator
|
||||
* @return {List} ranges
|
||||
*/
|
||||
|
||||
getDecoratedRanges() {
|
||||
return this.getRangesForCharacters(this.decorations || this.characters)
|
||||
getDecoratedRanges(block, decorator) {
|
||||
const decorations = this.getDecoratedCharacters(block, decorator)
|
||||
return this.getRangesForCharacters(decorations)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -87,26 +87,47 @@ function Plugin(options = {}) {
|
||||
*/
|
||||
|
||||
onBeforeInput(e, state, editor) {
|
||||
const transform = state.transform().insertText(e.data)
|
||||
const synthetic = transform.apply()
|
||||
const resolved = editor.resolveState(synthetic)
|
||||
const { renderDecorations } = editor
|
||||
const { startOffset, startText, startBlock } = state
|
||||
|
||||
// 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
|
||||
// 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 = (
|
||||
state.isCollapsed &&
|
||||
state.startText.text != '' &&
|
||||
state.cursorMarks == null &&
|
||||
resolved.equals(synthetic)
|
||||
chars.equals(nextChars)
|
||||
)
|
||||
|
||||
state = isNative
|
||||
? transform.apply({ isNative })
|
||||
: synthetic
|
||||
// Add the `isNative` flag directly, so we don't have to re-transform.
|
||||
if (isNative) {
|
||||
next = next.merge({ isNative })
|
||||
}
|
||||
|
||||
// If not native, prevent default so that the DOM remains untouched.
|
||||
if (!isNative) e.preventDefault()
|
||||
return state
|
||||
|
||||
// Return the new state.
|
||||
return next
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -303,56 +324,18 @@ function Plugin(options = {}) {
|
||||
* The core `onSelect` handler.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @param {Object} select
|
||||
* @param {State} state
|
||||
* @param {Editor} editor
|
||||
* @return {State or Null}
|
||||
*/
|
||||
|
||||
onSelect(e, state, editor) {
|
||||
let { document, selection } = state
|
||||
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
|
||||
)
|
||||
|
||||
onSelect(e, select, state, editor) {
|
||||
const { selection, isNative } = select
|
||||
return state
|
||||
.transform()
|
||||
.moveTo(selection)
|
||||
.focus()
|
||||
.moveTo({
|
||||
anchorKey: anchor.key,
|
||||
anchorOffset: anchor.offset,
|
||||
focusKey: focus.key,
|
||||
focusOffset: focus.offset
|
||||
})
|
||||
.apply({ isNative })
|
||||
},
|
||||
|
||||
|
@@ -13,17 +13,14 @@ const ATTRIBUTE = 'data-offset-key'
|
||||
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 {State} state
|
||||
* @param {List} ranges
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function findBounds(key, index, state) {
|
||||
const text = state.document.assertDescendant(key)
|
||||
const ranges = text.getDecoratedRanges()
|
||||
function findBounds(index, ranges) {
|
||||
const range = ranges.get(index)
|
||||
const start = ranges
|
||||
.slice(0, index)
|
||||
@@ -41,33 +38,26 @@ function findBounds(key, index, state) {
|
||||
* From a `element`, find the closest parent's offset key.
|
||||
*
|
||||
* @param {Element} element
|
||||
* @return {String or Null}
|
||||
*/
|
||||
|
||||
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
|
||||
* @param {Number} offset
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function findPoint(element, offset, state) {
|
||||
let offsetKey = findKey(element)
|
||||
function findKey(element, offset) {
|
||||
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
|
||||
// "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)
|
||||
if (!offsetKey) {
|
||||
const children = element.querySelectorAll(SELECTOR)
|
||||
else if (children.length) {
|
||||
let child = children[0]
|
||||
|
||||
if (offset != 0) {
|
||||
@@ -78,8 +68,44 @@ function findPoint(element, offset, state) {
|
||||
offsetKey = child.getAttribute(ATTRIBUTE)
|
||||
}
|
||||
|
||||
const { key, index } = parse(offsetKey)
|
||||
const { start, end } = findBounds(key, index, state)
|
||||
// Otherwise, for void node scenarios, a cousin element will be selected, and
|
||||
// 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.
|
||||
offset = start + offset
|
||||
@@ -88,27 +114,13 @@ function findPoint(element, offset, state) {
|
||||
|
||||
return {
|
||||
key,
|
||||
index,
|
||||
start,
|
||||
end,
|
||||
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`.
|
||||
*
|
||||
@@ -147,7 +159,6 @@ export default {
|
||||
findBounds,
|
||||
findKey,
|
||||
findPoint,
|
||||
findRange,
|
||||
parse,
|
||||
stringify
|
||||
}
|
||||
|
Reference in New Issue
Block a user