1
0
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:
Ian Storm Taylor 2016-06-15 19:46:53 -07:00
parent 64574c4f64
commit 1788139445
17 changed files with 1115 additions and 635 deletions

File diff suppressed because one or more lines are too long

View File

@ -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
}
}

View File

@ -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}
/>
)
}

View File

@ -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
View 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

View File

@ -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
View 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

View File

@ -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
}
}
/**

View File

@ -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
}
}
/**

View File

@ -31,6 +31,10 @@ class Selection extends SelectionRecord {
)
}
get isForward() {
return ! this.isBackward
}
get startKey() {
return this.isBackward
? this.focusKey

View File

@ -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.
*

View File

@ -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
})
}

View File

@ -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.

View 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
}, [])
}

View 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)
}, []))
}

View 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}`
}