diff --git a/src/ContextMenu.ts b/src/ContextMenu.ts index 8b3c2170d..685ca7765 100644 --- a/src/ContextMenu.ts +++ b/src/ContextMenu.ts @@ -18,6 +18,8 @@ export class ContextMenu { current_submenu?: ContextMenu lock?: boolean + controller: AbortController = new AbortController() + /** * @todo Interface for values requires functionality change - currently accepts * an array of strings, functions, objects, nulls, or undefined. @@ -74,23 +76,26 @@ export class ContextMenu { // delay so the mouse up event is not caught by this element }, 100) + // Close the context menu when a click occurs outside this context menu or its submenus + const { signal } = this.controller + const eventOptions = { capture: true, signal } + + if (!this.parentMenu) { + document.addEventListener("pointerdown", (e) => { + if (e.target instanceof Node && !this.containsNode(e.target)) { + this.close() + } + }, eventOptions) + } + // this prevents the default context browser menu to open in case this menu was created when pressing right button - root.addEventListener( - "pointerup", - function (e) { - // console.log("pointerevents: ContextMenu up root prevent"); - e.preventDefault() - return true - }, - true, - ) + root.addEventListener("pointerup", e => e.preventDefault(), true) + + // Right button root.addEventListener( "contextmenu", - function (e) { - // right button - if (e.button != 2) return false - e.preventDefault() - return false + (e) => { + if (e.button === 2) e.preventDefault() }, true, ) @@ -98,11 +103,9 @@ export class ContextMenu { root.addEventListener( "pointerdown", (e) => { - // console.log("pointerevents: ContextMenu down"); if (e.button == 2) { this.close() e.preventDefault() - return true } }, true, @@ -179,6 +182,19 @@ export class ContextMenu { } } + /** + * Checks if {@link node} is inside this context menu or any of its submenus + * @param node The {@link Node} to check + * @param visited A set of visited menus to avoid circular references + * @returns `true` if {@link node} is inside this context menu or any of its submenus + */ + containsNode(node: Node, visited: Set = new Set()): boolean { + if (visited.has(this)) return false + visited.add(this) + + return this.current_submenu?.containsNode(node, visited) || this.root.contains(node) + } + addItem( name: string | null, value: string | IContextMenuValue | null, @@ -315,6 +331,7 @@ export class ContextMenu { } close(e?: MouseEvent, ignore_parent_menu?: boolean): void { + this.controller.abort() this.root.remove() if (this.parentMenu && !ignore_parent_menu) { this.parentMenu.lock = false @@ -343,8 +360,6 @@ export class ContextMenu { const evt = document.createEvent("CustomEvent") evt.initCustomEvent(event_name, true, true, params) if (element.dispatchEvent) element.dispatchEvent(evt) - // @ts-expect-error - else if (element.__events) element.__events.dispatchEvent(evt) // else nothing seems binded here so nothing to do return evt }