import { DeesElement, property, html, customElement, type TemplateResult, state, css, cssManager } from '@design.estate/dees-element'; export interface IContextMenuItem { name: string; iconName?: string; action: () => void | Promise; disabled?: boolean; } @customElement('wcc-contextmenu') export class WccContextmenu extends DeesElement { // Static method to show context menu at position public static async show( event: MouseEvent, menuItems: IContextMenuItem[] ): Promise { event.preventDefault(); event.stopPropagation(); // Remove any existing context menu const existing = document.querySelector('wcc-contextmenu'); if (existing) { existing.remove(); } const menu = new WccContextmenu(); menu.menuItems = menuItems; menu.x = event.clientX; menu.y = event.clientY; document.body.appendChild(menu); // Wait for render then adjust position if needed await menu.updateComplete; menu.adjustPosition(); } @property({ type: Array }) accessor menuItems: IContextMenuItem[] = []; @property({ type: Number }) accessor x: number = 0; @property({ type: Number }) accessor y: number = 0; @state() accessor visible: boolean = false; private boundHandleOutsideClick = this.handleOutsideClick.bind(this); private boundHandleKeydown = this.handleKeydown.bind(this); public static styles = [ css` :host { position: fixed; z-index: 10000; opacity: 0; transform: scale(0.95) translateY(-5px); transition: opacity 0.15s ease, transform 0.15s ease; pointer-events: none; } :host(.visible) { opacity: 1; transform: scale(1) translateY(0); pointer-events: auto; } .menu { min-width: 160px; background: #0f0f0f; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 6px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); padding: 4px 0; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; font-size: 12px; } .menu-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; color: #ccc; cursor: pointer; transition: background 0.1s ease; user-select: none; } .menu-item:hover { background: rgba(59, 130, 246, 0.15); color: #fff; } .menu-item.disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; } .menu-item .icon { font-family: 'Material Symbols Outlined'; font-size: 16px; font-weight: normal; font-style: normal; line-height: 1; letter-spacing: normal; text-transform: none; white-space: nowrap; word-wrap: normal; direction: ltr; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24; opacity: 0.7; } .menu-item:hover .icon { opacity: 1; } .menu-item .label { flex: 1; } ` ]; public render(): TemplateResult { return html` `; } async connectedCallback() { await super.connectedCallback(); // Delay adding listeners to avoid immediate close requestAnimationFrame(() => { document.addEventListener('click', this.boundHandleOutsideClick); document.addEventListener('contextmenu', this.boundHandleOutsideClick); document.addEventListener('keydown', this.boundHandleKeydown); this.classList.add('visible'); }); } async disconnectedCallback() { await super.disconnectedCallback(); document.removeEventListener('click', this.boundHandleOutsideClick); document.removeEventListener('contextmenu', this.boundHandleOutsideClick); document.removeEventListener('keydown', this.boundHandleKeydown); } private adjustPosition() { const rect = this.getBoundingClientRect(); const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; let x = this.x; let y = this.y; // Adjust if menu goes off right edge if (x + rect.width > windowWidth - 10) { x = windowWidth - rect.width - 10; } // Adjust if menu goes off bottom edge if (y + rect.height > windowHeight - 10) { y = windowHeight - rect.height - 10; } // Ensure not off left or top if (x < 10) x = 10; if (y < 10) y = 10; this.style.left = `${x}px`; this.style.top = `${y}px`; } private handleOutsideClick(e: Event) { const path = e.composedPath(); if (!path.includes(this)) { this.close(); } } private handleKeydown(e: KeyboardEvent) { if (e.key === 'Escape') { this.close(); } } private async handleItemClick(item: IContextMenuItem) { if (item.disabled) return; await item.action(); this.close(); } private close() { this.classList.remove('visible'); setTimeout(() => this.remove(), 150); } }