import { customElement, html, DeesElement, type TemplateResult, cssManager, css, state, } from '@design.estate/dees-element'; import { zIndexRegistry } from '../00zindex.js'; import { type ISlashMenuItem } from './wysiwyg.types.js'; import { WysiwygShortcuts } from './wysiwyg.shortcuts.js'; declare global { interface HTMLElementTagNameMap { 'dees-slash-menu': DeesSlashMenu; } } @customElement('dees-slash-menu') export class DeesSlashMenu extends DeesElement { private static instance: DeesSlashMenu; public static getInstance(): DeesSlashMenu { if (!DeesSlashMenu.instance) { DeesSlashMenu.instance = new DeesSlashMenu(); document.body.appendChild(DeesSlashMenu.instance); } return DeesSlashMenu.instance; } @state() public visible: boolean = false; @state() private position: { x: number; y: number } = { x: 0, y: 0 }; @state() private filter: string = ''; @state() private selectedIndex: number = 0; @state() private menuZIndex: number = 1000; private callback: ((type: string) => void) | null = null; public static styles = [ cssManager.defaultStyles, css` :host { position: fixed; pointer-events: none; top: 0; left: 0; width: 0; height: 0; } .slash-menu { position: fixed; background: ${cssManager.bdTheme('#ffffff', '#262626')}; border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')}; border-radius: 8px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); padding: 4px; min-width: 220px; max-height: 300px; overflow-y: auto; pointer-events: auto; user-select: none; animation: fadeInScale 0.15s ease-out; } @keyframes fadeInScale { from { opacity: 0; transform: scale(0.95) translateY(-10px); } to { opacity: 1; transform: scale(1) translateY(0); } } .slash-menu-item { padding: 10px 12px; cursor: pointer; transition: all 0.15s ease; display: flex; align-items: center; gap: 12px; border-radius: 4px; color: ${cssManager.bdTheme('#000000', '#e0e0e0')}; font-size: 14px; } .slash-menu-item:hover, .slash-menu-item.selected { background: ${cssManager.bdTheme('#f0f0f0', '#333333')}; color: ${cssManager.bdTheme('#000000', '#ffffff')}; } .slash-menu-item .icon { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; font-size: 16px; color: ${cssManager.bdTheme('#666', '#999')}; font-weight: 600; } .slash-menu-item:hover .icon, .slash-menu-item.selected .icon { color: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; } `, ]; render(): TemplateResult { if (!this.visible) return html``; // Ensure z-index is applied to host element this.style.zIndex = this.menuZIndex.toString(); const menuItems = this.getFilteredMenuItems(); return html`
${menuItems.map((item, index) => html`
${item.icon} ${item.label}
`)}
`; } private getFilteredMenuItems(): ISlashMenuItem[] { const allItems = WysiwygShortcuts.getSlashMenuItems(); return allItems.filter(item => this.filter === '' || item.label.toLowerCase().includes(this.filter.toLowerCase()) ); } private selectItem(type: string): void { if (this.callback) { this.callback(type); } this.hide(); } public show(position: { x: number; y: number }, callback: (type: string) => void): void { this.position = position; this.callback = callback; this.filter = ''; this.selectedIndex = 0; // Get z-index from registry and apply immediately this.menuZIndex = zIndexRegistry.getNextZIndex(); zIndexRegistry.register(this, this.menuZIndex); this.style.zIndex = this.menuZIndex.toString(); this.visible = true; } public hide(): void { this.visible = false; this.callback = null; this.filter = ''; this.selectedIndex = 0; // Unregister from z-index registry zIndexRegistry.unregister(this); } public updateFilter(filter: string): void { this.filter = filter; this.selectedIndex = 0; } public navigate(direction: 'up' | 'down'): void { const items = this.getFilteredMenuItems(); if (direction === 'down') { this.selectedIndex = (this.selectedIndex + 1) % items.length; } else { this.selectedIndex = this.selectedIndex === 0 ? items.length - 1 : this.selectedIndex - 1; } } public selectCurrent(): void { const items = this.getFilteredMenuItems(); if (items[this.selectedIndex]) { this.selectItem(items[this.selectedIndex].type); } } public firstUpdated(): void { // Set up event delegation this.shadowRoot?.addEventListener('mousedown', (e: MouseEvent) => { const menu = this.shadowRoot?.querySelector('.slash-menu'); if (menu && menu.contains(e.target as Node)) { // Prevent focus loss e.preventDefault(); e.stopPropagation(); } }); this.shadowRoot?.addEventListener('click', (e: MouseEvent) => { const target = e.target as HTMLElement; const menuItem = target.closest('.slash-menu-item') as HTMLElement; if (menuItem) { e.preventDefault(); e.stopPropagation(); const itemType = menuItem.getAttribute('data-item-type'); if (itemType) { this.selectItem(itemType); } } }); this.shadowRoot?.addEventListener('mouseenter', (e: MouseEvent) => { const target = e.target as HTMLElement; const menuItem = target.closest('.slash-menu-item') as HTMLElement; if (menuItem) { const index = parseInt(menuItem.getAttribute('data-item-index') || '0', 10); this.selectedIndex = index; } }, true); // Use capture phase this.shadowRoot?.addEventListener('focus', (e: FocusEvent) => { const menu = this.shadowRoot?.querySelector('.slash-menu'); if (menu && menu.contains(e.target as Node)) { // Prevent menu from taking focus e.preventDefault(); e.stopPropagation(); } }, true); // Use capture phase } }