1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-13 10:44:02 +02:00

handle split

This commit is contained in:
Ian Storm Taylor
2016-06-16 16:43:02 -07:00
parent ec7da72562
commit e0ca80384e
6 changed files with 410 additions and 93 deletions

View File

@@ -13,7 +13,7 @@ const state = {
kind: 'node', kind: 'node',
type: 'code', type: 'code',
data: {}, data: {},
children: [ nodes: [
{ {
type: 'text', type: 'text',
ranges: [ ranges: [
@@ -29,7 +29,7 @@ const state = {
kind: 'node', kind: 'node',
type: 'paragraph', type: 'paragraph',
data: {}, data: {},
children: [ nodes: [
{ {
type: 'text', type: 'text',
ranges: [ ranges: [
@@ -114,8 +114,7 @@ class App extends React.Component {
renderMark={renderMark} renderMark={renderMark}
state={this.state.state} state={this.state.state}
onChange={(state) => { onChange={(state) => {
console.log('Selection:', state.selection.toJS()) console.log('State:', state.toJS())
console.log('Text:', state.nodes.last().children.first().characters.map(c => c.text).toJS())
this.setState({ state }) this.setState({ state })
}} }}
/> />

View File

@@ -153,9 +153,9 @@ class Content extends React.Component {
} }
const Component = renderNode(node) const Component = renderNode(node)
const children = node.children const children = node.nodes
.toArray() .toArray()
.map(child => this.renderNode(child)) .map(node => this.renderNode(node))
return ( return (
<Component <Component

View File

@@ -8,9 +8,9 @@ import { Map, OrderedMap, Record } from 'immutable'
*/ */
const NodeRecord = new Record({ const NodeRecord = new Record({
children: new OrderedMap(),
data: new Map(), data: new Map(),
key: null, key: null,
nodes: new OrderedMap(),
type: null type: null
}) })
@@ -29,9 +29,9 @@ class Node extends NodeRecord {
static create(object) { static create(object) {
return new Node({ return new Node({
children: Node.createMap(object.children), data: new Map(object.data || {}),
data: new Map(object.data),
key: uid(4), key: uid(4),
nodes: Node.createMap(object.nodes || []),
type: object.type type: object.type
}) })
} }
@@ -54,56 +54,57 @@ class Node extends NodeRecord {
} }
/** /**
* Set a new value for a child node by `key`. * Get the length of the concatenated text of the node.
* *
* @param {String} key * @return {Number} length
* @param {Node} node
* @return {Node} node
*/ */
setNode(key, node) { get length() {
if (this.children.get(key)) { return this.text.length
const children = this.children.set(key, node)
return this.set('children', children)
}
const children = this.children.map((child) => {
return child instanceof Node
? child.setNode(key, node)
: child
})
return this.set('children', children)
} }
/** /**
* Recursively find children nodes by `iterator`. * Get the concatenated text `string` of all child nodes.
*
* @return {String} text
*/
get text() {
return this
.filterNodes(node => node.type == 'text')
.map(node => node.characters)
.flatten()
.map(character => character.text)
.join('')
}
/**
* Recursively find nodes nodes by `iterator`.
* *
* @param {Function} iterator * @param {Function} iterator
* @return {Node} node * @return {Node} node
*/ */
findNode(iterator) { findNode(iterator) {
const shallow = this.children.find(iterator) const shallow = this.nodes.find(iterator)
if (shallow != null) return shallow if (shallow != null) return shallow
const deep = this.children return this.nodes
.map(node => node instanceof Node ? node.findNode(iterator) : null) .map(node => node instanceof Node ? node.findNode(iterator) : null)
.filter(node => node) .filter(node => node)
.first() .first()
return deep
} }
/** /**
* Recursively filter children nodes with `iterator`. * Recursively filter nodes nodes with `iterator`.
* *
* @param {Function} iterator * @param {Function} iterator
* @return {OrderedMap} matches * @return {OrderedMap} matches
*/ */
filterNodes(iterator) { filterNodes(iterator) {
const shallow = this.children.filter(iterator) const shallow = this.nodes.filter(iterator)
const deep = this.children const deep = this.nodes
.map(node => node instanceof Node ? node.filterNodes(iterator) : null) .map(node => node instanceof Node ? node.filterNodes(iterator) : null)
.filter(node => node) .filter(node => node)
.reduce((all, map) => { .reduce((all, map) => {
@@ -113,6 +114,115 @@ class Node extends NodeRecord {
return deep return deep
} }
/**
* Get a child node by `key`.
*
* @param {String} key
* @return {Node or Null}
*/
getNode(key) {
return this.findNode(node => node.key == key) || null
}
/**
* Get the child node after the one by `key`.
*
* @param {String} key
* @return {Node or Null}
*/
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 the child text node at `offset`.
*
* @param {String} offset
* @return {Node or Null}
*/
getNodeAtOffset(offset) {
let match = null
let i
this.nodes.forEach((node) => {
if (!node.length > offset + i) return
match = node.type == 'text'
? node
: node.getNodeAtOffset(offset - i)
i += node.length
})
return match
}
/**
* Get the parent of a child node by `key`.
*
* @param {String} key
* @return {Node or Null}
*/
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
}
/**
* Push a new `node` onto the map of nodes.
*
* @param {Node} node
* @return {Node} node
*/
pushNode(node) {
let nodes = this.nodes.set(node.key, node)
return this.merge({ nodes })
}
/**
* Set a new value for a child node by `key`.
*
* @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.set('nodes', nodes)
}
const nodes = this.nodes.map((child) => {
return child instanceof Node
? child.setNode(key, node)
: child
})
return this.merge({ nodes })
}
} }
/** /**

View File

@@ -1,6 +1,7 @@
import Selection from './selection' import Selection from './selection'
import Node from './node' import Node from './node'
import Text from './text'
import toCamel from 'to-camel-case' import toCamel from 'to-camel-case'
import { OrderedMap, Record } from 'immutable' import { OrderedMap, Record } from 'immutable'
@@ -43,26 +44,89 @@ class State extends StateRecord {
*/ */
/** /**
* Set a new value for a child node by `key`. * Get the concatenated text of all nodes.
* *
* @param {String} key * @return {String} text
* @param {Node} node
* @return {Node} node
*/ */
setNode(key, node) { get text() {
if (this.nodes.get(key)) { return this.nodes
const nodes = this.nodes.set(key, node) .map(node => node.text)
return this.merge({ nodes }) .join('')
} }
const nodes = this.nodes.map((child) => { /**
return child instanceof Node * Get a node by `key`.
? child.setNode(key, node) *
: child * @param {String} key
* @return {Node or Null}
*/
getNode(key) {
return this.findNode(node => node.key == key) || null
}
/**
* Get the child node after the one by `key`.
*
* @param {String} key
* @return {Node or Null}
*/
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 the child text node at `offset`.
*
* @param {String} offset
* @return {Node or Null}
*/
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 this.merge({ nodes }) return node
}
/**
* Get the parent of a child node by `key`.
*
* @param {String} key
* @return {Node or Null}
*/
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
} }
/** /**
@@ -102,6 +166,41 @@ class State extends StateRecord {
return deep return deep
} }
/**
* Push a new `node` onto the map of nodes.
*
* @param {Node} node
* @return {Node} node
*/
pushNode(node) {
let notes = this.notes.set(node.key, node)
return this.merge({ notes })
}
/**
* Set a new value for a child node by `key`.
*
* @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 })
}
/** /**
* *
* TRANSFORMS. * TRANSFORMS.
@@ -115,11 +214,12 @@ class State extends StateRecord {
/** /**
* Backspace a single character. * Backspace a single character.
* *
* @param {Selection} selection (optional)
* @return {State} state * @return {State} state
*/ */
backspace(selection = this.selection) { backspace() {
const { selection } = this
// when not collapsed, remove the entire selection // when not collapsed, remove the entire selection
if (!selection.isCollapsed) { if (!selection.isCollapsed) {
return this return this
@@ -131,12 +231,11 @@ class State extends StateRecord {
if (selection.isAtStartOf(this)) return this if (selection.isAtStartOf(this)) return this
// otherwise, remove one character behind of the cursor // otherwise, remove one character behind of the cursor
let { startKey, endOffset } = selection const { startKey, endOffset } = selection
let { nodes } = this const node = this.getNode(startKey)
let node = this.findNode(node => node.key == startKey) const startOffset = endOffset - 1
let startOffset = endOffset - 1
return this return this
.removeText(node, startOffset, endOffset) .removeCharacters(node, startOffset, endOffset)
.moveTo({ .moveTo({
anchorOffset: startOffset, anchorOffset: startOffset,
focusOffset: startOffset focusOffset: startOffset
@@ -158,8 +257,7 @@ class State extends StateRecord {
focusOffset: anchorOffset focusOffset: anchorOffset
}) })
let state = this.merge({ selection }) return this.merge({ selection })
return state
} }
/** /**
@@ -170,25 +268,25 @@ class State extends StateRecord {
collapseForward() { collapseForward() {
let { selection } = this let { selection } = this
let { focusKey, focusOffset } = selection const { focusKey, focusOffset } = selection
selection = selection.merge({ selection = selection.merge({
anchorKey: focusKey, anchorKey: focusKey,
anchorOffset: focusOffset anchorOffset: focusOffset
}) })
let state = this.merge({ selection }) return this.merge({ selection })
return state
} }
/** /**
* Delete a single character. * Delete a single character.
* *
* @param {Selection} selection (optional)
* @return {State} state * @return {State} state
*/ */
delete(selection = this.selection) { delete() {
const { selection } = this
// when not collapsed, remove the entire selection // when not collapsed, remove the entire selection
if (!selection.isCollapsed) { if (!selection.isCollapsed) {
return this return this
@@ -200,11 +298,10 @@ class State extends StateRecord {
if (selection.isAtEndOf(this)) return this if (selection.isAtEndOf(this)) return this
// otherwise, remove one character ahead of the cursor // otherwise, remove one character ahead of the cursor
let { startKey, startOffset } = selection const { startKey, startOffset } = selection
let { nodes } = this const node = this.getNode(startKey)
let node = this.findNode(node => node.key == startKey) const endOffset = startOffset + 1
let endOffset = startOffset + 1 return this.removeCharacters(node, startOffset, endOffset)
return this.removeText(node, startOffset, endOffset)
} }
/** /**
@@ -219,62 +316,151 @@ class State extends StateRecord {
*/ */
moveTo(properties) { moveTo(properties) {
let selection = this.selection.merge(properties) const selection = this.selection.merge(properties)
let state = this.merge({ selection }) return this.merge({ selection })
return state }
/**
* Normalize all nodes, ensuring that no two text nodes are adjacent.
*
* @return {State} state
*/
normalize() {
// TODO
} }
/** /**
* Remove the existing selection's content. * Remove the existing selection's content.
* *
* @param {Selection} selection (optional) * @param {Selection} selection
* @return {State} state * @return {State} state
*/ */
removeSelection(selection = this.selection) { removeSelection(selection) {
// if already collapsed, there's nothing to remove // if already collapsed, there's nothing to remove
if (selection.isCollapsed) return this if (selection.isCollapsed) return this
// if the start and end nodes are the same, just remove the matching text // if the start and end nodes are the same, just remove the matching text
let { nodes } = this const { startKey, startOffset, endKey, endOffset } = selection
let { startKey, startOffset, endKey, endOffset } = selection if (startKey == endKey) return this.removeCharacters(startKey, startOffset, endOffset)
let startNode = nodes.get(startKey)
let endNode = nodes.get(endKey)
if (startNode == endNode) return this.removeText(startNode, startOffset, endOffset)
// otherwise, remove all of the other nodes between them... // otherwise, remove all of the other nodes between them...
nodes = nodes const nodes = this.nodes
.takeUntil(node => node.key == startKey) .takeUntil(node => node.key == startKey)
.take(1) .take(1)
.skipUntil(node => node.key == endKey) .skipUntil(node => node.key == endKey)
.take(Infinity) .take(Infinity)
// ...and remove the text from the first and last nodes // ...and remove the text from the first and last nodes
let state = this.merge({ nodes }) const startNode = this.getNode(startKey)
state = state.removeText(startNode, startOffset, startNode.text.length) return this
state = state.removeText(endNode, 0, endOffset) .merge({ nodes })
return state .removeCharacters(startKey, startOffset, startNode.text.length)
.removeCharacters(endKey, 0, endOffset)
} }
/** /**
* Remove the text from a `node`. * Remove characters from a node by `key` between offsets.
* *
* @param {Node} node * @param {String} key
* @param {Number} startOffset * @param {Number} startOffset
* @param {Number} endOffset * @param {Number} endOffset
* @return {State} state * @return {State} state
*/ */
removeText(node, startOffset, endOffset) { removeCharacters(key, startOffset, endOffset) {
let { nodes } = this let node = this.getNode(key)
let { characters } = node let { characters } = node
characters = characters.filterNot((char, i) => { characters = node.characters.filterNot((char, i) => {
return startOffset <= i && i < endOffset return startOffset <= i && i < endOffset
}) })
node = node.merge({ characters }) node = node.merge({ characters })
let state = this.setNode(node.key, node) return this.setNode(key, node)
}
/**
* Split at a `selection`.
*
* @return {State} state
*/
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()
return state.moveTo({
anchorKey: text.key,
anchorOffset: 0,
focusKey: text.key,
focusOffset: 0
})
}
/**
* Split the nodes at a `selection`.
*
* @param {Selection} selection
* @return {State} state
*/
splitSelection(selection) {
let state = this
// if there's an existing selection, remove it first
if (!selection.isCollapsed) {
state = state.removeSelection(selection)
selection = selection.merge({
focusKey: selection.anchorKey,
focusOffset: selection.anchorOffset
})
}
// then split the node at the selection
const { startKey, startOffset } = selection
const text = state.getNode(startKey)
const parent = state.getParentOfNode(text.key)
// split the characters
const { characters , length } = text
const firstCharacters = characters.take(startOffset)
const secondCharacters = characters.takeLast(length - startOffset)
// 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)
// Create a brand new second node with the second set of characters.
let secondText = Text.create({})
secondText = secondText.set('characters', secondCharacters)
let secondNode = Node.create({
type: firstNode.type,
data: firstNode.data
})
secondNode = secondNode.pushNode(secondText)
// Replace the old parent node in the grandparent with the two new ones.
let grandparent = state.getParentOfNode(parent.key)
const befores = grandparent.nodes.takeUntil(node => node.key == parent.key)
const afters = grandparent.nodes.skipUntil(node => node.key == parent.key).rest()
const nodes = befores
.set(firstNode.key, firstNode)
.set(secondNode.key, secondNode)
.concat(afters)
if (grandparent == state) {
state = state.merge({ nodes })
} else {
grandparent = grandparent.merge({ nodes })
state = state.setNode(grandparent.key, grandparent)
}
return state return state
} }

View File

@@ -8,7 +8,7 @@ import { List, Record } from 'immutable'
*/ */
const TextRecord = new Record({ const TextRecord = new Record({
characters: new List, characters: new List(),
key: null key: null
}) })
@@ -26,13 +26,35 @@ class Text extends TextRecord {
*/ */
static create(attrs) { static create(attrs) {
const characters = convertRangesToCharacters(attrs.ranges) const characters = convertRangesToCharacters(attrs.ranges || [])
return new Text({ return new Text({
key: uid(4), key: uid(4),
characters characters
}) })
} }
/**
* Get the length of the concatenated text of the node.
*
* @return {Number} length
*/
get length() {
return this.text.length
}
/**
* Get the concatenated text of the node.
*
* @return {String} text
*/
get text() {
return this.characters
.map(char => char.text)
.join('')
}
} }
/** /**

View File

@@ -67,7 +67,7 @@ const CORE_PLUGIN = {
} }
/** /**
* Does an `e` have the the word-level modifier? * Does an `e` have the word-level modifier?
* *
* @param {Event} e * @param {Event} e
* @return {Boolean} * @return {Boolean}
@@ -80,7 +80,7 @@ function isWord(e) {
} }
/** /**
* Does an `e` have the the control modifier? * Does an `e` have the control modifier?
* *
* @param {Event} e * @param {Event} e
* @return {Boolean} * @return {Boolean}
@@ -91,7 +91,7 @@ function isCtrl(e) {
} }
/** /**
* Does an `e` have the the option modifier? * Does an `e` have the option modifier?
* *
* @param {Event} e * @param {Event} e
* @return {Boolean} * @return {Boolean}
@@ -113,7 +113,7 @@ function isShift(e) {
} }
/** /**
* Does an `e` have the the command modifier? * Does an `e` have the command modifier?
* *
* @param {Event} e * @param {Event} e
* @return {Boolean} * @return {Boolean}