diff --git a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts
index bb2f08dfca..26774c3d24 100644
--- a/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts
+++ b/packages/core/src/api/blockManipulation/commands/moveBlocks/moveBlocks.ts
@@ -11,6 +11,8 @@ import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor";
import { BlockIdentifier } from "../../../../schema/index.js";
import { getNearestBlockPos } from "../../../getBlockInfoFromPos.js";
import { getNodeById } from "../../../nodeUtil.js";
+import { insertBlocks } from "../insertBlocks/insertBlocks.js";
+import { removeAndInsertBlocks } from "../replaceBlocks/replaceBlocks.js";
type BlockSelectionData = (
| {
@@ -148,24 +150,26 @@ export function moveBlocks(
referenceBlock: BlockIdentifier,
placement: "before" | "after",
) {
- editor.transact(() => {
- // A `columnList` reference can be dissolved by `fixColumnList` when its
- // `column`s are removed, leaving its ID invalid for re-insertion. Anchor
- // to an adjacent block instead, which is unaffected by the removal.
- const refBlock = editor.getBlock(referenceBlock);
- if (refBlock?.type === "columnList") {
- const adjacent =
- placement === "after"
- ? editor.getNextBlock(refBlock)
- : editor.getPrevBlock(refBlock);
- if (adjacent) {
- referenceBlock = adjacent;
- placement = placement === "after" ? "before" : "after";
- }
- }
-
- editor.removeBlocks(blocks);
- editor.insertBlocks(flattenColumns(blocks), referenceBlock, placement);
+ editor.transact((tr) => {
+ // Don't fix columns/columnLists in the removal step. Since a move is a
+ // rearrangement rather than a deletion, columns that it empties out are
+ // deliberately left as-is instead of being collapsed - this keeps moves
+ // free of side effects (and reversible by moving back), and matches
+ // dragging a block out of a column, which doesn't collapse it either.
+ // Fixing them mid-move also broke the following case:
+ //
+ //
+ // Paragraph
+ //
+ // When the non-empty block is moved up, the column is seen as empty and
+ // collapsed in the removal step, so the following insertion fails.
+ removeAndInsertBlocks(tr, blocks, [], { fixColumns: false });
+ insertBlocks(
+ tr,
+ flattenColumns(blocks),
+ referenceBlock,
+ placement,
+ );
});
}
diff --git a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts
index f1e946f909..a4f5b8da89 100644
--- a/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts
+++ b/packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts
@@ -20,6 +20,9 @@ export function removeAndInsertBlocks<
tr: Transaction,
blocksToRemove: BlockIdentifier[],
blocksToInsert: PartialBlock[],
+ options: {
+ fixColumns?: boolean;
+ } = {},
): {
insertedBlocks: Block[];
removedBlocks: Block[];
@@ -112,7 +115,12 @@ export function removeAndInsertBlocks<
);
}
- columnListPositions.forEach((pos) => fixColumnList(tr, pos));
+ // Collapses empty columns/columnLists. Callers where the removal isn't a
+ // deletion can opt out - e.g. `moveBlocks` re-inserts the blocks elsewhere
+ // and deliberately leaves emptied columns as-is.
+ if (options.fixColumns !== false) {
+ columnListPositions.forEach((pos) => fixColumnList(tr, pos));
+ }
// Converts the nodes created from `blocksToInsert` into full `Block`s.
const insertedBlocks = nodesToInsert.map((node) =>
diff --git a/packages/xl-multi-column/src/test/commands/__snapshots__/moveBlocks.test.ts.snap b/packages/xl-multi-column/src/test/commands/__snapshots__/moveBlocks.test.ts.snap
index 4d6a2cf1f7..d9021ec0e0 100644
--- a/packages/xl-multi-column/src/test/commands/__snapshots__/moveBlocks.test.ts.snap
+++ b/packages/xl-multi-column/src/test/commands/__snapshots__/moveBlocks.test.ts.snap
@@ -1,5 +1,183 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+exports[`Move past empty sibling within a column > Move down below empty sibling 1`] = `
+[
+ {
+ "children": [
+ {
+ "children": [
+ {
+ "children": [],
+ "content": [
+ {
+ "styles": {},
+ "text": "Text 0",
+ "type": "text",
+ },
+ ],
+ "id": "text-0",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ {
+ "children": [],
+ "content": [],
+ "id": "empty-0",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ ],
+ "content": undefined,
+ "id": "column-empty-0",
+ "props": {
+ "width": 1,
+ },
+ "type": "column",
+ },
+ {
+ "children": [
+ {
+ "children": [],
+ "content": [],
+ "id": "empty-1",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ {
+ "children": [],
+ "content": [
+ {
+ "styles": {},
+ "text": "Text 1",
+ "type": "text",
+ },
+ ],
+ "id": "text-1",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ ],
+ "content": undefined,
+ "id": "column-empty-1",
+ "props": {
+ "width": 1,
+ },
+ "type": "column",
+ },
+ ],
+ "content": undefined,
+ "id": "column-list-empty",
+ "props": {},
+ "type": "columnList",
+ },
+]
+`;
+
+exports[`Move past empty sibling within a column > Move up above empty sibling 1`] = `
+[
+ {
+ "children": [
+ {
+ "children": [
+ {
+ "children": [],
+ "content": [
+ {
+ "styles": {},
+ "text": "Text 0",
+ "type": "text",
+ },
+ ],
+ "id": "text-0",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ {
+ "children": [],
+ "content": [],
+ "id": "empty-0",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ ],
+ "content": undefined,
+ "id": "column-empty-0",
+ "props": {
+ "width": 1,
+ },
+ "type": "column",
+ },
+ {
+ "children": [
+ {
+ "children": [],
+ "content": [],
+ "id": "empty-1",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ {
+ "children": [],
+ "content": [
+ {
+ "styles": {},
+ "text": "Text 1",
+ "type": "text",
+ },
+ ],
+ "id": "text-1",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ ],
+ "content": undefined,
+ "id": "column-empty-1",
+ "props": {
+ "width": 1,
+ },
+ "type": "column",
+ },
+ ],
+ "content": undefined,
+ "id": "column-list-empty",
+ "props": {},
+ "type": "columnList",
+ },
+]
+`;
+
exports[`Test moveBlocksDown > Move into column list 1`] = `
[
{
@@ -565,21 +743,60 @@ exports[`Test moveBlocksDown > Selection across columns 1`] = `
"type": "paragraph",
},
{
- "children": [],
- "content": [
+ "children": [
{
- "styles": {},
- "text": "Column Paragraph 0",
- "type": "text",
+ "children": [
+ {
+ "children": [],
+ "content": [
+ {
+ "styles": {},
+ "text": "Column Paragraph 0",
+ "type": "text",
+ },
+ ],
+ "id": "column-paragraph-0",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ ],
+ "content": undefined,
+ "id": "column-0",
+ "props": {
+ "width": 1,
+ },
+ "type": "column",
+ },
+ {
+ "children": [
+ {
+ "children": [],
+ "content": [],
+ "id": "1",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ ],
+ "content": undefined,
+ "id": "0",
+ "props": {
+ "width": 1,
+ },
+ "type": "column",
},
],
- "id": "column-paragraph-0",
- "props": {
- "backgroundColor": "default",
- "textAlignment": "left",
- "textColor": "default",
- },
- "type": "paragraph",
+ "content": undefined,
+ "id": "column-list-0",
+ "props": {},
+ "type": "columnList",
},
{
"children": [],
@@ -2067,72 +2284,111 @@ exports[`Test moveBlocksUp > Selection across columns 1`] = `
"type": "paragraph",
},
{
- "children": [],
- "content": [
- {
- "styles": {},
- "text": "Column Paragraph 1",
- "type": "text",
- },
- ],
- "id": "column-paragraph-1",
- "props": {
- "backgroundColor": "default",
- "textAlignment": "left",
- "textColor": "default",
- },
- "type": "paragraph",
- },
- {
- "children": [],
- "content": [
- {
- "styles": {},
- "text": "Column Paragraph 2",
- "type": "text",
- },
- ],
- "id": "column-paragraph-2",
- "props": {
- "backgroundColor": "default",
- "textAlignment": "left",
- "textColor": "default",
- },
- "type": "paragraph",
- },
- {
- "children": [],
- "content": [
+ "children": [
{
- "styles": {},
- "text": "Column Paragraph 3",
- "type": "text",
- },
- ],
- "id": "column-paragraph-3",
- "props": {
- "backgroundColor": "default",
- "textAlignment": "left",
- "textColor": "default",
- },
- "type": "paragraph",
- },
- {
- "children": [],
- "content": [
+ "children": [
+ {
+ "children": [],
+ "content": [
+ {
+ "styles": {},
+ "text": "Column Paragraph 1",
+ "type": "text",
+ },
+ ],
+ "id": "column-paragraph-1",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ {
+ "children": [],
+ "content": [
+ {
+ "styles": {},
+ "text": "Column Paragraph 2",
+ "type": "text",
+ },
+ ],
+ "id": "column-paragraph-2",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ {
+ "children": [],
+ "content": [
+ {
+ "styles": {},
+ "text": "Column Paragraph 3",
+ "type": "text",
+ },
+ ],
+ "id": "column-paragraph-3",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ {
+ "children": [],
+ "content": [
+ {
+ "styles": {},
+ "text": "Column Paragraph 0",
+ "type": "text",
+ },
+ ],
+ "id": "column-paragraph-0",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ ],
+ "content": undefined,
+ "id": "column-0",
+ "props": {
+ "width": 1,
+ },
+ "type": "column",
+ },
{
- "styles": {},
- "text": "Column Paragraph 0",
- "type": "text",
+ "children": [
+ {
+ "children": [],
+ "content": [],
+ "id": "1",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ ],
+ "content": undefined,
+ "id": "0",
+ "props": {
+ "width": 1,
+ },
+ "type": "column",
},
],
- "id": "column-paragraph-0",
- "props": {
- "backgroundColor": "default",
- "textAlignment": "left",
- "textColor": "default",
- },
- "type": "paragraph",
+ "content": undefined,
+ "id": "column-list-0",
+ "props": {},
+ "type": "columnList",
},
{
"children": [],
@@ -2379,89 +2635,128 @@ exports[`Test moveBlocksUp > Selection starts in column, ends outside 1`] = `
"type": "paragraph",
},
{
- "children": [],
- "content": [
- {
- "styles": {},
- "text": "Column Paragraph 0",
- "type": "text",
- },
- ],
- "id": "column-paragraph-0",
- "props": {
- "backgroundColor": "default",
- "textAlignment": "left",
- "textColor": "default",
- },
- "type": "paragraph",
- },
- {
- "children": [],
- "content": [
- {
- "styles": {},
- "text": "Column Paragraph 1",
- "type": "text",
- },
- ],
- "id": "column-paragraph-1",
- "props": {
- "backgroundColor": "default",
- "textAlignment": "left",
- "textColor": "default",
- },
- "type": "paragraph",
- },
- {
- "children": [],
- "content": [
- {
- "styles": {},
- "text": "Column Paragraph 2",
- "type": "text",
- },
- ],
- "id": "column-paragraph-2",
- "props": {
- "backgroundColor": "default",
- "textAlignment": "left",
- "textColor": "default",
- },
- "type": "paragraph",
- },
- {
- "children": [],
- "content": [
+ "children": [
{
- "styles": {},
- "text": "Column Paragraph 3",
- "type": "text",
+ "children": [
+ {
+ "children": [],
+ "content": [
+ {
+ "styles": {},
+ "text": "Column Paragraph 0",
+ "type": "text",
+ },
+ ],
+ "id": "column-paragraph-0",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ {
+ "children": [],
+ "content": [
+ {
+ "styles": {},
+ "text": "Column Paragraph 1",
+ "type": "text",
+ },
+ ],
+ "id": "column-paragraph-1",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ {
+ "children": [],
+ "content": [
+ {
+ "styles": {},
+ "text": "Column Paragraph 2",
+ "type": "text",
+ },
+ ],
+ "id": "column-paragraph-2",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ {
+ "children": [],
+ "content": [
+ {
+ "styles": {},
+ "text": "Column Paragraph 3",
+ "type": "text",
+ },
+ ],
+ "id": "column-paragraph-3",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ {
+ "children": [],
+ "content": [
+ {
+ "styles": {},
+ "text": "Paragraph 2",
+ "type": "text",
+ },
+ ],
+ "id": "paragraph-2",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ ],
+ "content": undefined,
+ "id": "column-0",
+ "props": {
+ "width": 1,
+ },
+ "type": "column",
},
- ],
- "id": "column-paragraph-3",
- "props": {
- "backgroundColor": "default",
- "textAlignment": "left",
- "textColor": "default",
- },
- "type": "paragraph",
- },
- {
- "children": [],
- "content": [
{
- "styles": {},
- "text": "Paragraph 2",
- "type": "text",
+ "children": [
+ {
+ "children": [],
+ "content": [],
+ "id": "0",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ ],
+ "content": undefined,
+ "id": "column-1",
+ "props": {
+ "width": 1,
+ },
+ "type": "column",
},
],
- "id": "paragraph-2",
- "props": {
- "backgroundColor": "default",
- "textAlignment": "left",
- "textColor": "default",
- },
- "type": "paragraph",
+ "content": undefined,
+ "id": "column-list-0",
+ "props": {},
+ "type": "columnList",
},
]
`;
@@ -2521,89 +2816,128 @@ exports[`Test moveBlocksUp > Selection starts in first column, ends outside 1`]
"type": "paragraph",
},
{
- "children": [],
- "content": [
- {
- "styles": {},
- "text": "Column Paragraph 1",
- "type": "text",
- },
- ],
- "id": "column-paragraph-1",
- "props": {
- "backgroundColor": "default",
- "textAlignment": "left",
- "textColor": "default",
- },
- "type": "paragraph",
- },
- {
- "children": [],
- "content": [
- {
- "styles": {},
- "text": "Column Paragraph 2",
- "type": "text",
- },
- ],
- "id": "column-paragraph-2",
- "props": {
- "backgroundColor": "default",
- "textAlignment": "left",
- "textColor": "default",
- },
- "type": "paragraph",
- },
- {
- "children": [],
- "content": [
- {
- "styles": {},
- "text": "Column Paragraph 3",
- "type": "text",
- },
- ],
- "id": "column-paragraph-3",
- "props": {
- "backgroundColor": "default",
- "textAlignment": "left",
- "textColor": "default",
- },
- "type": "paragraph",
- },
- {
- "children": [],
- "content": [
+ "children": [
{
- "styles": {},
- "text": "Paragraph 2",
- "type": "text",
+ "children": [
+ {
+ "children": [],
+ "content": [
+ {
+ "styles": {},
+ "text": "Column Paragraph 1",
+ "type": "text",
+ },
+ ],
+ "id": "column-paragraph-1",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ {
+ "children": [],
+ "content": [
+ {
+ "styles": {},
+ "text": "Column Paragraph 2",
+ "type": "text",
+ },
+ ],
+ "id": "column-paragraph-2",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ {
+ "children": [],
+ "content": [
+ {
+ "styles": {},
+ "text": "Column Paragraph 3",
+ "type": "text",
+ },
+ ],
+ "id": "column-paragraph-3",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ {
+ "children": [],
+ "content": [
+ {
+ "styles": {},
+ "text": "Paragraph 2",
+ "type": "text",
+ },
+ ],
+ "id": "paragraph-2",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ {
+ "children": [],
+ "content": [
+ {
+ "styles": {},
+ "text": "Column Paragraph 0",
+ "type": "text",
+ },
+ ],
+ "id": "column-paragraph-0",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ ],
+ "content": undefined,
+ "id": "column-0",
+ "props": {
+ "width": 1,
+ },
+ "type": "column",
},
- ],
- "id": "paragraph-2",
- "props": {
- "backgroundColor": "default",
- "textAlignment": "left",
- "textColor": "default",
- },
- "type": "paragraph",
- },
- {
- "children": [],
- "content": [
{
- "styles": {},
- "text": "Column Paragraph 0",
- "type": "text",
+ "children": [
+ {
+ "children": [],
+ "content": [],
+ "id": "1",
+ "props": {
+ "backgroundColor": "default",
+ "textAlignment": "left",
+ "textColor": "default",
+ },
+ "type": "paragraph",
+ },
+ ],
+ "content": undefined,
+ "id": "0",
+ "props": {
+ "width": 1,
+ },
+ "type": "column",
},
],
- "id": "column-paragraph-0",
- "props": {
- "backgroundColor": "default",
- "textAlignment": "left",
- "textColor": "default",
- },
- "type": "paragraph",
+ "content": undefined,
+ "id": "column-list-0",
+ "props": {},
+ "type": "columnList",
},
]
`;
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 156895be44..6970c6c036 100644
--- a/packages/xl-multi-column/src/test/commands/moveBlocks.test.ts
+++ b/packages/xl-multi-column/src/test/commands/moveBlocks.test.ts
@@ -1,4 +1,4 @@
-import { describe, expect, it } from "vite-plus/test";
+import { beforeEach, describe, expect, it } from "vite-plus/test";
import { setupTestEnv } from "../setupTestEnv.js";
@@ -151,3 +151,48 @@ describe("Test moveBlocksDown", () => {
expect(getEditor().document).toMatchSnapshot();
});
});
+
+describe("Move past empty sibling within a column", () => {
+ beforeEach(() => {
+ getEditor().replaceBlocks(getEditor().document, [
+ {
+ id: "column-list-empty",
+ type: "columnList",
+ children: [
+ {
+ id: "column-empty-0",
+ type: "column",
+ children: [
+ { id: "empty-0", type: "paragraph" },
+ { id: "text-0", type: "paragraph", content: "Text 0" },
+ ],
+ },
+ {
+ id: "column-empty-1",
+ type: "column",
+ children: [
+ { id: "empty-1", type: "paragraph" },
+ { id: "text-1", type: "paragraph", content: "Text 1" },
+ ],
+ },
+ ],
+ },
+ ]);
+ });
+
+ it("Move up above empty sibling", () => {
+ getEditor().setTextCursorPosition("text-0");
+
+ expect(() => getEditor().moveBlocksUp()).not.toThrow();
+
+ expect(getEditor().document).toMatchSnapshot();
+ });
+
+ it("Move down below empty sibling", () => {
+ getEditor().setTextCursorPosition("empty-0");
+
+ expect(() => getEditor().moveBlocksDown()).not.toThrow();
+
+ expect(getEditor().document).toMatchSnapshot();
+ });
+});