diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts index 8d4591123e..28adddbd2f 100644 --- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts +++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts @@ -1,3 +1,4 @@ +import { Fragment, Slice } from "prosemirror-model"; import { NodeSelection, Selection, @@ -5,12 +6,15 @@ import { Transaction, } from "prosemirror-state"; import { CellSelection } from "prosemirror-tables"; +import { ReplaceStep } from "prosemirror-transform"; import { Block } from "../../../../blocks/defaultBlocks.js"; import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor"; import { BlockIdentifier } from "../../../../schema/index.js"; import { getNearestBlockPos } from "../../../getBlockInfoFromPos.js"; +import { blockToNode } from "../../../nodeConversions/blockToNode.js"; import { getNodeById } from "../../../nodeUtil.js"; +import { getPmSchema } from "../../../pmUtil.js"; type BlockSelectionData = ( | { @@ -170,8 +174,39 @@ export function moveSelectedBlocksAndSelection( ]; const selectionData = getBlockSelectionData(editor); + // Resolve the destination to a position upfront. `removeBlocks` can + // collapse a `columnList` (via `fixColumnList`) and destroy + // `referenceBlock`, so a later ID lookup would fail. + const referenceId = + typeof referenceBlock === "string" ? referenceBlock : referenceBlock.id; + const referenceInfo = getNodeById(referenceId, tr.doc); + if (!referenceInfo) { + throw new Error(`Block with ID ${referenceId} not found`); + } + const targetPos = + placement === "before" + ? referenceInfo.posBeforeNode + : referenceInfo.posBeforeNode + referenceInfo.node.nodeSize; + const stepsBeforeRemove = tr.steps.length; + editor.removeBlocks(blocks); - editor.insertBlocks(flattenColumns(blocks), referenceBlock, placement); + + let mappedTargetPos = targetPos; + for (let i = stepsBeforeRemove; i < tr.steps.length; i++) { + mappedTargetPos = tr.steps[i].getMap().map(mappedTargetPos); + } + + const pmSchema = getPmSchema(tr); + const nodesToInsert = flattenColumns(blocks).map((block) => + blockToNode(block, pmSchema), + ); + tr.step( + new ReplaceStep( + mappedTargetPos, + mappedTargetPos, + new Slice(Fragment.from(nodesToInsert), 0, 0), + ), + ); updateBlockSelectionFromData(tr, selectionData); }); diff --git a/packages/xl-multi-column/src/test/commands/moveBlocks.test.ts b/packages/xl-multi-column/src/test/commands/moveBlocks.test.ts index fc0909ba3a..a02a787813 100644 --- a/packages/xl-multi-column/src/test/commands/moveBlocks.test.ts +++ b/packages/xl-multi-column/src/test/commands/moveBlocks.test.ts @@ -55,3 +55,85 @@ describe("Test moveBlocksDown", () => { expect(getEditor().document).toMatchSnapshot(); }); }); + +// Regression tests for https://github.com/TypeCellOS/BlockNote/issues/2594: +// when a column contains only a single block, moving that block out of the +// column triggers `fixColumnList` to collapse the columnList, which used to +// invalidate the destination `referenceBlock`. +describe("Test moveBlocks with single-block columns", () => { + it("Move out of a single-block column does not throw", () => { + const editor = getEditor(); + editor.replaceBlocks(editor.document, [ + { + id: "column-list-single", + type: "columnList", + children: [ + { + id: "column-a", + type: "column", + children: [ + { + id: "only-paragraph-a", + type: "paragraph", + content: "A", + }, + ], + }, + { + id: "column-b", + type: "column", + children: [ + { + id: "only-paragraph-b", + type: "paragraph", + content: "B", + }, + ], + }, + ], + }, + { id: "below", type: "paragraph", content: "below" }, + ]); + editor.setTextCursorPosition("only-paragraph-a"); + + expect(() => editor.moveBlocksUp()).not.toThrow(); + }); + + it("Move out of a single-block column when columnList is preceded by a sibling", () => { + const editor = getEditor(); + editor.replaceBlocks(editor.document, [ + { id: "above", type: "paragraph", content: "above" }, + { + id: "column-list-single-2", + type: "columnList", + children: [ + { + id: "column-c", + type: "column", + children: [ + { + id: "only-paragraph-c", + type: "paragraph", + content: "C", + }, + ], + }, + { + id: "column-d", + type: "column", + children: [ + { + id: "only-paragraph-d", + type: "paragraph", + content: "D", + }, + ], + }, + ], + }, + ]); + editor.setTextCursorPosition("only-paragraph-c"); + + expect(() => editor.moveBlocksUp()).not.toThrow(); + }); +});