import { html, type TemplateResult } from '@design.estate/dees-element'; export interface IFormatButton { command: string; icon: string; label: string; shortcut?: string; action?: () => void; } export class WysiwygFormatting { static readonly formatButtons: IFormatButton[] = [ { command: 'bold', icon: 'B', label: 'Bold', shortcut: '⌘B' }, { command: 'italic', icon: 'I', label: 'Italic', shortcut: '⌘I' }, { command: 'underline', icon: 'U', label: 'Underline', shortcut: '⌘U' }, { command: 'strikeThrough', icon: 'S̶', label: 'Strikethrough' }, { command: 'code', icon: '{ }', label: 'Inline Code' }, { command: 'link', icon: '🔗', label: 'Link', shortcut: '⌘K' }, ]; static renderFormattingMenu( position: { x: number; y: number }, onFormat: (command: string) => void ): TemplateResult { return html`
${this.formatButtons.map(button => html` `)}
`; } static applyFormat(command: string, value?: string): boolean { // Save current selection const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) return false; const range = selection.getRangeAt(0); // Apply format based on command switch (command) { case 'bold': this.wrapSelection(range, 'strong'); break; case 'italic': this.wrapSelection(range, 'em'); break; case 'underline': this.wrapSelection(range, 'u'); break; case 'strikeThrough': this.wrapSelection(range, 's'); break; case 'code': this.wrapSelection(range, 'code'); break; case 'link': // Don't use prompt - return false to indicate we need async input if (!value) { return false; } this.wrapSelectionWithLink(range, value); break; } return true; } private static wrapSelection(range: Range, tagName: string): void { const selection = window.getSelection(); if (!selection) return; // Check if we're already wrapped in this tag const parentElement = range.commonAncestorContainer.parentElement; if (parentElement && parentElement.tagName.toLowerCase() === tagName) { // Unwrap const parent = parentElement.parentNode; while (parentElement.firstChild) { parent?.insertBefore(parentElement.firstChild, parentElement); } parent?.removeChild(parentElement); // Restore selection selection.removeAllRanges(); selection.addRange(range); } else { // Wrap selection const wrapper = document.createElement(tagName); try { // Extract and wrap contents const contents = range.extractContents(); wrapper.appendChild(contents); range.insertNode(wrapper); // Select the wrapped content range.selectNodeContents(wrapper); selection.removeAllRanges(); selection.addRange(range); } catch (e) { console.error('Failed to wrap selection:', e); } } } private static wrapSelectionWithLink(range: Range, url: string): void { const selection = window.getSelection(); if (!selection) return; const link = document.createElement('a'); link.href = url; link.target = '_blank'; link.rel = 'noopener noreferrer'; try { const contents = range.extractContents(); link.appendChild(contents); range.insertNode(link); // Select the link range.selectNodeContents(link); selection.removeAllRanges(); selection.addRange(range); } catch (e) { console.error('Failed to create link:', e); } } static getSelectionCoordinates(shadowRoot?: ShadowRoot): { x: number, y: number } | null { // Try shadow root selection first, then window let selection = shadowRoot && (shadowRoot as any).getSelection ? (shadowRoot as any).getSelection() : null; if (!selection || selection.rangeCount === 0) { selection = window.getSelection(); } console.log('getSelectionCoordinates - selection:', selection); if (!selection || selection.rangeCount === 0) { console.log('No selection or no ranges'); return null; } const range = selection.getRangeAt(0); const rect = range.getBoundingClientRect(); console.log('Range rect:', rect); if (rect.width === 0) { console.log('Rect width is 0'); return null; } const coords = { x: rect.left + (rect.width / 2), y: Math.max(45, rect.top - 45) // Position above selection, but ensure it's not negative }; console.log('Returning coords:', coords); return coords; } static isFormattingApplied(command: string): boolean { const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) return false; const range = selection.getRangeAt(0); const container = range.commonAncestorContainer; const element = container.nodeType === Node.TEXT_NODE ? container.parentElement : container as Element; if (!element) return false; // Check if formatting is applied by looking at parent elements switch (command) { case 'bold': return !!element.closest('b, strong'); case 'italic': return !!element.closest('i, em'); case 'underline': return !!element.closest('u'); case 'strikeThrough': return !!element.closest('s, strike'); case 'code': return !!element.closest('code'); case 'link': return !!element.closest('a'); default: return false; } } static hasSelection(): boolean { const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) { console.log('No selection or no ranges'); return false; } // Check if we actually have selected text (not just collapsed cursor) const selectedText = selection.toString(); if (!selectedText || selectedText.length === 0) { console.log('No text selected'); return false; } return true; } static getSelectedText(): string { const selection = window.getSelection(); return selection ? selection.toString() : ''; } }