2025-06-23 17:36:39 +00:00
|
|
|
import { html, type TemplateResult } from '@design.estate/dees-element';
|
|
|
|
import { type IBlock } from './wysiwyg.types.js';
|
|
|
|
import { WysiwygConverters } from './wysiwyg.converters.js';
|
|
|
|
|
|
|
|
export class WysiwygBlocks {
|
|
|
|
static renderListContent(content: string, metadata?: any): string {
|
|
|
|
const items = content.split('\n').filter(item => item.trim());
|
|
|
|
if (items.length === 0) return '';
|
|
|
|
const listTag = metadata?.listType === 'ordered' ? 'ol' : 'ul';
|
2025-06-24 10:45:06 +00:00
|
|
|
// Don't escape HTML to preserve formatting
|
|
|
|
return `<${listTag}>${items.map(item => `<li>${item}</li>`).join('')}</${listTag}>`;
|
2025-06-23 17:36:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static renderBlock(
|
|
|
|
block: IBlock,
|
|
|
|
isSelected: boolean,
|
|
|
|
handlers: {
|
|
|
|
onInput: (e: InputEvent) => void;
|
|
|
|
onKeyDown: (e: KeyboardEvent) => void;
|
|
|
|
onFocus: () => void;
|
|
|
|
onBlur: () => void;
|
|
|
|
onCompositionStart: () => void;
|
|
|
|
onCompositionEnd: () => void;
|
2025-06-23 21:15:04 +00:00
|
|
|
onMouseUp?: (e: MouseEvent) => void;
|
2025-06-23 17:36:39 +00:00
|
|
|
}
|
|
|
|
): TemplateResult {
|
|
|
|
if (block.type === 'divider') {
|
|
|
|
return html`
|
|
|
|
<div
|
|
|
|
class="block divider"
|
|
|
|
data-block-id="${block.id}"
|
|
|
|
>
|
|
|
|
<hr>
|
|
|
|
</div>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (block.type === 'list') {
|
|
|
|
return html`
|
|
|
|
<div
|
|
|
|
class="block list ${isSelected ? 'selected' : ''}"
|
|
|
|
data-block-id="${block.id}"
|
|
|
|
contenteditable="true"
|
|
|
|
@input="${handlers.onInput}"
|
|
|
|
@keydown="${handlers.onKeyDown}"
|
|
|
|
@focus="${handlers.onFocus}"
|
|
|
|
@blur="${handlers.onBlur}"
|
|
|
|
@compositionstart="${handlers.onCompositionStart}"
|
|
|
|
@compositionend="${handlers.onCompositionEnd}"
|
2025-06-23 21:15:04 +00:00
|
|
|
@mouseup="${(e: MouseEvent) => {
|
|
|
|
console.log('Block mouseup event fired');
|
|
|
|
if (handlers.onMouseUp) handlers.onMouseUp(e);
|
|
|
|
}}"
|
2025-06-23 17:36:39 +00:00
|
|
|
.innerHTML="${this.renderListContent(block.content, block.metadata)}"
|
|
|
|
></div>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
2025-06-23 21:28:58 +00:00
|
|
|
// Special rendering for code blocks with language indicator
|
|
|
|
if (block.type === 'code') {
|
|
|
|
const language = block.metadata?.language || 'plain text';
|
|
|
|
return html`
|
|
|
|
<div class="code-block-container">
|
|
|
|
<div class="code-language">${language}</div>
|
|
|
|
<div
|
|
|
|
class="block ${block.type} ${isSelected ? 'selected' : ''}"
|
|
|
|
contenteditable="true"
|
|
|
|
@input="${handlers.onInput}"
|
|
|
|
@keydown="${handlers.onKeyDown}"
|
|
|
|
@focus="${handlers.onFocus}"
|
|
|
|
@blur="${handlers.onBlur}"
|
|
|
|
@compositionstart="${handlers.onCompositionStart}"
|
|
|
|
@compositionend="${handlers.onCompositionEnd}"
|
|
|
|
@mouseup="${(e: MouseEvent) => {
|
|
|
|
console.log('Block mouseup event fired');
|
|
|
|
if (handlers.onMouseUp) handlers.onMouseUp(e);
|
|
|
|
}}"
|
2025-06-24 07:21:09 +00:00
|
|
|
.textContent="${block.content || ''}"
|
2025-06-23 21:28:58 +00:00
|
|
|
></div>
|
|
|
|
</div>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
2025-06-24 07:21:09 +00:00
|
|
|
const blockElement = html`
|
2025-06-23 17:36:39 +00:00
|
|
|
<div
|
|
|
|
class="block ${block.type} ${isSelected ? 'selected' : ''}"
|
|
|
|
contenteditable="true"
|
|
|
|
@input="${handlers.onInput}"
|
|
|
|
@keydown="${handlers.onKeyDown}"
|
|
|
|
@focus="${handlers.onFocus}"
|
|
|
|
@blur="${handlers.onBlur}"
|
|
|
|
@compositionstart="${handlers.onCompositionStart}"
|
|
|
|
@compositionend="${handlers.onCompositionEnd}"
|
2025-06-23 21:15:04 +00:00
|
|
|
@mouseup="${(e: MouseEvent) => {
|
|
|
|
console.log('Block mouseup event fired');
|
|
|
|
if (handlers.onMouseUp) handlers.onMouseUp(e);
|
|
|
|
}}"
|
2025-06-24 07:21:09 +00:00
|
|
|
.innerHTML="${block.content || ''}"
|
2025-06-23 17:36:39 +00:00
|
|
|
></div>
|
|
|
|
`;
|
2025-06-24 07:21:09 +00:00
|
|
|
|
|
|
|
return blockElement;
|
2025-06-23 17:36:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static setCursorToEnd(element: HTMLElement): void {
|
|
|
|
const sel = window.getSelection();
|
2025-06-24 10:45:06 +00:00
|
|
|
if (!sel) return;
|
|
|
|
|
|
|
|
const range = document.createRange();
|
|
|
|
|
|
|
|
// Handle different content types
|
|
|
|
if (element.childNodes.length === 0) {
|
|
|
|
// Empty element - add a zero-width space to enable cursor
|
|
|
|
const textNode = document.createTextNode('\u200B');
|
|
|
|
element.appendChild(textNode);
|
|
|
|
range.setStart(textNode, 1);
|
|
|
|
range.collapse(true);
|
|
|
|
} else {
|
|
|
|
// Find the last text node or element
|
|
|
|
const lastNode = this.getLastNode(element);
|
|
|
|
if (lastNode.nodeType === Node.TEXT_NODE) {
|
|
|
|
range.setStart(lastNode, lastNode.textContent?.length || 0);
|
|
|
|
} else {
|
|
|
|
range.setStartAfter(lastNode);
|
|
|
|
}
|
|
|
|
range.collapse(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
sel.removeAllRanges();
|
|
|
|
sel.addRange(range);
|
|
|
|
|
|
|
|
// Remove zero-width space if it was added
|
|
|
|
if (element.textContent === '\u200B') {
|
|
|
|
element.textContent = '';
|
|
|
|
}
|
2025-06-23 17:36:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static setCursorToStart(element: HTMLElement): void {
|
|
|
|
const sel = window.getSelection();
|
2025-06-24 10:45:06 +00:00
|
|
|
if (!sel) return;
|
|
|
|
|
|
|
|
const range = document.createRange();
|
|
|
|
|
|
|
|
// Handle different content types
|
|
|
|
if (element.childNodes.length === 0) {
|
|
|
|
// Empty element
|
|
|
|
range.setStart(element, 0);
|
|
|
|
range.collapse(true);
|
|
|
|
} else {
|
|
|
|
// Find the first text node or element
|
|
|
|
const firstNode = this.getFirstNode(element);
|
|
|
|
if (firstNode.nodeType === Node.TEXT_NODE) {
|
|
|
|
range.setStart(firstNode, 0);
|
|
|
|
} else {
|
|
|
|
range.setStartBefore(firstNode);
|
|
|
|
}
|
|
|
|
range.collapse(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
sel.removeAllRanges();
|
|
|
|
sel.addRange(range);
|
|
|
|
}
|
|
|
|
|
|
|
|
private static getLastNode(element: Node): Node {
|
|
|
|
if (element.childNodes.length === 0) {
|
|
|
|
return element;
|
|
|
|
}
|
|
|
|
|
|
|
|
const lastChild = element.childNodes[element.childNodes.length - 1];
|
|
|
|
if (lastChild.nodeType === Node.TEXT_NODE || lastChild.childNodes.length === 0) {
|
|
|
|
return lastChild;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.getLastNode(lastChild);
|
|
|
|
}
|
|
|
|
|
|
|
|
private static getFirstNode(element: Node): Node {
|
|
|
|
if (element.childNodes.length === 0) {
|
|
|
|
return element;
|
|
|
|
}
|
|
|
|
|
|
|
|
const firstChild = element.childNodes[0];
|
|
|
|
if (firstChild.nodeType === Node.TEXT_NODE || firstChild.childNodes.length === 0) {
|
|
|
|
return firstChild;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.getFirstNode(firstChild);
|
2025-06-23 17:36:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static focusListItem(listElement: HTMLElement): void {
|
|
|
|
const firstLi = listElement.querySelector('li');
|
|
|
|
if (firstLi) {
|
|
|
|
firstLi.focus();
|
|
|
|
const range = document.createRange();
|
|
|
|
const sel = window.getSelection();
|
|
|
|
range.selectNodeContents(firstLi);
|
|
|
|
range.collapse(true);
|
|
|
|
sel!.removeAllRanges();
|
|
|
|
sel!.addRange(range);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|