mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-06 23:36:31 +02:00
lots of progress and cleanup
This commit is contained in:
@@ -10,40 +10,33 @@ import ReactDOM from 'react-dom'
|
|||||||
const state = {
|
const state = {
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
{
|
||||||
kind: 'node',
|
|
||||||
type: 'code',
|
type: 'code',
|
||||||
data: {},
|
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
ranges: [
|
ranges: [
|
||||||
{
|
{
|
||||||
text: 'A\nfew\nlines\nof\ncode.',
|
text: 'A\nfew\nlines\nof\ncode.'
|
||||||
marks: []
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: 'node',
|
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
data: {},
|
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
ranges: [
|
ranges: [
|
||||||
{
|
{
|
||||||
text: 'A ',
|
text: 'A '
|
||||||
marks: []
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'simple',
|
text: 'simple',
|
||||||
marks: ['bold']
|
marks: ['bold']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: ' paragraph of text.',
|
text: ' paragraph of text.'
|
||||||
marks: []
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
|
|
||||||
|
import OffsetKey from '../utils/offset-key'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import Text from './text'
|
import Text from './text'
|
||||||
import TextModel from '../models/text'
|
import TextModel from '../models/text'
|
||||||
import findSelection from '../utils/find-selection'
|
|
||||||
import keycode from 'keycode'
|
import keycode from 'keycode'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,37 +66,28 @@ class Content extends React.Component {
|
|||||||
|
|
||||||
const el = ReactDOM.findDOMNode(this)
|
const el = ReactDOM.findDOMNode(this)
|
||||||
const { anchorNode, anchorOffset, focusNode, focusOffset } = native
|
const { anchorNode, anchorOffset, focusNode, focusOffset } = native
|
||||||
const anchorIsText = anchorNode.nodeType == 3
|
const anchor = OffsetKey.findPoint(anchorNode, anchorOffset)
|
||||||
const focusIsText = focusNode.nodeType == 3
|
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.
|
const isBackward = (
|
||||||
if (anchorIsText && focusIsText) {
|
(edges.size == 2 && edges.first().key == focus.key) ||
|
||||||
const anchor = findSelection(anchorNode, anchorOffset)
|
(edges.size == 1 && anchor.offset > focus.offset)
|
||||||
const focus = findSelection(focusNode, focusOffset)
|
)
|
||||||
const { nodes } = state
|
|
||||||
|
|
||||||
const startAndEnd = state.filterNodes((node) => {
|
selection = selection.merge({
|
||||||
return node.key == anchor.key || node.key == focus.key
|
anchorKey: anchor.key,
|
||||||
})
|
anchorOffset: anchor.offset,
|
||||||
|
focusKey: focus.key,
|
||||||
|
focusOffset: focus.offset,
|
||||||
|
isBackward: isBackward,
|
||||||
|
isFocused: true
|
||||||
|
})
|
||||||
|
|
||||||
const isBackward = (
|
state = state.set('selection', selection)
|
||||||
(startAndEnd.size == 2 && startAndEnd.first().key == focus.key) ||
|
this.onChange(state)
|
||||||
(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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
|
|
||||||
|
import OffsetKey from '../utils/offset-key'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import createOffsetKey from '../utils/create-offset-key'
|
import createOffsetKey from '../utils/create-offset-key'
|
||||||
@@ -96,26 +97,24 @@ class Leaf extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const { node, range } = this.props
|
const { node, range } = this.props
|
||||||
const { text } = range
|
const { text } = range
|
||||||
const offsetKey = createOffsetKey(node, range)
|
|
||||||
const styles = this.renderStyles()
|
const styles = this.renderStyles()
|
||||||
|
const offsetKey = OffsetKey.stringify({
|
||||||
|
key: node.key,
|
||||||
|
start: range.offset,
|
||||||
|
end: range.offset + range.text.length
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
style={styles}
|
style={styles}
|
||||||
data-key={offsetKey}
|
data-offset-key={offsetKey}
|
||||||
data-type='leaf'
|
data-type='leaf'
|
||||||
>
|
>
|
||||||
{text}
|
{text == '' ? <br/> : text}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderOffsetKey() {
|
|
||||||
const { node, offset, text } = this.props
|
|
||||||
const key = `${node.key}.${offset}-${offset + text.length}`
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
renderStyles() {
|
renderStyles() {
|
||||||
const { range, renderMark } = this.props
|
const { range, renderMark } = this.props
|
||||||
const { marks } = range
|
const { marks } = range
|
||||||
|
@@ -20,7 +20,9 @@ class Text extends React.Component {
|
|||||||
const { node } = this.props
|
const { node } = this.props
|
||||||
const { characters } = node
|
const { characters } = node
|
||||||
const ranges = convertCharactersToRanges(characters)
|
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 (
|
return (
|
||||||
<span
|
<span
|
||||||
@@ -47,6 +49,14 @@ class Text extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderSpacerLeaf() {
|
||||||
|
return this.renderLeaf({
|
||||||
|
offset: 0,
|
||||||
|
text: '',
|
||||||
|
marks: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -128,11 +128,15 @@ class Node extends NodeRecord {
|
|||||||
/**
|
/**
|
||||||
* Get the child node after the one by `key`.
|
* Get the child node after the one by `key`.
|
||||||
*
|
*
|
||||||
* @param {String} key
|
* @param {String or Node} key
|
||||||
* @return {Node or Null}
|
* @return {Node or Null}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getNodeAfter(key) {
|
getNextNode(key) {
|
||||||
|
if (typeof key != 'string') {
|
||||||
|
key = key.key
|
||||||
|
}
|
||||||
|
|
||||||
const shallow = this.nodes
|
const shallow = this.nodes
|
||||||
.skipUntil(node => node.key == key)
|
.skipUntil(node => node.key == key)
|
||||||
.rest()
|
.rest()
|
||||||
@@ -141,11 +145,61 @@ class Node extends NodeRecord {
|
|||||||
if (shallow != null) return shallow
|
if (shallow != null) return shallow
|
||||||
|
|
||||||
return this.nodes
|
return this.nodes
|
||||||
.map(node => node instanceof Node ? node.getNodeAfter(key) : null)
|
.map(node => node instanceof Node ? node.getNextNode(key) : null)
|
||||||
.filter(node => node)
|
.filter(node => node)
|
||||||
.first()
|
.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`.
|
* 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
|
* @param {String or Node} key
|
||||||
* @return {Node or Null}
|
* @return {Boolean} true
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getParentOfNode(key) {
|
hasNode(key) {
|
||||||
if (this.nodes.get(key)) return this
|
if (typeof key != 'string') {
|
||||||
let node = null
|
key = key.key
|
||||||
|
}
|
||||||
|
|
||||||
this.nodes.forEach((child) => {
|
const shallow = this.nodes.has(key)
|
||||||
if (!(child instanceof Node)) return
|
if (shallow) return true
|
||||||
const match = child.getParentOfNode(key)
|
|
||||||
if (match) node = match
|
|
||||||
})
|
|
||||||
|
|
||||||
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.
|
* Push a new `node` onto the map of nodes.
|
||||||
*
|
*
|
||||||
|
* @param {String or Node} key
|
||||||
* @param {Node} node
|
* @param {Node} node
|
||||||
* @return {Node} node
|
* @return {Node} node
|
||||||
*/
|
*/
|
||||||
|
|
||||||
pushNode(node) {
|
pushNode(key, node) {
|
||||||
let nodes = this.nodes.set(node.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 })
|
return this.merge({ nodes })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a new value for a child node by `key`.
|
* Set a new value for a child node by `key`.
|
||||||
*
|
*
|
||||||
* @param {String} key
|
* @param {String or Node} key
|
||||||
* @param {Node} node
|
* @param {Node} node
|
||||||
* @return {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)) {
|
if (this.nodes.get(key)) {
|
||||||
const nodes = this.nodes.set(key, node)
|
const nodes = this.nodes.set(key, node)
|
||||||
return this.set('nodes', nodes)
|
return this.set('nodes', nodes)
|
||||||
@@ -216,7 +300,7 @@ class Node extends NodeRecord {
|
|||||||
|
|
||||||
const nodes = this.nodes.map((child) => {
|
const nodes = this.nodes.map((child) => {
|
||||||
return child instanceof Node
|
return child instanceof Node
|
||||||
? child.setNode(key, node)
|
? child.updateNode(key, node)
|
||||||
: child
|
: child
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -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
|
* @return {Boolean} isAtStart
|
||||||
*/
|
*/
|
||||||
|
|
||||||
isAtStartOf(state) {
|
isAtStartOf(node) {
|
||||||
const { nodes } = state
|
const { startKey, startOffset } = this
|
||||||
const { startKey } = this
|
const first = node.type == 'text' ? node : node.nodes.first()
|
||||||
const first = nodes.first()
|
return startKey == first.key && startOffset == 0
|
||||||
return startKey == first.key
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
* @return {Boolean} isAtEnd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
isAtEndOf(state) {
|
isAtEndOf(node) {
|
||||||
const { nodes } = state
|
const { endKey, endOffset } = this
|
||||||
const { endKey } = this
|
const last = node.type == 'text' ? node : node.nodes.last()
|
||||||
const last = nodes.last()
|
return endKey == last.key && endOffset == last.length
|
||||||
return endKey == last.key
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,40 @@ const StateRecord = new Record({
|
|||||||
selection: new Selection()
|
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.
|
* State.
|
||||||
*/
|
*/
|
||||||
@@ -34,15 +68,25 @@ class State extends StateRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Get whether the selection is collapsed.
|
||||||
*
|
*
|
||||||
* NODES HELPERS.
|
* @return {Boolean} isCollapsed
|
||||||
* ==============
|
|
||||||
*
|
|
||||||
* These are all nodes-like helper functions that help with actions related to
|
|
||||||
* the recursively-nested node tree.
|
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
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.
|
* 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 {String} anchorKey
|
||||||
* @return {Node or Null}
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getNode(key) {
|
get anchorKey() {
|
||||||
return this.findNode(node => node.key == key) || null
|
return this.selection.anchorKey
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the child node after the one by `key`.
|
* Get the anchor offset.
|
||||||
*
|
*
|
||||||
* @param {String} key
|
* @return {String} anchorOffset
|
||||||
* @return {Node or Null}
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getNodeAfter(key) {
|
get anchorOffset() {
|
||||||
const shallow = this.nodes
|
return this.selection.anchorOffset
|
||||||
.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 the child text node at `offset`.
|
* Get the focus key.
|
||||||
*
|
*
|
||||||
* @param {String} offset
|
* @return {String} focusKey
|
||||||
* @return {Node or Null}
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getNodeAtOffset(offset) {
|
get focusKey() {
|
||||||
let node = null
|
return this.selection.focusKey
|
||||||
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 the parent of a child node by `key`.
|
* Get the focus offset.
|
||||||
*
|
*
|
||||||
* @param {String} key
|
* @return {String} focusOffset
|
||||||
* @return {Node or Null}
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getParentOfNode(key) {
|
get focusOffset() {
|
||||||
if (this.nodes.get(key)) return this
|
return this.selection.focusOffset
|
||||||
let node = null
|
|
||||||
|
|
||||||
this.nodes.forEach((child) => {
|
|
||||||
if (!(child instanceof Node)) return
|
|
||||||
const match = child.getParentOfNode(key)
|
|
||||||
if (match) node = match
|
|
||||||
})
|
|
||||||
|
|
||||||
return node
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively find children nodes by `iterator`.
|
* Get the start key.
|
||||||
*
|
*
|
||||||
* @param {Function} iterator
|
* @return {String} startKey
|
||||||
* @return {OrderedMap} matches
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
findNode(iterator) {
|
get startKey() {
|
||||||
const shallow = this.nodes.find(iterator)
|
return this.selection.startKey
|
||||||
if (shallow != null) return shallow
|
|
||||||
|
|
||||||
const deep = this.nodes
|
|
||||||
.map(node => node instanceof Node ? node.findNode(iterator) : null)
|
|
||||||
.filter(node => node)
|
|
||||||
.first()
|
|
||||||
return deep
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively filter children nodes with `iterator`.
|
* Get the start offset.
|
||||||
*
|
*
|
||||||
* @param {Function} iterator
|
* @return {String} startOffset
|
||||||
* @return {OrderedMap} matches
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
filterNodes(iterator) {
|
get startOffset() {
|
||||||
const shallow = this.nodes.filter(iterator)
|
return this.selection.startOffset
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
* @return {Node} node
|
||||||
*/
|
*/
|
||||||
|
|
||||||
pushNode(node) {
|
get anchorNode() {
|
||||||
let notes = this.notes.set(node.key, node)
|
return this.getNode(this.anchorKey)
|
||||||
return this.merge({ notes })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a new value for a child node by `key`.
|
* Get the focus node.
|
||||||
*
|
*
|
||||||
* @param {String} key
|
|
||||||
* @param {Node} node
|
|
||||||
* @return {Node} node
|
* @return {Node} node
|
||||||
*/
|
*/
|
||||||
|
|
||||||
setNode(key, node) {
|
get focusNode() {
|
||||||
if (this.nodes.get(key)) {
|
return this.getNode(this.focusKey)
|
||||||
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 the start node.
|
||||||
*
|
*
|
||||||
* TRANSFORMS.
|
* @return {Node} node
|
||||||
* -----------
|
|
||||||
*
|
|
||||||
* These are all transform helper functions that map to a specific transform
|
|
||||||
* type that you can apply to a state.
|
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
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
|
* @return {State} state
|
||||||
*/
|
*/
|
||||||
|
|
||||||
backspace() {
|
backspace() {
|
||||||
const { selection } = this
|
let state = this
|
||||||
|
|
||||||
// when not collapsed, remove the entire selection
|
// When not collapsed, remove the entire selection.
|
||||||
if (!selection.isCollapsed) {
|
if (!state.isCollapsed) {
|
||||||
return this
|
state = state.removeRange()
|
||||||
.removeSelection(selection)
|
state = state.moveToStart()
|
||||||
.collapseBackward()
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
// when already at the start of the content, there's nothing to do
|
// When already at the start of the content, there's nothing to do.
|
||||||
if (selection.isAtStartOf(this)) return this
|
if (state.isAtStartOf(state)) return state
|
||||||
|
|
||||||
// otherwise, remove one character behind of the cursor
|
// When at start of a node, merge backwards into the previous node.
|
||||||
const { startKey, endOffset } = selection
|
const { startNode } = state
|
||||||
const node = this.getNode(startKey)
|
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
|
const startOffset = endOffset - 1
|
||||||
return this
|
state = state.removeCharacters(startNode.key, startOffset, endOffset)
|
||||||
.removeCharacters(node, startOffset, endOffset)
|
state = state.moveBackward()
|
||||||
.moveTo({
|
return state
|
||||||
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 })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -285,39 +285,23 @@ class State extends StateRecord {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
delete() {
|
delete() {
|
||||||
const { selection } = this
|
let state = this
|
||||||
|
|
||||||
// when not collapsed, remove the entire selection
|
// When not collapsed, remove the entire selection range.
|
||||||
if (!selection.isCollapsed) {
|
if (!state.isCollapsed) {
|
||||||
return this
|
state = state.removeRange()
|
||||||
.removeSelection(selection)
|
state = state.moveToStart()
|
||||||
.collapseBackward()
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
// when already at the end of the content, there's nothing to do
|
// When already at the end of the content, there's nothing to do.
|
||||||
if (selection.isAtEndOf(this)) return this
|
if (state.isAtEndOf(state)) return state
|
||||||
|
|
||||||
// otherwise, remove one character ahead of the cursor
|
// Otherwise, remove one character ahead of the cursor.
|
||||||
const { startKey, startOffset } = selection
|
const { startOffset, startNode } = state
|
||||||
const node = this.getNode(startKey)
|
|
||||||
const endOffset = startOffset + 1
|
const endOffset = startOffset + 1
|
||||||
return this.removeCharacters(node, startOffset, endOffset)
|
state = state.removeCharacters(startNode.key, startOffset, endOffset)
|
||||||
}
|
return state
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -370,15 +354,15 @@ class State extends StateRecord {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
removeCharacters(key, startOffset, endOffset) {
|
removeCharacters(key, startOffset, endOffset) {
|
||||||
let node = this.getNode(key)
|
let state = this
|
||||||
let { characters } = node
|
let node = state.getNode(key)
|
||||||
|
const characters = node.characters.filterNot((char, i) => {
|
||||||
characters = node.characters.filterNot((char, i) => {
|
|
||||||
return startOffset <= i && i < endOffset
|
return startOffset <= i && i < endOffset
|
||||||
})
|
})
|
||||||
|
|
||||||
node = node.merge({ characters })
|
node = node.merge({ characters })
|
||||||
return this.setNode(key, node)
|
state = state.updateNode(node)
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -388,12 +372,13 @@ class State extends StateRecord {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
split() {
|
split() {
|
||||||
let { selection } = this
|
let state = this
|
||||||
let state = this.splitSelection(selection)
|
const { selection } = state
|
||||||
let { anchorKey } = state.selection
|
state = state.splitRange(selection)
|
||||||
let parent = state.getParentOfNode(anchorKey)
|
const { anchorKey } = state.selection
|
||||||
let node = state.getNodeAfter(parent.key)
|
const parent = state.getParentNode(anchorKey)
|
||||||
let text = node.nodes.first()
|
const next = state.getNextNode(parent)
|
||||||
|
const text = next.nodes.first()
|
||||||
return state.moveTo({
|
return state.moveTo({
|
||||||
anchorKey: text.key,
|
anchorKey: text.key,
|
||||||
anchorOffset: 0,
|
anchorOffset: 0,
|
||||||
@@ -409,12 +394,12 @@ class State extends StateRecord {
|
|||||||
* @return {State} state
|
* @return {State} state
|
||||||
*/
|
*/
|
||||||
|
|
||||||
splitSelection(selection) {
|
splitRange(selection) {
|
||||||
let state = this
|
let state = this
|
||||||
|
|
||||||
// if there's an existing selection, remove it first
|
// if there's an existing selection, remove it first
|
||||||
if (!selection.isCollapsed) {
|
if (!selection.isCollapsed) {
|
||||||
state = state.removeSelection(selection)
|
state = state.removeRange(selection)
|
||||||
selection = selection.merge({
|
selection = selection.merge({
|
||||||
focusKey: selection.anchorKey,
|
focusKey: selection.anchorKey,
|
||||||
focusOffset: selection.anchorOffset
|
focusOffset: selection.anchorOffset
|
||||||
@@ -424,7 +409,7 @@ class State extends StateRecord {
|
|||||||
// then split the node at the selection
|
// then split the node at the selection
|
||||||
const { startKey, startOffset } = selection
|
const { startKey, startOffset } = selection
|
||||||
const text = state.getNode(startKey)
|
const text = state.getNode(startKey)
|
||||||
const parent = state.getParentOfNode(text.key)
|
const parent = state.getParentNode(text)
|
||||||
|
|
||||||
// split the characters
|
// split the characters
|
||||||
const { characters , length } = text
|
const { characters , length } = text
|
||||||
@@ -433,7 +418,7 @@ class State extends StateRecord {
|
|||||||
|
|
||||||
// Create a new first node with only the first set of characters.
|
// Create a new first node with only the first set of characters.
|
||||||
const firstText = text.set('characters', firstCharacters)
|
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.
|
// Create a brand new second node with the second set of characters.
|
||||||
let secondText = Text.create({})
|
let secondText = Text.create({})
|
||||||
@@ -446,7 +431,7 @@ class State extends StateRecord {
|
|||||||
secondNode = secondNode.pushNode(secondText)
|
secondNode = secondNode.pushNode(secondText)
|
||||||
|
|
||||||
// Replace the old parent node in the grandparent with the two new ones.
|
// 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 befores = grandparent.nodes.takeUntil(node => node.key == parent.key)
|
||||||
const afters = grandparent.nodes.skipUntil(node => node.key == parent.key).rest()
|
const afters = grandparent.nodes.skipUntil(node => node.key == parent.key).rest()
|
||||||
const nodes = befores
|
const nodes = befores
|
||||||
@@ -458,14 +443,79 @@ class State extends StateRecord {
|
|||||||
state = state.merge({ nodes })
|
state = state.merge({ nodes })
|
||||||
} else {
|
} else {
|
||||||
grandparent = grandparent.merge({ nodes })
|
grandparent = grandparent.merge({ nodes })
|
||||||
state = state.setNode(grandparent.key, grandparent)
|
state = state.updateNode(grandparent)
|
||||||
}
|
}
|
||||||
|
|
||||||
return state
|
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.
|
* Export.
|
||||||
*/
|
*/
|
||||||
|
@@ -55,6 +55,16 @@ class Text extends TextRecord {
|
|||||||
.join('')
|
.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable type to match other nodes.
|
||||||
|
*
|
||||||
|
* @return {String} type
|
||||||
|
*/
|
||||||
|
|
||||||
|
get type() {
|
||||||
|
return 'text'
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -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
|
|
||||||
}
|
|
@@ -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
90
lib/utils/offset-key.js
Normal 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
|
||||||
|
}
|
Reference in New Issue
Block a user