Skip to content

Commit 6bd7529

Browse files
committed
feat: add configurable event handling for SideMenu
1 parent 0c4e911 commit 6bd7529

File tree

2 files changed

+100
-12
lines changed

2 files changed

+100
-12
lines changed

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

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ export class SideMenuView<
139139

140140
public isDragOrigin = false;
141141

142+
public useHandleDOMEvents = false;
143+
142144
constructor(
143145
private readonly editor: BlockNoteEditor<BSchema, I, S>,
144146
private readonly pmView: EditorView,
@@ -171,12 +173,13 @@ export class SideMenuView<
171173
true,
172174
);
173175

174-
// Shows or updates menu position whenever the cursor moves, if the menu isn't frozen.
175-
this.pmView.root.addEventListener(
176-
"mousemove",
177-
this.onMouseMove as EventListener,
178-
true,
179-
);
176+
if (!this.useHandleDOMEvents) {
177+
this.pmView.root.addEventListener(
178+
"mousemove",
179+
this.onMouseMove as EventListener,
180+
true,
181+
);
182+
}
180183

181184
// Hides and unfreezes the menu whenever the user presses a key.
182185
this.pmView.root.addEventListener(
@@ -561,6 +564,11 @@ export class SideMenuView<
561564

562565
this.mousePos = { x: event.clientX, y: event.clientY };
563566

567+
if (this.useHandleDOMEvents) {
568+
this.updateStateFromMousePos();
569+
return;
570+
}
571+
564572
// We want the full area of the editor to check if the cursor is hovering
565573
// above it though.
566574
const editorOuterBoundingBox = this.pmView.dom.getBoundingClientRect();
@@ -598,6 +606,30 @@ export class SideMenuView<
598606
this.updateStateFromMousePos();
599607
};
600608

609+
onMouseLeave = (event: MouseEvent) => {
610+
if (this.menuFrozen) {
611+
return false;
612+
}
613+
614+
const editorOuterBoundingBox = this.pmView.dom.getBoundingClientRect();
615+
const cursorWithinEditor =
616+
event.clientX > editorOuterBoundingBox.left &&
617+
event.clientX < editorOuterBoundingBox.right &&
618+
event.clientY > editorOuterBoundingBox.top &&
619+
event.clientY < editorOuterBoundingBox.bottom;
620+
621+
if (cursorWithinEditor) {
622+
return false;
623+
}
624+
625+
if (this.state?.show) {
626+
this.state.show = false;
627+
this.emitUpdate(this.state);
628+
}
629+
630+
return false;
631+
};
632+
601633
private dispatchSyntheticEvent(event: DragEvent) {
602634
const evt = new Event(event.type as "dragover", event) as any;
603635
const dropPointBoundingBox = (
@@ -648,11 +680,15 @@ export class SideMenuView<
648680
this.state.show = false;
649681
this.emitUpdate(this.state);
650682
}
651-
this.pmView.root.removeEventListener(
652-
"mousemove",
653-
this.onMouseMove as EventListener,
654-
true,
655-
);
683+
684+
if (!this.useHandleDOMEvents) {
685+
this.pmView.root.removeEventListener(
686+
"mousemove",
687+
this.onMouseMove as EventListener,
688+
true,
689+
);
690+
}
691+
656692
this.pmView.root.removeEventListener(
657693
"dragstart",
658694
this.onDragStart as EventListener,
@@ -678,6 +714,26 @@ export class SideMenuView<
678714
);
679715
this.pmView.root.removeEventListener("scroll", this.onScroll, true);
680716
}
717+
718+
setUseHandleDOMEvents = (value: boolean) => {
719+
if (!this.useHandleDOMEvents && value) {
720+
this.pmView.root.removeEventListener(
721+
"mousemove",
722+
this.onMouseMove as EventListener,
723+
true,
724+
);
725+
}
726+
727+
if (this.useHandleDOMEvents && !value) {
728+
this.pmView.root.addEventListener(
729+
"mousemove",
730+
this.onMouseMove as EventListener,
731+
true,
732+
);
733+
}
734+
735+
this.useHandleDOMEvents = value;
736+
};
681737
}
682738

683739
export const sideMenuPluginKey = new PluginKey("SideMenuPlugin");
@@ -704,6 +760,22 @@ export class SideMenuProsemirrorPlugin<
704760
});
705761
return this.view;
706762
},
763+
props: {
764+
handleDOMEvents: {
765+
mousemove: (_view, event) => {
766+
if (this.view?.useHandleDOMEvents) {
767+
this.view.onMouseMove(event);
768+
}
769+
return false;
770+
},
771+
mouseleave: (_view, event) => {
772+
if (this.view?.useHandleDOMEvents) {
773+
this.view.onMouseLeave(event);
774+
}
775+
return false;
776+
},
777+
},
778+
},
707779
}),
708780
);
709781
}
@@ -739,6 +811,15 @@ export class SideMenuProsemirrorPlugin<
739811
this.view.isDragOrigin = false;
740812
}
741813
};
814+
/**
815+
* Sets whether to use ProseMirror's handleDOMEvents for mousemove tracking instead of addEventListener.
816+
*
817+
* - When `true`: Uses handleDOMEvents (mousemove + mouseleave) - scoped to ProseMirror
818+
* - When `false` (default): Uses addEventListener on root element - original behavior
819+
*/
820+
setUseHandleDOMEvents = (value: boolean) => {
821+
this.view?.setUseHandleDOMEvents(value);
822+
};
742823
/**
743824
* Freezes the side menu. When frozen, the side menu will stay
744825
* attached to the same block regardless of which block is hovered by the

packages/react/src/components/SideMenu/SideMenuController.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
InlineContentSchema,
77
StyleSchema,
88
} from "@blocknote/core";
9-
import { FC } from "react";
9+
import { FC, useEffect } from "react";
1010

1111
import { UseFloatingOptions } from "@floating-ui/react";
1212
import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
@@ -22,6 +22,7 @@ export const SideMenuController = <
2222
>(props: {
2323
sideMenu?: FC<SideMenuProps<BSchema, I, S>>;
2424
floatingOptions?: Partial<UseFloatingOptions>;
25+
useHandleDOMEvents?: boolean;
2526
}) => {
2627
const editor = useBlockNoteEditor<BSchema, I, S>();
2728

@@ -32,6 +33,12 @@ export const SideMenuController = <
3233
unfreezeMenu: editor.sideMenu.unfreezeMenu,
3334
};
3435

36+
useEffect(() => {
37+
if (props.useHandleDOMEvents) {
38+
editor.sideMenu.setUseHandleDOMEvents(props.useHandleDOMEvents);
39+
}
40+
}, [editor.sideMenu, props.useHandleDOMEvents]);
41+
3542
const state = useUIPluginState(
3643
editor.sideMenu.onUpdate.bind(editor.sideMenu),
3744
);

0 commit comments

Comments
 (0)