Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { Fragment, Slice } from "prosemirror-model";
import {
NodeSelection,
Selection,
TextSelection,
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 = (
| {
Expand Down Expand Up @@ -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);
});
Expand Down
82 changes: 82 additions & 0 deletions packages/xl-multi-column/src/test/commands/moveBlocks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});