113 lines
3.3 KiB
TypeScript
113 lines
3.3 KiB
TypeScript
import type { Component } from "vue";
|
|
|
|
export interface ContextMenuItem {
|
|
label: string;
|
|
action?: () => void;
|
|
disabled?: boolean | (() => boolean);
|
|
separator?: boolean;
|
|
icon?: Component;
|
|
iconClass?: string;
|
|
title?: string;
|
|
indentLevel?: number;
|
|
checked?: boolean;
|
|
// Raw shortcut syntax such as `Mod+C` or `Shift+Alt+U`; display formatting stays in this component.
|
|
shortcut?: string;
|
|
variant?: "default" | "destructive";
|
|
visible?: boolean;
|
|
children?: ContextMenuItem[];
|
|
}
|
|
|
|
export type ContextMenuClose = () => void;
|
|
|
|
/** Marker attribute on scrollable context menu / submenu roots. */
|
|
export const CONTEXT_MENU_SCROLL_ROOT_ATTR = "data-dbx-context-menu";
|
|
|
|
export function isContextMenuInternalScroll(event: Event): boolean {
|
|
const target = event.target;
|
|
return target instanceof Element && target.closest(`[${CONTEXT_MENU_SCROLL_ROOT_ATTR}]`) !== null;
|
|
}
|
|
|
|
export interface ContextMenuRegistration {
|
|
activate(): void;
|
|
setOpen(open: boolean): void;
|
|
dispose(): void;
|
|
}
|
|
|
|
export interface ContextMenuRegistry {
|
|
register(close: ContextMenuClose): ContextMenuRegistration;
|
|
}
|
|
|
|
export function createContextMenuRegistry(documentTarget: EventTarget, windowTarget: EventTarget): ContextMenuRegistry {
|
|
const openMenus = new Set<ContextMenuClose>();
|
|
let hostCount = 0;
|
|
let listenersAttached = false;
|
|
|
|
function closeAll() {
|
|
const closers = [...openMenus];
|
|
openMenus.clear();
|
|
for (const close of closers) close();
|
|
}
|
|
|
|
function closeAllOnScroll(event: Event) {
|
|
// Ignore scroll that originates from within an open menu/submenu.
|
|
if (isContextMenuInternalScroll(event)) return;
|
|
closeAll();
|
|
}
|
|
|
|
function attachListeners() {
|
|
if (listenersAttached) return;
|
|
documentTarget.addEventListener("contextmenu", closeAll, true);
|
|
documentTarget.addEventListener("scroll", closeAllOnScroll, true);
|
|
windowTarget.addEventListener("resize", closeAll);
|
|
listenersAttached = true;
|
|
}
|
|
|
|
function detachListeners() {
|
|
if (!listenersAttached) return;
|
|
documentTarget.removeEventListener("contextmenu", closeAll, true);
|
|
documentTarget.removeEventListener("scroll", closeAllOnScroll, true);
|
|
windowTarget.removeEventListener("resize", closeAll);
|
|
listenersAttached = false;
|
|
openMenus.clear();
|
|
}
|
|
|
|
return {
|
|
register(close) {
|
|
hostCount += 1;
|
|
attachListeners();
|
|
let disposed = false;
|
|
|
|
return {
|
|
activate() {
|
|
if (disposed) return;
|
|
const closers = [...openMenus];
|
|
openMenus.clear();
|
|
for (const activeClose of closers) {
|
|
if (activeClose !== close) activeClose();
|
|
}
|
|
openMenus.add(close);
|
|
},
|
|
setOpen(open) {
|
|
if (disposed) return;
|
|
if (open) openMenus.add(close);
|
|
else openMenus.delete(close);
|
|
},
|
|
dispose() {
|
|
if (disposed) return;
|
|
disposed = true;
|
|
openMenus.delete(close);
|
|
hostCount -= 1;
|
|
if (hostCount === 0) detachListeners();
|
|
},
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
let globalContextMenuRegistry: ContextMenuRegistry | undefined;
|
|
|
|
export function registerGlobalContextMenu(close: ContextMenuClose): ContextMenuRegistration {
|
|
globalContextMenuRegistry ??= createContextMenuRegistry(document, window);
|
|
return globalContextMenuRegistry.register(close);
|
|
}
|