1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-16 12:14:14 +02:00

Fix pasting exactly two blocks (#5875)

* Refactor `insertFragment` without altering behaviour

* Fix inserting a fragment containing exactly two blocks

* Fix and unskip insertFragment list test
This commit is contained in:
Joe Anderson
2025-05-24 21:54:44 +01:00
committed by GitHub
parent 747ebfda0a
commit 896bc9c7c7
7 changed files with 142 additions and 39 deletions

View File

@@ -0,0 +1,5 @@
---
'slate': patch
---
Fix: Inserting a fragment containing exactly two blocks merges those blocks together.

View File

@@ -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

View File

@@ -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,

View File

@@ -0,0 +1,31 @@
/** @jsx jsx */
import { Transforms } from 'slate'
import { jsx } from '../../..'
export const run = (editor, options = {}) => {
Transforms.insertFragment(
editor,
<fragment>
<block>one</block>
</fragment>,
options
)
}
export const input = (
<editor>
<block>
wo
<cursor />
rd
</block>
</editor>
)
export const output = (
<editor>
<block>
woone
<cursor />
rd
</block>
</editor>
)

View File

@@ -0,0 +1,33 @@
/** @jsx jsx */
import { Transforms } from 'slate'
import { jsx } from '../../..'
export const run = (editor, options = {}) => {
Transforms.insertFragment(
editor,
<fragment>
<block>one</block>
<block>two</block>
</fragment>,
options
)
}
export const input = (
<editor>
<block>
wo
<cursor />
rd
</block>
</editor>
)
export const output = (
<editor>
<block>woone</block>
<block>
two
<cursor />
rd
</block>
</editor>
)

View File

@@ -5,10 +5,12 @@ import { jsx } from '../../..'
export const run = (editor, options = {}) => {
Transforms.insertFragment(
editor,
<block>
<block>3</block>
<block>4</block>
</block>,
<fragment>
<block>
<block>3</block>
<block>4</block>
</block>
</fragment>,
options
)
}
@@ -33,4 +35,3 @@ export const output = (
</block>
</editor>
)
export const skip = true