diff --git a/.changeset/slow-steaks-play.md b/.changeset/slow-steaks-play.md
new file mode 100644
index 000000000..87f4d6799
--- /dev/null
+++ b/.changeset/slow-steaks-play.md
@@ -0,0 +1,5 @@
+---
+'slate': patch
+---
+
+Fix: Inserting a fragment containing exactly two blocks merges those blocks together.
diff --git a/packages/slate/src/interfaces/node.ts b/packages/slate/src/interfaces/node.ts
index f2e759203..755578b40 100644
--- a/packages/slate/src/interfaces/node.ts
+++ b/packages/slate/src/interfaces/node.ts
@@ -120,7 +120,7 @@ export interface NodeInterface {
extractProps: (node: Node) => NodeProps
/**
- * Get the first node entry in a root node from a path.
+ * Get the first leaf node entry in a root node from a path.
*/
first: (root: Node, path: Path) => NodeEntry
@@ -156,7 +156,7 @@ export interface NodeInterface {
isNodeList: (value: any, options?: NodeIsNodeOptions) => value is Node[]
/**
- * Get the last node entry in a root node from a path.
+ * Get the last leaf node entry in a root node from a path.
*/
last: (root: Node, path: Path) => NodeEntry
diff --git a/packages/slate/src/transforms-text/insert-fragment.ts b/packages/slate/src/transforms-text/insert-fragment.ts
index 78780b273..88f370e5a 100644
--- a/packages/slate/src/transforms-text/insert-fragment.ts
+++ b/packages/slate/src/transforms-text/insert-fragment.ts
@@ -3,7 +3,7 @@ import { Editor } from '../interfaces/editor'
import { Range } from '../interfaces/range'
import { Path } from '../interfaces/path'
import { Element } from '../interfaces/element'
-import { Node, NodeEntry } from '../interfaces/node'
+import { Descendant, Node, NodeEntry } from '../interfaces/node'
import { Text } from '../interfaces/text'
import { TextTransforms } from '../interfaces/transforms/text'
import { getDefaultInsertLocation } from '../utils'
@@ -77,25 +77,31 @@ export const insertFragment: TextTransforms['insertFragment'] = (
const isBlockStart = Editor.isStart(editor, at, blockPath)
const isBlockEnd = Editor.isEnd(editor, at, blockPath)
const isBlockEmpty = isBlockStart && isBlockEnd
- const mergeStart = !isBlockStart || (isBlockStart && isBlockEnd)
- const mergeEnd = !isBlockEnd
- const [, firstPath] = Node.first({ children: fragment }, [])
- const [, lastPath] = Node.last({ children: fragment }, [])
+ const [, firstLeafPath] = Node.first({ children: fragment }, [])
+ const [, lastLeafPath] = Node.last({ children: fragment }, [])
- const matches: NodeEntry[] = []
- const matcher = ([n, p]: NodeEntry) => {
+ // For each node in the fragment, determine what level of wrapping should
+ // be kept. At minimum, all text nodes will be inserted, but if
+ // `shouldInsert` returns true for some ancestor of a particular text node,
+ // then the entire ancestor will be inserted rather than inserting the text
+ // nodes individually.
+ const shouldInsert = ([n, p]: NodeEntry) => {
const isRoot = p.length === 0
if (isRoot) {
return false
}
+ // If the destination block is empty, insert all top-level blocks of the
+ // fragment.
if (isBlockEmpty) {
return true
}
+ // Unless we're at the start of the destination block, unwrap any
+ // non-void blocks that contain the first leaf node in the fragment.
if (
- mergeStart &&
- Path.isAncestor(p, firstPath) &&
+ !isBlockStart &&
+ Path.isAncestor(p, firstLeafPath) &&
Element.isElement(n) &&
!editor.isVoid(n) &&
!editor.isInline(n)
@@ -103,9 +109,11 @@ export const insertFragment: TextTransforms['insertFragment'] = (
return false
}
+ // Unless we're at the end of the destination block, unwrap any non-void
+ // blocks that contain the last leaf node in the fragment.
if (
- mergeEnd &&
- Path.isAncestor(p, lastPath) &&
+ !isBlockEnd &&
+ Path.isAncestor(p, lastLeafPath) &&
Element.isElement(n) &&
!editor.isVoid(n) &&
!editor.isInline(n)
@@ -113,30 +121,51 @@ export const insertFragment: TextTransforms['insertFragment'] = (
return false
}
+ // Always insert void nodes, inline elements and text nodes.
return true
}
- for (const entry of Node.nodes({ children: fragment }, { pass: matcher })) {
- if (matcher(entry)) {
- matches.push(entry)
- }
- }
-
- const starts = []
- const middles = []
- const ends = []
+ // Whether the current node is in the first block of the fragment.
let starting = true
- let hasBlocks = false
- for (const [node] of matches) {
- if (Element.isElement(node) && !editor.isInline(node)) {
+ // Inline nodes in the first block of the fragment, to be merged with the
+ // destination block.
+ const starts: Descendant[] = []
+
+ // Blocks in the middle of the fragment.
+ const middles: Element[] = []
+
+ // Inline nodes in the last block of the fragment, to be merged with the
+ // destination block. If the fragment contains only one block, this will be
+ // empty.
+ const ends: Descendant[] = []
+
+ for (const entry of Node.nodes(
+ { children: fragment },
+ { pass: shouldInsert }
+ )) {
+ const [node, path] = entry
+
+ // If we encounter a block that does not contain the first leaf, we're no
+ // longer in the first block of the fragment.
+ if (
+ starting &&
+ Element.isElement(node) &&
+ !editor.isInline(node) &&
+ !Path.isAncestor(path, firstLeafPath)
+ ) {
starting = false
- hasBlocks = true
- middles.push(node)
- } else if (starting) {
- starts.push(node)
- } else {
- ends.push(node)
+ }
+
+ if (shouldInsert(entry)) {
+ if (Element.isElement(node) && !editor.isInline(node)) {
+ starting = false
+ middles.push(node)
+ } else if (starting) {
+ starts.push(node)
+ } else {
+ ends.push(node)
+ }
}
}
@@ -161,15 +190,19 @@ export const insertFragment: TextTransforms['insertFragment'] = (
isInlineEnd ? Path.next(inlinePath) : inlinePath
)
+ // If the fragment contains inlines in multiple distinct blocks, split the
+ // destination block.
+ const splitBlock = ends.length > 0
+
Transforms.splitNodes(editor, {
at,
match: n =>
- hasBlocks
+ splitBlock
? Element.isElement(n) && Editor.isBlock(editor, n)
: Text.isText(n) || Editor.isInline(editor, n),
- mode: hasBlocks ? 'lowest' : 'highest',
+ mode: splitBlock ? 'lowest' : 'highest',
always:
- hasBlocks &&
+ splitBlock &&
(!isBlockStart || starts.length > 0) &&
(!isBlockEnd || ends.length > 0),
voids,
diff --git a/packages/slate/test/transforms/insertFragment/of-blocks/block-middle.tsx b/packages/slate/test/transforms/insertFragment/of-blocks/block-middle-3.tsx
similarity index 100%
rename from packages/slate/test/transforms/insertFragment/of-blocks/block-middle.tsx
rename to packages/slate/test/transforms/insertFragment/of-blocks/block-middle-3.tsx
diff --git a/packages/slate/test/transforms/insertFragment/of-blocks/blocks-middle-1.tsx b/packages/slate/test/transforms/insertFragment/of-blocks/blocks-middle-1.tsx
new file mode 100644
index 000000000..0f0ae0515
--- /dev/null
+++ b/packages/slate/test/transforms/insertFragment/of-blocks/blocks-middle-1.tsx
@@ -0,0 +1,31 @@
+/** @jsx jsx */
+import { Transforms } from 'slate'
+import { jsx } from '../../..'
+
+export const run = (editor, options = {}) => {
+ Transforms.insertFragment(
+ editor,
+
+ one
+ ,
+ options
+ )
+}
+export const input = (
+
+
+ wo
+
+ rd
+
+
+)
+export const output = (
+
+
+ woone
+
+ rd
+
+
+)
diff --git a/packages/slate/test/transforms/insertFragment/of-blocks/blocks-middle-2.tsx b/packages/slate/test/transforms/insertFragment/of-blocks/blocks-middle-2.tsx
new file mode 100644
index 000000000..1b3dba369
--- /dev/null
+++ b/packages/slate/test/transforms/insertFragment/of-blocks/blocks-middle-2.tsx
@@ -0,0 +1,33 @@
+/** @jsx jsx */
+import { Transforms } from 'slate'
+import { jsx } from '../../..'
+
+export const run = (editor, options = {}) => {
+ Transforms.insertFragment(
+ editor,
+
+ one
+ two
+ ,
+ options
+ )
+}
+export const input = (
+
+
+ wo
+
+ rd
+
+
+)
+export const output = (
+
+ woone
+
+ two
+
+ rd
+
+
+)
diff --git a/packages/slate/test/transforms/insertFragment/of-lists/merge-lists.tsx b/packages/slate/test/transforms/insertFragment/of-lists/merge-lists.tsx
index e4e7fa157..bd5775c5e 100644
--- a/packages/slate/test/transforms/insertFragment/of-lists/merge-lists.tsx
+++ b/packages/slate/test/transforms/insertFragment/of-lists/merge-lists.tsx
@@ -5,10 +5,12 @@ import { jsx } from '../../..'
export const run = (editor, options = {}) => {
Transforms.insertFragment(
editor,
-
- 3
- 4
- ,
+
+
+ 3
+ 4
+
+ ,
options
)
}
@@ -33,4 +35,3 @@ export const output = (
)
-export const skip = true