Skip to content

Commit 77c2549

Browse files
fix: Drag and drop event handling (#1413)
* Fixed drag and drop event handling * Added comments * Refactored code
1 parent f5171d3 commit 77c2549

File tree

1 file changed

+76
-28
lines changed

1 file changed

+76
-28
lines changed

packages/core/src/extensions/SideMenu/SideMenuPlugin.ts

Lines changed: 76 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { DOMParser, Slice } from "@tiptap/pm/model";
2-
import { EditorState, Plugin, PluginKey, PluginView } from "@tiptap/pm/state";
2+
import {
3+
EditorState,
4+
Plugin,
5+
PluginKey,
6+
TextSelection,
7+
PluginView,
8+
} from "@tiptap/pm/state";
39
import { EditorView } from "@tiptap/pm/view";
410

511
import { Block } from "../../blocks/defaultBlocks.js";
@@ -268,21 +274,56 @@ export class SideMenuView<
268274
}
269275
};
270276

271-
/**
272-
* If the event is outside the editor contents,
273-
* we dispatch a fake event, so that we can still drop the content
274-
* when dragging / dropping to the side of the editor
275-
*/
276277
onDrop = (event: DragEvent) => {
278+
// Content from outside a BlockNote editor is being dropped - just let
279+
// ProseMirror's default behaviour handle it.
280+
if (this.pmView.dragging === null) {
281+
return;
282+
}
283+
277284
this.editor._tiptapEditor.commands.blur();
278285

279-
// ProseMirror doesn't remove the dragged content if it's dropped outside
280-
// the editor (e.g. to other editors), so we need to do it manually. Since
281-
// the dragged content is the same as the selected content, we can just
282-
// delete the selection.
283-
if (this.isDragOrigin && !this.pmView.dom.contains(event.target as Node)) {
284-
this.pmView.dispatch(this.pmView.state.tr.deleteSelection());
286+
// When ProseMirror handles a drop event on the editor while
287+
// `view.dragging` is set, it deletes the selected content. However, if
288+
// a block from a different editor is being dropped, this causes some
289+
// issues that the code below fixes:
290+
if (!this.isDragOrigin && this.pmView.dom.contains(event.target as Node)) {
291+
// 1. Because the editor selection is unrelated to the dragged content,
292+
// we don't want PM to delete its content. Therefore, we collapse the
293+
// selection.
294+
this.pmView.dispatch(
295+
this.pmView.state.tr.setSelection(
296+
TextSelection.create(
297+
this.pmView.state.tr.doc,
298+
this.pmView.state.tr.selection.to
299+
)
300+
)
301+
);
302+
} else if (
303+
this.isDragOrigin &&
304+
!this.pmView.dom.contains(event.target as Node)
305+
) {
306+
// 2. Because the editor from which the block originates doesn't get a
307+
// drop event on it, PM doesn't delete its selected content. Therefore, we
308+
// need to do so manually.
309+
//
310+
// Note: Deleting the selected content from the editor from which the
311+
// block originates, may change its height. This can cause the position of
312+
// the editor in which the block is being dropping to shift, before it
313+
// can handle the drop event. That in turn can cause the drop to happen
314+
// somewhere other than the user intended. To get around this, we delay
315+
// deleting the selected content until all editors have had the chance to
316+
// handle the event.
317+
setTimeout(
318+
() => this.pmView.dispatch(this.pmView.state.tr.deleteSelection()),
319+
0
320+
);
285321
}
322+
// 3. PM only clears `view.dragging` on the editor that the block was
323+
// dropped, so we manually have to clear it on all the others. However,
324+
// PM also needs to read `view.dragging` while handling the event, so we
325+
// use a `setTimeout` to ensure it's only cleared after that.
326+
setTimeout(() => (this.pmView.dragging = null), 0);
286327

287328
if (
288329
this.sideMenuDetection === "editor" ||
@@ -298,6 +339,11 @@ export class SideMenuView<
298339
});
299340

300341
if (!pos || pos.inside === -1) {
342+
/**
343+
* When `this.sideMenuSelection === "viewport"`, if the event is outside the
344+
* editor contents, we dispatch a fake event, so that we can still drop the
345+
* content when dragging / dropping to the side of the editor
346+
*/
301347
const evt = this.createSyntheticEvent(event);
302348
// console.log("dispatch fake drop");
303349
this.pmView.dom.dispatchEvent(evt);
@@ -323,25 +369,27 @@ export class SideMenuView<
323369
* access `dataTransfer` contents on `dragstart` and `drop` events.
324370
*/
325371
onDragStart = (event: DragEvent) => {
326-
if (!this.pmView.dragging) {
327-
const html = event.dataTransfer?.getData("blocknote/html");
328-
if (!html) {
329-
return;
330-
}
372+
const html = event.dataTransfer?.getData("blocknote/html");
373+
if (!html) {
374+
return;
375+
}
376+
377+
if (this.pmView.dragging) {
378+
throw new Error("New drag was started while an existing drag is ongoing");
379+
}
331380

332-
const element = document.createElement("div");
333-
element.innerHTML = html;
381+
const element = document.createElement("div");
382+
element.innerHTML = html;
334383

335-
const parser = DOMParser.fromSchema(this.pmView.state.schema);
336-
const node = parser.parse(element, {
337-
topNode: this.pmView.state.schema.nodes["blockGroup"].create(),
338-
});
384+
const parser = DOMParser.fromSchema(this.pmView.state.schema);
385+
const node = parser.parse(element, {
386+
topNode: this.pmView.state.schema.nodes["blockGroup"].create(),
387+
});
339388

340-
this.pmView.dragging = {
341-
slice: new Slice(node.content, 0, 0),
342-
move: true,
343-
};
344-
}
389+
this.pmView.dragging = {
390+
slice: new Slice(node.content, 0, 0),
391+
move: true,
392+
};
345393
};
346394

347395
/**

0 commit comments

Comments
 (0)