mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-10 17:24:02 +02:00
got backwards selections working!
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -55,10 +55,10 @@ const state = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
selection: {
|
selection: {
|
||||||
anchorKey: '3.4',
|
anchorKey: '4',
|
||||||
anchorOffset: 9,
|
anchorOffset: 14,
|
||||||
focusKey: '3.4',
|
focusKey: '4',
|
||||||
focusOffset: 18
|
focusOffset: 14
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import TextNode from './text-node'
|
import Text from './text'
|
||||||
import TextNodeModel from '../models/text-node'
|
import TextNode from '../models/text-node'
|
||||||
import findSelection from '../utils/find-selection'
|
import findSelection from '../utils/find-selection'
|
||||||
import keycode from 'keycode'
|
import keycode from 'keycode'
|
||||||
|
|
||||||
@@ -25,6 +25,24 @@ class Content extends React.Component {
|
|||||||
state: React.PropTypes.object.isRequired,
|
state: React.PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Before the component updates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
componentWillUpdate() {
|
||||||
|
this.updating = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After the component updates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.updating = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On change, bubble up.
|
* On change, bubble up.
|
||||||
*
|
*
|
||||||
@@ -57,11 +75,15 @@ class Content extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
onSelect(e) {
|
onSelect(e) {
|
||||||
|
// don't handle the selection if we're rendering, since it is about to be
|
||||||
|
// set by the
|
||||||
|
if (this.updating) return
|
||||||
|
|
||||||
let { state } = this.props
|
let { state } = this.props
|
||||||
let { selection } = state
|
let { selection } = state
|
||||||
const native = window.getSelection()
|
const native = window.getSelection()
|
||||||
|
|
||||||
// no selection is active, so unset `hasFocus`
|
// No selection is active, so unset `hasFocus`.
|
||||||
if (!native.rangeCount && selection.hasFocus) {
|
if (!native.rangeCount && selection.hasFocus) {
|
||||||
selection = selection.set('hasFocus', false)
|
selection = selection.set('hasFocus', false)
|
||||||
state = state.set('selection', selection)
|
state = state.set('selection', selection)
|
||||||
@@ -74,14 +96,26 @@ class Content extends React.Component {
|
|||||||
const anchorIsText = anchorNode.nodeType == 3
|
const anchorIsText = anchorNode.nodeType == 3
|
||||||
const focusIsText = focusNode.nodeType == 3
|
const focusIsText = focusNode.nodeType == 3
|
||||||
|
|
||||||
// if both text nodes, find their parent's and create the selection
|
// If both are text nodes, find their parents and create the selection.
|
||||||
if (anchorIsText && focusIsText) {
|
if (anchorIsText && focusIsText) {
|
||||||
const anchor = findSelection(anchorNode, anchorOffset)
|
const anchor = findSelection(anchorNode, anchorOffset)
|
||||||
const focus = findSelection(focusNode, focusOffset)
|
const focus = findSelection(focusNode, focusOffset)
|
||||||
|
const { nodes } = state
|
||||||
|
|
||||||
|
const startAndEnd = state.filterNodes((node) => {
|
||||||
|
return node.key == anchor.key || node.key == focus.key
|
||||||
|
})
|
||||||
|
|
||||||
|
const isBackward = (
|
||||||
|
(startAndEnd.size == 2 && startAndEnd.first().key == focus.key) ||
|
||||||
|
(startAndEnd.size == 1 && anchor.offset > focus.offset)
|
||||||
|
)
|
||||||
|
|
||||||
selection = selection.set('anchorKey', anchor.key)
|
selection = selection.set('anchorKey', anchor.key)
|
||||||
selection = selection.set('anchorOffset', anchor.offset)
|
selection = selection.set('anchorOffset', anchor.offset)
|
||||||
selection = selection.set('focusKey', focus.key)
|
selection = selection.set('focusKey', focus.key)
|
||||||
selection = selection.set('focusOffset', focus.offset)
|
selection = selection.set('focusOffset', focus.offset)
|
||||||
|
selection = selection.set('isBackward', isBackward)
|
||||||
selection = selection.set('hasFocus', true)
|
selection = selection.set('hasFocus', true)
|
||||||
state = state.set('selection', selection)
|
state = state.set('selection', selection)
|
||||||
this.onChange(state)
|
this.onChange(state)
|
||||||
@@ -123,14 +157,15 @@ class Content extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
renderNode(node) {
|
renderNode(node) {
|
||||||
const { renderMark, renderNode } = this.props
|
const { renderMark, renderNode, state } = this.props
|
||||||
|
|
||||||
if (node instanceof TextNodeModel) {
|
if (node instanceof TextNode) {
|
||||||
return (
|
return (
|
||||||
<TextNode
|
<Text
|
||||||
key={node.key}
|
key={node.key}
|
||||||
node={node}
|
node={node}
|
||||||
renderMark={renderMark}
|
renderMark={renderMark}
|
||||||
|
state={state}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -145,6 +180,7 @@ class Content extends React.Component {
|
|||||||
{...node}
|
{...node}
|
||||||
key={node.key}
|
key={node.key}
|
||||||
children={children}
|
children={children}
|
||||||
|
state={state}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -1,28 +0,0 @@
|
|||||||
|
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LeafNode.
|
|
||||||
*/
|
|
||||||
|
|
||||||
class LeafNode extends React.Component {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
styles: React.PropTypes.object.isRequired,
|
|
||||||
text: React.PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { text, styles } = this.props
|
|
||||||
return (
|
|
||||||
<span style={styles} data-type='leaf'>{text}</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default LeafNode
|
|
136
lib/components/leaf.js
Normal file
136
lib/components/leaf.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import createOffsetKey from '../utils/create-offset-key'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LeafNode.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class LeafNode extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
node: React.PropTypes.object.isRequired,
|
||||||
|
range: React.PropTypes.object.isRequired,
|
||||||
|
renderMark: React.PropTypes.func.isRequired,
|
||||||
|
state: React.PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.updateSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.updateSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelection() {
|
||||||
|
const { state } = this.props
|
||||||
|
const { selection } = state
|
||||||
|
|
||||||
|
// If the selection is not focused we have nothing to do.
|
||||||
|
if (!selection.hasFocus) return
|
||||||
|
|
||||||
|
const { anchorKey, anchorOffset, focusKey, focusOffset } = selection
|
||||||
|
const { node, range } = this.props
|
||||||
|
const { key } = node
|
||||||
|
const { offset, text } = range
|
||||||
|
const start = offset
|
||||||
|
const end = offset + text.length
|
||||||
|
|
||||||
|
// If neither matches, the selection doesn't start or end here, so exit.
|
||||||
|
const hasStart = key == anchorKey && start <= anchorOffset && anchorOffset <= end
|
||||||
|
const hasEnd = key == focusKey && start <= focusOffset && focusOffset <= end
|
||||||
|
if (!hasStart && !hasEnd) return
|
||||||
|
|
||||||
|
// We have a selection to render, so prepare a few things...
|
||||||
|
const native = window.getSelection()
|
||||||
|
const el = ReactDOM.findDOMNode(this).firstChild
|
||||||
|
|
||||||
|
// If both the start and end are here, set the selection all at once.
|
||||||
|
if (hasStart && hasEnd) {
|
||||||
|
native.removeAllRanges()
|
||||||
|
const range = document.createRange()
|
||||||
|
range.setStart(el, anchorOffset - offset)
|
||||||
|
native.addRange(range)
|
||||||
|
native.extend(el, focusOffset - offset)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the selection is forward, we can set things in sequence. In
|
||||||
|
// the first leaf to render, reset the selection and set the new start. And
|
||||||
|
// then in the second leaf to render, extend to the new end.
|
||||||
|
if (selection.isForward) {
|
||||||
|
if (hasStart) {
|
||||||
|
native.removeAllRanges()
|
||||||
|
const range = document.createRange()
|
||||||
|
range.setStart(el, anchorOffset - offset)
|
||||||
|
native.addRange(range)
|
||||||
|
} else if (hasEnd) {
|
||||||
|
native.extend(el, focusOffset - offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, if the selection is backward, we need to hack the order a bit.
|
||||||
|
// In the first leaf to render, set a phony start anchor to store the true
|
||||||
|
// end position. And then in the second leaf to render, set the start and
|
||||||
|
// extend the end to the stored value.
|
||||||
|
else {
|
||||||
|
if (hasEnd) {
|
||||||
|
native.removeAllRanges()
|
||||||
|
const range = document.createRange()
|
||||||
|
range.setStart(el, focusOffset - offset)
|
||||||
|
native.addRange(range)
|
||||||
|
} else if (hasStart) {
|
||||||
|
const endNode = native.focusNode
|
||||||
|
const endOffset = native.focusOffset
|
||||||
|
native.removeAllRanges()
|
||||||
|
const range = document.createRange()
|
||||||
|
range.setStart(el, anchorOffset - offset)
|
||||||
|
native.addRange(range)
|
||||||
|
native.extend(endNode, endOffset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { node, range } = this.props
|
||||||
|
const { text } = range
|
||||||
|
const offsetKey = createOffsetKey(node, range)
|
||||||
|
const styles = this.renderStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={styles}
|
||||||
|
data-key={offsetKey}
|
||||||
|
data-type='leaf'
|
||||||
|
>
|
||||||
|
{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
|
||||||
|
return marks.reduce((styles, mark) => {
|
||||||
|
return {
|
||||||
|
...styles,
|
||||||
|
...renderMark(mark),
|
||||||
|
}
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default LeafNode
|
@@ -1,141 +0,0 @@
|
|||||||
|
|
||||||
import LeafNode from './leaf-node'
|
|
||||||
import React from 'react'
|
|
||||||
import xor from 'lodash/xor'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TextNode.
|
|
||||||
*/
|
|
||||||
|
|
||||||
class TextNode extends React.Component {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
node: React.PropTypes.object.isRequired,
|
|
||||||
renderMark: React.PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { node, renderMark } = this.props
|
|
||||||
const { characters } = node
|
|
||||||
const ranges = characters
|
|
||||||
.toArray()
|
|
||||||
.reduce((ranges, char, i) => {
|
|
||||||
const previous = i == 0 ? null : characters.get(i - 1)
|
|
||||||
const { text } = char
|
|
||||||
const marks = char.marks.toArray().map(mark => mark.type)
|
|
||||||
|
|
||||||
if (previous) {
|
|
||||||
const previousMarks = previous.marks.toArray().map(mark => mark.type)
|
|
||||||
const diff = xor(marks, previousMarks)
|
|
||||||
if (!diff.length) {
|
|
||||||
const previousRange = ranges[ranges.length - 1]
|
|
||||||
previousRange.text += text
|
|
||||||
return ranges
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const offset = ranges.map(range => range.text).join('').length
|
|
||||||
ranges.push({ text, marks, offset })
|
|
||||||
return ranges
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const leaves = ranges.map((range) => {
|
|
||||||
const key = `${node.key}.${range.offset}-${range.text.length}`
|
|
||||||
const styles = range.marks.reduce((styles, mark) => {
|
|
||||||
return {
|
|
||||||
...styles,
|
|
||||||
...renderMark(mark),
|
|
||||||
}
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={key}
|
|
||||||
data-key={key}
|
|
||||||
data-type='leaf'
|
|
||||||
style={styles}
|
|
||||||
>
|
|
||||||
{range.text}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={node.key}
|
|
||||||
data-key={node.key}
|
|
||||||
data-type='text'
|
|
||||||
>
|
|
||||||
{leaves}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// render() {
|
|
||||||
// const { node, renderMark } = this.props
|
|
||||||
// const { text, marks } = node
|
|
||||||
// const length = text.length
|
|
||||||
// const leaves = []
|
|
||||||
// let index = 0
|
|
||||||
// let previousIndex = index
|
|
||||||
|
|
||||||
// while (index < length) {
|
|
||||||
// const currentMarks = findMarks(marks, index)
|
|
||||||
// const nextMarks = findMarks(marks, index + 1)
|
|
||||||
// const changes = xor(currentMarks, nextMarks)
|
|
||||||
|
|
||||||
// if (!changes.length && index != length - 1) {
|
|
||||||
// index++
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const key = `${node.key}.${previousIndex}-${index}`
|
|
||||||
// const string = text.slice(previousIndex, index)
|
|
||||||
// const styles = currentMarks.reduce((styles, mark) => {
|
|
||||||
// return {
|
|
||||||
// ...styles,
|
|
||||||
// ...renderMark(mark),
|
|
||||||
// }
|
|
||||||
// }, {})
|
|
||||||
|
|
||||||
// const leaf = (
|
|
||||||
// <LeafNode
|
|
||||||
// key={key}
|
|
||||||
// styles={styles}
|
|
||||||
// text={string}
|
|
||||||
// />
|
|
||||||
// )
|
|
||||||
|
|
||||||
// leaves.push(leaf)
|
|
||||||
// previousIndex = index
|
|
||||||
// index++
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <span key={node.key} data-type='text'>{leaves}</span>
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Find matching `marks` at `index`.
|
|
||||||
// *
|
|
||||||
// * @param {Array} marks
|
|
||||||
// * @param {Number} index
|
|
||||||
// * @return {Array} marks
|
|
||||||
// */
|
|
||||||
|
|
||||||
// function findMarks(marks, index) {
|
|
||||||
// return marks
|
|
||||||
// .filter(mark => mark.start < index)
|
|
||||||
// .filter(mark => mark.end + 1 > index)
|
|
||||||
// .map(mark => mark.type)
|
|
||||||
// .sort()
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default TextNode
|
|
56
lib/components/text.js
Normal file
56
lib/components/text.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
|
||||||
|
import Leaf from './leaf'
|
||||||
|
import React from 'react'
|
||||||
|
import convertCharactersToRanges from '../utils/convert-characters-to-ranges'
|
||||||
|
import createOffsetKey from '../utils/create-offset-key'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TextNode.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class TextNode extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
node: React.PropTypes.object.isRequired,
|
||||||
|
renderMark: React.PropTypes.func.isRequired,
|
||||||
|
state: React.PropTypes.object.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { node } = this.props
|
||||||
|
const { characters } = node
|
||||||
|
const ranges = convertCharactersToRanges(characters)
|
||||||
|
const leaves = ranges.map(range => this.renderLeaf(range))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={node.key}
|
||||||
|
data-key={node.key}
|
||||||
|
data-type='text'
|
||||||
|
>
|
||||||
|
{leaves}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLeaf(range) {
|
||||||
|
const { node, renderMark, state } = this.props
|
||||||
|
const key = createOffsetKey(node, range)
|
||||||
|
return (
|
||||||
|
<Leaf
|
||||||
|
key={key}
|
||||||
|
range={range}
|
||||||
|
node={node}
|
||||||
|
renderMark={renderMark}
|
||||||
|
state={state}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default TextNode
|
@@ -22,6 +22,20 @@ class NodeMap extends OrderedMap {
|
|||||||
return new NodeMap(attrs)
|
return new NodeMap(attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filterDeep(...args) {
|
||||||
|
const shallow = this.filter(...args)
|
||||||
|
const deep = shallow.map(node => node.children.filter(...args))
|
||||||
|
const all = shallow.reduce((map, node, key) => {
|
||||||
|
map = map.concat(node)
|
||||||
|
map = map.concat(deep.get(key))
|
||||||
|
return map
|
||||||
|
}, new NodeMap())
|
||||||
|
|
||||||
|
debugger
|
||||||
|
|
||||||
|
return all
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
import NodeMap from './node-map'
|
import NodeMap from './node-map'
|
||||||
import { Map, Record } from 'immutable'
|
import TextNode from './text-node'
|
||||||
|
import { Map, OrderedMap, Record } from 'immutable'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record.
|
* Record.
|
||||||
@@ -24,10 +25,38 @@ class Node extends NodeRecord {
|
|||||||
key: attrs.key,
|
key: attrs.key,
|
||||||
type: attrs.type,
|
type: attrs.type,
|
||||||
data: new Map(attrs.data),
|
data: new Map(attrs.data),
|
||||||
children: NodeMap.create(attrs.children)
|
children: Node.createMap(attrs.children)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static createMap(array) {
|
||||||
|
return new OrderedMap(array.reduce((map, node) => {
|
||||||
|
map[node.key] = node.type == 'text'
|
||||||
|
? TextNode.create(node)
|
||||||
|
: Node.create(node)
|
||||||
|
return map
|
||||||
|
}, {}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively filter children nodes with `iterator`.
|
||||||
|
*
|
||||||
|
* @param {Function} iterator
|
||||||
|
* @return {OrderedMap} matches
|
||||||
|
*/
|
||||||
|
|
||||||
|
filterNodes(iterator) {
|
||||||
|
const shallow = this.children.filter(iterator)
|
||||||
|
const deep = this.children
|
||||||
|
.map(node => node instanceof Node ? node.filterNodes(iterator) : null)
|
||||||
|
.filter(node => node)
|
||||||
|
.reduce((all, map) => {
|
||||||
|
return all.concat(map)
|
||||||
|
}, shallow)
|
||||||
|
|
||||||
|
return deep
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -31,6 +31,10 @@ class Selection extends SelectionRecord {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isForward() {
|
||||||
|
return ! this.isBackward
|
||||||
|
}
|
||||||
|
|
||||||
get startKey() {
|
get startKey() {
|
||||||
return this.isBackward
|
return this.isBackward
|
||||||
? this.focusKey
|
? this.focusKey
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
|
|
||||||
import Selection from './selection'
|
import Selection from './selection'
|
||||||
import NodeMap from './node-map'
|
import NodeMap from './node-map'
|
||||||
|
import Node from './node'
|
||||||
import toCamel from 'to-camel-case'
|
import toCamel from 'to-camel-case'
|
||||||
import { Record } from 'immutable'
|
import { OrderedMap, Record } from 'immutable'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record.
|
* Record.
|
||||||
@@ -32,6 +33,25 @@ class State extends StateRecord {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively filter children nodes with `iterator`.
|
||||||
|
*
|
||||||
|
* @param {Function} iterator
|
||||||
|
* @return {OrderedMap} matches
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a single character.
|
* Delete a single character.
|
||||||
*
|
*
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
import CharacterList from './character-list'
|
import CharacterList from './character-list'
|
||||||
|
import convertRangesToCharacters from '../utils/convert-ranges-to-characters'
|
||||||
import { Record } from 'immutable'
|
import { Record } from 'immutable'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,21 +19,10 @@ const TextNodeRecord = new Record({
|
|||||||
class TextNode extends TextNodeRecord {
|
class TextNode extends TextNodeRecord {
|
||||||
|
|
||||||
static create(attrs) {
|
static create(attrs) {
|
||||||
const characters = attrs.ranges.reduce((characters, range) => {
|
const characters = convertRangesToCharacters(attrs.ranges)
|
||||||
const chars = range.text
|
|
||||||
.split('')
|
|
||||||
.map(char => {
|
|
||||||
return {
|
|
||||||
text: char,
|
|
||||||
marks: range.marks
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return characters.concat(chars)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return new TextNode({
|
return new TextNode({
|
||||||
key: attrs.key,
|
key: attrs.key,
|
||||||
characters: CharacterList.create(characters)
|
characters
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import keycode from 'keycode'
|
import keycode from 'keycode'
|
||||||
import { IS_WINDOWS, IS_MAC } from '../utils/detect'
|
import { IS_WINDOWS, IS_MAC } from '../utils/environment'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The core plugin.
|
* The core plugin.
|
||||||
|
33
lib/utils/convert-characters-to-ranges.js
Normal file
33
lib/utils/convert-characters-to-ranges.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
import xor from 'lodash/xor'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a `characters` list to `ranges`.
|
||||||
|
*
|
||||||
|
* @param {CharacterList} characters
|
||||||
|
* @return {Array} ranges
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function convertCharactersToRanges(characters) {
|
||||||
|
return characters
|
||||||
|
.toArray()
|
||||||
|
.reduce((ranges, char, i) => {
|
||||||
|
const previous = i == 0 ? null : characters.get(i - 1)
|
||||||
|
const { text } = char
|
||||||
|
const marks = char.marks.toArray().map(mark => mark.type)
|
||||||
|
|
||||||
|
if (previous) {
|
||||||
|
const previousMarks = previous.marks.toArray().map(mark => mark.type)
|
||||||
|
const diff = xor(marks, previousMarks)
|
||||||
|
if (!diff.length) {
|
||||||
|
const previousRange = ranges[ranges.length - 1]
|
||||||
|
previousRange.text += text
|
||||||
|
return ranges
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = ranges.map(range => range.text).join('').length
|
||||||
|
ranges.push({ text, marks, offset })
|
||||||
|
return ranges
|
||||||
|
}, [])
|
||||||
|
}
|
23
lib/utils/convert-ranges-to-characters.js
Normal file
23
lib/utils/convert-ranges-to-characters.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
import CharacterList from '../models/character-list'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a `characters` list to `ranges`.
|
||||||
|
*
|
||||||
|
* @param {CharacterList} characters
|
||||||
|
* @return {Array} ranges
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function convertRangesToCharacters(ranges) {
|
||||||
|
return CharacterList.create(ranges.reduce((characters, range) => {
|
||||||
|
const chars = range.text
|
||||||
|
.split('')
|
||||||
|
.map(char => {
|
||||||
|
return {
|
||||||
|
text: char,
|
||||||
|
marks: range.marks
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return characters.concat(chars)
|
||||||
|
}, []))
|
||||||
|
}
|
16
lib/utils/create-offset-key.js
Normal file
16
lib/utils/create-offset-key.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Create an offset key from a `node` and a `range`.
|
||||||
|
*
|
||||||
|
* @param {Node} node
|
||||||
|
* @param {Object} range
|
||||||
|
* @property {Number} offset
|
||||||
|
* @property {String} text
|
||||||
|
* @return {String} offsetKey
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function createOffsetKey(node, range) {
|
||||||
|
const start = range.offset
|
||||||
|
const end = range.offset + range.text.length
|
||||||
|
return `${node.key}.${start}-${end}`
|
||||||
|
}
|
Reference in New Issue
Block a user