mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-02-24 17:23:07 +01:00
869 lines
23 KiB
JavaScript
869 lines
23 KiB
JavaScript
|
|
import Block from '../models/block'
|
|
import Inline from '../models/inline'
|
|
import Normalize from '../utils/normalize'
|
|
import Selection from '../models/selection'
|
|
import Text from '../models/text'
|
|
import isInRange from '../utils/is-in-range'
|
|
import uid from '../utils/uid'
|
|
import { List, Set } from 'immutable'
|
|
|
|
/**
|
|
* Add a new `mark` to the characters at `range`.
|
|
*
|
|
* @param {Transform} transform
|
|
* @param {Selection} range
|
|
* @param {Mixed} mark
|
|
* @return {Transform}
|
|
*/
|
|
|
|
export function addMarkAtRange(transform, range, mark) {
|
|
if (range.isCollapsed) return transform
|
|
|
|
const { state } = transform
|
|
const { document } = state
|
|
const { startKey, startOffset, endKey, endOffset } = range
|
|
const texts = document.getTextsAtRange(range)
|
|
|
|
texts.forEach((text) => {
|
|
const { key } = text
|
|
let index = 0
|
|
let length = text.length
|
|
|
|
if (key == startKey) index = startOffset
|
|
if (key == endKey) length = endOffset
|
|
if (key == startKey && key == endKey) length = endOffset - startOffset
|
|
|
|
transform.addMarkByKey(key, index, length, mark)
|
|
})
|
|
|
|
return transform
|
|
}
|
|
|
|
/**
|
|
* Delete everything in a `range`.
|
|
*
|
|
* @param {Transform} transform
|
|
* @param {Selection} range
|
|
* @return {Transform}
|
|
*/
|
|
|
|
export function deleteAtRange(transform, range) {
|
|
if (range.isCollapsed) return transform
|
|
|
|
const { startKey, startOffset, endKey, endOffset } = range
|
|
|
|
if (startKey == endKey) {
|
|
const index = startOffset
|
|
const length = endOffset - startOffset
|
|
return transform.removeTextByKey(startKey, index, length)
|
|
}
|
|
|
|
let { state } = transform
|
|
let { document } = state
|
|
let ancestor = document.getCommonAncestor(startKey, endKey)
|
|
const startChild = ancestor.getHighestChild(startKey)
|
|
const endChild = ancestor.getHighestChild(endKey)
|
|
const startOff = startChild.getOffset(startKey) + startOffset
|
|
const endOff = endChild.getOffset(endKey) + endOffset
|
|
|
|
transform.splitNodeByKey(startChild.key, startOff)
|
|
transform.splitNodeByKey(endChild.key, endOff)
|
|
|
|
state = transform.state
|
|
document = state.document
|
|
ancestor = document.getCommonAncestor(startKey, endKey)
|
|
|
|
const startBlock = document.getClosestBlock(startKey)
|
|
const endBlock = document.getClosestBlock(document.getNextText(endKey))
|
|
const startIndex = ancestor.nodes.indexOf(startBlock)
|
|
const endIndex = ancestor.nodes.indexOf(endBlock)
|
|
const endLonelyParent = ancestor.getHighestChild(endBlock, (parent) => {
|
|
return parent.nodes.size == 1
|
|
})
|
|
|
|
ancestor.nodes.slice(startIndex + 1, endIndex).forEach((child) => {
|
|
transform.removeNodeByKey(child.key)
|
|
})
|
|
|
|
endBlock.nodes.forEach((child, i) => {
|
|
const newKey = startBlock.key
|
|
const newIndex = startBlock.nodes.size + i
|
|
transform.moveNodeByKey(child.key, newKey, newIndex)
|
|
})
|
|
|
|
transform.removeNodeByKey(endLonelyParent.key)
|
|
transform.normalizeDocument()
|
|
return transform
|
|
}
|
|
|
|
/**
|
|
* Delete backward `n` characters at a `range`.
|
|
*
|
|
* @param {Transform} transform
|
|
* @param {Selection} range
|
|
* @param {Number} n (optional)
|
|
* @return {Transform}
|
|
*/
|
|
|
|
export function deleteBackwardAtRange(transform, range, n = 1) {
|
|
const { state } = transform
|
|
const { document } = state
|
|
const { startKey, focusOffset } = range
|
|
const text = document.getDescendant(startKey)
|
|
const block = document.getClosestBlock(startKey)
|
|
const inline = document.getClosestInline(startKey)
|
|
|
|
if (range.isExpanded) {
|
|
return transform.deleteAtRange(range)
|
|
}
|
|
|
|
if (block && block.isVoid) {
|
|
return transform.removeNodeByKey(block.key)
|
|
}
|
|
|
|
if (inline && inline.isVoid) {
|
|
return transform.removeNodeByKey(inline.key)
|
|
}
|
|
|
|
if (range.isAtStartOf(document)) {
|
|
return transform
|
|
}
|
|
|
|
if (range.isAtStartOf(text)) {
|
|
const prev = document.getPreviousText(text)
|
|
const prevBlock = document.getClosestBlock(prev)
|
|
const prevInline = document.getClosestInline(prev)
|
|
|
|
if (prevBlock && prevBlock.isVoid) {
|
|
return transform.removeNodeByKey(prevBlock.key)
|
|
}
|
|
|
|
if (prevInline && prevInline.isVoid) {
|
|
return transform.removeNodeByKey(prevInline.key)
|
|
}
|
|
|
|
range = range.merge({
|
|
anchorKey: prev.key,
|
|
anchorOffset: prev.length,
|
|
})
|
|
|
|
return transform.deleteAtRange(range)
|
|
}
|
|
|
|
range = range.merge({
|
|
focusOffset: focusOffset - 1,
|
|
isBackward: true,
|
|
})
|
|
|
|
return transform.deleteAtRange(range)
|
|
}
|
|
|
|
/**
|
|
* Delete forward `n` characters at a `range`.
|
|
*
|
|
* @param {Transform} transform
|
|
* @param {Selection} range
|
|
* @param {Number} n (optional)
|
|
* @return {Transform}
|
|
*/
|
|
|
|
export function deleteForwardAtRange(transform, range, n = 1) {
|
|
const { state } = transform
|
|
const { document } = state
|
|
const { startKey, focusOffset } = range
|
|
const text = document.getDescendant(startKey)
|
|
const inline = document.getClosestInline(startKey)
|
|
const block = document.getClosestBlock(startKey)
|
|
|
|
if (range.isExpanded) {
|
|
return transform.deleteAtRange(range)
|
|
}
|
|
|
|
if (block && block.isVoid) {
|
|
return transform.removeNodeByKey(block.key)
|
|
}
|
|
|
|
if (inline && inline.isVoid) {
|
|
return transform.removeNodeByKey(inline.key)
|
|
}
|
|
|
|
if (range.isAtEndOf(document)) {
|
|
return transform
|
|
}
|
|
|
|
if (range.isAtEndOf(text)) {
|
|
const next = document.getNextText(text)
|
|
|
|
range = range.merge({
|
|
focusKey: next.key,
|
|
focusOffset: 0
|
|
})
|
|
|
|
return transform.deleteAtRange(range)
|
|
}
|
|
|
|
range = range.merge({
|
|
focusOffset: focusOffset + 1
|
|
})
|
|
|
|
return transform.deleteAtRange(range)
|
|
}
|
|
|
|
/**
|
|
* Insert a `block` node at `range`.
|
|
*
|
|
* @param {Transform} transform
|
|
* @param {Selection} range
|
|
* @param {Block or String or Object} block
|
|
* @return {Transform}
|
|
*/
|
|
|
|
export function insertBlockAtRange(transform, range, block) {
|
|
block = Normalize.block(block)
|
|
|
|
if (range.isExpanded) {
|
|
transform.deleteAtRange(range)
|
|
range = range.collapseToStart()
|
|
}
|
|
|
|
const { state } = transform
|
|
const { document } = state
|
|
const { startKey, startOffset } = range
|
|
const startText = document.assertDescendant(startKey)
|
|
const startBlock = document.getClosestBlock(startKey)
|
|
const parent = document.getParent(startBlock)
|
|
const index = parent.nodes.indexOf(startBlock)
|
|
|
|
if (startBlock.isVoid) {
|
|
transform.insertNodeByKey(parent.key, index + 1, block)
|
|
}
|
|
|
|
else if (startBlock.isEmpty) {
|
|
transform.removeNodeByKey(startBlock.key)
|
|
transform.insertNodeByKey(parent.key, index, block)
|
|
}
|
|
|
|
else if (range.isAtStartOf(startBlock)) {
|
|
transform.insertNodeByKey(parent.key, index, block)
|
|
}
|
|
|
|
else if (range.isAtEndOf(startBlock)) {
|
|
transform.insertNodeByKey(parent.key, index + 1, block)
|
|
}
|
|
|
|
else {
|
|
const offset = startBlock.getOffset(startText) + startOffset
|
|
transform.splitNodeByKey(startBlock.key, offset)
|
|
transform.insertNodeByKey(parent.key, index + 1, block)
|
|
}
|
|
|
|
transform.normalizeDocument()
|
|
return transform
|
|
}
|
|
|
|
/**
|
|
* Insert a `fragment` at a `range`.
|
|
*
|
|
* @param {Transform} transform
|
|
* @param {Selection} range
|
|
* @param {Document} fragment
|
|
* @return {Transform}
|
|
*/
|
|
|
|
export function insertFragmentAtRange(transform, range, fragment) {
|
|
if (range.isExpanded) {
|
|
transform.deleteAtRange(range)
|
|
range = range.collapseToStart()
|
|
}
|
|
|
|
if (!fragment.length) return transform
|
|
|
|
fragment = fragment.mapDescendants(child => child.set('key', uid()))
|
|
|
|
const { startKey, startOffset } = range
|
|
let { state } = transform
|
|
let { document } = state
|
|
let startText = document.getDescendant(startKey)
|
|
let startBlock = document.getClosestBlock(startText)
|
|
let startChild = startBlock.getHighestChild(startText)
|
|
const parent = document.getParent(startBlock)
|
|
const index = parent.nodes.indexOf(startBlock)
|
|
const offset = startChild == startText
|
|
? startOffset
|
|
: startChild.getOffset(startText) + startOffset
|
|
|
|
const blocks = fragment.getBlocks()
|
|
const firstBlock = blocks.first()
|
|
const lastBlock = blocks.last()
|
|
|
|
if (firstBlock != lastBlock) {
|
|
const lonelyParent = fragment.getFurthest(firstBlock, p => p.nodes.size == 1)
|
|
const lonelyChild = lonelyParent || firstBlock
|
|
const startIndex = parent.nodes.indexOf(startBlock)
|
|
fragment = fragment.removeDescendant(lonelyChild)
|
|
|
|
fragment.nodes.forEach((node, i) => {
|
|
const newIndex = startIndex + i + 2
|
|
transform.insertNodeByKey(parent.key, newIndex, node)
|
|
})
|
|
}
|
|
|
|
transform.splitNodeByKey(startChild.key, offset)
|
|
|
|
state = transform.state
|
|
document = state.document
|
|
startText = document.getDescendant(startKey)
|
|
startBlock = document.getClosestBlock(startKey)
|
|
startChild = startBlock.getHighestChild(startText)
|
|
|
|
if (firstBlock != lastBlock) {
|
|
const nextChild = startBlock.getNextSibling(startChild)
|
|
const nextNodes = startBlock.nodes.skipUntil(n => n == nextChild)
|
|
const lastIndex = lastBlock.nodes.size
|
|
|
|
nextNodes.forEach((node, i) => {
|
|
const newIndex = lastIndex + i + 1
|
|
transform.moveNodeByKey(node.key, lastBlock.key, newIndex)
|
|
})
|
|
}
|
|
|
|
if (startBlock.isEmpty) {
|
|
transform.removeNodeByKey(startBlock.key)
|
|
transform.insertNodeByKey(parent.key, index, firstBlock)
|
|
} else {
|
|
const inlineChild = startBlock.getHighestChild(startText)
|
|
const inlineIndex = startBlock.nodes.indexOf(inlineChild)
|
|
|
|
firstBlock.nodes.forEach((inline, i) => {
|
|
const newIndex = inlineIndex + i + 1
|
|
transform.insertNodeByKey(startBlock.key, newIndex, inline)
|
|
})
|
|
}
|
|
|
|
transform.normalizeDocument()
|
|
return transform
|
|
}
|
|
|
|
/**
|
|
* Insert an `inline` node at `range`.
|
|
*
|
|
* @param {Transform} transform
|
|
* @param {Selection} range
|
|
* @param {Inline or String or Object} inline
|
|
* @return {Transform}
|
|
*/
|
|
|
|
export function insertInlineAtRange(transform, range, inline) {
|
|
inline = Normalize.inline(inline)
|
|
|
|
if (range.isExpanded) {
|
|
transform.deleteAtRange(range)
|
|
range = range.collapseToStart()
|
|
}
|
|
|
|
const { state } = transform
|
|
const { document } = state
|
|
const { startKey, startOffset } = range
|
|
const parent = document.getParent(startKey)
|
|
const startText = document.assertDescendant(startKey)
|
|
const index = parent.nodes.indexOf(startText)
|
|
|
|
if (parent.isVoid) {
|
|
return transform
|
|
}
|
|
|
|
transform.splitNodeByKey(startKey, startOffset)
|
|
transform.insertNodeByKey(parent.key, index + 1, inline)
|
|
transform.normalizeDocument()
|
|
return transform
|
|
}
|
|
|
|
/**
|
|
* Insert `text` at a `range`, with optional `marks`.
|
|
*
|
|
* @param {Transform} transform
|
|
* @param {Selection} range
|
|
* @param {String} text
|
|
* @param {Set} marks (optional)
|
|
* @return {Transform}
|
|
*/
|
|
|
|
export function insertTextAtRange(transform, range, text, marks) {
|
|
const { state } = transform
|
|
const { document } = state
|
|
const { startKey, startOffset } = range
|
|
const parent = document.getParent(startKey)
|
|
|
|
if (parent.isVoid) {
|
|
return transform
|
|
}
|
|
|
|
if (range.isExpanded) {
|
|
transform.deleteAtRange(range)
|
|
}
|
|
|
|
transform.insertTextByKey(startKey, startOffset, text, marks)
|
|
return transform
|
|
}
|
|
|
|
/**
|
|
* Remove an existing `mark` to the characters at `range`.
|
|
*
|
|
* @param {Transform} transform
|
|
* @param {Selection} range
|
|
* @param {Mark or String} mark (optional)
|
|
* @return {Transform}
|
|
*/
|
|
|
|
export function removeMarkAtRange(transform, range, mark) {
|
|
if (range.isCollapsed) return transform
|
|
|
|
const { state } = transform
|
|
const { document } = state
|
|
const texts = document.getTextsAtRange(range)
|
|
const { startKey, startOffset, endKey, endOffset } = range
|
|
|
|
texts.forEach((text) => {
|
|
const { key } = text
|
|
let index = 0
|
|
let length = text.length
|
|
|
|
if (key == startKey) index = startOffset
|
|
if (key == endKey) length = endOffset
|
|
if (key == startKey && key == endKey) length = endOffset - startOffset
|
|
|
|
transform.removeMarkByKey(key, index, length, mark)
|
|
})
|
|
|
|
return transform
|
|
}
|
|
|
|
/**
|
|
* Set the `properties` of block nodes in a `range`.
|
|
*
|
|
* @param {Transform} transform
|
|
* @param {Selection} range
|
|
* @param {Object || String} properties
|
|
* @return {Transform}
|
|
*/
|
|
|
|
export function setBlockAtRange(transform, range, properties) {
|
|
const { state } = transform
|
|
const { document } = state
|
|
const blocks = document.getBlocksAtRange(range)
|
|
|
|
blocks.forEach((block) => {
|
|
transform.setNodeByKey(block.key, properties)
|
|
})
|
|
|
|
return transform
|
|
}
|
|
|
|
/**
|
|
* Set the `properties` of inline nodes in a `range`.
|
|
*
|
|
* @param {Transform} transform
|
|
* @param {Selection} range
|
|
* @param {Object || String} properties
|
|
* @return {Transform}
|
|
*/
|
|
|
|
export function setInlineAtRange(transform, range, properties) {
|
|
const { state } = transform
|
|
const { document } = state
|
|
const inlines = document.getInlinesAtRange(range)
|
|
|
|
inlines.forEach((inline) => {
|
|
transform.setNodeByKey(inline.key, properties)
|
|
})
|
|
|
|
return transform
|
|
}
|
|
|
|
/**
|
|
* Split the block nodes at a `range`, to optional `height`.
|
|
*
|
|
* @param {Transform} transform
|
|
* @param {Selection} range
|
|
* @param {Number} height (optional)
|
|
* @return {Transform}
|
|
*/
|
|
|
|
export function splitBlockAtRange(transform, range, height = 1) {
|
|
if (range.isExpanded) {
|
|
transform.deleteAtRange(range)
|
|
range = range.collapseToStart()
|
|
}
|
|
|
|
const { startKey, startOffset } = range
|
|
const { state } = transform
|
|
const { document } = state
|
|
let node = document.assertDescendant(startKey)
|
|
let parent = document.getClosestBlock(node)
|
|
let offset = startOffset
|
|
let h = 0
|
|
|
|
while (parent && parent.kind == 'block' && h < height) {
|
|
offset += parent.getOffset(node)
|
|
node = parent
|
|
parent = document.getClosestBlock(parent)
|
|
h++
|
|
}
|
|
|
|
transform.splitNodeByKey(node.key, offset)
|
|
transform.normalizeDocument()
|
|
return transform
|
|
}
|
|
|
|
/**
|
|
* Split the inline nodes at a `range`, to optional `height`.
|
|
*
|
|
* @param {Transform} transform
|
|
* @param {Selection} range
|
|
* @param {Number} height (optiona)
|
|
* @return {Transform}
|
|
*/
|
|
|
|
export function splitInlineAtRange(transform, range, height = Infinity) {
|
|
if (range.isExpanded) {
|
|
transform.deleteAtRange(range)
|
|
range = range.collapseToStart()
|
|
}
|
|
|
|
const { startKey, startOffset } = range
|
|
const { state } = transform
|
|
const { document } = state
|
|
let node = document.assertDescendant(startKey)
|
|
let parent = document.getClosestInline(node)
|
|
let offset = startOffset
|
|
let h = 0
|
|
|
|
while (parent && parent.kind == 'inline' && h < height) {
|
|
offset += parent.getOffset(node)
|
|
node = parent
|
|
parent = document.getClosestInline(parent)
|
|
h++
|
|
}
|
|
|
|
return transform.splitNodeByKey(node.key, offset)
|
|
}
|
|
|
|
/**
|
|
* Add or remove a `mark` from the characters at `range`, depending on whether
|
|
* it's already there.
|
|
*
|
|
* @param {Transform} transform
|
|
* @param {Selection} range
|
|
* @param {Mixed} mark
|
|
* @return {Transform}
|
|
*/
|
|
|
|
export function toggleMarkAtRange(transform, range, mark) {
|
|
if (range.isCollapsed) return transform
|
|
|
|
mark = Normalize.mark(mark)
|
|
|
|
const { state } = transform
|
|
const { document } = state
|
|
const marks = document.getMarksAtRange(range)
|
|
const exists = marks.some(m => m.equals(mark))
|
|
|
|
if (exists) {
|
|
transform.removeMarkAtRange(range, mark)
|
|
} else {
|
|
transform.addMarkAtRange(range, mark)
|
|
}
|
|
|
|
return transform
|
|
}
|
|
|
|
/**
|
|
* Unwrap all of the block nodes in a `range` from a block with `properties`.
|
|
*
|
|
* @param {Transform} transform
|
|
* @param {Selection} range
|
|
* @param {String or Object} properties
|
|
* @return {Transform}
|
|
*/
|
|
|
|
export function unwrapBlockAtRange(transform, range, properties) {
|
|
properties = Normalize.nodeProperties(properties)
|
|
|
|
let { state } = transform
|
|
let { document } = state
|
|
const blocks = document.getBlocksAtRange(range)
|
|
const wrappers = blocks
|
|
.map((block) => {
|
|
return document.getClosest(block, (parent) => {
|
|
if (parent.kind != 'block') return false
|
|
if (properties.type != null && parent.type != properties.type) return false
|
|
if (properties.isVoid != null && parent.isVoid != properties.isVoid) return false
|
|
if (properties.data != null && !parent.data.isSuperset(properties.data)) return false
|
|
return true
|
|
})
|
|
})
|
|
.filter(exists => exists)
|
|
.toSet()
|
|
.toList()
|
|
|
|
wrappers.forEach((block) => {
|
|
const first = block.nodes.first()
|
|
const last = block.nodes.last()
|
|
const parent = document.getParent(block)
|
|
const index = parent.nodes.indexOf(block)
|
|
|
|
const children = block.nodes.filter((child) => {
|
|
return blocks.some(b => child == b || child.hasDescendant(b))
|
|
})
|
|
|
|
const firstMatch = children.first()
|
|
let lastMatch = children.last()
|
|
|
|
if (first == firstMatch && last == lastMatch) {
|
|
block.nodes.forEach((child, i) => {
|
|
transform.moveNodeByKey(child.key, parent.key, index + i)
|
|
})
|
|
|
|
transform.removeNodeByKey(block.key)
|
|
}
|
|
|
|
else if (last == lastMatch) {
|
|
block.nodes
|
|
.skipUntil(n => n == firstMatch)
|
|
.forEach((child, i) => {
|
|
transform.moveNodeByKey(child.key, parent.key, index + 1 + i)
|
|
})
|
|
}
|
|
|
|
else if (first == firstMatch) {
|
|
block.nodes
|
|
.takeUntil(n => n == lastMatch)
|
|
.push(lastMatch)
|
|
.forEach((child, i) => {
|
|
transform.moveNodeByKey(child.key, parent.key, index + i)
|
|
})
|
|
}
|
|
|
|
else {
|
|
const offset = block.getOffset(firstMatch)
|
|
|
|
transform.splitNodeByKey(block.key, offset)
|
|
state = transform.state
|
|
document = state.document
|
|
const extra = document.getPreviousSibling(firstMatch)
|
|
|
|
children.forEach((child, i) => {
|
|
transform.moveNodeByKey(child.key, parent.key, index + 1 + i)
|
|
})
|
|
|
|
transform.removeNodeByKey(extra.key)
|
|
}
|
|
})
|
|
|
|
transform.normalizeDocument()
|
|
return transform
|
|
}
|
|
|
|
/**
|
|
* Unwrap the inline nodes in a `range` from an inline with `properties`.
|
|
*
|
|
* @param {Transform} transform
|
|
* @param {Selection} range
|
|
* @param {String or Object} properties
|
|
* @return {Transform}
|
|
*/
|
|
|
|
export function unwrapInlineAtRange(transform, range, properties) {
|
|
properties = Normalize.nodeProperties(properties)
|
|
|
|
const { state } = transform
|
|
const { document } = state
|
|
const texts = document.getTexts()
|
|
const inlines = texts
|
|
.map((text) => {
|
|
return document.getClosest(text, (parent) => {
|
|
if (parent.kind != 'inline') return false
|
|
if (properties.type != null && parent.type != properties.type) return false
|
|
if (properties.isVoid != null && parent.isVoid != properties.isVoid) return false
|
|
if (properties.data != null && !parent.data.isSuperset(properties.data)) return false
|
|
return true
|
|
})
|
|
})
|
|
.filter(exists => exists)
|
|
.toSet()
|
|
.toList()
|
|
|
|
inlines.forEach((inline) => {
|
|
const parent = document.getParent(inline)
|
|
const index = parent.nodes.indexOf(inline)
|
|
|
|
inline.nodes.forEach((child, i) => {
|
|
transform.moveNodeByKey(child.key, parent.key, index + i)
|
|
})
|
|
})
|
|
|
|
transform.normalizeDocument()
|
|
return transform
|
|
}
|
|
|
|
/**
|
|
* Wrap all of the blocks in a `range` in a new `block`.
|
|
*
|
|
* @param {Transform} transform
|
|
* @param {Selection} range
|
|
* @param {Block || Object || String} block
|
|
* @return {Transform}
|
|
*/
|
|
|
|
export function wrapBlockAtRange(transform, range, block) {
|
|
block = Normalize.block(block)
|
|
|
|
const { state } = transform
|
|
const { document } = state
|
|
const blocks = document.getBlocksAtRange(range)
|
|
const depth = blocks.map(n => document.getDepth(n)).min()
|
|
|
|
const siblings = blocks
|
|
.map((node) => {
|
|
const d = document.getDepth(node)
|
|
if (d == depth) return node
|
|
return document.getClosest(node, p => document.getDepth(p) == depth)
|
|
})
|
|
.toSet()
|
|
.toList()
|
|
|
|
const first = siblings.first()
|
|
const parent = document.getParent(first)
|
|
const index = parent.nodes.indexOf(first)
|
|
|
|
transform.insertNodeByKey(parent.key, index + 1, block)
|
|
|
|
siblings.forEach((node, i) => {
|
|
transform.moveNodeByKey(node.key, block.key, i)
|
|
})
|
|
|
|
return transform
|
|
}
|
|
|
|
/**
|
|
* Wrap the text and inlines in a `range` in a new `inline`.
|
|
*
|
|
* @param {Transform} transform
|
|
* @param {Selection} range
|
|
* @param {Inline || Object || String} inline
|
|
* @return {Transform}
|
|
*/
|
|
|
|
export function wrapInlineAtRange(transform, range, inline) {
|
|
if (range.isCollapsed) return transform
|
|
|
|
inline = Normalize.inline(inline)
|
|
|
|
const { startKey, startOffset, endKey, endOffset } = range
|
|
let { state } = transform
|
|
let { document } = state
|
|
const blocks = document.getBlocksAtRange(range)
|
|
let startBlock = document.getClosestBlock(startKey)
|
|
let endBlock = document.getClosestBlock(endKey)
|
|
let startChild = startBlock.getHighestChild(startKey)
|
|
let endChild = endBlock.getHighestChild(endKey)
|
|
const startIndex = startBlock.nodes.indexOf(startChild)
|
|
const endIndex = endBlock.nodes.indexOf(endChild)
|
|
|
|
const startOff = startChild.key == startKey
|
|
? startOffset
|
|
: startChild.getOffset(startKey) + startOffset
|
|
|
|
const endOff = endChild.key == endKey
|
|
? endOffset
|
|
: endChild.getOffset(endKey) + endOffset
|
|
|
|
if (startBlock == endBlock) {
|
|
transform.splitNodeByKey(endChild.key, endOff)
|
|
transform.splitNodeByKey(startChild.key, startOff)
|
|
|
|
state = transform.state
|
|
document = state.document
|
|
startBlock = document.getClosestBlock(startKey)
|
|
startChild = startBlock.getHighestChild(startKey)
|
|
const startInner = document.getNextSibling(startChild)
|
|
const startInnerIndex = startBlock.nodes.indexOf(startInner)
|
|
const endInner = startKey == endKey ? startInner : startBlock.getHighestChild(endKey)
|
|
const inlines = startBlock.nodes
|
|
.skipUntil(n => n == startInner)
|
|
.takeUntil(n => n == endInner)
|
|
.push(endInner)
|
|
|
|
const node = inline.merge({ key: uid() })
|
|
|
|
transform.insertNodeByKey(startBlock.key, startInnerIndex, node)
|
|
|
|
inlines.forEach((child, i) => {
|
|
transform.moveNodeByKey(child.key, node.key, i)
|
|
})
|
|
}
|
|
|
|
else {
|
|
transform.splitNodeByKey(startChild.key, startOff)
|
|
transform.splitNodeByKey(endChild.key, endOff)
|
|
|
|
state = transform.state
|
|
document = state.document
|
|
startBlock = document.getDescendant(startBlock.key)
|
|
endBlock = document.getDescendant(endBlock.key)
|
|
|
|
const startInlines = startBlock.nodes.slice(startIndex + 1)
|
|
const endInlines = endBlock.nodes.slice(0, endIndex + 1)
|
|
const startNode = inline.merge({ key: uid() })
|
|
const endNode = inline.merge({ key: uid() })
|
|
|
|
transform.insertNodeByKey(startBlock.key, startIndex + 1, startNode)
|
|
transform.insertNodeByKey(endBlock.key, endIndex + 1, endNode)
|
|
|
|
startInlines.forEach((child, i) => {
|
|
transform.moveNodeByKey(child.key, startNode.key, i)
|
|
})
|
|
|
|
endInlines.forEach((child, i) => {
|
|
transform.moveNodeByKey(child.key, endNode.key, i)
|
|
})
|
|
|
|
blocks.slice(1, -1).forEach((block) => {
|
|
const node = inline.merge({ key: uid() })
|
|
transform.insertNodeByKey(block.key, 0, node)
|
|
|
|
block.nodes.forEach((child, i) => {
|
|
transform.moveNodeByKey(child.key, node.key, i)
|
|
})
|
|
})
|
|
}
|
|
|
|
transform.normalizeDocument()
|
|
return transform
|
|
}
|
|
|
|
/**
|
|
* Wrap the text in a `range` in a prefix/suffix.
|
|
*
|
|
* @param {Transform} transform
|
|
* @param {Selection} range
|
|
* @param {String} prefix
|
|
* @param {String} suffix (optional)
|
|
* @return {Transform}
|
|
*/
|
|
|
|
export function wrapTextAtRange(transform, range, prefix, suffix = prefix) {
|
|
const { startKey, endKey } = range
|
|
const start = range.collapseToStart()
|
|
let end = range.collapseToEnd()
|
|
|
|
if (startKey == endKey) {
|
|
end = end.moveForward(prefix.length)
|
|
}
|
|
|
|
transform.insertTextAtRange(start, prefix)
|
|
transform.insertTextAtRange(end, suffix)
|
|
return transform
|
|
}
|