mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-22 15:02:51 +02:00
move document transforms to node interface
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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('')
|
||||
}
|
||||
|
@@ -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`.
|
||||
*
|
||||
|
Reference in New Issue
Block a user