1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-22 23:12:52 +02:00

move document transforms to node interface

This commit is contained in:
Ian Storm Taylor
2016-06-19 12:12:23 -07:00
parent 2a13c05f09
commit c0802c57e0
3 changed files with 314 additions and 305 deletions

View File

@@ -1,9 +1,5 @@
import Character from './character'
import Element from './element'
import Node from './node'
import Selection from './selection'
import Text from './text'
import { OrderedMap, Record } from 'immutable'
/**
@@ -48,7 +44,7 @@ class Document extends Record(DEFAULTS) {
*/
get text() {
return this
return this.nodes
.map(node => node.text)
.join('')
}
@@ -63,303 +59,6 @@ class Document extends Record(DEFAULTS) {
return 'document'
}
/**
* Delete everything in a `range`.
*
* @param {Selection} range
* @return {Document} document
*/
deleteAtRange(range) {
let document = this
// If the range is collapsed, there's nothing to do.
if (range.isCollapsed) return document
const { startKey, startOffset, endKey, endOffset } = range
let startNode = document.getNode(startKey)
// If the start and end nodes are the same, remove the matching characters.
if (startKey == endKey) {
let { characters } = startNode
characters = characters.filterNot((char, i) => {
return startOffset <= i && i < endOffset
})
startNode = startNode.merge({ characters })
document = document.updateNode(startNode)
return document
}
// Otherwise, remove the text from the first and last nodes...
const startRange = Selection.create({
anchorKey: startKey,
anchorOffset: startOffset,
focusKey: startKey,
focusOffset: startNode.length
})
const endRange = Selection.create({
anchorKey: endKey,
anchorOffset: 0,
focusKey: endKey,
focusOffset: endOffset
})
document = document.deleteAtRange(startRange)
document = document.deleteAtRange(endRange)
// Then remove any nodes in between the top-most start and end nodes...
let startParent = document.getParentNode(startKey)
let endParent = document.getParentNode(endKey)
const startGrandestParent = document.nodes.find((node) => {
return node == startParent || node.hasNode(startParent)
})
const endGrandestParent = document.nodes.find((node) => {
return node == endParent || node.hasNode(endParent)
})
const nodes = document.nodes
.takeUntil(node => node == startGrandestParent)
.set(startGrandestParent.key, startGrandestParent)
.concat(document.nodes.skipUntil(node => node == endGrandestParent))
document = document.merge({ nodes })
// Then add the end parent's nodes to the start parent node.
const newNodes = startParent.nodes.concat(endParent.nodes)
startParent = startParent.merge({ nodes: newNodes })
document = document.updateNode(startParent)
// Then remove the end parent.
let endGrandparent = document.getParentNode(endParent)
if (endGrandparent == document) {
document = document.removeNode(endParent)
} else {
endGrandparent = endGrandparent.removeNode(endParent)
document = document.updateNode(endGrandparent)
}
// Normalize the document.
return document.normalize()
}
/**
* Delete backward `n` characters at a `range`.
*
* @param {Selection} range
* @param {Number} n (optional)
* @return {Document} document
*/
deleteBackwardAtRange(range, n = 1) {
let document = this
// When collapsed at the end of the document, there's nothing to do.
if (range.isCollapsed && range.isAtEndOf(document)) return document
// When the range is still expanded, just do a regular delete.
if (range.isExpanded) return document.deleteAtRange(range)
// When at start of a text node, merge forwards into the next text node.
const { startKey } = range
const startNode = document.getNode(startKey)
if (range.isAtStartOf(startNode)) {
const parent = document.getParentNode(startNode)
const previous = document.getPreviousNode(parent).nodes.first()
range = range.extendBackwardToEndOf(previous)
document = document.deleteAtRange(range)
return document
}
// Otherwise, remove `n` characters behind of the cursor.
range = range.extendBackward(n)
document = document.deleteAtRange(range)
// Normalize the document.
return document.normalize()
}
/**
* Delete forward `n` characters at a `range`.
*
* @param {Selection} range
* @param {Number} n (optional)
* @return {Document} document
*/
deleteForwardAtRange(range, n = 1) {
let document = this
// When collapsed at the end of the document, there's nothing to do.
if (range.isCollapsed && range.isAtEndOf(document)) return document
// When the range is still expanded, just do a regular delete.
if (range.isExpanded) return document.deleteAtRange(range)
// When at end of a text node, merge forwards into the next text node.
const { startKey } = range
const startNode = document.getNode(startKey)
if (range.isAtEndOf(startNode)) {
const parent = document.getParentNode(startNode)
const next = document.getNextNode(parent).nodes.first()
range = range.extendForwardToStartOf(next)
document = document.deleteAtRange(range)
return document
}
// Otherwise, remove `n` characters ahead of the cursor.
range = range.extendForward(n)
document = document.deleteAtRange(range)
// Normalize the document.
return document.normalize()
}
/**
* Insert `text` at a `range`.
*
* @param {Selection} range
* @param {String} text
* @return {Document} document
*/
insertTextAtRange(range, text) {
let document = this
// When still expanded, remove the current range first.
if (range.isExpanded) {
document = document.deleteAtRange(range)
range = range.moveToStart()
}
let { startKey, startOffset } = range
let startNode = document.getNode(startKey)
let { characters } = startNode
// Create a list of the new characters, with the right marks.
const marks = characters.has(startOffset)
? characters.get(startOffset).marks
: null
const newCharacters = text.split('').reduce((list, char) => {
const obj = { text }
if (marks) obj.marks = marks
return list.push(Character.create(obj))
}, Character.createList())
// Splice in the new characters.
const resumeOffset = startOffset + text.length - 1
characters = characters.slice(0, startOffset)
.concat(newCharacters)
.concat(characters.slice(resumeOffset, Infinity))
// Update the existing text node.
startNode = startNode.merge({ characters })
document = document.updateNode(startNode)
// Normalize the document.
return document.normalize()
}
/**
* Normalize the document, joining any two adjacent text nodes.
*
* @return {Document} document
*/
normalize() {
let document = this
let first = document.findNode((node) => {
if (node.type != 'text') return
const parent = document.getParentNode(node)
const next = parent.getNextNode(node)
return next && next.type == 'text'
})
// If no text node was followed by another, do nothing.
if (!first) return document
// Otherwise, add the text of the second node to the first...
let parent = document.getParentNode(first)
const second = parent.getNextNode(first)
const characters = first.characters.concat(second.characters)
first = first.merge({ characters })
parent = parent.updateNode(first)
// Then remove the second node.
parent = parent.removeNode(second)
document = document.updateNode(parent)
// Finally, recurse by normalizing again.
return document.normalize()
}
/**
* Split the nodes at a `range`.
*
* @param {Selection} range
* @return {Document} document
*/
splitAtRange(range) {
let document = this
// If the range is expanded, remove it first.
if (range.isExpanded) {
document = document.deleteAtRange(range)
range = range.moveToStart()
}
const { startKey, startOffset } = range
const startNode = document.getNode(startKey)
// Split the text node's characters.
const { characters, length } = startNode
const firstCharacters = characters.take(startOffset)
const secondCharacters = characters.takeLast(length - startOffset)
// Create a new first element with only the first set of characters.
const parent = document.getParentNode(startNode)
const firstText = startNode.set('characters', firstCharacters)
const firstElement = parent.updateNode(firstText)
// Create a brand new second element with the second set of characters.
let secondText = Text.create({})
let secondElement = Element.create({
type: firstElement.type,
data: firstElement.data
})
secondText = secondText.set('characters', secondCharacters)
secondElement = secondElement.pushNode(secondText)
// Replace the old parent node in the grandparent with the two new ones.
let grandparent = document.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
.set(firstElement.key, firstElement)
.set(secondElement.key, secondElement)
.concat(afters)
// If the document is the grandparent, just merge, otherwise deep merge.
if (grandparent == document) {
document = document.merge({ nodes })
} else {
grandparent = grandparent.merge({ nodes })
document = document.updateNode(grandparent)
}
// Normalize the document.
return document.normalize()
}
}
/**

View File

@@ -42,8 +42,7 @@ class Element extends Record(DEFAULTS) {
static createMap(elements = []) {
return elements.reduce((map, element) => {
map = map.set(element.key, element)
return map
return map.set(element.key, element)
}, new OrderedMap())
}
@@ -64,7 +63,7 @@ class Element extends Record(DEFAULTS) {
*/
get text() {
return this
return this.nodes
.map(node => node.text)
.join('')
}

View File

@@ -1,4 +1,8 @@
import Character from './character'
import Element from './element'
import Selection from './selection'
import Text from './text'
import { OrderedMap } from 'immutable'
/**
@@ -10,6 +14,168 @@ import { OrderedMap } from 'immutable'
const Node = {
/**
* Delete everything in a `range`.
*
* @param {Selection} range
* @return {Node} node
*/
deleteAtRange(range) {
let node = this
// If the range is collapsed, there's nothing to do.
if (range.isCollapsed) return node
// Make sure the children exist.
const { startKey, startOffset, endKey, endOffset } = range
if (!node.hasNode(startKey)) throw new Error('Could not find that start node.')
if (!node.hasNode(endKey)) throw new Error('Could not find that end node.')
let startNode = node.getNode(startKey)
// If the start and end nodes are the same, remove the matching characters.
if (startKey == endKey) {
let { characters } = startNode
characters = characters.filterNot((char, i) => {
return startOffset <= i && i < endOffset
})
startNode = startNode.merge({ characters })
node = node.updateNode(startNode)
return node
}
// Otherwise, remove the text from the first and last nodes...
const startRange = Selection.create({
anchorKey: startKey,
anchorOffset: startOffset,
focusKey: startKey,
focusOffset: startNode.length
})
const endRange = Selection.create({
anchorKey: endKey,
anchorOffset: 0,
focusKey: endKey,
focusOffset: endOffset
})
node = node.deleteAtRange(startRange)
node = node.deleteAtRange(endRange)
// Then remove any nodes in between the top-most start and end nodes...
let startParent = node.getParentNode(startKey)
let endParent = node.getParentNode(endKey)
const startGrandestParent = node.nodes.find((child) => {
return child == startParent || child.hasNode(startParent)
})
const endGrandestParent = node.nodes.find((child) => {
return child == endParent || child.hasNode(endParent)
})
const nodes = node.nodes
.takeUntil(child => child == startGrandestParent)
.set(startGrandestParent.key, startGrandestParent)
.concat(node.nodes.skipUntil(child => child == endGrandestParent))
node = node.merge({ nodes })
// Then add the end parent's nodes to the start parent node.
const newNodes = startParent.nodes.concat(endParent.nodes)
startParent = startParent.merge({ nodes: newNodes })
node = node.updateNode(startParent)
// Then remove the end parent.
let endGrandparent = node.getParentNode(endParent)
if (endGrandparent == node) {
node = node.removeNode(endParent)
} else {
endGrandparent = endGrandparent.removeNode(endParent)
node = node.updateNode(endGrandparent)
}
// Normalize the node.
return node.normalize()
},
/**
* Delete backward `n` characters at a `range`.
*
* @param {Selection} range
* @param {Number} n (optional)
* @return {Node} node
*/
deleteBackwardAtRange(range, n = 1) {
let node = this
// When collapsed at the start of the node, there's nothing to do.
if (range.isCollapsed && range.isAtStartOf(node)) return node
// When the range is still expanded, just do a regular delete.
if (range.isExpanded) return node.deleteAtRange(range)
// When at start of a text node, merge forwards into the next text node.
const { startKey } = range
const startNode = node.getNode(startKey)
if (range.isAtStartOf(startNode)) {
const parent = node.getParentNode(startNode)
const previous = node.getPreviousNode(parent).nodes.first()
range = range.extendBackwardToEndOf(previous)
node = node.deleteAtRange(range)
return node
}
// Otherwise, remove `n` characters behind of the cursor.
range = range.extendBackward(n)
node = node.deleteAtRange(range)
// Normalize the node.
return node.normalize()
},
/**
* Delete forward `n` characters at a `range`.
*
* @param {Selection} range
* @param {Number} n (optional)
* @return {Node} node
*/
deleteForwardAtRange(range, n = 1) {
let node = this
// When collapsed at the end of the node, there's nothing to do.
if (range.isCollapsed && range.isAtEndOf(node)) return node
// When the range is still expanded, just do a regular delete.
if (range.isExpanded) return node.deleteAtRange(range)
// When at end of a text node, merge forwards into the next text node.
const { startKey } = range
const startNode = node.getNode(startKey)
if (range.isAtEndOf(startNode)) {
const parent = node.getParentNode(startNode)
const next = node.getNextNode(parent).nodes.first()
range = range.extendForwardToStartOf(next)
node = node.deleteAtRange(range)
return node
}
// Otherwise, remove `n` characters ahead of the cursor.
range = range.extendForward(n)
node = node.deleteAtRange(range)
// Normalize the node.
return node.normalize()
},
/**
* Recursively find nodes nodes by `iterator`.
*
@@ -175,6 +341,91 @@ const Node = {
return false
},
/**
* Insert `text` at a `range`.
*
* @param {Selection} range
* @param {String} text
* @return {Document} node
*/
insertTextAtRange(range, text) {
let node = this
// When still expanded, remove the current range first.
if (range.isExpanded) {
node = node.deleteAtRange(range)
range = range.moveToStart()
}
let { startKey, startOffset } = range
let startNode = node.getNode(startKey)
let { characters } = startNode
// Create a list of the new characters, with the right marks.
const marks = characters.has(startOffset)
? characters.get(startOffset).marks
: null
const newCharacters = text.split('').reduce((list, char) => {
const obj = { text }
if (marks) obj.marks = marks
return list.push(Character.create(obj))
}, Character.createList())
// Splice in the new characters.
const resumeOffset = startOffset + text.length - 1
characters = characters.slice(0, startOffset)
.concat(newCharacters)
.concat(characters.slice(resumeOffset, Infinity))
// Update the existing text node.
startNode = startNode.merge({ characters })
node = node.updateNode(startNode)
// Normalize the node.
return node.normalize()
},
/**
* Normalize the node, joining any two adjacent text child nodes.
*
* @return {Node} node
*/
normalize() {
let node = this
let first = node.findNode((child) => {
if (child.type != 'text') return
const parent = node.getParentNode(child)
const next = parent.getNextNode(child)
return next && next.type == 'text'
})
// If no text node was followed by another, do nothing.
if (!first) return node
// Otherwise, add the text of the second node to the first...
let parent = node.getParentNode(first)
const second = parent.getNextNode(first)
const characters = first.characters.concat(second.characters)
first = first.merge({ characters })
parent = parent.updateNode(first)
// Then remove the second node.
parent = parent.removeNode(second)
// If the parent isn't this node, it needs to be updated.
if (parent != node) {
node = node.updateNode(parent)
} else {
node = parent
}
// Finally, recurse by normalizing again.
return node.normalize()
},
/**
* Push a new `node` onto the map of nodes.
*
@@ -209,6 +460,66 @@ const Node = {
return this.merge({ nodes })
},
/**
* Split the nodes at a `range`.
*
* @param {Selection} range
* @return {Node} node
*/
splitAtRange(range) {
let node = this
// If the range is expanded, remove it first.
if (range.isExpanded) {
node = node.deleteAtRange(range)
range = range.moveToStart()
}
const { startKey, startOffset } = range
const startNode = node.getNode(startKey)
// Split the text node's characters.
const { characters, length } = startNode
const firstCharacters = characters.take(startOffset)
const secondCharacters = characters.takeLast(length - startOffset)
// Create a new first element with only the first set of characters.
const parent = node.getParentNode(startNode)
const firstText = startNode.set('characters', firstCharacters)
const firstElement = parent.updateNode(firstText)
// Create a brand new second element with the second set of characters.
let secondText = Text.create({})
let secondElement = Element.create({
type: firstElement.type,
data: firstElement.data
})
secondText = secondText.set('characters', secondCharacters)
secondElement = secondElement.pushNode(secondText)
// Replace the old parent node in the grandparent with the two new ones.
let grandparent = node.getParentNode(parent)
const befores = grandparent.nodes.takeUntil(child => child.key == parent.key)
const afters = grandparent.nodes.skipUntil(child => child.key == parent.key).rest()
const nodes = befores
.set(firstElement.key, firstElement)
.set(secondElement.key, secondElement)
.concat(afters)
// If the node is the grandparent, just merge, otherwise deep merge.
if (grandparent == node) {
node = node.merge({ nodes })
} else {
grandparent = grandparent.merge({ nodes })
node = node.updateNode(grandparent)
}
// Normalize the node.
return node.normalize()
},
/**
* Set a new value for a child node by `key`.
*