Skip to content

Commit 98404ae

Browse files
fix: Various multi-column bugs (#1240)
* Fixed various multi-column bugs * Refactored `removeBlocksWithCallback` * Added `replaceBlocks` unit test * Added additional unit tests * Fixed error on live reload * fix: drop handling edge case --------- Co-authored-by: yousefed <[email protected]>
1 parent 5653454 commit 98404ae

File tree

10 files changed

+1041
-115
lines changed

10 files changed

+1041
-115
lines changed

packages/core/src/api/blockManipulation/commands/removeBlocks/__snapshots__/removeBlocks.test.ts.snap

Lines changed: 440 additions & 0 deletions
Large diffs are not rendered by default.

packages/core/src/api/blockManipulation/commands/removeBlocks/removeBlocks.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,10 @@ describe("Test removeBlocks", () => {
3131

3232
expect(getEditor().document).toMatchSnapshot();
3333
});
34+
35+
it("Remove all child blocks", () => {
36+
removeBlocks(getEditor(), ["nested-paragraph-0"]);
37+
38+
expect(getEditor().document).toMatchSnapshot();
39+
});
3440
});
Lines changed: 2 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import { Node } from "prosemirror-model";
2-
import { Transaction } from "prosemirror-state";
3-
41
import { Block } from "../../../../blocks/defaultBlocks.js";
52
import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor";
63
import {
@@ -9,84 +6,7 @@ import {
96
InlineContentSchema,
107
StyleSchema,
118
} from "../../../../schema/index.js";
12-
import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js";
13-
14-
export function removeBlocksWithCallback<
15-
BSchema extends BlockSchema,
16-
I extends InlineContentSchema,
17-
S extends StyleSchema
18-
>(
19-
editor: BlockNoteEditor<BSchema, I, S>,
20-
blocksToRemove: BlockIdentifier[],
21-
// Should return new removedSize.
22-
callback?: (
23-
node: Node,
24-
pos: number,
25-
tr: Transaction,
26-
removedSize: number
27-
) => number
28-
): Block<BSchema, I, S>[] {
29-
const ttEditor = editor._tiptapEditor;
30-
const tr = ttEditor.state.tr;
31-
32-
const idsOfBlocksToRemove = new Set<string>(
33-
blocksToRemove.map((block) =>
34-
typeof block === "string" ? block : block.id
35-
)
36-
);
37-
const removedBlocks: Block<BSchema, I, S>[] = [];
38-
let removedSize = 0;
39-
40-
ttEditor.state.doc.descendants((node, pos) => {
41-
// Skips traversing nodes after all target blocks have been removed.
42-
if (idsOfBlocksToRemove.size === 0) {
43-
return false;
44-
}
45-
46-
// Keeps traversing nodes if block with target ID has not been found.
47-
if (
48-
!node.type.isInGroup("bnBlock") ||
49-
!idsOfBlocksToRemove.has(node.attrs.id)
50-
) {
51-
return true;
52-
}
53-
54-
// Saves the block that is being deleted.
55-
removedBlocks.push(
56-
nodeToBlock(
57-
node,
58-
editor.schema.blockSchema,
59-
editor.schema.inlineContentSchema,
60-
editor.schema.styleSchema,
61-
editor.blockCache
62-
)
63-
);
64-
idsOfBlocksToRemove.delete(node.attrs.id);
65-
66-
// Removes the block and calculates the change in document size.
67-
removedSize = callback?.(node, pos, tr, removedSize) || removedSize;
68-
const oldDocSize = tr.doc.nodeSize;
69-
tr.delete(pos - removedSize - 1, pos - removedSize + node.nodeSize + 1);
70-
const newDocSize = tr.doc.nodeSize;
71-
removedSize += oldDocSize - newDocSize;
72-
73-
return false;
74-
});
75-
76-
// Throws an error if now all blocks could be found.
77-
if (idsOfBlocksToRemove.size > 0) {
78-
const notFoundIds = [...idsOfBlocksToRemove].join("\n");
79-
80-
throw Error(
81-
"Blocks with the following IDs could not be found in the editor: " +
82-
notFoundIds
83-
);
84-
}
85-
86-
editor.dispatch(tr);
87-
88-
return removedBlocks;
89-
}
9+
import { removeAndInsertBlocks } from "../replaceBlocks/replaceBlocks.js";
9010

9111
export function removeBlocks<
9212
BSchema extends BlockSchema,
@@ -96,5 +16,5 @@ export function removeBlocks<
9616
editor: BlockNoteEditor<BSchema, I, S>,
9717
blocksToRemove: BlockIdentifier[]
9818
): Block<BSchema, I, S>[] {
99-
return removeBlocksWithCallback(editor, blocksToRemove);
19+
return removeAndInsertBlocks(editor, blocksToRemove, []).removedBlocks;
10020
}
Lines changed: 96 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { Node } from "prosemirror-model";
2-
31
import { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js";
42
import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor";
53
import {
@@ -8,11 +6,11 @@ import {
86
InlineContentSchema,
97
StyleSchema,
108
} from "../../../../schema/index.js";
9+
import { Node } from "prosemirror-model";
1110
import { blockToNode } from "../../../nodeConversions/blockToNode.js";
1211
import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js";
13-
import { removeBlocksWithCallback } from "../removeBlocks/removeBlocks.js";
1412

15-
export function replaceBlocks<
13+
export function removeAndInsertBlocks<
1614
BSchema extends BlockSchema,
1715
I extends InlineContentSchema,
1816
S extends StyleSchema
@@ -24,35 +22,98 @@ export function replaceBlocks<
2422
insertedBlocks: Block<BSchema, I, S>[];
2523
removedBlocks: Block<BSchema, I, S>[];
2624
} {
25+
const ttEditor = editor._tiptapEditor;
26+
let tr = ttEditor.state.tr;
27+
28+
// Converts the `PartialBlock`s to ProseMirror nodes to insert them into the
29+
// document.
2730
const nodesToInsert: Node[] = [];
2831
for (const block of blocksToInsert) {
2932
nodesToInsert.push(
3033
blockToNode(block, editor.pmSchema, editor.schema.styleSchema)
3134
);
3235
}
3336

37+
const idsOfBlocksToRemove = new Set<string>(
38+
blocksToRemove.map((block) =>
39+
typeof block === "string" ? block : block.id
40+
)
41+
);
42+
const removedBlocks: Block<BSchema, I, S>[] = [];
43+
3444
const idOfFirstBlock =
3545
typeof blocksToRemove[0] === "string"
3646
? blocksToRemove[0]
3747
: blocksToRemove[0].id;
38-
const removedBlocks = removeBlocksWithCallback(
39-
editor,
40-
blocksToRemove,
41-
(node, pos, tr, removedSize) => {
42-
if (node.attrs.id === idOfFirstBlock) {
43-
const oldDocSize = tr.doc.nodeSize;
44-
tr.insert(pos, nodesToInsert);
45-
const newDocSize = tr.doc.nodeSize;
46-
47-
return removedSize + oldDocSize - newDocSize;
48-
}
49-
50-
return removedSize;
48+
let removedSize = 0;
49+
50+
ttEditor.state.doc.descendants((node, pos) => {
51+
// Skips traversing nodes after all target blocks have been removed.
52+
if (idsOfBlocksToRemove.size === 0) {
53+
return false;
54+
}
55+
56+
// Keeps traversing nodes if block with target ID has not been found.
57+
if (
58+
!node.type.isInGroup("bnBlock") ||
59+
!idsOfBlocksToRemove.has(node.attrs.id)
60+
) {
61+
return true;
62+
}
63+
64+
// Saves the block that is being deleted.
65+
removedBlocks.push(
66+
nodeToBlock(
67+
node,
68+
editor.schema.blockSchema,
69+
editor.schema.inlineContentSchema,
70+
editor.schema.styleSchema,
71+
editor.blockCache
72+
)
73+
);
74+
idsOfBlocksToRemove.delete(node.attrs.id);
75+
76+
if (blocksToInsert.length > 0 && node.attrs.id === idOfFirstBlock) {
77+
const oldDocSize = tr.doc.nodeSize;
78+
tr = tr.insert(pos, nodesToInsert);
79+
const newDocSize = tr.doc.nodeSize;
80+
81+
removedSize += oldDocSize - newDocSize;
5182
}
52-
);
5383

54-
// Now that the `PartialBlock`s have been converted to nodes, we can
55-
// re-convert them into full `Block`s.
84+
const oldDocSize = tr.doc.nodeSize;
85+
// Checks if the block is the only child of its parent. In this case, we
86+
// need to delete the parent `blockGroup` node instead of just the
87+
// `blockContainer`.
88+
const $pos = tr.doc.resolve(pos - removedSize);
89+
if (
90+
$pos.node().type.name === "blockGroup" &&
91+
$pos.node($pos.depth - 1).type.name !== "doc" &&
92+
$pos.node().childCount === 1
93+
) {
94+
tr = tr.delete($pos.before(), $pos.after());
95+
} else {
96+
tr = tr.delete(pos - removedSize, pos - removedSize + node.nodeSize);
97+
}
98+
const newDocSize = tr.doc.nodeSize;
99+
removedSize += oldDocSize - newDocSize;
100+
101+
return false;
102+
});
103+
104+
// Throws an error if now all blocks could be found.
105+
if (idsOfBlocksToRemove.size > 0) {
106+
const notFoundIds = [...idsOfBlocksToRemove].join("\n");
107+
108+
throw Error(
109+
"Blocks with the following IDs could not be found in the editor: " +
110+
notFoundIds
111+
);
112+
}
113+
114+
editor.dispatch(tr);
115+
116+
// Converts the nodes created from `blocksToInsert` into full `Block`s.
56117
const insertedBlocks: Block<BSchema, I, S>[] = [];
57118
for (const node of nodesToInsert) {
58119
insertedBlocks.push(
@@ -68,3 +129,18 @@ export function replaceBlocks<
68129

69130
return { insertedBlocks, removedBlocks };
70131
}
132+
133+
export function replaceBlocks<
134+
BSchema extends BlockSchema,
135+
I extends InlineContentSchema,
136+
S extends StyleSchema
137+
>(
138+
editor: BlockNoteEditor<BSchema, I, S>,
139+
blocksToRemove: BlockIdentifier[],
140+
blocksToInsert: PartialBlock<BSchema, I, S>[]
141+
): {
142+
insertedBlocks: Block<BSchema, I, S>[];
143+
removedBlocks: Block<BSchema, I, S>[];
144+
} {
145+
return removeAndInsertBlocks(editor, blocksToRemove, blocksToInsert);
146+
}

packages/xl-multi-column/src/extensions/ColumnResize/ColumnResizeExtension.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -296,9 +296,11 @@ class ColumnResizePluginView implements PluginView {
296296
this.editor.sideMenu.unfreezeMenu();
297297
};
298298

299-
// This is a required method for PluginView, so we get a type error if we
300-
// don't implement it.
301-
update: undefined;
299+
destroy() {
300+
this.view.dom.removeEventListener("mousedown", this.mouseDownHandler);
301+
document.body.removeEventListener("mousemove", this.mouseMoveHandler);
302+
document.body.removeEventListener("mouseup", this.mouseUpHandler);
303+
}
302304
}
303305

304306
const createColumnResizePlugin = (editor: BlockNoteEditor<any, any, any>) =>

packages/xl-multi-column/src/extensions/DropCursor/MultiColumnDropCursorPlugin.ts

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -130,17 +130,24 @@ export function multiColumnDropCursor(
130130
(b) => b.id === blockInfo.bnBlock.node.attrs.id
131131
);
132132

133-
const newChildren = columnList.children.toSpliced(
134-
position === "left" ? index : index + 1,
135-
0,
136-
{
133+
const newChildren = columnList.children
134+
// If the dragged block is in one of the columns, remove it.
135+
.map((column) => ({
136+
...column,
137+
children: column.children.filter(
138+
(block) => block.id !== draggedBlock.id
139+
),
140+
}))
141+
// Remove empty columns (can happen when dragged block is removed).
142+
.filter((column) => column.children.length > 0)
143+
// Insert the dragged block in the correct position.
144+
.toSpliced(position === "left" ? index : index + 1, 0, {
137145
type: "column",
138146
children: [draggedBlock],
139147
props: {},
140148
content: undefined,
141149
id: UniqueID.options.generateID(),
142-
}
143-
);
150+
});
144151

145152
editor.removeBlocks([draggedBlock]);
146153

@@ -201,14 +208,25 @@ class DropCursorView {
201208
const handler = (e: Event) => {
202209
(this as any)[name](e);
203210
};
204-
editorView.dom.addEventListener(name, handler);
211+
editorView.dom.addEventListener(
212+
name,
213+
handler,
214+
// drop event captured in bubbling phase to make sure
215+
// "cursorPos" is set to undefined before the "handleDrop" handler is called
216+
// (otherwise an error could be thrown, see https://github.com/TypeCellOS/BlockNote/pull/1240)
217+
name === "drop" ? true : undefined
218+
);
205219
return { name, handler };
206220
});
207221
}
208222

209223
destroy() {
210224
this.handlers.forEach(({ name, handler }) =>
211-
this.editorView.dom.removeEventListener(name, handler)
225+
this.editorView.dom.removeEventListener(
226+
name,
227+
handler,
228+
name === "drop" ? true : undefined
229+
)
212230
);
213231
}
214232

@@ -267,6 +285,10 @@ class DropCursorView {
267285
) {
268286
const block = this.editorView.nodeDOM(this.cursorPos.pos);
269287

288+
if (!block) {
289+
throw new Error("nodeDOM returned null in updateOverlay");
290+
}
291+
270292
const blockRect = (block as HTMLElement).getBoundingClientRect();
271293
const halfWidth = (this.width / 2) * scaleY;
272294
const left =
@@ -434,7 +456,7 @@ class DropCursorView {
434456
target = point;
435457
}
436458
}
437-
// console.log("target", target);
459+
438460
this.setCursor({ pos: target, position });
439461
this.scheduleRemoval(5000);
440462
}
@@ -445,7 +467,7 @@ class DropCursorView {
445467
}
446468

447469
drop() {
448-
this.scheduleRemoval(20);
470+
this.setCursor(undefined);
449471
}
450472

451473
dragleave(event: DragEvent) {

0 commit comments

Comments
 (0)