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