1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-02-25 01:33:37 +01:00
slate/lib/models/node.js

974 lines
24 KiB
JavaScript
Raw Normal View History

2016-06-15 12:07:12 -07:00
import Block from './block'
import Character from './character'
import Mark from './mark'
import Selection from './selection'
import Text from './text'
2016-06-20 17:38:56 -07:00
import { List, OrderedMap, OrderedSet, Set } from 'immutable'
2016-06-15 12:07:12 -07:00
/**
* Node.
*
* And interface that `Document`, `Block` and `Inline` all implement, to make
* working with the recursive node tree easier.
2016-06-15 12:07:12 -07:00
*/
const Node = {
2016-06-20 12:57:31 -07:00
/**
* Assert that the node has a child by `key`.
*
* @param {String or Node} key
*/
assertHasNode(key) {
if (!this.hasNode(key)) throw new Error('Could not find that child node.')
},
/**
* Delete everything in a `range`.
*
* @param {Selection} range
* @return {Node} node
*/
deleteAtRange(range) {
let node = this
range = range.normalize(node)
// 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
2016-06-20 12:57:31 -07:00
node.assertHasNode(startKey)
node.assertHasNode(endKey)
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
range = range.normalize(node)
// 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
range = range.normalize(node)
// 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()
},
/**
2016-06-16 16:43:02 -07:00
* Recursively find nodes nodes by `iterator`.
*
* @param {Function} iterator
* @return {Node} node
*/
findNode(iterator) {
2016-06-16 16:43:02 -07:00
const shallow = this.nodes.find(iterator)
if (shallow != null) return shallow
2016-06-16 16:43:02 -07:00
return this.nodes
.map(node => node.kind == 'text' ? null : node.findNode(iterator))
.filter(node => node)
.first()
},
2016-06-15 19:46:53 -07:00
/**
2016-06-16 16:43:02 -07:00
* Recursively filter nodes nodes with `iterator`.
2016-06-15 19:46:53 -07:00
*
* @param {Function} iterator
* @return {OrderedMap} matches
*/
filterNodes(iterator) {
2016-06-16 16:43:02 -07:00
const shallow = this.nodes.filter(iterator)
const deep = this.nodes
.map(node => node.kind == 'text' ? null : node.filterNodes(iterator))
2016-06-15 19:46:53 -07:00
.filter(node => node)
.reduce((all, map) => {
return all.concat(map)
}, shallow)
return deep
},
2016-06-15 19:46:53 -07:00
2016-06-20 12:57:31 -07:00
/**
* Get a list of the characters in a `range`.
*
* @param {Selection} range
* @return {List} characters
*/
getCharactersAtRange(range) {
range = range.normalize(this)
2016-06-20 12:57:31 -07:00
const texts = this.getTextNodesAtRange(range)
let list = new List()
texts.forEach((text) => {
let { characters } = text
characters = characters.filter((char, i) => isInRange(i, text, range))
list = list.concat(characters)
})
return list
},
2016-06-20 13:21:24 -07:00
/**
* Get the first text child node.
*
* @return {Text or Null} text
*/
getFirstTextNode() {
return this.findNode(node => node.kind == 'text') || null
2016-06-20 13:21:24 -07:00
},
/**
* Get the last text child node.
*
* @return {Text or Null} text
*/
getLastTextNode() {
const texts = this.filterNodes(node => node.kind == 'text')
2016-06-20 17:38:56 -07:00
return texts.last() || null
2016-06-20 13:21:24 -07:00
},
2016-06-20 12:57:31 -07:00
/**
* Get a set of the marks in a `range`.
*
* @param {Selection} range
* @return {Set} marks
*/
getMarksAtRange(range) {
range = range.normalize(this)
2016-06-20 12:57:31 -07:00
const { startKey, startOffset, endKey } = range
// If the selection isn't set, return nothing.
if (startKey == null || endKey == null) return new Set()
// If the range is collapsed, and at the start of the node, check the
// previous text node.
if (range.isCollapsed && startOffset == 0) {
const previous = this.getPreviousTextNode(startKey)
if (!previous) return new Set()
const char = text.characters.get(previous.length - 1)
return char.marks
}
// If the range is collapsed, check the character before the start.
if (range.isCollapsed) {
const text = this.getNode(startKey)
const char = text.characters.get(range.startOffset - 1)
return char.marks
}
// Otherwise, get a set of the marks for each character in the range.
const characters = this.getCharactersAtRange(range)
let set = new Set()
characters.forEach((char) => {
set = set.union(char.marks)
})
return set
},
2016-06-16 16:43:02 -07:00
/**
* Get a child node by `key`.
*
* @param {String} key
* @return {Node or Null}
*/
getNode(key) {
2016-06-20 12:57:31 -07:00
key = normalizeKey(key)
2016-06-16 16:43:02 -07:00
return this.findNode(node => node.key == key) || null
},
2016-06-16 16:43:02 -07:00
2016-06-20 12:57:31 -07:00
/**
* Get the child text node at an `offset`.
*
2016-06-20 17:38:56 -07:00
* @param {String or Node} key
* @return {Number} offset
2016-06-20 12:57:31 -07:00
*/
getNodeOffset(key) {
this.assertHasNode(key)
const match = this.getNode(key)
// Get all of the nodes that come before the matching child.
const child = this.nodes.find((node) => {
if (node == match) return true
return node.kind == 'text'
2016-06-20 12:57:31 -07:00
? false
: node.hasNode(match)
})
const befores = this.nodes.takeUntil(node => node.key == child.key)
// Calculate the offset of the nodes before the matching child.
2016-06-20 17:38:56 -07:00
const offset = befores.reduce((offset, child) => {
return offset + child.length
}, 0)
2016-06-20 12:57:31 -07:00
// If the child's parent is this node, return the offset of all of the nodes
// before it, otherwise recurse.
return this.nodes.has(match.key)
? offset
: offset + child.getNodeOffset(key)
},
2016-06-16 16:43:02 -07:00
/**
* Get the child node after the one by `key`.
*
2016-06-17 00:09:54 -07:00
* @param {String or Node} key
2016-06-16 16:43:02 -07:00
* @return {Node or Null}
*/
2016-06-17 00:09:54 -07:00
getNextNode(key) {
2016-06-20 12:57:31 -07:00
key = normalizeKey(key)
2016-06-17 00:09:54 -07:00
2016-06-16 16:43:02 -07:00
const shallow = this.nodes
.skipUntil(node => node.key == key)
.rest()
.first()
if (shallow != null) return shallow
return this.nodes
.map(node => node.kind == 'text' ? null : node.getNextNode(key))
2016-06-16 16:43:02 -07:00
.filter(node => node)
.first()
},
2016-06-16 16:43:02 -07:00
2016-06-17 00:09:54 -07:00
/**
* Get the child node before the one by `key`.
*
* @param {String or Node} key
* @return {Node or Null}
*/
getPreviousNode(key) {
2016-06-20 12:57:31 -07:00
key = normalizeKey(key)
2016-06-17 00:09:54 -07:00
const matches = this.nodes.get(key)
if (matches) {
return this.nodes
.takeUntil(node => node.key == key)
.last()
}
return this.nodes
.map(node => node.kind == 'text' ? null : node.getPreviousNode(key))
2016-06-17 00:09:54 -07:00
.filter(node => node)
.first()
},
2016-06-17 00:09:54 -07:00
2016-06-20 12:57:31 -07:00
/**
* Get the previous text node by `key`.
*
* @param {String or Node} key
* @return {Node or Null}
*/
getPreviousTextNode(key) {
key = normalizeKey(key)
// Create a new selection starting at the first text node.
const first = this.findNode(node => node.kind == 'text')
2016-06-20 12:57:31 -07:00
const range = Selection.create({
anchorKey: first.key,
anchorOffset: 0,
focusKey: key,
focusOffset: 0
})
2016-06-20 17:38:56 -07:00
const texts = this.getTextNodesAtRange(range)
const previous = texts.get(texts.size - 2)
2016-06-20 12:57:31 -07:00
return previous
},
2016-06-17 00:09:54 -07:00
/**
* Get the parent of a child node by `key`.
*
* @param {String or Node} key
* @return {Node or Null}
*/
getParentNode(key) {
2016-06-20 12:57:31 -07:00
key = normalizeKey(key)
2016-06-17 00:09:54 -07:00
if (this.nodes.get(key)) return this
let node = null
this.nodes.forEach((child) => {
if (child.kind == 'text') return
2016-06-17 00:09:54 -07:00
const match = child.getParentNode(key)
if (match) node = match
})
return node
},
2016-06-17 00:09:54 -07:00
2016-06-16 16:43:02 -07:00
/**
2016-06-20 12:57:31 -07:00
* Get the child text node at an `offset`.
2016-06-16 16:43:02 -07:00
*
* @param {String} offset
* @return {Node or Null}
*/
2016-06-20 12:57:31 -07:00
getTextNodeAtOffset(offset) {
let length = 0
let texts = this.filterNodes(node => node.kind == 'text')
let match = texts.find((node) => {
length += node.length
return length >= offset
2016-06-16 16:43:02 -07:00
})
return match
},
2016-06-16 16:43:02 -07:00
2016-06-20 12:57:31 -07:00
/**
* Get all of the text nodes in a `range`.
*
* @param {Selection} range
* @return {OrderedMap} nodes
*/
getTextNodesAtRange(range) {
range = range.normalize(this)
2016-06-20 12:57:31 -07:00
const { startKey, endKey } = range
// If the selection isn't formed, return an empty map.
2016-06-20 12:57:31 -07:00
if (startKey == null || endKey == null) return new OrderedMap()
// Assert that the nodes exist before searching.
2016-06-20 12:57:31 -07:00
this.assertHasNode(startKey)
this.assertHasNode(endKey)
2016-06-20 17:38:56 -07:00
// Return the text nodes after the start offset and before the end offset.
2016-06-20 12:57:31 -07:00
const endNode = this.getNode(endKey)
const texts = this.filterNodes(node => node.kind == 'text')
2016-06-20 17:38:56 -07:00
const afterStart = texts.skipUntil(node => node.key == startKey)
const upToEnd = afterStart.takeUntil(node => node.key == endKey)
let matches = upToEnd.set(endNode.key, endNode)
return matches
},
2016-06-20 12:57:31 -07:00
2016-06-20 17:38:56 -07:00
/**
* Get the closets block nodes for each text node in a `range`.
2016-06-20 17:38:56 -07:00
*
* @param {Selection} range
* @return {OrderedMap} nodes
*/
getBlockNodesAtRange(range) {
2016-06-20 17:38:56 -07:00
const node = this
range = range.normalize(node)
2016-06-20 17:38:56 -07:00
const texts = node.getTextNodesAtRange(range)
const blocks = texts.map(text => node.getClosestBlockNode(text))
return blocks
},
/**
* Get the node's closest block parent node.
*
* @param {Node} node
* @return {Node} node
*/
getClosestBlockNode(node) {
let parent = this.getParentNode(node)
while (parent && parent.kind != 'block') {
parent = this.getParentNode(parent)
}
return parent
},
/**
* Get the node's closest inline parent node.
*
* @return {Node} node
*/
getClosestInlineNode() {
let parent = this.getParentNode(node)
2016-06-20 17:38:56 -07:00
while (parent && parent.kind != 'inline') {
parent = this.getParentNode(parent)
}
return parent
2016-06-20 12:57:31 -07:00
},
2016-06-16 16:43:02 -07:00
/**
2016-06-17 00:09:54 -07:00
* Recursively check if a child node exists by `key`.
2016-06-16 16:43:02 -07:00
*
2016-06-17 00:09:54 -07:00
* @param {String or Node} key
* @return {Boolean} true
2016-06-16 16:43:02 -07:00
*/
2016-06-17 00:09:54 -07:00
hasNode(key) {
2016-06-20 12:57:31 -07:00
key = normalizeKey(key)
2016-06-21 17:08:15 -07:00
return !! this.nodes.find((node) => {
return node.kind == 'text'
? node.key == key
: node.key == key || node.hasNode(key)
})
},
2016-06-16 16:43:02 -07:00
/**
* Insert `text` at a `range`.
*
* @param {Selection} range
* @param {String} text
2016-06-20 12:57:31 -07:00
* @return {Node} node
*/
insertTextAtRange(range, text) {
let node = this
range = range.normalize(node)
// 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
2016-06-20 12:57:31 -07:00
// Create a list of the new characters, with the marks from the previous
// character if one exists.
const prevOffset = startOffset - 1
const marks = characters.has(prevOffset)
? characters.get(prevOffset).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()
},
2016-06-20 12:57:31 -07:00
/**
* Add a new `mark` to the characters at `range`.
*
* @param {Selection} range
* @param {Mark or String} mark
2016-06-20 12:57:31 -07:00
* @return {Node} node
*/
markAtRange(range, mark) {
let node = this
range = range.normalize(node)
2016-06-20 12:57:31 -07:00
// Allow for just passing a type for convenience.
if (typeof mark == 'string') {
mark = new Mark({ type: mark })
}
2016-06-20 12:57:31 -07:00
// When the range is collapsed, do nothing.
if (range.isCollapsed) return node
// Otherwise, find each of the text nodes within the range.
const { startKey, startOffset, endKey, endOffset } = range
let texts = node.getTextNodesAtRange(range)
// Apply the mark to each of the text nodes's matching characters.
texts = texts.map((text) => {
let characters = text.characters.map((char, i) => {
if (!isInRange(i, text, range)) return char
let { marks } = char
marks = marks.add(mark)
return char.merge({ marks })
})
return text.merge({ characters })
})
// Update each of the text nodes.
texts.forEach((text) => {
node = node.updateNode(text)
})
return node
},
/**
* Normalize the node by joining any two adjacent text child nodes.
*
* @return {Node} node
*/
normalize() {
let node = this
// See if there are any adjacent text nodes.
let firstAdjacent = node.findNode((child) => {
if (child.kind != 'text') return
const parent = node.getParentNode(child)
const next = parent.getNextNode(child)
return next && next.kind == 'text'
})
// If no text nodes are adjacent, abort.
if (!firstAdjacent) return node
// Fix an adjacent text node if one exists.
let parent = node.getParentNode(firstAdjacent)
const second = parent.getNextNode(firstAdjacent)
const characters = firstAdjacent.characters.concat(second.characters)
firstAdjacent = firstAdjacent.merge({ characters })
parent = parent.updateNode(firstAdjacent)
// 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
}
// Recurse by normalizing again.
return node.normalize()
},
2016-06-16 16:43:02 -07:00
/**
* Push a new `node` onto the map of nodes.
*
2016-06-17 00:09:54 -07:00
* @param {String or Node} key
2016-06-20 12:57:31 -07:00
* @param {Node} node (optional)
2016-06-16 16:43:02 -07:00
* @return {Node} node
*/
2016-06-17 00:09:54 -07:00
pushNode(key, node) {
2016-06-20 12:57:31 -07:00
if (arguments.length == 1) {
2016-06-17 00:09:54 -07:00
node = key
2016-06-20 12:57:31 -07:00
key = normalizeKey(key)
2016-06-17 00:09:54 -07:00
}
let nodes = this.nodes.set(key, node)
return this.merge({ nodes })
},
2016-06-17 00:09:54 -07:00
/**
* Remove a `node` from the children node map.
*
* @param {String or Node} key
* @return {Node} node
*/
removeNode(key) {
2016-06-20 12:57:31 -07:00
key = normalizeKey(key)
2016-06-17 00:09:54 -07:00
let nodes = this.nodes.remove(key)
2016-06-16 16:43:02 -07:00
return this.merge({ nodes })
},
2016-06-16 16:43:02 -07:00
2016-06-20 17:38:56 -07:00
/**
* Set the direct parent of text nodes in a range to `type`.
*
* @param {Selection} range
* @return {Node} node
*/
setTypeAtRange(range, type) {
let node = this
range = range.normalize(node)
2016-06-20 17:38:56 -07:00
const texts = node.getTextNodesAtRange(range)
let parents = new OrderedSet()
// Find the direct parent of each text node.
texts.forEach((text) => {
const parent = node.has(text.key) ? node : node.getParentNode(text)
parents = parents.add(parent)
})
// Set the new type for each parent.
parents = parents.forEach((parent) => {
if (parent == node) {
node = node.merge({ type })
} else {
parent = parent.merge({ type })
node = node.updateNode(parent)
}
})
return node
},
/**
* Split the nodes at a `range`.
*
* @param {Selection} range
* @return {Node} node
*/
splitAtRange(range) {
let node = this
range = range.normalize(node)
// 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 = Block.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()
},
2016-06-20 12:57:31 -07:00
/**
* Remove an existing `mark` to the characters at `range`.
*
* @param {Selection} range
* @param {Mark or String} mark
2016-06-20 12:57:31 -07:00
* @return {Node} node
*/
unmarkAtRange(range, mark) {
let node = this
range = range.normalize(node)
2016-06-20 12:57:31 -07:00
// Allow for just passing a type for convenience.
if (typeof mark == 'string') {
mark = new Mark({ type: mark })
}
2016-06-20 12:57:31 -07:00
// When the range is collapsed, do nothing.
if (range.isCollapsed) return node
// Otherwise, find each of the text nodes within the range.
let texts = node.getTextNodesAtRange(range)
// Apply the mark to each of the text nodes's matching characters.
texts = texts.map((text) => {
let characters = text.characters.map((char, i) => {
if (!isInRange(i, text, range)) return char
let { marks } = char
marks = marks.remove(mark)
return char.merge({ marks })
})
return text.merge({ characters })
})
// Update each of the text nodes.
texts.forEach((text) => {
node = node.updateNode(text)
})
return node
},
2016-06-16 16:43:02 -07:00
/**
* Set a new value for a child node by `key`.
*
2016-06-17 00:09:54 -07:00
* @param {String or Node} key
2016-06-20 12:57:31 -07:00
* @param {Node} node (optional)
2016-06-16 16:43:02 -07:00
* @return {Node} node
*/
2016-06-17 00:09:54 -07:00
updateNode(key, node) {
2016-06-20 12:57:31 -07:00
if (arguments.length == 1) {
2016-06-17 00:09:54 -07:00
node = key
2016-06-20 12:57:31 -07:00
key = normalizeKey(key)
2016-06-17 00:09:54 -07:00
}
2016-06-16 16:43:02 -07:00
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.kind == 'text' ? child : child.updateNode(key, node)
2016-06-16 16:43:02 -07:00
})
return this.merge({ nodes })
2016-06-20 17:38:56 -07:00
},
/**
2016-06-21 18:00:18 -07:00
* Wrap all of the nodes in a `range` in a new parent node of `type`.
2016-06-20 17:38:56 -07:00
*
* @param {Selection} range
2016-06-21 18:00:18 -07:00
* @param {String} type
2016-06-20 17:38:56 -07:00
* @return {Node} node
*/
2016-06-21 18:00:18 -07:00
wrapAtRange(range, type) {
range = range.normalize(this)
let node = this
2016-06-21 18:00:18 -07:00
let blocks = node.getBlockNodesAtRange(range)
2016-06-20 17:38:56 -07:00
2016-06-21 18:00:18 -07:00
// Iterate each of the block nodes, wrapping them.
blocks.forEach((block) => {
let isDirectChild = node.nodes.has(block.key)
let parent = isDirectChild ? node : node.getParentNode(block)
2016-06-20 17:38:56 -07:00
2016-06-21 18:00:18 -07:00
// Create a new wrapper containing the block.
let nodes = Block.createMap([ block ])
let wrapper = Block.create({ type, nodes })
2016-06-20 17:38:56 -07:00
2016-06-21 18:00:18 -07:00
// Replace the block in it's parent with the wrapper.
nodes = parent.nodes.takeUntil(node => node == block)
.set(wrapper.key, wrapper)
.concat(parent.nodes.skipUntil(node => node == block).rest())
2016-06-20 17:38:56 -07:00
2016-06-21 18:00:18 -07:00
// Update the parent.
if (isDirectChild) {
node = node.merge({ nodes })
} else {
parent = parent.merge({ nodes })
node = node.updateNode(parent)
}
})
2016-06-20 17:38:56 -07:00
2016-06-21 18:00:18 -07:00
return node.normalize()
2016-06-16 16:43:02 -07:00
}
2016-06-20 17:38:56 -07:00
/**
* Unwrap the node
*/
2016-06-15 12:07:12 -07:00
}
2016-06-20 12:57:31 -07:00
/**
* Normalize a `key`, from a key string or a node.
*
* @param {String or Node} key
* @return {String} key
*/
function normalizeKey(key) {
if (typeof key == 'string') return key
return key.key
}
/**
* Check if an `index` of a `text` node is in a `range`.
*
* @param {Number} index
* @param {Text} text
* @param {Selection} range
* @return {Set} characters
*/
function isInRange(index, text, range) {
const { startKey, startOffset, endKey, endOffset } = range
let matcher
if (text.key == startKey && text.key == endKey) {
return startOffset <= index && index < endOffset
} else if (text.key == startKey) {
return startOffset <= index
} else if (text.key == endKey) {
return index < endOffset
} else {
return true
}
}
2016-06-15 12:07:12 -07:00
/**
* Export.
*/
export default Node