11import { 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" ;
39import { EditorView } from "@tiptap/pm/view" ;
410
511import { 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