mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-04-21 13:51:59 +02:00
got backwards selections working!
This commit is contained in:
parent
64574c4f64
commit
1788139445
File diff suppressed because one or more lines are too long
@ -55,10 +55,10 @@ const state = {
|
||||
}
|
||||
],
|
||||
selection: {
|
||||
anchorKey: '3.4',
|
||||
anchorOffset: 9,
|
||||
focusKey: '3.4',
|
||||
focusOffset: 18
|
||||
anchorKey: '4',
|
||||
anchorOffset: 14,
|
||||
focusKey: '4',
|
||||
focusOffset: 14
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import TextNode from './text-node'
|
||||
import TextNodeModel from '../models/text-node'
|
||||
import Text from './text'
|
||||
import TextNode from '../models/text-node'
|
||||
import findSelection from '../utils/find-selection'
|
||||
import keycode from 'keycode'
|
||||
|
||||
@ -25,6 +25,24 @@ class Content extends React.Component {
|
||||
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.
|
||||
*
|
||||
@ -57,11 +75,15 @@ class Content extends React.Component {
|
||||
*/
|
||||
|
||||
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 { selection } = state
|
||||
const native = window.getSelection()
|
||||
|
||||
// no selection is active, so unset `hasFocus`
|
||||
// No selection is active, so unset `hasFocus`.
|
||||
if (!native.rangeCount && selection.hasFocus) {
|
||||
selection = selection.set('hasFocus', false)
|
||||
state = state.set('selection', selection)
|
||||
@ -74,14 +96,26 @@ class Content extends React.Component {
|
||||
const anchorIsText = anchorNode.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) {
|
||||
const anchor = findSelection(anchorNode, anchorOffset)
|
||||
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('anchorOffset', anchor.offset)
|
||||
selection = selection.set('focusKey', focus.key)
|
||||
selection = selection.set('focusOffset', focus.offset)
|
||||
selection = selection.set('isBackward', isBackward)
|
||||
selection = selection.set('hasFocus', true)
|
||||
state = state.set('selection', selection)
|
||||
this.onChange(state)
|
||||
@ -123,14 +157,15 @@ class Content extends React.Component {
|
||||
*/
|
||||
|
||||
renderNode(node) {
|
||||
const { renderMark, renderNode } = this.props
|
||||
const { renderMark, renderNode, state } = this.props
|
||||
|
||||
if (node instanceof TextNodeModel) {
|
||||
if (node instanceof TextNode) {
|
||||
return (
|
||||
<TextNode
|
||||
<Text
|
||||
key={node.key}
|
||||
node={node}
|
||||
renderMark={renderMark}
|
||||
state={state}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -145,6 +180,7 @@ class Content extends React.Component {
|
||||
{...node}
|
||||
key={node.key}
|
||||
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)
|
||||
}
|
||||
|
||||
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 { Map, Record } from 'immutable'
|
||||
import TextNode from './text-node'
|
||||
import { Map, OrderedMap, Record } from 'immutable'
|
||||
|
||||
/**
|
||||
* Record.
|
||||
@ -24,10 +25,38 @@ class Node extends NodeRecord {
|
||||
key: attrs.key,
|
||||
type: attrs.type,
|
||||
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() {
|
||||
return this.isBackward
|
||||
? this.focusKey
|
||||
|
@ -1,8 +1,9 @@
|
||||
|
||||
import Selection from './selection'
|
||||
import NodeMap from './node-map'
|
||||
import Node from './node'
|
||||
import toCamel from 'to-camel-case'
|
||||
import { Record } from 'immutable'
|
||||
import { OrderedMap, Record } from 'immutable'
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
@ -1,5 +1,6 @@
|
||||
|
||||
import CharacterList from './character-list'
|
||||
import convertRangesToCharacters from '../utils/convert-ranges-to-characters'
|
||||
import { Record } from 'immutable'
|
||||
|
||||
/**
|
||||
@ -18,21 +19,10 @@ const TextNodeRecord = new Record({
|
||||
class TextNode extends TextNodeRecord {
|
||||
|
||||
static create(attrs) {
|
||||
const characters = attrs.ranges.reduce((characters, range) => {
|
||||
const chars = range.text
|
||||
.split('')
|
||||
.map(char => {
|
||||
return {
|
||||
text: char,
|
||||
marks: range.marks
|
||||
}
|
||||
})
|
||||
return characters.concat(chars)
|
||||
}, [])
|
||||
|
||||
const characters = convertRangesToCharacters(attrs.ranges)
|
||||
return new TextNode({
|
||||
key: attrs.key,
|
||||
characters: CharacterList.create(characters)
|
||||
characters
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
|
||||
import keycode from 'keycode'
|
||||
import { IS_WINDOWS, IS_MAC } from '../utils/detect'
|
||||
import { IS_WINDOWS, IS_MAC } from '../utils/environment'
|
||||
|
||||
/**
|
||||
* 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}`
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user