Files
dees-catalog/ts_web/elements/wysiwyg/wysiwyg.formatting.ts

230 lines
6.8 KiB
TypeScript
Raw Normal View History

2025-06-23 21:15:04 +00:00
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`
<div
class="formatting-menu"
style="top: ${position.y}px; left: ${position.x}px;"
@mousedown="${(e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); }}"
@click="${(e: MouseEvent) => e.stopPropagation()}"
>
${this.formatButtons.map(button => html`
<button
class="format-button ${button.command}"
@click="${() => onFormat(button.command)}"
title="${button.label}${button.shortcut ? ` (${button.shortcut})` : ''}"
>
<span class="${button.command === 'code' ? 'code-icon' : ''}">${button.icon}</span>
</button>
`)}
</div>
`;
}
2025-06-24 10:45:06 +00:00
static applyFormat(command: string, value?: string): boolean {
2025-06-23 21:15:04 +00:00
// Save current selection
const selection = window.getSelection();
2025-06-24 10:45:06 +00:00
if (!selection || selection.rangeCount === 0) return false;
2025-06-23 21:15:04 +00:00
const range = selection.getRangeAt(0);
// Apply format based on command
switch (command) {
case 'bold':
2025-06-24 10:45:06 +00:00
this.wrapSelection(range, 'strong');
break;
2025-06-23 21:15:04 +00:00
case 'italic':
2025-06-24 10:45:06 +00:00
this.wrapSelection(range, 'em');
break;
2025-06-23 21:15:04 +00:00
case 'underline':
2025-06-24 10:45:06 +00:00
this.wrapSelection(range, 'u');
break;
2025-06-23 21:15:04 +00:00
case 'strikeThrough':
2025-06-24 10:45:06 +00:00
this.wrapSelection(range, 's');
2025-06-23 21:15:04 +00:00
break;
case 'code':
2025-06-24 10:45:06 +00:00
this.wrapSelection(range, 'code');
2025-06-23 21:15:04 +00:00
break;
case 'link':
2025-06-24 10:45:06 +00:00
// Don't use prompt - return false to indicate we need async input
if (!value) {
return false;
2025-06-23 21:15:04 +00:00
}
2025-06-24 10:45:06 +00:00
this.wrapSelectionWithLink(range, value);
2025-06-23 21:15:04 +00:00
break;
}
2025-06-24 10:45:06 +00:00
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);
}
2025-06-23 21:15:04 +00:00
}
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 {
2025-06-24 10:45:06 +00:00
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;
2025-06-23 21:15:04 +00:00
}
}
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() : '';
}
}