diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 51c54be196..7dd1a85621 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -215,6 +215,17 @@ export interface BlockNoteEditorOptions< */ setIdAttribute?: boolean; + /** + * A CSS class name that constrains the side menu and drag-and-drop behavior. + * When set, the side menu will only appear and drag events will only be + * processed when the mouse is within the bounds of the closest ancestor + * element matching this class name. If the mouse is outside the scoping + * element, the side menu hides and drag events are ignored for this editor. + * + * @example "my-editor-container" + */ + sideMenuScopeClassName?: string; + /** * Determines behavior when pressing Tab (or Shift-Tab) while multiple blocks are selected and a toolbar is open. * - `"prefer-navigate-ui"`: Changes focus to the toolbar. User must press Escape to close toolbar before indenting blocks. Better for keyboard accessibility. diff --git a/packages/core/src/extensions/SideMenu/SideMenu.ts b/packages/core/src/extensions/SideMenu/SideMenu.ts index 635929a756..735ab64e31 100644 --- a/packages/core/src/extensions/SideMenu/SideMenu.ts +++ b/packages/core/src/extensions/SideMenu/SideMenu.ts @@ -142,11 +142,20 @@ export class SideMenuView< public isDragOrigin = false; + /** + * Optional CSS class name that constrains side menu and drag behavior. + * When set, the side menu only activates when the mouse is within the + * bounds of the closest ancestor element matching this class. + */ + private readonly sideMenuScopeClassName?: string; + constructor( private readonly editor: BlockNoteEditor, private readonly pmView: EditorView, emitUpdate: (state: SideMenuState) => void, + sideMenuScopeClassName?: string, ) { + this.sideMenuScopeClassName = sideMenuScopeClassName; this.emitUpdate = () => { if (!this.state) { throw new Error("Attempting to update uninitialized side menu"); @@ -194,11 +203,56 @@ export class SideMenuView< this.emitUpdate(this.state); }; + /** + * Checks if the given coordinates are within the bounds of the scoping + * element (an ancestor of the editor with the configured class name). + * Returns `true` if no scoping class is configured (i.e. no restriction). + */ + private isWithinScope = (coords: { + clientX: number; + clientY: number; + }): boolean => { + if (!this.sideMenuScopeClassName) { + return true; + } + + const scopeElement = this.pmView.dom.closest( + `.${this.sideMenuScopeClassName}`, + ); + if (!scopeElement) { + // If the editor isn't inside an element with the scoping class, + // fall back to unrestricted behavior. + return true; + } + + const rect = scopeElement.getBoundingClientRect(); + return ( + coords.clientX >= rect.left && + coords.clientX <= rect.right && + coords.clientY >= rect.top && + coords.clientY <= rect.bottom + ); + }; + updateStateFromMousePos = () => { if (this.menuFrozen || !this.mousePos) { return; } + // If the mouse is outside the scoping element, hide the side menu. + if ( + !this.isWithinScope({ + clientX: this.mousePos.x, + clientY: this.mousePos.y, + }) + ) { + if (this.state?.show) { + this.state.show = false; + this.updateState(this.state); + } + return; + } + const closestEditor = this.findClosestEditorElement({ clientX: this.mousePos.x, clientY: this.mousePos.y, @@ -380,6 +434,14 @@ export class SideMenuView< return; } + // If the drag is outside the scoping element, ignore it. + if ( + !this.isWithinScope({ clientX: event.clientX, clientY: event.clientY }) + ) { + this.closeDropCursor(); + return; + } + // Relevance gate: Only handle drags that belong to BlockNote // This prevents interference with external drag-and-drop libraries // by avoiding calls to closeDropCursor() for non-BlockNote drags @@ -508,6 +570,14 @@ export class SideMenuView< return; } + // If the drop is outside the scoping element, ignore it. + if ( + !this.isWithinScope({ clientX: event.clientX, clientY: event.clientY }) + ) { + this.closeDropCursor(); + return; + } + // Relevance gate: Only handle drags that belong to BlockNote // This prevents interference with external drag-and-drop libraries const isBlockNoteDrag =