import { customElement, html, DeesElement, type TemplateResult, cssManager, css, state, } from '@design.estate/dees-element'; import { zIndexRegistry } from '../00zindex.js'; import { WysiwygFormatting } from './wysiwyg.formatting.js'; declare global { interface HTMLElementTagNameMap { 'dees-formatting-menu': DeesFormattingMenu; } } @customElement('dees-formatting-menu') export class DeesFormattingMenu extends DeesElement { private static instance: DeesFormattingMenu; public static getInstance(): DeesFormattingMenu { if (!DeesFormattingMenu.instance) { DeesFormattingMenu.instance = new DeesFormattingMenu(); document.body.appendChild(DeesFormattingMenu.instance); } return DeesFormattingMenu.instance; } @state() public visible: boolean = false; @state() private position: { x: number; y: number } = { x: 0, y: 0 }; @state() private menuZIndex: number = 1000; private callback: ((command: string) => void | Promise) | null = null; public static styles = [ cssManager.defaultStyles, css` :host { position: fixed; pointer-events: none; top: 0; left: 0; width: 0; height: 0; } .formatting-menu { position: fixed; background: ${cssManager.bdTheme('#ffffff', '#262626')}; border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')}; border-radius: 6px; box-shadow: 0 2px 16px rgba(0, 0, 0, 0.15); padding: 4px; display: flex; gap: 2px; pointer-events: auto; user-select: none; animation: fadeInScale 0.15s ease-out; } @keyframes fadeInScale { from { opacity: 0; transform: scale(0.95) translateY(5px); } to { opacity: 1; transform: scale(1) translateY(0); } } .format-button { width: 32px; height: 32px; border: none; background: transparent; cursor: pointer; border-radius: 4px; transition: all 0.15s ease; display: flex; align-items: center; justify-content: center; color: ${cssManager.bdTheme('#000000', '#e0e0e0')}; font-weight: 600; font-size: 14px; position: relative; } .format-button:hover { background: ${cssManager.bdTheme('#f0f0f0', '#333333')}; color: ${cssManager.bdTheme('#0066cc', '#4d94ff')}; } .format-button:active { transform: scale(0.95); } .format-button.bold { font-weight: 700; } .format-button.italic { font-style: italic; } .format-button.underline { text-decoration: underline; } .format-button .code-icon { font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace; font-size: 12px; } `, ]; render(): TemplateResult { if (!this.visible) return html``; // Apply z-index to host element this.style.zIndex = this.menuZIndex.toString(); return html`
${WysiwygFormatting.formatButtons.map(button => html` `)}
`; } private applyFormat(command: string): void { if (this.callback) { this.callback(command); } // Don't hide menu after applying format (except for link) if (command === 'link') { this.hide(); } } public show(position: { x: number; y: number }, callback: (command: string) => void | Promise): void { console.log('FormattingMenu.show called:', { position, visible: this.visible }); this.position = position; this.callback = callback; // 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; // Unregister from z-index registry zIndexRegistry.unregister(this); } public updatePosition(position: { x: number; y: number }): void { this.position = position; } public firstUpdated(): void { // Set up event delegation for the menu this.shadowRoot?.addEventListener('mousedown', (e: MouseEvent) => { const menu = this.shadowRoot?.querySelector('.formatting-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 button = target.closest('.format-button') as HTMLElement; if (button) { e.preventDefault(); e.stopPropagation(); const command = button.getAttribute('data-command'); if (command) { this.applyFormat(command); } } }); this.shadowRoot?.addEventListener('focus', (e: FocusEvent) => { const menu = this.shadowRoot?.querySelector('.formatting-menu'); if (menu && menu.contains(e.target as Node)) { // Prevent menu from taking focus e.preventDefault(); e.stopPropagation(); } }, true); // Use capture phase } }