From 6bd7529e6cc70379325fa0ba40ae74eed0ef074e Mon Sep 17 00:00:00 2001 From: Aslam Date: Mon, 20 Oct 2025 16:34:40 +0700 Subject: [PATCH] feat: add configurable event handling for SideMenu --- .../src/extensions/SideMenu/SideMenuPlugin.ts | 103 ++++++++++++++++-- .../SideMenu/SideMenuController.tsx | 9 +- 2 files changed, 100 insertions(+), 12 deletions(-) diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts index cc3d588272..76312c3a8b 100644 --- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts +++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts @@ -139,6 +139,8 @@ export class SideMenuView< public isDragOrigin = false; + public useHandleDOMEvents = false; + constructor( private readonly editor: BlockNoteEditor, private readonly pmView: EditorView, @@ -171,12 +173,13 @@ export class SideMenuView< true, ); - // Shows or updates menu position whenever the cursor moves, if the menu isn't frozen. - this.pmView.root.addEventListener( - "mousemove", - this.onMouseMove as EventListener, - true, - ); + if (!this.useHandleDOMEvents) { + this.pmView.root.addEventListener( + "mousemove", + this.onMouseMove as EventListener, + true, + ); + } // Hides and unfreezes the menu whenever the user presses a key. this.pmView.root.addEventListener( @@ -561,6 +564,11 @@ export class SideMenuView< this.mousePos = { x: event.clientX, y: event.clientY }; + if (this.useHandleDOMEvents) { + this.updateStateFromMousePos(); + return; + } + // We want the full area of the editor to check if the cursor is hovering // above it though. const editorOuterBoundingBox = this.pmView.dom.getBoundingClientRect(); @@ -598,6 +606,30 @@ export class SideMenuView< this.updateStateFromMousePos(); }; + onMouseLeave = (event: MouseEvent) => { + if (this.menuFrozen) { + return false; + } + + const editorOuterBoundingBox = this.pmView.dom.getBoundingClientRect(); + const cursorWithinEditor = + event.clientX > editorOuterBoundingBox.left && + event.clientX < editorOuterBoundingBox.right && + event.clientY > editorOuterBoundingBox.top && + event.clientY < editorOuterBoundingBox.bottom; + + if (cursorWithinEditor) { + return false; + } + + if (this.state?.show) { + this.state.show = false; + this.emitUpdate(this.state); + } + + return false; + }; + private dispatchSyntheticEvent(event: DragEvent) { const evt = new Event(event.type as "dragover", event) as any; const dropPointBoundingBox = ( @@ -648,11 +680,15 @@ export class SideMenuView< this.state.show = false; this.emitUpdate(this.state); } - this.pmView.root.removeEventListener( - "mousemove", - this.onMouseMove as EventListener, - true, - ); + + if (!this.useHandleDOMEvents) { + this.pmView.root.removeEventListener( + "mousemove", + this.onMouseMove as EventListener, + true, + ); + } + this.pmView.root.removeEventListener( "dragstart", this.onDragStart as EventListener, @@ -678,6 +714,26 @@ export class SideMenuView< ); this.pmView.root.removeEventListener("scroll", this.onScroll, true); } + + setUseHandleDOMEvents = (value: boolean) => { + if (!this.useHandleDOMEvents && value) { + this.pmView.root.removeEventListener( + "mousemove", + this.onMouseMove as EventListener, + true, + ); + } + + if (this.useHandleDOMEvents && !value) { + this.pmView.root.addEventListener( + "mousemove", + this.onMouseMove as EventListener, + true, + ); + } + + this.useHandleDOMEvents = value; + }; } export const sideMenuPluginKey = new PluginKey("SideMenuPlugin"); @@ -704,6 +760,22 @@ export class SideMenuProsemirrorPlugin< }); return this.view; }, + props: { + handleDOMEvents: { + mousemove: (_view, event) => { + if (this.view?.useHandleDOMEvents) { + this.view.onMouseMove(event); + } + return false; + }, + mouseleave: (_view, event) => { + if (this.view?.useHandleDOMEvents) { + this.view.onMouseLeave(event); + } + return false; + }, + }, + }, }), ); } @@ -739,6 +811,15 @@ export class SideMenuProsemirrorPlugin< this.view.isDragOrigin = false; } }; + /** + * Sets whether to use ProseMirror's handleDOMEvents for mousemove tracking instead of addEventListener. + * + * - When `true`: Uses handleDOMEvents (mousemove + mouseleave) - scoped to ProseMirror + * - When `false` (default): Uses addEventListener on root element - original behavior + */ + setUseHandleDOMEvents = (value: boolean) => { + this.view?.setUseHandleDOMEvents(value); + }; /** * Freezes the side menu. When frozen, the side menu will stay * attached to the same block regardless of which block is hovered by the diff --git a/packages/react/src/components/SideMenu/SideMenuController.tsx b/packages/react/src/components/SideMenu/SideMenuController.tsx index 935dd9865b..424f054186 100644 --- a/packages/react/src/components/SideMenu/SideMenuController.tsx +++ b/packages/react/src/components/SideMenu/SideMenuController.tsx @@ -6,7 +6,7 @@ import { InlineContentSchema, StyleSchema, } from "@blocknote/core"; -import { FC } from "react"; +import { FC, useEffect } from "react"; import { UseFloatingOptions } from "@floating-ui/react"; import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js"; @@ -22,6 +22,7 @@ export const SideMenuController = < >(props: { sideMenu?: FC>; floatingOptions?: Partial; + useHandleDOMEvents?: boolean; }) => { const editor = useBlockNoteEditor(); @@ -32,6 +33,12 @@ export const SideMenuController = < unfreezeMenu: editor.sideMenu.unfreezeMenu, }; + useEffect(() => { + if (props.useHandleDOMEvents) { + editor.sideMenu.setUseHandleDOMEvents(props.useHandleDOMEvents); + } + }, [editor.sideMenu, props.useHandleDOMEvents]); + const state = useUIPluginState( editor.sideMenu.onUpdate.bind(editor.sideMenu), );