mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-09-01 11:12:42 +02:00
more refactoring transforms
This commit is contained in:
@@ -6,7 +6,7 @@ import Selection from '../models/selection'
|
||||
import Text from '../models/text'
|
||||
import isInRange from '../utils/is-in-range'
|
||||
import uid from '../utils/uid'
|
||||
import { Set } from 'immutable'
|
||||
import { List, Set } from 'immutable'
|
||||
|
||||
/**
|
||||
* Add a new `mark` to the characters at `range`.
|
||||
@@ -586,90 +586,80 @@ export function toggleMarkAtRange(transform, range, mark) {
|
||||
*/
|
||||
|
||||
export function unwrapBlockAtRange(transform, range, properties) {
|
||||
let { state } = transform
|
||||
properties = Normalize.nodeProperties(properties)
|
||||
|
||||
let { state } = transform
|
||||
let { document } = state
|
||||
|
||||
// Get the deepest blocks in the range.
|
||||
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()
|
||||
|
||||
// Get the matching wrapper blocks.
|
||||
const wrappers = blocks.reduce((memo, text) => {
|
||||
const match = document.getClosest(text, (parent) => {
|
||||
if (parent.kind != 'block') return false
|
||||
if (properties.type && parent.type != properties.type) return false
|
||||
if (properties.data && !parent.data.isSuperset(properties.data)) return false
|
||||
return true
|
||||
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))
|
||||
})
|
||||
|
||||
if (match) memo = memo.add(match)
|
||||
return memo
|
||||
}, new Set())
|
||||
|
||||
// For each of the wrapper blocks...
|
||||
wrappers.forEach((wrapper) => {
|
||||
const first = wrapper.nodes.first()
|
||||
const last = wrapper.nodes.last()
|
||||
const parent = document.getParent(wrapper)
|
||||
|
||||
// Get the wrapped direct children.
|
||||
const children = wrapper.nodes.filter((child) => {
|
||||
return blocks.some(block => child == block || child.hasDescendant(block))
|
||||
})
|
||||
|
||||
// Determine what the new nodes should be...
|
||||
const firstMatch = children.first()
|
||||
const lastMatch = children.last()
|
||||
let nodes
|
||||
let lastMatch = children.last()
|
||||
|
||||
// If the first and last both match, remove the wrapper completely.
|
||||
if (first == firstMatch && last == lastMatch) {
|
||||
nodes = parent.nodes.takeUntil(n => n == wrapper)
|
||||
.concat(wrapper.nodes)
|
||||
.concat(parent.nodes.skipUntil(n => n == wrapper).rest())
|
||||
block.nodes.forEach((child, i) => {
|
||||
transform.moveNodeByKey(child.key, parent.key, index + i)
|
||||
})
|
||||
|
||||
transform.removeNodeByKey(block.key)
|
||||
}
|
||||
|
||||
// If only the last child matches, move the last nodes.
|
||||
else if (last == lastMatch) {
|
||||
const remain = wrapper.nodes.takeUntil(n => n == firstMatch)
|
||||
const updated = wrapper.merge({ nodes: remain })
|
||||
nodes = parent.nodes.takeUntil(n => n == wrapper)
|
||||
.push(updated)
|
||||
.concat(children)
|
||||
.concat(parent.nodes.skipUntil(n => n == wrapper).rest())
|
||||
block.nodes
|
||||
.skipUntil(n => n == firstMatch)
|
||||
.forEach((child, i) => {
|
||||
transform.moveNodeByKey(child.key, parent.key, index + 1 + i)
|
||||
})
|
||||
}
|
||||
|
||||
// If only the first child matches, move the first ones.
|
||||
else if (first == firstMatch) {
|
||||
const remain = wrapper.nodes.skipUntil(n => n == lastMatch).rest()
|
||||
const updated = wrapper.merge({ nodes: remain })
|
||||
nodes = parent.nodes.takeUntil(n => n == wrapper)
|
||||
.concat(children)
|
||||
.push(updated)
|
||||
.concat(parent.nodes.skipUntil(n => n == wrapper).rest())
|
||||
block.nodes
|
||||
.takeUntil(n => n == lastMatch)
|
||||
.push(lastMatch)
|
||||
.forEach((child, i) => {
|
||||
transform.moveNodeByKey(child.key, parent.key, index + i)
|
||||
})
|
||||
}
|
||||
|
||||
// Otherwise, move the middle ones.
|
||||
else {
|
||||
const firsts = wrapper.nodes.takeUntil(n => n == firstMatch)
|
||||
const lasts = wrapper.nodes.skipUntil(n => n == lastMatch).rest()
|
||||
const updatedFirst = wrapper.merge({ nodes: firsts })
|
||||
const updatedLast = wrapper.merge({ nodes: lasts })
|
||||
nodes = parent.nodes.takeUntil(n => n == wrapper)
|
||||
.push(updatedFirst)
|
||||
.concat(children)
|
||||
.push(updatedLast)
|
||||
.concat(parent.nodes.skipUntil(n => n == wrapper).rest())
|
||||
}
|
||||
const offset = block.getOffset(firstMatch)
|
||||
|
||||
document = parent == document
|
||||
? document.merge({ nodes })
|
||||
: document.updateDescendant(parent.merge({ nodes }))
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
document = document.normalize()
|
||||
state = state.merge({ document })
|
||||
transform.state = state
|
||||
transform.normalizeDocument()
|
||||
return transform
|
||||
}
|
||||
|
||||
@@ -683,183 +673,163 @@ export function unwrapBlockAtRange(transform, range, properties) {
|
||||
*/
|
||||
|
||||
export function unwrapInlineAtRange(transform, range, properties) {
|
||||
let { state } = transform
|
||||
properties = Normalize.nodeProperties(properties)
|
||||
let { document } = state
|
||||
let blocks = document.getInlinesAtRange(range)
|
||||
|
||||
// Find the closest matching inline wrappers of each text node.
|
||||
const { state } = transform
|
||||
const { document } = state
|
||||
const texts = document.getTexts()
|
||||
const wrappers = texts.reduce((memo, text) => {
|
||||
const match = document.getClosest(text, (parent) => {
|
||||
if (parent.kind != 'inline') return false
|
||||
if (properties.type && parent.type != properties.type) return false
|
||||
if (properties.data && !parent.data.isSuperset(properties.data)) return false
|
||||
return true
|
||||
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()
|
||||
|
||||
if (match) memo = memo.add(match)
|
||||
return memo
|
||||
}, new Set())
|
||||
inlines.forEach((inline) => {
|
||||
const parent = document.getParent(inline)
|
||||
const index = parent.nodes.indexOf(inline)
|
||||
|
||||
// Replace each of the wrappers with their child nodes.
|
||||
wrappers.forEach((wrapper) => {
|
||||
const parent = document.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.
|
||||
document = parent == document
|
||||
? document.merge({ nodes })
|
||||
: document.updateDescendant(parent.merge({ nodes }))
|
||||
inline.nodes.forEach((child, i) => {
|
||||
transform.moveNodeByKey(child.key, parent.key, index + i)
|
||||
})
|
||||
})
|
||||
|
||||
document = document.normalize()
|
||||
state = state.merge({ document })
|
||||
transform.state = state
|
||||
transform.normalizeDocument()
|
||||
return transform
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap all of the blocks in a `range` in a new block with `properties`.
|
||||
* Wrap all of the blocks in a `range` in a new `block`.
|
||||
*
|
||||
* @param {Transform} transform
|
||||
* @param {Selection} range
|
||||
* @param {String or Object} properties
|
||||
* @param {Block || Object || String} block
|
||||
* @return {Transform}
|
||||
*/
|
||||
|
||||
export function wrapBlockAtRange(transform, range, properties) {
|
||||
let { state } = transform
|
||||
properties = Normalize.nodeProperties(properties)
|
||||
let { document } = state
|
||||
export function wrapBlockAtRange(transform, range, block) {
|
||||
block = Normalize.block(block)
|
||||
|
||||
// Get the block nodes, sorted by depth.
|
||||
const { state } = transform
|
||||
const { document } = state
|
||||
const blocks = document.getBlocksAtRange(range)
|
||||
const sorted = blocks.sort((a, b) => {
|
||||
const da = document.getDepth(a)
|
||||
const db = document.getDepth(b)
|
||||
if (da == db) return 0
|
||||
else if (da > db) return -1
|
||||
else return 1
|
||||
})
|
||||
const depth = blocks.map(n => document.getDepth(n)).min()
|
||||
|
||||
// Get the lowest common siblings, relative to the highest block.
|
||||
const highest = sorted.first()
|
||||
const depth = document.getDepth(highest)
|
||||
const siblings = blocks.reduce((memo, block) => {
|
||||
const sibling = document.getDepth(block) == depth
|
||||
? block
|
||||
: document.getClosest(block, (p) => document.getDepth(p) == depth)
|
||||
memo = memo.push(sibling)
|
||||
return memo
|
||||
}, Block.createList())
|
||||
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()
|
||||
|
||||
// Wrap the siblings in a new block.
|
||||
const wrapper = Block.create({
|
||||
nodes: siblings,
|
||||
type: properties.type,
|
||||
data: properties.data
|
||||
})
|
||||
|
||||
// Replace the siblings with the wrapper.
|
||||
const first = siblings.first()
|
||||
const last = siblings.last()
|
||||
const parent = document.getParent(highest)
|
||||
const nodes = parent.nodes
|
||||
.takeUntil(n => n == first)
|
||||
.push(wrapper)
|
||||
.concat(parent.nodes.skipUntil(n => n == last).rest())
|
||||
const parent = document.getParent(first)
|
||||
const index = parent.nodes.indexOf(first)
|
||||
|
||||
// Update the parent.
|
||||
document = parent == document
|
||||
? document.merge({ nodes })
|
||||
: document.updateDescendant(parent.merge({ nodes }))
|
||||
transform.insertNodeByKey(parent.key, index + 1, block)
|
||||
|
||||
siblings.forEach((node, i) => {
|
||||
transform.moveNodeByKey(node.key, block.key, i)
|
||||
})
|
||||
|
||||
state = state.merge({ document })
|
||||
transform.state = state
|
||||
return transform
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the text and inlines in a `range` in a new inline with `properties`.
|
||||
* Wrap the text and inlines in a `range` in a new `inline`.
|
||||
*
|
||||
* @param {Transform} transform
|
||||
* @param {Selection} range
|
||||
* @param {String or Object} properties
|
||||
* @param {Inline || Object || String} inline
|
||||
* @return {Transform}
|
||||
*/
|
||||
|
||||
export function wrapInlineAtRange(transform, range, properties) {
|
||||
let { state } = transform
|
||||
properties = Normalize.nodeProperties(properties)
|
||||
let { document } = state
|
||||
|
||||
// If collapsed, there's nothing to wrap.
|
||||
export function wrapInlineAtRange(transform, range, inline) {
|
||||
if (range.isCollapsed) return transform
|
||||
|
||||
// Split at the start of the range.
|
||||
const start = range.collapseToStart()
|
||||
transform = splitInlineAtRange(transform, start)
|
||||
state = transform.state
|
||||
document = state.document
|
||||
inline = Normalize.inline(inline)
|
||||
|
||||
// Determine the new end of the range, and split there.
|
||||
const { startKey, startOffset, endKey, endOffset } = range
|
||||
const firstNode = document.getDescendant(startKey)
|
||||
const nextNode = document.getNextText(startKey)
|
||||
const end = startKey != endKey
|
||||
? range.collapseToEnd()
|
||||
: Selection.create({
|
||||
anchorKey: nextNode.key,
|
||||
anchorOffset: endOffset - startOffset,
|
||||
focusKey: nextNode.key,
|
||||
focusOffset: endOffset - startOffset
|
||||
})
|
||||
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)
|
||||
|
||||
transform = splitInlineAtRange(transform, end)
|
||||
state = transform.state
|
||||
document = state.document
|
||||
const startOff = startChild.key == startKey
|
||||
? startOffset
|
||||
: startChild.getOffset(startKey)
|
||||
|
||||
// Calculate the new range to wrap around.
|
||||
const endNode = document.getDescendant(end.anchorKey)
|
||||
range = Selection.create({
|
||||
anchorKey: nextNode.key,
|
||||
anchorOffset: 0,
|
||||
focusKey: endNode.key,
|
||||
focusOffset: endNode.length
|
||||
})
|
||||
const endOff = endChild.key == endKey
|
||||
? endOffset
|
||||
: endChild.getOffset(endKey)
|
||||
|
||||
// Get the furthest inline nodes in the range.
|
||||
const texts = document.getTextsAtRange(range)
|
||||
const children = texts.map(text => document.getFurthestInline(text) || text)
|
||||
if (startBlock == endBlock) {
|
||||
transform.splitNodeByKey(endChild.key, endOff)
|
||||
transform.splitNodeByKey(startChild.key, startOff)
|
||||
|
||||
// Iterate each of the child nodes, wrapping them.
|
||||
children.forEach((child) => {
|
||||
const wrapper = Inline.create({
|
||||
nodes: [child],
|
||||
type: properties.type,
|
||||
data: properties.data
|
||||
state = transform.state
|
||||
document = state.document
|
||||
startBlock = document.getClosestBlock(startKey)
|
||||
const startInner = document.getNextText(startKey)
|
||||
const innerParent = document.getParent(startInner)
|
||||
const startInnerIndex = innerParent.nodes.indexOf(startInner)
|
||||
const endInner = startKey == endKey ? startInner : document.getDescendant(endKey)
|
||||
const inlines = startBlock.nodes
|
||||
.skipUntil(n => n == startInner)
|
||||
.takeUntil(n => n == endInner)
|
||||
.push(endInner)
|
||||
|
||||
const node = inline.merge({
|
||||
key: uid(),
|
||||
nodes: inlines
|
||||
})
|
||||
|
||||
// Replace the child in it's parent with the wrapper.
|
||||
const parent = document.getParent(child)
|
||||
const nodes = parent.nodes.takeUntil(n => n == child)
|
||||
.push(wrapper)
|
||||
.concat(parent.nodes.skipUntil(n => n == child).rest())
|
||||
debugger
|
||||
|
||||
// Update the parent.
|
||||
document = parent == document
|
||||
? document.merge({ nodes })
|
||||
: document.updateDescendant(parent.merge({ nodes }))
|
||||
})
|
||||
transform.insertNodeByKey(startBlock.key, startInnerIndex, node)
|
||||
|
||||
document = document.normalize()
|
||||
state = state.merge({ document })
|
||||
transform.state = state
|
||||
inlines.forEach((child, i) => {
|
||||
transform.removeNodeByKey(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)
|
||||
// startChild = startBlock.
|
||||
|
||||
// const startInlines = startBlock.skip(startIndex)
|
||||
|
||||
blocks.rest().butLast().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
|
||||
}
|
||||
|
||||
|
@@ -108,6 +108,7 @@ export function insertTextByKey(transform, key, offset, text, marks) {
|
||||
export function moveNodeByKey(transform, key, newKey, newIndex) {
|
||||
let { state } = transform
|
||||
let { document } = state
|
||||
debugger
|
||||
const node = document.assertDescendant(key)
|
||||
const path = document.getPath(node)
|
||||
const newPath = document.getPath(newKey)
|
||||
@@ -118,7 +119,7 @@ export function moveNodeByKey(transform, key, newKey, newIndex) {
|
||||
parent = parent.removeNode(index)
|
||||
document = isParent ? parent : document.updateDescendant(parent)
|
||||
|
||||
let target = document.assertDescendant(newKey)
|
||||
let target = document.key == newKey ? document : document.assertDescendant(newKey)
|
||||
const isTarget = document == target
|
||||
|
||||
target = target.insertNode(newIndex, node)
|
||||
|
@@ -27,7 +27,7 @@ export default function (state) {
|
||||
.apply()
|
||||
|
||||
const updated = next.document.getTexts().get(1)
|
||||
debugger
|
||||
|
||||
assert.deepEqual(
|
||||
next.selection.toJS(),
|
||||
range.merge({
|
||||
|
Reference in New Issue
Block a user