Skip to content

Commit fa8a46d

Browse files
committed
fix(core): address nested drag selection and column drop history
1 parent cc4f00a commit fa8a46d

6 files changed

Lines changed: 88 additions & 3 deletions

File tree

‎packages/core/src/extensions/SideMenu/SideMenu.browser.test.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,52 @@ import { afterEach, expect, it } from "vite-plus/test";
22

33
import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
44
import { SideMenuExtension } from "./SideMenu.js";
5+
import { MultipleNodeSelection } from "./MultipleNodeSelection.js";
6+
import { getNodeById } from "../../api/nodeUtil.js";
57
import { unsetDragImage } from "./dragging.js";
68

79
const cleanup: (() => void)[] = [];
810

11+
it("drags only the child when its parent has a single-block selection", () => {
12+
const container = document.createElement("div");
13+
document.body.appendChild(container);
14+
cleanup.push(() => container.remove());
15+
const editor = BlockNoteEditor.create({
16+
initialContent: [
17+
{
18+
id: "parent",
19+
type: "paragraph",
20+
content: "Parent",
21+
children: [{ id: "child", type: "paragraph", content: "Child" }],
22+
},
23+
],
24+
});
25+
editor.mount(container);
26+
cleanup.push(() => editor._tiptapEditor.destroy());
27+
const view = editor.prosemirrorView;
28+
cleanup.push(() => unsetDragImage(view.root));
29+
const parent = getNodeById("parent", view.state.doc)!;
30+
editor.transact((tr) =>
31+
tr.setSelection(
32+
MultipleNodeSelection.create(
33+
tr.doc,
34+
parent.posBeforeNode,
35+
parent.posBeforeNode + parent.node.nodeSize,
36+
),
37+
),
38+
);
39+
editor
40+
.getExtension(SideMenuExtension)!
41+
.blockDragStart(
42+
{ dataTransfer: new DataTransfer(), clientY: 0 },
43+
editor.getBlock("child")!,
44+
);
45+
expect(view.state.selection).toBeInstanceOf(MultipleNodeSelection);
46+
expect(view.state.selection.content().content.firstChild?.attrs.id).toBe(
47+
"child",
48+
);
49+
});
50+
951
afterEach(() => {
1052
cleanup
1153
.splice(0)

‎packages/core/src/extensions/SideMenu/dragging.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,8 @@ export function dragStart<
183183
const draggedBlockInSelection = from <= pos && pos < to;
184184
const multipleBlocksSelected =
185185
selection.$anchor.node() !== selection.$head.node() ||
186-
selection instanceof MultipleNodeSelection;
186+
(selection instanceof MultipleNodeSelection &&
187+
selection.nodes.length > 1);
187188

188189
if (draggedBlockInSelection && multipleBlocksSelected) {
189190
view.dispatch(

‎packages/xl-multi-column/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"@blocknote/core": "workspace:^",
7070
"@blocknote/react": "workspace:^",
7171
"@tiptap/core": "^3.31.3",
72+
"@tiptap/pm": "^3.31.3",
7273
"prosemirror-model": "^1.25.11",
7374
"prosemirror-state": "^1.4.4",
7475
"prosemirror-view": "^1.42.5"

‎packages/xl-multi-column/src/extensions/DropCursor/multiColumnHandleDropPlugin.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
nodeToBlock,
88
} from "@blocknote/core";
99
import { UniqueID } from "@blocknote/core/extensions";
10+
import { closeHistory } from "@tiptap/pm/history";
1011
import type { Node } from "prosemirror-model";
1112
import { Plugin } from "prosemirror-state";
1213
import type { EditorView } from "prosemirror-view";
@@ -75,6 +76,10 @@ export function createMultiColumnHandleDropPlugin(
7576
return false; // Let ProseMirror handle empty slice drops
7677
}
7778
const draggedBlockIds = new Set(draggedBlocks.map((block) => block.id));
79+
let lastDraggedBlock = draggedBlocks.at(-1)!;
80+
while (lastDraggedBlock.children.length > 0) {
81+
lastDraggedBlock = lastDraggedBlock.children.at(-1)!;
82+
}
7883

7984
if (blockInfo.blockNoteType === "column") {
8085
// The user is dropping the target column's entire contents on the
@@ -214,7 +219,7 @@ export function createMultiColumnHandleDropPlugin(
214219
});
215220
}
216221
// Keep typing at the dropped blocks, not their old position.
217-
editor.setTextCursorPosition(draggedBlocks.at(-1)!, "end");
222+
editor.setTextCursorPosition(lastDraggedBlock, "end");
218223
});
219224
} else {
220225
// Create new columnList with blocks as columns
@@ -255,10 +260,12 @@ export function createMultiColumnHandleDropPlugin(
255260
},
256261
],
257262
);
258-
editor.setTextCursorPosition(draggedBlocks.at(-1)!, "end");
263+
editor.setTextCursorPosition(lastDraggedBlock, "end");
259264
});
260265
}
261266

267+
// Close after the drop: the next keystroke must be a separate undo step.
268+
editor.transact((tr) => closeHistory(tr));
262269
return true; // Prevent default ProseMirror drop behavior
263270
},
264271
},

‎packages/xl-multi-column/src/test/extensions/columnDrop.test.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getNodeById } from "@blocknote/core";
2+
import { closeHistory } from "@tiptap/pm/history";
23
import { Fragment, Slice } from "prosemirror-model";
34
import { describe, expect, it, vi } from "vite-plus/test";
45

@@ -30,6 +31,36 @@ function dropOnEdge(ids: string[], target: string, position: "left" | "right") {
3031
}
3132

3233
describe("column edge drops", () => {
34+
it.each(["column-0", "paragraph-1"])(
35+
"places the cursor in the final nested child when dropping beside %s",
36+
(target) => {
37+
const editor = getEditor();
38+
dropOnEdge(["paragraph-0"], target, "right");
39+
expect(editor.getTextCursorPosition().block.id).toBe(
40+
"nested-paragraph-0",
41+
);
42+
expect(editor.prosemirrorState.selection.$from.parentOffset).toBe(
43+
"Nested Paragraph 0".length,
44+
);
45+
},
46+
);
47+
48+
it.each(["column-0", "paragraph-0"])(
49+
"undoes immediate typing separately from the drop beside %s",
50+
(target) => {
51+
const editor = getEditor();
52+
editor.transact((tr) => closeHistory(tr));
53+
const before = editor.document;
54+
dropOnEdge(["paragraph-1"], target, "right");
55+
const dropped = editor.document;
56+
editor.transact((tr) => tr.insertText("typed"));
57+
expect(editor.undo()).toBe(true);
58+
expect(editor.document).toEqual(dropped);
59+
expect(editor.undo()).toBe(true);
60+
expect(editor.document).toEqual(before);
61+
},
62+
);
63+
3364
it.each(["left", "right"] as const)(
3465
"moves a mixed selection to an emptied target's %s edge",
3566
(edge) => {

‎pnpm-lock.yaml‎

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)