1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-17 12:41:44 +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 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 first: (root: Node, path: Path) => NodeEntry
@@ -156,7 +156,7 @@ export interface NodeInterface {
isNodeList: (value: any, options?: NodeIsNodeOptions) => value is Node[] 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 last: (root: Node, path: Path) => NodeEntry

View File

@@ -3,7 +3,7 @@ import { Editor } from '../interfaces/editor'
import { Range } from '../interfaces/range' import { Range } from '../interfaces/range'
import { Path } from '../interfaces/path' import { Path } from '../interfaces/path'
import { Element } from '../interfaces/element' import { Element } from '../interfaces/element'
import { Node, NodeEntry } from '../interfaces/node' import { Descendant, Node, NodeEntry } from '../interfaces/node'
import { Text } from '../interfaces/text' import { Text } from '../interfaces/text'
import { TextTransforms } from '../interfaces/transforms/text' import { TextTransforms } from '../interfaces/transforms/text'
import { getDefaultInsertLocation } from '../utils' import { getDefaultInsertLocation } from '../utils'
@@ -77,25 +77,31 @@ export const insertFragment: TextTransforms['insertFragment'] = (
const isBlockStart = Editor.isStart(editor, at, blockPath) const isBlockStart = Editor.isStart(editor, at, blockPath)
const isBlockEnd = Editor.isEnd(editor, at, blockPath) const isBlockEnd = Editor.isEnd(editor, at, blockPath)
const isBlockEmpty = isBlockStart && isBlockEnd const isBlockEmpty = isBlockStart && isBlockEnd
const mergeStart = !isBlockStart || (isBlockStart && isBlockEnd) const [, firstLeafPath] = Node.first({ children: fragment }, [])
const mergeEnd = !isBlockEnd const [, lastLeafPath] = Node.last({ children: fragment }, [])
const [, firstPath] = Node.first({ children: fragment }, [])
const [, lastPath] = Node.last({ children: fragment }, [])
const matches: NodeEntry[] = [] // For each node in the fragment, determine what level of wrapping should
const matcher = ([n, p]: NodeEntry) => { // 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 const isRoot = p.length === 0
if (isRoot) { if (isRoot) {
return false return false
} }
// If the destination block is empty, insert all top-level blocks of the
// fragment.
if (isBlockEmpty) { if (isBlockEmpty) {
return true 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 ( if (
mergeStart && !isBlockStart &&
Path.isAncestor(p, firstPath) && Path.isAncestor(p, firstLeafPath) &&
Element.isElement(n) && Element.isElement(n) &&
!editor.isVoid(n) && !editor.isVoid(n) &&
!editor.isInline(n) !editor.isInline(n)
@@ -103,9 +109,11 @@ export const insertFragment: TextTransforms['insertFragment'] = (
return false 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 ( if (
mergeEnd && !isBlockEnd &&
Path.isAncestor(p, lastPath) && Path.isAncestor(p, lastLeafPath) &&
Element.isElement(n) && Element.isElement(n) &&
!editor.isVoid(n) && !editor.isVoid(n) &&
!editor.isInline(n) !editor.isInline(n)
@@ -113,30 +121,51 @@ export const insertFragment: TextTransforms['insertFragment'] = (
return false return false
} }
// Always insert void nodes, inline elements and text nodes.
return true return true
} }
for (const entry of Node.nodes({ children: fragment }, { pass: matcher })) { // Whether the current node is in the first block of the fragment.
if (matcher(entry)) {
matches.push(entry)
}
}
const starts = []
const middles = []
const ends = []
let starting = true let starting = true
let hasBlocks = false
for (const [node] of matches) { // Inline nodes in the first block of the fragment, to be merged with the
if (Element.isElement(node) && !editor.isInline(node)) { // 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 starting = false
hasBlocks = true }
middles.push(node)
} else if (starting) { if (shouldInsert(entry)) {
starts.push(node) if (Element.isElement(node) && !editor.isInline(node)) {
} else { starting = false
ends.push(node) 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 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, { Transforms.splitNodes(editor, {
at, at,
match: n => match: n =>
hasBlocks splitBlock
? Element.isElement(n) && Editor.isBlock(editor, n) ? Element.isElement(n) && Editor.isBlock(editor, n)
: Text.isText(n) || Editor.isInline(editor, n), : Text.isText(n) || Editor.isInline(editor, n),
mode: hasBlocks ? 'lowest' : 'highest', mode: splitBlock ? 'lowest' : 'highest',
always: always:
hasBlocks && splitBlock &&
(!isBlockStart || starts.length > 0) && (!isBlockStart || starts.length > 0) &&
(!isBlockEnd || ends.length > 0), (!isBlockEnd || ends.length > 0),
voids, 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 = {}) => { export const run = (editor, options = {}) => {
Transforms.insertFragment( Transforms.insertFragment(
editor, editor,
<block> <fragment>
<block>3</block> <block>
<block>4</block> <block>3</block>
</block>, <block>4</block>
</block>
</fragment>,
options options
) )
} }
@@ -33,4 +35,3 @@ export const output = (
</block> </block>
</editor> </editor>
) )
export const skip = true