import { html, type TemplateResult } from '@design.estate/dees-element'; import { WysiwygSelection } from './wysiwyg.selection.js'; 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(...shadowRoots: ShadowRoot[]): { x: number, y: number } | null { // Get selection info using the new utility that handles Shadow DOM const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots); console.log('getSelectionCoordinates - selectionInfo:', selectionInfo); if (!selectionInfo) { console.log('No selection info available'); return null; } // Create a range from the selection info to get bounding rect const range = WysiwygSelection.createRangeFromInfo(selectionInfo); const rect = range.getBoundingClientRect(); console.log('Range rect:', rect); if (rect.width === 0 && rect.height === 0) { console.log('Rect width and height are 0, trying different approach'); // Sometimes the rect is collapsed, let's try getting the caret position if ('caretPositionFromPoint' in document) { const selection = window.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); const tempSpan = document.createElement('span'); tempSpan.textContent = '\u200B'; // Zero-width space range.insertNode(tempSpan); const spanRect = tempSpan.getBoundingClientRect(); tempSpan.remove(); if (spanRect.width > 0 || spanRect.height > 0) { const coords = { x: spanRect.left, y: Math.max(45, spanRect.top - 45) }; console.log('Used span trick for coords:', coords); return coords; } } } 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; } }