feat(editor): Add wysiwyg editor
This commit is contained in:
149
ts_web/elements/wysiwyg/wysiwyg.formatting.ts
Normal file
149
ts_web/elements/wysiwyg/wysiwyg.formatting.ts
Normal file
@ -0,0 +1,149 @@
|
||||
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>
|
||||
`;
|
||||
}
|
||||
|
||||
static applyFormat(command: string, value?: string): void {
|
||||
// Save current selection
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) return;
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
|
||||
// Apply format based on command
|
||||
switch (command) {
|
||||
case 'bold':
|
||||
case 'italic':
|
||||
case 'underline':
|
||||
case 'strikeThrough':
|
||||
document.execCommand(command, false);
|
||||
break;
|
||||
|
||||
case 'code':
|
||||
// For inline code, wrap selection in <code> tags
|
||||
const codeElement = document.createElement('code');
|
||||
try {
|
||||
codeElement.appendChild(range.extractContents());
|
||||
range.insertNode(codeElement);
|
||||
|
||||
// Select the newly created code element
|
||||
range.selectNodeContents(codeElement);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
} catch (e) {
|
||||
// Fallback to execCommand if range manipulation fails
|
||||
document.execCommand('fontName', false, 'monospace');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'link':
|
||||
const url = value || prompt('Enter URL:');
|
||||
if (url) {
|
||||
document.execCommand('createLink', false, url);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
try {
|
||||
return document.queryCommandState(command);
|
||||
} catch {
|
||||
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() : '';
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user