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:
5
.changeset/slow-steaks-play.md
Normal file
5
.changeset/slow-steaks-play.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'slate': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix: Inserting a fragment containing exactly two blocks merges those blocks together.
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
|
@@ -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>
|
||||||
|
)
|
@@ -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>
|
||||||
|
)
|
@@ -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
|
|
||||||
|
Reference in New Issue
Block a user