diff --git a/.eslintrc b/.eslintrc
index 57c7c0b4e..df7882be4 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -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",
diff --git a/examples/code-highlighting/index.js b/examples/code-highlighting/index.js
index 681377e52..00eed5659 100644
--- a/examples/code-highlighting/index.js
+++ b/examples/code-highlighting/index.js
@@ -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()
diff --git a/lib/components/content.js b/lib/components/content.js
index e67fdaa0e..ffe2ca860 100644
--- a/lib/components/content.js
+++ b/lib/components/content.js
@@ -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 (
)
}
diff --git a/lib/components/editor.js b/lib/components/editor.js
index 3e2e5647c..5b2a4a532 100644
--- a/lib/components/editor.js
+++ b/lib/components/editor.js
@@ -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
- }
-
}
/**
diff --git a/lib/components/leaf.js b/lib/components/leaf.js
index b0f820671..e5cf4d24c 100644
--- a/lib/components/leaf.js
+++ b/lib/components/leaf.js
@@ -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)
diff --git a/lib/components/node.js b/lib/components/node.js
index 6b05e9740..9369dd03d 100644
--- a/lib/components/node.js
+++ b/lib/components/node.js
@@ -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 (
)
}
@@ -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 (
diff --git a/lib/components/text.js b/lib/components/text.js
index b6ab80764..35dea6cac 100644
--- a/lib/components/text.js
+++ b/lib/components/text.js
@@ -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}
/>
)
diff --git a/lib/components/void.js b/lib/components/void.js
index 06c6a6b7e..e4de309ba 100644
--- a/lib/components/void.js
+++ b/lib/components/void.js
@@ -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 (
-
+
{this.renderSpacer()}
- {children}
+ {children}
)
}
+ /**
+ * 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
- }
-
}
/**
diff --git a/lib/models/node.js b/lib/models/node.js
index ec0c63ad2..9051bb7af 100644
--- a/lib/models/node.js
+++ b/lib/models/node.js
@@ -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)
}
}
diff --git a/lib/models/text.js b/lib/models/text.js
index 96c759a0c..cddbc9668 100644
--- a/lib/models/text.js
+++ b/lib/models/text.js
@@ -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)
}
/**
diff --git a/lib/plugins/core.js b/lib/plugins/core.js
index 4a145320c..3fa94af80 100644
--- a/lib/plugins/core.js
+++ b/lib/plugins/core.js
@@ -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 })
},
diff --git a/lib/utils/offset-key.js b/lib/utils/offset-key.js
index 65c5826bf..df8def3bc 100644
--- a/lib/utils/offset-key.js
+++ b/lib/utils/offset-key.js
@@ -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
}