mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-29 09:59:48 +02:00
[proposal] Meld fragment into document in Commands.insertFragmentAtRange
(#2273)
#### Is this adding or improving a _feature_ or fixing a _bug_? Fixing a bug. This is a proposed solution for https://github.com/ianstormtaylor/slate/issues/2064 (which also aims to maintain the existing _single nested block_ behavior added in https://github.com/ianstormtaylor/slate/pull/1366). #### What's the new behavior? Today, we always insert all nodes of the fragment at the range. After this change, if the fragment is wrapped by/nested in blocks, and the nesting pattern already exists at the range, we meld the fragment contents into the document’s existing nesting pattern as much as possible. Put another way, we can skip adding the fragment’s wrapping nodes if equivalent nodes already wrap the range. Here’s a quick sketch that may better demonstrate the intention:  Here, both the document and fragment wrap their content in two paragraphs. Therefore, we can add the fragment contents within the existing pair of nested paragraphs. #### How does this change work? A new function is added, `findInsertionNode`. Given a `fragment`, `document`, and `startKey`, it returns a node from the fragment to be inserted into the document. By default, the node returned will be the `fragment`, but if a common nesting pattern is detected, a sub-node of the fragment will be returned. The detection algorithm is as follows: 1. Ensure the fragment has a single child node, call it `fragmentInner` 2. Find the furthest document ancestor of the `startKey` matching `fragmentInner.type`. Call this `documentInner` 3. Drill into both `documentInner` and `fragmentInner` as long as each has a single child node and their types match. The stopping point for the fragment is the sub-node to insert into the document. `Commands.insertFragmentAtRange` remains mostly unchanged. One important modification is that the _single nested block_ (again, see https://github.com/ianstormtaylor/slate/pull/1366) only happens if the found insertion node is the fragment. For anything more inner, we meld. #### Have you checked that...? * [x] The new code matches the existing patterns and styles. * [x] The tests pass with `yarn test`. * [x] The linter passes with `yarn lint`. (Fix errors with `yarn prettier`.) * [x] The relevant examples still work. (Run examples with `yarn watch`.) #### Does this fix any issues or need any specific reviewers? Fixes: https://github.com/ianstormtaylor/slate/issues/2064
This commit is contained in:
committed by
Ian Storm Taylor
parent
9cdd6c9637
commit
4e906b3be2
@@ -668,6 +668,7 @@ Commands.insertFragmentAtRange = (change, range, fragment) => {
|
||||
const lastChild = fragment.nodes.last()
|
||||
const firstBlock = blocks.first()
|
||||
const lastBlock = blocks.last()
|
||||
const insertionNode = findInsertionNode(fragment, document, startBlock.key)
|
||||
|
||||
// If the fragment only contains a void block, use `insertBlock` instead.
|
||||
if (firstBlock === lastBlock && change.isVoid(firstBlock)) {
|
||||
@@ -675,9 +676,12 @@ Commands.insertFragmentAtRange = (change, range, fragment) => {
|
||||
return
|
||||
}
|
||||
|
||||
// If the fragment starts or ends with single nested block, (e.g., table),
|
||||
// do not merge this fragment with existing blocks.
|
||||
if (firstChild.hasBlockChildren() || lastChild.hasBlockChildren()) {
|
||||
// If inserting the entire fragment and it starts or ends with a single
|
||||
// nested block, e.g. a table, we do not merge it with existing blocks.
|
||||
if (
|
||||
insertionNode === fragment &&
|
||||
(firstChild.hasBlockChildren() || lastChild.hasBlockChildren())
|
||||
) {
|
||||
fragment.nodes.reverse().forEach(node => {
|
||||
change.insertBlockAtRange(range, node)
|
||||
})
|
||||
@@ -685,17 +689,18 @@ Commands.insertFragmentAtRange = (change, range, fragment) => {
|
||||
}
|
||||
|
||||
// If the first and last block aren't the same, we need to insert all of the
|
||||
// nodes after the fragment's first block at the index.
|
||||
// nodes after the insertion node's first block at the index.
|
||||
if (firstBlock != lastBlock) {
|
||||
const lonelyParent = fragment.getFurthest(
|
||||
const lonelyParent = insertionNode.getFurthest(
|
||||
firstBlock.key,
|
||||
p => p.nodes.size == 1
|
||||
)
|
||||
const lonelyChild = lonelyParent || firstBlock
|
||||
const startIndex = parent.nodes.indexOf(startBlock)
|
||||
fragment = fragment.removeNode(lonelyChild.key)
|
||||
|
||||
fragment.nodes.forEach((node, i) => {
|
||||
const startIndex = parent.nodes.indexOf(startBlock)
|
||||
const excludingLonelyChild = insertionNode.removeNode(lonelyChild.key)
|
||||
|
||||
excludingLonelyChild.nodes.forEach((node, i) => {
|
||||
const newIndex = startIndex + i + 1
|
||||
change.insertNodeByKey(parent.key, newIndex, node)
|
||||
})
|
||||
@@ -750,6 +755,34 @@ Commands.insertFragmentAtRange = (change, range, fragment) => {
|
||||
})
|
||||
}
|
||||
|
||||
const findInsertionNode = (fragment, document, startKey) => {
|
||||
const hasSingleNode = object => object && object.nodes.size === 1
|
||||
const firstNode = object => object && object.nodes.first()
|
||||
let node = fragment
|
||||
|
||||
if (hasSingleNode(fragment)) {
|
||||
let fragmentInner = firstNode(fragment)
|
||||
|
||||
const matches = documentNode => documentNode.type === fragmentInner.type
|
||||
let documentInner = document.getFurthest(startKey, matches)
|
||||
|
||||
if (documentInner === document.getParent(startKey)) node = fragmentInner
|
||||
|
||||
while (hasSingleNode(fragmentInner) && hasSingleNode(documentInner)) {
|
||||
fragmentInner = firstNode(fragmentInner)
|
||||
documentInner = firstNode(documentInner)
|
||||
|
||||
if (fragmentInner.type === documentInner.type) {
|
||||
node = fragmentInner
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert an `inline` node at `range`.
|
||||
*
|
||||
|
@@ -0,0 +1,49 @@
|
||||
/** @jsx h */
|
||||
|
||||
import h from '../../../helpers/h'
|
||||
|
||||
export default function(change) {
|
||||
change.insertFragment(
|
||||
<document>
|
||||
<code>
|
||||
<quote>
|
||||
<paragraph>2</paragraph>
|
||||
<paragraph>3</paragraph>
|
||||
</quote>
|
||||
</code>
|
||||
</document>
|
||||
)
|
||||
}
|
||||
|
||||
export const input = (
|
||||
<value>
|
||||
<document>
|
||||
<paragraph>
|
||||
<code>
|
||||
<quote>
|
||||
<paragraph>
|
||||
1<cursor />
|
||||
</paragraph>
|
||||
</quote>
|
||||
</code>
|
||||
</paragraph>
|
||||
</document>
|
||||
</value>
|
||||
)
|
||||
|
||||
export const output = (
|
||||
<value>
|
||||
<document>
|
||||
<paragraph>
|
||||
<code>
|
||||
<quote>
|
||||
<paragraph>12</paragraph>
|
||||
<paragraph>
|
||||
3<cursor />
|
||||
</paragraph>
|
||||
</quote>
|
||||
</code>
|
||||
</paragraph>
|
||||
</document>
|
||||
</value>
|
||||
)
|
@@ -0,0 +1,41 @@
|
||||
/** @jsx h */
|
||||
|
||||
import h from '../../../helpers/h'
|
||||
|
||||
export default function(change) {
|
||||
change.insertFragment(
|
||||
<document>
|
||||
<list>
|
||||
<item>3</item>
|
||||
<item>4</item>
|
||||
</list>
|
||||
</document>
|
||||
)
|
||||
}
|
||||
|
||||
export const input = (
|
||||
<value>
|
||||
<document>
|
||||
<list>
|
||||
<item>1</item>
|
||||
<item>
|
||||
2<cursor />
|
||||
</item>
|
||||
</list>
|
||||
</document>
|
||||
</value>
|
||||
)
|
||||
|
||||
export const output = (
|
||||
<value>
|
||||
<document>
|
||||
<list>
|
||||
<item>1</item>
|
||||
<item>23</item>
|
||||
<item>
|
||||
4<cursor />
|
||||
</item>
|
||||
</list>
|
||||
</document>
|
||||
</value>
|
||||
)
|
@@ -29,14 +29,10 @@ export const output = (
|
||||
<value>
|
||||
<document>
|
||||
<quote>
|
||||
<paragraph>wo</paragraph>
|
||||
<paragraph>woone</paragraph>
|
||||
<quote>
|
||||
<quote>one</quote>
|
||||
<quote>two</quote>
|
||||
tword<cursor />
|
||||
</quote>
|
||||
<paragraph>
|
||||
rd<cursor />
|
||||
</paragraph>
|
||||
</quote>
|
||||
</document>
|
||||
</value>
|
||||
|
Reference in New Issue
Block a user