1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-01-17 21:49:20 +01:00

lots of progress and cleanup

This commit is contained in:
Ian Storm Taylor 2016-06-17 00:09:54 -07:00
parent e0ca80384e
commit 8ecf90bf7c
11 changed files with 676 additions and 349 deletions

View File

@ -10,40 +10,33 @@ import ReactDOM from 'react-dom'
const state = {
nodes: [
{
kind: 'node',
type: 'code',
data: {},
nodes: [
{
type: 'text',
ranges: [
{
text: 'A\nfew\nlines\nof\ncode.',
marks: []
text: 'A\nfew\nlines\nof\ncode.'
}
]
}
]
},
{
kind: 'node',
type: 'paragraph',
data: {},
nodes: [
{
type: 'text',
ranges: [
{
text: 'A ',
marks: []
text: 'A '
},
{
text: 'simple',
marks: ['bold']
},
{
text: ' paragraph of text.',
marks: []
text: ' paragraph of text.'
}
]
}

View File

@ -1,9 +1,9 @@
import OffsetKey from '../utils/offset-key'
import React from 'react'
import ReactDOM from 'react-dom'
import Text from './text'
import TextModel from '../models/text'
import findSelection from '../utils/find-selection'
import keycode from 'keycode'
/**
@ -66,37 +66,28 @@ class Content extends React.Component {
const el = ReactDOM.findDOMNode(this)
const { anchorNode, anchorOffset, focusNode, focusOffset } = native
const anchorIsText = anchorNode.nodeType == 3
const focusIsText = focusNode.nodeType == 3
const anchor = OffsetKey.findPoint(anchorNode, anchorOffset)
const focus = OffsetKey.findPoint(focusNode, focusOffset)
const edges = state.filterNodes((node) => {
return node.key == anchor.key || node.key == focus.key
})
// If both are text nodes, find their parents and create the selection.
if (anchorIsText && focusIsText) {
const anchor = findSelection(anchorNode, anchorOffset)
const focus = findSelection(focusNode, focusOffset)
const { nodes } = state
const isBackward = (
(edges.size == 2 && edges.first().key == focus.key) ||
(edges.size == 1 && anchor.offset > focus.offset)
)
const startAndEnd = state.filterNodes((node) => {
return node.key == anchor.key || node.key == focus.key
})
selection = selection.merge({
anchorKey: anchor.key,
anchorOffset: anchor.offset,
focusKey: focus.key,
focusOffset: focus.offset,
isBackward: isBackward,
isFocused: true
})
const isBackward = (
(startAndEnd.size == 2 && startAndEnd.first().key == focus.key) ||
(startAndEnd.size == 1 && anchor.offset > focus.offset)
)
selection = selection.merge({
anchorKey: anchor.key,
anchorOffset: anchor.offset,
focusKey: focus.key,
focusOffset: focus.offset,
isBackward: isBackward,
isFocused: true
})
state = state.set('selection', selection)
this.onChange(state)
return
}
state = state.set('selection', selection)
this.onChange(state)
}
/**

View File

@ -1,4 +1,5 @@
import OffsetKey from '../utils/offset-key'
import React from 'react'
import ReactDOM from 'react-dom'
import createOffsetKey from '../utils/create-offset-key'
@ -96,26 +97,24 @@ class Leaf extends React.Component {
render() {
const { node, range } = this.props
const { text } = range
const offsetKey = createOffsetKey(node, range)
const styles = this.renderStyles()
const offsetKey = OffsetKey.stringify({
key: node.key,
start: range.offset,
end: range.offset + range.text.length
})
return (
<span
style={styles}
data-key={offsetKey}
data-offset-key={offsetKey}
data-type='leaf'
>
{text}
{text == '' ? <br/> : text}
</span>
)
}
renderOffsetKey() {
const { node, offset, text } = this.props
const key = `${node.key}.${offset}-${offset + text.length}`
return key
}
renderStyles() {
const { range, renderMark } = this.props
const { marks } = range

View File

@ -20,7 +20,9 @@ class Text extends React.Component {
const { node } = this.props
const { characters } = node
const ranges = convertCharactersToRanges(characters)
const leaves = ranges.map(range => this.renderLeaf(range))
const leaves = ranges.length
? ranges.map(range => this.renderLeaf(range))
: this.renderSpacerLeaf()
return (
<span
@ -47,6 +49,14 @@ class Text extends React.Component {
)
}
renderSpacerLeaf() {
return this.renderLeaf({
offset: 0,
text: '',
marks: []
})
}
}
/**

View File

@ -128,11 +128,15 @@ class Node extends NodeRecord {
/**
* Get the child node after the one by `key`.
*
* @param {String} key
* @param {String or Node} key
* @return {Node or Null}
*/
getNodeAfter(key) {
getNextNode(key) {
if (typeof key != 'string') {
key = key.key
}
const shallow = this.nodes
.skipUntil(node => node.key == key)
.rest()
@ -141,11 +145,61 @@ class Node extends NodeRecord {
if (shallow != null) return shallow
return this.nodes
.map(node => node instanceof Node ? node.getNodeAfter(key) : null)
.map(node => node instanceof Node ? node.getNextNode(key) : null)
.filter(node => node)
.first()
}
/**
* Get the child node before the one by `key`.
*
* @param {String or Node} key
* @return {Node or Null}
*/
getPreviousNode(key) {
if (typeof key != 'string') {
key = key.key
}
const matches = this.nodes.get(key)
if (matches) {
return this.nodes
.takeUntil(node => node.key == key)
.last()
}
return this.nodes
.map(node => node instanceof Node ? node.getPreviousNode(key) : null)
.filter(node => node)
.first()
}
/**
* Get the parent of a child node by `key`.
*
* @param {String or Node} key
* @return {Node or Null}
*/
getParentNode(key) {
if (typeof key != 'string') {
key = key.key
}
if (this.nodes.get(key)) return this
let node = null
this.nodes.forEach((child) => {
if (!(child instanceof Node)) return
const match = child.getParentNode(key)
if (match) node = match
})
return node
}
/**
* Get the child text node at `offset`.
*
@ -169,46 +223,76 @@ class Node extends NodeRecord {
}
/**
* Get the parent of a child node by `key`.
* Recursively check if a child node exists by `key`.
*
* @param {String} key
* @return {Node or Null}
* @param {String or Node} key
* @return {Boolean} true
*/
getParentOfNode(key) {
if (this.nodes.get(key)) return this
let node = null
hasNode(key) {
if (typeof key != 'string') {
key = key.key
}
this.nodes.forEach((child) => {
if (!(child instanceof Node)) return
const match = child.getParentOfNode(key)
if (match) node = match
})
const shallow = this.nodes.has(key)
if (shallow) return true
return node
const deep = this.nodes
.map(node => node instanceof Node ? node.hasNode(key) : false)
.some(has => has)
if (deep) return true
return false
}
/**
* Push a new `node` onto the map of nodes.
*
* @param {String or Node} key
* @param {Node} node
* @return {Node} node
*/
pushNode(node) {
let nodes = this.nodes.set(node.key, node)
pushNode(key, node) {
if (typeof key != 'string') {
node = key
key = node.key
}
let nodes = this.nodes.set(key, node)
return this.merge({ nodes })
}
/**
* Remove a `node` from the children node map.
*
* @param {String or Node} key
* @return {Node} node
*/
removeNode(key) {
if (typeof key != 'string') {
key = key.key
}
let nodes = this.nodes.remove(key)
return this.merge({ nodes })
}
/**
* Set a new value for a child node by `key`.
*
* @param {String} key
* @param {String or Node} key
* @param {Node} node
* @return {Node} node
*/
setNode(key, node) {
updateNode(key, node) {
if (typeof key != 'string') {
node = key
key = node.key
}
if (this.nodes.get(key)) {
const nodes = this.nodes.set(key, node)
return this.set('nodes', nodes)
@ -216,7 +300,7 @@ class Node extends NodeRecord {
const nodes = this.nodes.map((child) => {
return child instanceof Node
? child.setNode(key, node)
? child.updateNode(key, node)
: child
})

View File

@ -60,31 +60,190 @@ class Selection extends SelectionRecord {
}
/**
* Check whether the selection is at the start of a `state`.
* Check whether the selection is at the start of a `node`.
*
* @param {State} state
* @param {Node} node
* @return {Boolean} isAtStart
*/
isAtStartOf(state) {
const { nodes } = state
const { startKey } = this
const first = nodes.first()
return startKey == first.key
isAtStartOf(node) {
const { startKey, startOffset } = this
const first = node.type == 'text' ? node : node.nodes.first()
return startKey == first.key && startOffset == 0
}
/**
* Check whether the selection is at the end of a `state`.
* Check whether the selection is at the end of a `node`.
*
* @param {State} state
* @param {Node} node
* @return {Boolean} isAtEnd
*/
isAtEndOf(state) {
const { nodes } = state
const { endKey } = this
const last = nodes.last()
return endKey == last.key
isAtEndOf(node) {
const { endKey, endOffset } = this
const last = node.type == 'text' ? node : node.nodes.last()
return endKey == last.key && endOffset == last.length
}
/**
* Move the selection to a set of `properties`.
*
* @param {Object} properties
* @return {State} state
*/
moveTo(properties) {
return this.merge(properties)
}
/**
* Move the focus point to the anchor point.
*
* @return {Selection} selection
*/
moveToAnchor() {
return this.merge({
focusKey: this.anchorKey,
focusOffset: this.anchorOffset
})
}
/**
* Move the anchor point to the focus point.
*
* @return {Selection} selection
*/
moveToFocus() {
return this.merge({
anchorKey: this.focusKey,
anchorOffset: this.focusOffset
})
}
/**
* Move the end point to the start point.
*
* @return {Selection} selection
*/
moveToStart() {
return this.isBackward
? this.merge({
anchorKey: this.focusKey,
anchorOffset: this.focusOffset,
isBackward: false
})
: this.merge({
focusKey: this.anchorKey,
focusOffset: this.anchorOffset,
isBackward: false
})
}
/**
* Move the start point to the end point.
*
* @return {Selection} selection
*/
moveToEnd() {
return this.isBackward
? this.merge({
focusKey: this.anchorKey,
focusOffset: this.anchorOffset,
isBackward: false
})
: this.merge({
anchorKey: this.focusKey,
anchorOffset: this.focusOffset,
isBackward: false
})
}
/**
* Move to the start of a `node`.
*
* @return {Selection} selection
*/
moveToStartOf(node) {
return this.merge({
anchorKey: node.key,
anchorOffset: 0,
focusKey: node.key,
focusOffset: 0,
isBackward: false
})
}
/**
* Move to the end of a `node`.
*
* @return {Selection} selection
*/
moveToEndOf(node) {
return this.merge({
anchorKey: node.key,
anchorOffset: node.length,
focusKey: node.key,
focusOffset: node.length,
isBackward: false
})
}
/**
* Move to the entire range of a `node`.
*
* @return {Selection} selection
*/
moveToRangeOf(node) {
return this.merge({
anchorKey: node.key,
anchorOffset: 0,
focusKey: node.key,
focusOffset: node.length,
isBackward: false
})
}
/**
* Move the selection forward `n` characters.
*
* @param {Number} n
* @return {Selection} selection
*/
moveForward(n = 1) {
if (!this.isCollapsed) {
throw new Error('The selection must be collapsed to move forward.')
}
return this.merge({
anchorOffset: this.anchorOffset + n,
focusOffset: this.focusOffset + n
})
}
/**
* Move the selection backward `n` characters.
*
* @param {Number} n
* @return {Selection} selection
*/
moveBackward(n = 1) {
if (!this.isCollapsed) {
throw new Error('The selection must be collapsed to move backward.')
}
return this.merge({
anchorOffset: this.anchorOffset - n,
focusOffset: this.focusOffset - n
})
}
}

View File

@ -14,6 +14,40 @@ const StateRecord = new Record({
selection: new Selection()
})
/**
* Node-like methods, that should be mixed into the `State` prototype.
*/
const NODE_LIKE_METHODS = [
'filterNodes',
'findNode',
'getNextNode',
'getNode',
'getParentNode',
'getPreviousNode',
'hasNode',
'pushNode',
'removeNode',
'updateNode'
]
/**
* Selection-like methods, that should be mixed into the `State` prototype.
*/
const SELECTION_LIKE_METHODS = [
'moveTo',
'moveToAnchor',
'moveToEnd',
'moveToFocus',
'moveToStart',
'moveToStartOf',
'moveToEndOf',
'moveToRangeOf',
'moveForward',
'moveBackward'
]
/**
* State.
*/
@ -34,15 +68,25 @@ class State extends StateRecord {
}
/**
* Get whether the selection is collapsed.
*
* NODES HELPERS.
* ==============
*
* These are all nodes-like helper functions that help with actions related to
* the recursively-nested node tree.
*
* @return {Boolean} isCollapsed
*/
get isCollapsed() {
return this.selection.isCollapsed
}
/**
* Get the length of the concatenated text of all nodes.
*
* @return {Number} length
*/
get length() {
return this.text.length
}
/**
* Get the concatenated text of all nodes.
*
@ -56,226 +100,182 @@ class State extends StateRecord {
}
/**
* Get a node by `key`.
* Get the anchor key.
*
* @param {String} key
* @return {Node or Null}
* @return {String} anchorKey
*/
getNode(key) {
return this.findNode(node => node.key == key) || null
get anchorKey() {
return this.selection.anchorKey
}
/**
* Get the child node after the one by `key`.
* Get the anchor offset.
*
* @param {String} key
* @return {Node or Null}
* @return {String} anchorOffset
*/
getNodeAfter(key) {
const shallow = this.nodes
.skipUntil(node => node.key == key)
.rest()
.first()
if (shallow != null) return shallow
return this.nodes
.map(node => node instanceof Node ? node.getNodeAfter(key) : null)
.filter(node => node)
.first()
get anchorOffset() {
return this.selection.anchorOffset
}
/**
* Get the child text node at `offset`.
* Get the focus key.
*
* @param {String} offset
* @return {Node or Null}
* @return {String} focusKey
*/
getNodeAtOffset(offset) {
let node = null
let i
this.nodes.forEach((child) => {
const match = child.text.length > offset + i
if (!match) return
node = match.type == 'text'
? match
: match.getNodeAtOffset(offset - i)
})
return node
get focusKey() {
return this.selection.focusKey
}
/**
* Get the parent of a child node by `key`.
* Get the focus offset.
*
* @param {String} key
* @return {Node or Null}
* @return {String} focusOffset
*/
getParentOfNode(key) {
if (this.nodes.get(key)) return this
let node = null
this.nodes.forEach((child) => {
if (!(child instanceof Node)) return
const match = child.getParentOfNode(key)
if (match) node = match
})
return node
get focusOffset() {
return this.selection.focusOffset
}
/**
* Recursively find children nodes by `iterator`.
* Get the start key.
*
* @param {Function} iterator
* @return {OrderedMap} matches
* @return {String} startKey
*/
findNode(iterator) {
const shallow = this.nodes.find(iterator)
if (shallow != null) return shallow
const deep = this.nodes
.map(node => node instanceof Node ? node.findNode(iterator) : null)
.filter(node => node)
.first()
return deep
get startKey() {
return this.selection.startKey
}
/**
* Recursively filter children nodes with `iterator`.
* Get the start offset.
*
* @param {Function} iterator
* @return {OrderedMap} matches
* @return {String} startOffset
*/
filterNodes(iterator) {
const shallow = this.nodes.filter(iterator)
const deep = this.nodes
.map(node => node instanceof Node ? node.filterNodes(iterator) : null)
.filter(node => node)
.reduce((all, map) => {
return all.concat(map)
}, shallow)
return deep
get startOffset() {
return this.selection.startOffset
}
/**
* Push a new `node` onto the map of nodes.
* Get the end key.
*
* @return {String} endKey
*/
get endKey() {
return this.selection.endKey
}
/**
* Get the end offset.
*
* @return {String} endOffset
*/
get endOffset() {
return this.selection.endOffset
}
/**
* Get the anchor node.
*
* @param {Node} node
* @return {Node} node
*/
pushNode(node) {
let notes = this.notes.set(node.key, node)
return this.merge({ notes })
get anchorNode() {
return this.getNode(this.anchorKey)
}
/**
* Set a new value for a child node by `key`.
* Get the focus node.
*
* @param {String} key
* @param {Node} node
* @return {Node} node
*/
setNode(key, node) {
if (this.nodes.get(key)) {
const nodes = this.nodes.set(key, node)
return this.merge({ nodes })
}
const nodes = this.nodes.map((child) => {
return child instanceof Node
? child.setNode(key, node)
: child
})
return this.merge({ nodes })
get focusNode() {
return this.getNode(this.focusKey)
}
/**
* Get the start node.
*
* TRANSFORMS.
* -----------
*
* These are all transform helper functions that map to a specific transform
* type that you can apply to a state.
*
* @return {Node} node
*/
get startNode() {
return this.getNode(this.startKey)
}
/**
* Backspace a single character.
* Get the end node.
*
* @return {Node} node
*/
get endNode() {
return this.getNode(this.endKey)
}
/**
* Is the selection at the start of `node`?
*
* @param {Node} node
* @return {Boolean} isAtStart
*/
isAtStartOf(node) {
return this.selection.isAtStartOf(node)
}
/**
* Is the selection at the end of `node`?
*
* @param {Node} node
* @return {Boolean} isAtEnd
*/
isAtEndOf(node) {
return this.selection.isAtEndOf(node)
}
/**
* Backspace.
*
* @return {State} state
*/
backspace() {
const { selection } = this
let state = this
// when not collapsed, remove the entire selection
if (!selection.isCollapsed) {
return this
.removeSelection(selection)
.collapseBackward()
// When not collapsed, remove the entire selection.
if (!state.isCollapsed) {
state = state.removeRange()
state = state.moveToStart()
return state
}
// when already at the start of the content, there's nothing to do
if (selection.isAtStartOf(this)) return this
// When already at the start of the content, there's nothing to do.
if (state.isAtStartOf(state)) return state
// otherwise, remove one character behind of the cursor
const { startKey, endOffset } = selection
const node = this.getNode(startKey)
// When at start of a node, merge backwards into the previous node.
const { startNode } = state
if (state.isAtStartOf(startNode)) {
const parent = state.getParentNode(startNode)
const previous = state.getPreviousNode(parent).nodes.first()
const range = selection.moveToEndOf(previous)
state = state.removeRange(range)
return state
}
// Otherwise, remove one character behind of the cursor.
const { endOffset } = state
const startOffset = endOffset - 1
return this
.removeCharacters(node, startOffset, endOffset)
.moveTo({
anchorOffset: startOffset,
focusOffset: startOffset
})
}
/**
* Collapse the current selection backward, towards it's anchor point.
*
* @return {State} state
*/
collapseBackward() {
let { selection } = this
let { anchorKey, anchorOffset } = selection
selection = selection.merge({
focusKey: anchorKey,
focusOffset: anchorOffset
})
return this.merge({ selection })
}
/**
* Collapse the current selection forward, towards it's focus point.
*
* @return {State} state
*/
collapseForward() {
let { selection } = this
const { focusKey, focusOffset } = selection
selection = selection.merge({
anchorKey: focusKey,
anchorOffset: focusOffset
})
return this.merge({ selection })
state = state.removeCharacters(startNode.key, startOffset, endOffset)
state = state.moveBackward()
return state
}
/**
@ -285,39 +285,23 @@ class State extends StateRecord {
*/
delete() {
const { selection } = this
let state = this
// when not collapsed, remove the entire selection
if (!selection.isCollapsed) {
return this
.removeSelection(selection)
.collapseBackward()
// When not collapsed, remove the entire selection range.
if (!state.isCollapsed) {
state = state.removeRange()
state = state.moveToStart()
return state
}
// when already at the end of the content, there's nothing to do
if (selection.isAtEndOf(this)) return this
// When already at the end of the content, there's nothing to do.
if (state.isAtEndOf(state)) return state
// otherwise, remove one character ahead of the cursor
const { startKey, startOffset } = selection
const node = this.getNode(startKey)
// Otherwise, remove one character ahead of the cursor.
const { startOffset, startNode } = state
const endOffset = startOffset + 1
return this.removeCharacters(node, startOffset, endOffset)
}
/**
* Move the selection to a specific anchor and focus.
*
* @param {Object} properties
* @property {String} anchorKey (optional)
* @property {Number} anchorOffset (optional)
* @property {String} focusKey (optional)
* @property {String} focusOffset (optional)
* @return {State} state
*/
moveTo(properties) {
const selection = this.selection.merge(properties)
return this.merge({ selection })
state = state.removeCharacters(startNode.key, startOffset, endOffset)
return state
}
/**
@ -370,15 +354,15 @@ class State extends StateRecord {
*/
removeCharacters(key, startOffset, endOffset) {
let node = this.getNode(key)
let { characters } = node
characters = node.characters.filterNot((char, i) => {
let state = this
let node = state.getNode(key)
const characters = node.characters.filterNot((char, i) => {
return startOffset <= i && i < endOffset
})
node = node.merge({ characters })
return this.setNode(key, node)
state = state.updateNode(node)
return state
}
/**
@ -388,12 +372,13 @@ class State extends StateRecord {
*/
split() {
let { selection } = this
let state = this.splitSelection(selection)
let { anchorKey } = state.selection
let parent = state.getParentOfNode(anchorKey)
let node = state.getNodeAfter(parent.key)
let text = node.nodes.first()
let state = this
const { selection } = state
state = state.splitRange(selection)
const { anchorKey } = state.selection
const parent = state.getParentNode(anchorKey)
const next = state.getNextNode(parent)
const text = next.nodes.first()
return state.moveTo({
anchorKey: text.key,
anchorOffset: 0,
@ -409,12 +394,12 @@ class State extends StateRecord {
* @return {State} state
*/
splitSelection(selection) {
splitRange(selection) {
let state = this
// if there's an existing selection, remove it first
if (!selection.isCollapsed) {
state = state.removeSelection(selection)
state = state.removeRange(selection)
selection = selection.merge({
focusKey: selection.anchorKey,
focusOffset: selection.anchorOffset
@ -424,7 +409,7 @@ class State extends StateRecord {
// then split the node at the selection
const { startKey, startOffset } = selection
const text = state.getNode(startKey)
const parent = state.getParentOfNode(text.key)
const parent = state.getParentNode(text)
// split the characters
const { characters , length } = text
@ -433,7 +418,7 @@ class State extends StateRecord {
// Create a new first node with only the first set of characters.
const firstText = text.set('characters', firstCharacters)
const firstNode = parent.setNode(firstText.key, firstText)
const firstNode = parent.updateNode(firstText)
// Create a brand new second node with the second set of characters.
let secondText = Text.create({})
@ -446,7 +431,7 @@ class State extends StateRecord {
secondNode = secondNode.pushNode(secondText)
// Replace the old parent node in the grandparent with the two new ones.
let grandparent = state.getParentOfNode(parent.key)
let grandparent = state.getParentNode(parent)
const befores = grandparent.nodes.takeUntil(node => node.key == parent.key)
const afters = grandparent.nodes.skipUntil(node => node.key == parent.key).rest()
const nodes = befores
@ -458,14 +443,79 @@ class State extends StateRecord {
state = state.merge({ nodes })
} else {
grandparent = grandparent.merge({ nodes })
state = state.setNode(grandparent.key, grandparent)
state = state.updateNode(grandparent)
}
return state
}
/**
* Merge the nodes between `selection`.
*
* @param {Selection} selection (optional)
* @return {State} state
*/
removeRange(selection = this.selection) {
let state = this
// If the selection is collapsed, there's nothing to do.
if (selection.isCollapsed) return state
// If the start and end nodes are the same, just remove the matching text.
const { startKey, startOffset, endKey, endOffset } = selection
if (startKey == endKey) {
return state.removeCharacters(startKey, startOffset, endOffset)
}
// Otherwise, remove the text from the first and last nodes...
let startText = state.getNode(startKey)
state = state.removeCharacters(startKey, startOffset, startText.length)
state = state.removeCharacters(endKey, 0, endOffset)
// Then remove any nodes in between the top-most start and end nodes...
let startNode = state.getParentNode(startKey)
let endNode = state.getParentNode(endKey)
const startParent = state.nodes.find(node => node == startNode || node.hasNode(startNode))
const endParent = state.nodes.find(node => node == endNode || node.hasNode(endNode))
const nodes = state.nodes
.takeUntil(node => node == startParent)
.set(startParent.key, startParent)
.concat(state.nodes.skipUntil(node => node == endParent))
state = state.merge({ nodes })
// Then bring the end text node into the start node.
let endText = state.getNode(endKey)
startNode = startNode.pushNode(endText)
endNode = endNode.removeNode(endText)
state = state.updateNode(startNode)
state = state.updateNode(endNode)
return state
}
}
/**
* Mix in node-like methods.
*/
NODE_LIKE_METHODS.forEach((method) => {
State.prototype[method] = Node.prototype[method]
})
/**
* Mix in selection-like methods.
*/
SELECTION_LIKE_METHODS.forEach((method) => {
State.prototype[method] = function (...args) {
let selection = this.selection[method](...args)
return this.merge({ selection })
}
})
/**
* Export.
*/

View File

@ -55,6 +55,16 @@ class Text extends TextRecord {
.join('')
}
/**
* Immutable type to match other nodes.
*
* @return {String} type
*/
get type() {
return 'text'
}
}
/**

View File

@ -1,24 +0,0 @@
/**
* Find the nearest parent of a `node` and return their offset key.
*
* @param {Node} node
* @return {String} key
*/
export default function findOffsetKey(node) {
let match = node
while (match && match != document.documentElement) {
if (
match instanceof Element &&
match.hasAttribute('data-key')
) {
return match.getAttribute('data-key')
}
match = match.parentNode
}
return null
}

View File

@ -1,35 +0,0 @@
import findOffsetKey from './find-offset-key'
/**
* Offset key splitter.
*/
const SPLITTER = /^(\w+)(?:\.(\d+)-(\d+))?$/
/**
* Find the selection anchor properties from a `node`.
*
* @param {Node} node
* @param {Number} nodeOffset
* @return {Object} anchor
* @property {String} anchorKey
* @property {Number} anchorOffset
*/
export default function findSelection(node, nodeOffset) {
const offsetKey = findOffsetKey(node)
if (!offsetKey) return null
const matches = SPLITTER.exec(offsetKey)
if (!matches) throw new Error(`Unknown offset key "${offsetKey}".`)
let [ all, key, offsetStart, offsetEnd ] = matches
offsetStart = parseInt(offsetStart, 10)
offsetEnd = parseInt(offsetEnd, 10)
return {
key: key,
offset: offsetStart + nodeOffset
}
}

90
lib/utils/offset-key.js Normal file
View File

@ -0,0 +1,90 @@
/**
* Offset key parser regex.
*/
const PARSER = /^(\w+)(?::(\d+)-(\d+))?$/
/**
* Offset key attribute name.
*/
const ATTRIBUTE = 'data-offset-key'
/**
* From a `node`, find the closest parent's offset key.
*
* @param {Node} node
* @return {String} key
*/
function findKey(node) {
if (node.nodeType == 3) node = node.parentNode
const parent = node.closest(`[${ATTRIBUTE}]`)
if (!parent) return null
return parent.getAttribute(ATTRIBUTE)
}
/**
* From a `node` and `offset`, find the closest parent's point.
*
* @param {Node} node
* @param {Offset} offset
* @return {String} key
*/
function findPoint(node, offset) {
const key = findKey(node)
const parsed = parse(key)
return {
key: parsed.key,
offset: parsed.start + offset
}
}
/**
* Parse an offset key `string`.
*
* @param {String} string
* @return {Object} parsed
*/
function parse(string) {
const matches = PARSER.exec(string)
if (!matches) throw new Error(`Invalid offset key string "${string}".`)
let [ original, key, start, end ] = matches
start = parseInt(start, 10)
end = parseInt(end, 10)
return {
key,
start,
end
}
}
/**
* Stringify an offset key `object`.
*
* @param {Object} object
* @property {String} key
* @property {Number} start
* @property {Number} end
* @return {String} key
*/
function stringify(object) {
return `${object.key}:${object.start}-${object.end}`
}
/**
* Export.
*/
export default {
findKey,
findPoint,
parse,
stringify
}