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:
parent
e0ca80384e
commit
8ecf90bf7c
@ -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.'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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: []
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -55,6 +55,16 @@ class Text extends TextRecord {
|
||||
.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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user