${language}
{
this.handleMouseUp(e);
this.handlers?.onMouseUp?.(e);
}}"
.textContent="${this.block.content || ''}"
>
`;
}
const placeholder = this.getPlaceholder();
const initialContent = this.getInitialContent();
return staticHtml`
{
this.handleMouseUp(e);
this.handlers?.onMouseUp?.(e);
}}"
>${unsafeStatic(initialContent)}
`;
}
private getPlaceholder(): string {
switch (this.block.type) {
case 'paragraph':
return "Type '/' for commands...";
case 'heading-1':
return 'Heading 1';
case 'heading-2':
return 'Heading 2';
case 'heading-3':
return 'Heading 3';
case 'quote':
return 'Quote';
default:
return '';
}
}
private getInitialContent(): string {
if (this.block.type === 'list') {
return WysiwygBlocks.renderListContent(this.block.content, this.block.metadata);
}
return this.block.content || '';
}
public focus(): void {
if (!this.blockElement) return;
// Ensure the element is focusable
if (!this.blockElement.hasAttribute('contenteditable')) {
this.blockElement.setAttribute('contenteditable', 'true');
}
this.blockElement.focus();
// If focus failed, try again after a microtask
if (document.activeElement !== this.blockElement) {
Promise.resolve().then(() => {
this.blockElement.focus();
});
}
}
public focusWithCursor(position: 'start' | 'end' | number = 'end'): void {
if (!this.blockElement) return;
// Ensure element is focusable first
if (!this.blockElement.hasAttribute('contenteditable')) {
this.blockElement.setAttribute('contenteditable', 'true');
}
// Focus the element
this.blockElement.focus();
// Set cursor position after focus is established
const setCursor = () => {
const selection = window.getSelection();
if (!selection) return;
if (position === 'start') {
this.setCursorToStart();
} else if (position === 'end') {
this.setCursorToEnd();
} else if (typeof position === 'number') {
// Set cursor at specific position
const range = document.createRange();
const textNode = this.getFirstTextNode(this.blockElement);
if (textNode) {
const length = textNode.textContent?.length || 0;
const safePosition = Math.min(position, length);
range.setStart(textNode, safePosition);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
} else if (this.blockElement.childNodes.length === 0) {
// Empty block - create a text node
const emptyText = document.createTextNode('');
this.blockElement.appendChild(emptyText);
range.setStart(emptyText, 0);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
}
};
// Ensure cursor is set after focus
if (document.activeElement === this.blockElement) {
setCursor();
} else {
// Wait for focus to be established
Promise.resolve().then(() => {
if (document.activeElement === this.blockElement) {
setCursor();
}
});
}
}
private getFirstTextNode(node: Node): Text | null {
if (node.nodeType === Node.TEXT_NODE) {
return node as Text;
}
for (let i = 0; i < node.childNodes.length; i++) {
const textNode = this.getFirstTextNode(node.childNodes[i]);
if (textNode) return textNode;
}
return null;
}
public getContent(): string {
if (!this.blockElement) return '';
if (this.block.type === 'list') {
const listItems = this.blockElement.querySelectorAll('li');
return Array.from(listItems).map(li => li.innerHTML || '').join('\n');
} else if (this.block.type === 'code') {
return this.blockElement.textContent || '';
} else {
return this.blockElement.innerHTML || '';
}
}
public setContent(content: string): void {
if (!this.blockElement) return;
// Store if we have focus
const hadFocus = document.activeElement === this.blockElement;
if (this.block.type === 'list') {
this.blockElement.innerHTML = WysiwygBlocks.renderListContent(content, this.block.metadata);
} else if (this.block.type === 'code') {
this.blockElement.textContent = content;
} else {
this.blockElement.innerHTML = content;
}
// Restore focus if we had it
if (hadFocus) {
this.blockElement.focus();
}
}
public setCursorToStart(): void {
WysiwygBlocks.setCursorToStart(this.blockElement);
}
public setCursorToEnd(): void {
WysiwygBlocks.setCursorToEnd(this.blockElement);
}
public focusListItem(): void {
if (this.block.type === 'list') {
WysiwygBlocks.focusListItem(this.blockElement);
}
}
/**
* Gets content split at cursor position
*/
public getSplitContent(): { before: string; after: string } | null {
if (!this.blockElement) return null;
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
return {
before: this.getContent(),
after: ''
};
}
const range = selection.getRangeAt(0);
// Check if selection is within this block
if (!this.blockElement.contains(range.commonAncestorContainer)) {
return null;
}
// Clone the range to extract content before and after cursor
const beforeRange = range.cloneRange();
beforeRange.selectNodeContents(this.blockElement);
beforeRange.setEnd(range.startContainer, range.startOffset);
const afterRange = range.cloneRange();
afterRange.selectNodeContents(this.blockElement);
afterRange.setStart(range.endContainer, range.endOffset);
// Extract content
const beforeFragment = beforeRange.cloneContents();
const afterFragment = afterRange.cloneContents();
// Convert to HTML
const tempDiv = document.createElement('div');
tempDiv.appendChild(beforeFragment);
const beforeHtml = tempDiv.innerHTML;
tempDiv.innerHTML = '';
tempDiv.appendChild(afterFragment);
const afterHtml = tempDiv.innerHTML;
return {
before: beforeHtml,
after: afterHtml
};
}
private handleMouseUp(_e: MouseEvent): void {
// Check if we have a selection within this block
setTimeout(() => {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
// Check if selection is within this block
if (this.blockElement && this.blockElement.contains(range.commonAncestorContainer)) {
const selectedText = selection.toString();
if (selectedText.length > 0) {
// Dispatch a custom event that can cross shadow DOM boundaries
this.dispatchEvent(new CustomEvent('block-text-selected', {
detail: {
text: selectedText,
blockId: this.block.id,
range: range
},
bubbles: true,
composed: true
}));
}
}
}
}, 10);
}
}