1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-01-18 05:59:13 +01:00

finish draft of insert fragment

This commit is contained in:
Ian Storm Taylor 2016-06-24 17:22:08 -07:00
parent ef21157dea
commit 0af3dbcc79
4 changed files with 1025 additions and 45 deletions

View File

@ -26,6 +26,11 @@
</blockquote>
<p></p>
<p></p>
<ul>
<li>one</li>
<li>two</li>
<li>three</li>
</ul>
<table border>
<tr>
<td>1</td>

View File

@ -45,6 +45,18 @@ const Node = {
}
},
/**
* Concat children `nodes` on to the end of the node.
*
* @param {List} nodes
* @return {Node} node
*/
concatChildren(nodes) {
nodes = this.nodes.concat(nodes)
return this.merge({ nodes })
},
/**
* Recursively find all ancestor nodes by `iterator`.
*
@ -107,6 +119,62 @@ const Node = {
}, Character.createList())
},
/**
* Get children before a child by `key`.
*
* @param {String or Node} key
* @return {Node} node
*/
getChildrenBefore(key) {
const child = this.getChild(key)
const index = this.nodes.indexOf(child)
const nodes = this.nodes.slice(0, index)
return nodes
},
/**
* Get children before a child by `key`, including the child.
*
* @param {String or Node} key
* @return {Node} node
*/
getChildrenBeforeIncluding(key) {
const child = this.getChild(key)
const index = this.nodes.indexOf(child)
const nodes = this.nodes.slice(0, index + 1)
return nodes
},
/**
* Get children after a child by `key`.
*
* @param {String or Node} key
* @return {Node} node
*/
getChildrenAfter(key) {
const child = this.getChild(key)
const index = this.nodes.indexOf(child)
const nodes = this.nodes.slice(index + 1)
return nodes
},
/**
* Get children after a child by `key`, including the child.
*
* @param {String or Node} key
* @return {Node} node
*/
getChildrenAfterIncluding(key) {
const child = this.getChild(key)
const index = this.nodes.indexOf(child)
const nodes = this.nodes.slice(index)
return nodes
},
/**
* Get closest parent of node by `key` that matches `iterator`.
*
@ -160,6 +228,35 @@ const Node = {
return this.nodes.find(node => node.key == key)
},
/**
* Get a descendant node by `key`.
*
* @param {String} key
* @return {Node or Null} node
*/
getDescendant(key) {
key = normalizeKey(key)
return this.findDescendant(node => node.key == key)
},
/**
* Get the depth of a child node by `key`, with optional `startAt`.
*
* @param {String or Node} key
* @param {Number} startAt (optional)
* @return {Number} depth
*/
getDepth(key, startAt = 1) {
key = normalizeKey(key)
this.assertHasDescendant(key)
if (this.hasChild(key)) return startAt
return this
.getHighestChild(key)
.getDepth(key, startAt + 1)
},
/**
* Get a fragment of the node at a `range`.
*
@ -198,51 +295,6 @@ const Node = {
return Document.create({ nodes })
},
/**
* Get the highest child ancestor of a node by `key`.
*
* @param {String or Node} key
* @return {Node or Null} node
*/
getHighestChild(key) {
key = normalizeKey(key)
return this.nodes.find(node => {
if (node.key == key) return true
if (node.kind == 'text') return false
return node.hasDescendant(key)
})
},
/**
* Get a descendant node by `key`.
*
* @param {String} key
* @return {Node or Null} node
*/
getDescendant(key) {
key = normalizeKey(key)
return this.findDescendant(node => node.key == key)
},
/**
* Get the depth of a child node by `key`, with optional `startAt`.
*
* @param {String or Node} key
* @param {Number} startAt (optional)
* @return {Number} depth
*/
getDepth(key, startAt = 1) {
key = normalizeKey(key)
this.assertHasDescendant(key)
if (this.hasChild(key)) return startAt
return this
.getHighestChild(key)
.getDepth(key, startAt + 1)
},
/**
* Get the furthest block parent of a node by `key`.
*
@ -279,6 +331,39 @@ const Node = {
return furthest
},
/**
* Get the highest child ancestor of a node by `key`.
*
* @param {String or Node} key
* @return {Node or Null} node
*/
getHighestChild(key) {
key = normalizeKey(key)
return this.nodes.find(node => {
if (node.key == key) return true
if (node.kind == 'text') return false
return node.hasDescendant(key)
})
},
/**
* Get the highest parent of a node by `key` which has an only child.
*
* @param {String or Node} key
* @return {Node or Null}
*/
getHighestOnlyChildParent(key) {
let match = null
let parent
while (parent = this.getParent(child)) {
if (parent == null || parent.nodes.size > 1) return match
match = parent
}
},
/**
* Get the closest inline nodes for each text node in a `range`.
*
@ -543,6 +628,38 @@ const Node = {
})
},
/**
* Insert child `nodes` after child by `key`.
*
* @param {String or Node} key
* @param {List} nodes
* @return {Node} node
*/
insertChildrenAfter(key, nodes) {
key = normalizeKey(key)
const child = this.getChild(key)
const index = this.nodex.indexOf(child)
nodes = this.nodes.splice(index + 1, 0, nodes)
return this.merge({ nodes })
},
/**
* Insert child `nodes` after child by `key`.
*
* @param {String or Node} key
* @param {List} nodes
* @return {Node} node
*/
insertChildrenBefore(key, nodes) {
key = normalizeKey(key)
const child = this.getChild(key)
const index = this.nodex.indexOf(child)
nodes = this.nodes.splice(index, 0, nodes)
return this.merge({ nodes })
},
/**
* Check if the inline nodes are split at a `range`.
*
@ -608,6 +725,34 @@ const Node = {
return node.normalize()
},
/**
* Remove children after a child by `key`.
*
* @param {String or Node} key
* @return {Node} node
*/
removeChildrenAfter(key) {
const child = this.getChild(key)
const index = this.nodes.indexOf(child)
const nodes = this.nodes.slice(0, index + 1)
return this.merge({ nodes })
},
/**
* Remove children after a child by `key`, including the child.
*
* @param {String or Node} key
* @return {Node} node
*/
removeChildrenAfterIncluding(key) {
const child = this.getChild(key)
const index = this.nodes.indexOf(child)
const nodes = this.nodes.slice(0, index)
return this.merge({ nodes })
},
/**
* Remove a `node` from the children node map.
*

View File

@ -401,6 +401,21 @@ class State extends Record(DEFAULTS) {
return state
}
/**
* Insert a `fragment` at the current selection.
*
* @param {List} fragment
* @return {State} state
*/
insertFragment(fragment) {
let state = this
let { document, selection } = state
let after = selection
//
}
/**
* Insert a `text` string at the current selection.
*

815
lib/models/transforms.js Normal file
View File

@ -0,0 +1,815 @@
import Block from './block'
import Character from './character'
import Data from './data'
import Document from './document'
import Inline from './inline'
import Mark from './mark'
import Selection from './selection'
import Text from './text'
import { List, Map, Set } from 'immutable'
/**
* Transforms.
*
* These are pulled out into their own file because they can get complex.
*/
const Transforms = {
/**
* Delete everything in a `range`.
*
* @param {Selection} range
* @return {Node} node
*/
deleteAtRange(range) {
if (range.isCollapsed) return this
let node = this
// Make sure the children exist.
const { startKey, startOffset, endKey, endOffset } = range
node.assertHasDescendant(startKey)
node.assertHasDescendant(endKey)
// If the start and end nodes are the same, just remove characters.
if (startKey == endKey) {
let text = node.getDescendant(startKey)
text = text.removeCharacters(startOffset, endOffset)
node = node.updateDescendant(text)
return node
}
// Split the blocks and determine the edge boundaries.
const start = range.moveToStart()
const end = range.moveToEnd()
node = node.splitBlockAtRange(start, Infinity)
node = node.splitBlockAtRange(end, Infinity)
const startText = node.getDescendant(startKey)
const startEdgeText = node.getNextText(startKey)
const endText = node.getNextText(endKey)
const endEdgeText = node.getDescendant(endKey)
// Remove the new blocks inside the edges.
const startEdgeBlock = node.getFurthestBlock(startEdgeText)
const endEdgeBlock = node.getFurthestBlock(endEdgeText)
const nodes = node.nodes
.takeUntil(n => n == startEdgeBlock)
.concat(node.nodes.skipUntil(n => n == endEdgeBlock).rest())
node = node.merge({ nodes })
// Take the end edge's split text and move it to the start edge.
let startBlock = node.getFurthestBlock(startText)
let endChild = node.getFurthestInline(endText) || endText
const startNodes = startBlock.nodes.push(endChild)
startBlock = startBlock.merge({ nodes: startNodes })
node = node.updateDescendant(startBlock)
// While the end child is an only child, remove the block it's in.
let endParent = node.getClosestBlock(endChild)
while (endParent && endParent.nodes.size == 1) {
endChild = endParent
endParent = node.getClosestBlock(endParent)
}
node = node.removeDescendant(endChild)
// Normalize the adjacent text nodes.
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.getDescendant(startKey)
if (range.isAtStartOf(startNode)) {
const previous = node.getPreviousText(startNode)
range = range.extendToEndOf(previous)
range = range.normalize(node)
return node.deleteAtRange(range)
}
// Otherwise, remove `n` characters behind of the cursor.
range = range.extendBackward(n)
range = range.normalize(node)
return node.deleteAtRange(range)
},
/**
* 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.getDescendant(startKey)
if (range.isAtEndOf(startNode)) {
const next = node.getNextText(startNode)
range = range.extendToStartOf(next)
range = range.normalize(node)
return node.deleteAtRange(range)
}
// Otherwise, remove `n` characters ahead of the cursor.
range = range.extendForward(n)
range = range.normalize(node)
return node.deleteAtRange(range)
},
/**
* Insert a `fragment` at a `range`.
*
* @param {Selection} range
* @param {List} fragment
* @return {Node} node
*/
insertFragmentAtRange(range, fragment) {
range = range.normalize(this)
let node = this
// If the range is expanded, delete first.
if (range.isExpanded) {
node = node.deleteAtRange(range)
range = range.moveToStart()
}
// If the fragment is empty, do nothing.
if (!fragment.nodes.size) return node
// Split the inlines if need be.
if (!node.isInlineSplitAtRange(range)) {
node = node.splitInlineAtRange(range)
}
// Insert the contents of the first block into the block at the cursor.
const texts = fragment.getTextNodes()
const firstText = texts.first()
const lastText = texts.last()
const firstBlock = fragment.getClosestBlock(firstText)
let lastBlock = fragment.getClosestBlock(lastText)
const { startKey, endKey } = range
let block = node.getClosestBlock(startKey)
let start = node.getDescendant(startKey)
if (!range.isAtEndOf(start)) start = node.getPreviousText(start)
const child = block.getHighestChild(start)
const nextChild = block.getNextSibling(child)
block = block.insertChildrenAfter(startChild, firstBlock.nodes)
node = node.updateDescendant(block)
// If there are no other siblings, that's it.
if (firstBlock == lastBlock) return node
// Otherwise, remove the fragment's first block's highest solo parent...
let highestParent = getHighestSoloParent(firstBlock)
if (highestParent) fragment = fragment.removeDescendant(highestParent)
// Then, add the inlines after the cursor from the current block to the
// start of the last block in the fragment.
lastBlock = lastBlock.concatChildren(block.getChildrenAfter(nextChild))
block = block.removeChildrenAfterIncluding(nextChild)
// Finally, add the fragment's children after the block.
node = node.insertChildrenAfter(block, fragment.nodes)
return node
},
/**
* Insert text `string` at a `range`.
*
* @param {Selection} range
* @param {String} string
* @return {Node} node
*/
insertTextAtRange(range, string) {
let node = this
// When still expanded, remove the current range first.
if (range.isExpanded) {
node = node.deleteAtRange(range)
range = range.moveToStart()
}
// Insert text at the range's offset.
const { startKey, startOffset } = range
let text = node.getDescendant(startKey)
text = text.insertText(string, startOffset)
node = node.updateDescendant(text)
return node
},
/**
* Add a new `mark` to the characters at `range`.
*
* @param {Selection} range
* @param {Mark or String} mark
* @return {Node} node
*/
markAtRange(range, mark) {
let node = this
// Allow for just passing a type for convenience.
if (typeof mark == 'string') {
mark = new Mark({ type: mark })
}
// 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.getTextsAtRange(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.updateDescendant(text)
})
return node
},
/**
* Set the block nodes in a range to `type`, with optional `data`.
*
* @param {Selection} range
* @param {String} type (optional)
* @param {Data} data (optional)
* @return {Node} node
*/
setBlockAtRange(range, type, data) {
let node = this
// Allow for passing data only.
if (typeof type == 'object') {
data = type
type = null
}
// Update each of the blocks.
const blocks = node.getBlocksAtRange(range)
blocks.forEach((block) => {
const obj = {}
if (type) obj.type = type
if (data) obj.data = Data.create(data)
block = block.merge(obj)
node = node.updateDescendant(block)
})
return node
},
/**
* Set the inline nodes in a range to `type`, with optional `data`.
*
* @param {Selection} range
* @param {String} type (optional)
* @param {Data} data (optional)
* @return {Node} node
*/
setInlineAtRange(range, type, data) {
let node = this
// Allow for passing data only.
if (typeof type == 'object') {
data = type
type = null
}
// Update each of the inlines.
const inlines = node.getInlinesAtRange(range)
inlines.forEach((inline) => {
const obj = {}
if (type) obj.type = type
if (data) obj.data = Data.create(data)
inline = inline.merge(obj)
node = node.updateDescendant(inline)
})
return node
},
/**
* Split the block nodes at a `range`, to optional `depth`.
*
* @param {Selection} range
* @param {Number} depth (optional)
* @return {Node} node
*/
splitBlockAtRange(range, depth = 1) {
let node = this
// If the range is expanded, remove it first.
if (range.isExpanded) {
node = node.deleteAtRange(range)
range = range.moveToStart()
}
// Split the inline nodes at the range.
node = node.splitInlineAtRange(range)
// Find the highest inline elements that were split.
const { startKey } = range
const firstText = node.getDescendant(startKey)
const secondText = node.getNextText(startKey)
const firstChild = node.getFurthestInline(firstText) || firstText
const secondChild = node.getFurthestInline(secondText) || secondText
let parent = node.getClosestBlock(firstChild)
let firstChildren = parent.nodes.takeUntil(n => n == firstChild).push(firstChild)
let secondChildren = parent.nodes.skipUntil(n => n == secondChild)
let d = 0
// While the parent is a block, split the block nodes.
while (parent && d < depth) {
const firstChild = parent.merge({ nodes: firstChildren })
const secondChild = Block.create({
nodes: secondChildren,
type: parent.type,
data: parent.data
})
firstChildren = Block.createList([firstChild])
secondChildren = Block.createList([secondChild])
// Add the new children.
const grandparent = node.getParent(parent)
const nodes = grandparent.nodes
.takeUntil(n => n.key == firstChild.key)
.push(firstChild)
.push(secondChild)
.concat(grandparent.nodes.skipUntil(n => n.key == firstChild.key).rest())
// Update the grandparent.
node = grandparent == node
? node.merge({ nodes })
: node.updateDescendant(grandparent.merge({ nodes }))
d++
parent = node.getClosestBlock(firstChild)
}
return node
},
/**
* Split the inline nodes at a `range`, to optional `depth`.
*
* @param {Selection} range
* @param {Number} depth (optiona)
* @return {Node} node
*/
splitInlineAtRange(range, depth = Infinity) {
let node = this
// If the range is expanded, remove it first.
if (range.isExpanded) {
node = node.deleteAtRange(range)
range = range.moveToStart()
}
// First split the text nodes.
node = node.splitTextAtRange(range)
// Find the children that were split.
const { startKey } = range
let firstChild = node.getDescendant(startKey)
let secondChild = node.getNextText(firstChild)
let parent = node.getClosestInline(firstChild)
let d = 0
// While the parent is an inline parent, split the inline nodes.
while (parent && d < depth) {
firstChild = parent.merge({ nodes: Inline.createList([firstChild]) })
secondChild = Inline.create({
nodes: [secondChild],
type: parent.type,
data: parent.data
})
// Split the children.
const grandparent = node.getParent(parent)
const nodes = grandparent.nodes
.takeUntil(n => n.key == firstChild.key)
.push(firstChild)
.push(secondChild)
.concat(grandparent.nodes.skipUntil(n => n.key == firstChild.key).rest())
// Update the grandparent.
node = grandparent == node
? node.merge({ nodes })
: node.updateDescendant(grandparent.merge({ nodes }))
d++
parent = node.getClosestInline(firstChild)
}
return node
},
/**
* Split the text nodes at a `range`.
*
* @param {Selection} range
* @return {Node} node
*/
splitTextAtRange(range) {
let node = this
// If the range is expanded, remove it first.
if (range.isExpanded) {
node = node.deleteAtRange(range)
range = range.moveToStart()
}
// Split the text node's characters.
const { startKey, startOffset } = range
const text = node.getDescendant(startKey)
const { characters } = text
const firstChars = characters.take(startOffset)
const secondChars = characters.skip(startOffset)
let firstChild = text.merge({ characters: firstChars })
let secondChild = Text.create({ characters: secondChars })
// Split the text nodes.
let parent = node.getParent(text)
const nodes = parent.nodes
.takeUntil(c => c.key == firstChild.key)
.push(firstChild)
.push(secondChild)
.concat(parent.nodes.skipUntil(n => n.key == firstChild.key).rest())
// Update the nodes.
parent = parent.merge({ nodes })
node = node.updateDescendant(parent)
return node
},
/**
* Remove an existing `mark` to the characters at `range`.
*
* @param {Selection} range
* @param {Mark or String} mark
* @return {Node} node
*/
unmarkAtRange(range, mark) {
let node = this
// Allow for just passing a type for convenience.
if (typeof mark == 'string') {
mark = new Mark({ type: mark })
}
// 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.getTextsAtRange(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.updateDescendant(text)
})
return node
},
/**
* Unwrap all of the block nodes in a `range` from a block node of `type.`
*
* @param {Selection} range
* @param {String} type (optional)
* @param {Data or Object} data (optional)
* @return {Node} node
*/
unwrapBlockAtRange(range, type, data) {
let node = this
// Allow for only data.
if (typeof type == 'object') {
data = type
type = null
}
// Ensure that data is immutable.
if (data) data = Data.create(data)
// Find the closest wrapping blocks of each text node.
const texts = node.getBlocksAtRange(range)
const wrappers = texts.reduce((wrappers, text) => {
const match = node.getClosest(text, (parent) => {
if (parent.kind != 'block') return false
if (type && parent.type != type) return false
if (data && !parent.data.isSuperset(data)) return false
return true
})
if (match) wrappers = wrappers.add(match)
return wrappers
}, new Set())
// Replace each of the wrappers with their child nodes.
wrappers.forEach((wrapper) => {
const parent = node.getParent(wrapper)
// Replace the wrapper in the parent's nodes with the block.
const nodes = parent.nodes.takeUntil(n => n == wrapper)
.concat(wrapper.nodes)
.concat(parent.nodes.skipUntil(n => n == wrapper).rest())
// Update the parent.
node = parent == node
? node.merge({ nodes })
: node.updateDescendant(parent.merge({ nodes }))
})
return node.normalize()
},
/**
* Unwrap the inline nodes in a `range` from an parent inline with `type`.
*
* @param {Selection} range
* @param {String} type (optional)
* @param {Data} data (optional)
* @return {Node} node
*/
unwrapInlineAtRange(range, type, data) {
let node = this
let blocks = node.getInlinesAtRange(range)
// Allow for no type.
if (typeof type == 'object') {
data = type
type = null
}
// Ensure that data is immutable.
if (data) data = Data.create(data)
// Find the closest matching inline wrappers of each text node.
const texts = this.getTextNodes()
const wrappers = texts.reduce((wrappers, text) => {
const match = node.getClosest(text, (parent) => {
if (parent.kind != 'inline') return false
if (type && parent.type != type) return false
if (data && !parent.data.isSuperset(data)) return false
return true
})
if (match) wrappers = wrappers.add(match)
return wrappers
}, new Set())
// Replace each of the wrappers with their child nodes.
wrappers.forEach((wrapper) => {
const parent = node.getParent(wrapper)
// Replace the wrapper in the parent's nodes with the block.
const nodes = parent.nodes.takeUntil(n => n == wrapper)
.concat(wrapper.nodes)
.concat(parent.nodes.skipUntil(n => n == wrapper).rest())
// Update the parent.
node = parent == node
? node.merge({ nodes })
: node.updateDescendant(parent.merge({ nodes }))
})
return node.normalize()
},
/**
* Wrap all of the blocks in a `range` in a new block node of `type`.
*
* @param {Selection} range
* @param {String} type
* @param {Data} data (optional)
* @return {Node} node
*/
wrapBlockAtRange(range, type, data) {
data = Data.create(data)
let node = this
// Get the block nodes, sorted by depth.
const blocks = node.getBlocksAtRange(range)
const sorted = blocks.sort((a, b) => {
const da = node.getDepth(a)
const db = node.getDepth(b)
if (da == db) return 0
if (da > db) return -1
if (da < db) return 1
})
// Get the lowest common siblings, relative to the highest block.
const highest = sorted.first()
const depth = node.getDepth(highest)
const siblings = blocks.reduce((siblings, block) => {
const sibling = node.getDepth(block) == depth
? block
: node.getClosest(block, (p) => node.getDepth(p) == depth)
siblings = siblings.push(sibling)
return siblings
}, Block.createList())
// Wrap the siblings in a new block.
const wrapper = Block.create({
nodes: siblings,
type,
data
})
// Replace the siblings with the wrapper.
const first = siblings.first()
const last = siblings.last()
const parent = node.getParent(highest)
const nodes = parent.nodes
.takeUntil(node => node == first)
.push(wrapper)
.concat(parent.nodes.skipUntil(node => node == last).rest())
// Update the parent.
node = parent == node
? node.merge({ nodes })
: node.updateDescendant(parent.merge({ nodes }))
return node
},
/**
* Wrap the text and inline nodes in a `range` with a new inline node.
*
* @param {Selection} range
* @param {String} type
* @param {Data} data (optional)
* @return {Node} node
*/
wrapInlineAtRange(range, type, data) {
data = Data.create(data)
let node = this
// If collapsed or unset, there's nothing to wrap.
if (range.isCollapsed || range.isUnset) return node
// Split at the start of the range.
const start = range.moveToStart()
node = node.splitInlineAtRange(start)
// Determine the new end of the range, and split there.
const { startKey, startOffset, endKey, endOffset } = range
const firstNode = node.getDescendant(startKey)
const nextNode = node.getNextText(startKey)
const end = startKey != endKey
? range.moveToEnd()
: Selection.create({
anchorKey: nextNode.key,
anchorOffset: endOffset - startOffset,
focusKey: nextNode.key,
focusOffset: endOffset - startOffset
})
node = node.splitInlineAtRange(end)
// Calculate the new range to wrap around.
const endNode = node.getDescendant(end.anchorKey)
range = Selection.create({
anchorKey: nextNode.key,
anchorOffset: 0,
focusKey: endNode.key,
focusOffset: endNode.length
})
// Get the furthest inline nodes in the range.
const texts = node.getTextsAtRange(range)
const children = texts.map(text => node.getFurthestInline(text) || text)
// Iterate each of the child nodes, wrapping them.
children.forEach((child) => {
const obj = {}
obj.nodes = [child]
obj.type = type
if (data) obj.data = data
const wrapper = Inline.create(obj)
// Replace the child in it's parent with the wrapper.
const parent = node.getParent(child)
const nodes = parent.nodes.takeUntil(n => n == child)
.push(wrapper)
.concat(parent.nodes.skipUntil(n => n == child).rest())
// Update the parent.
node = parent == node
? node.merge({ nodes })
: node.updateDescendant(parent.merge({ nodes }))
})
return node
}
}
/**
* 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
}
}
/**
* Export.
*/
export default Transforms