Improve Wysiwyg editor

This commit is contained in:
Juergen Kunz
2025-06-24 07:21:09 +00:00
parent 7ce282c500
commit e4a042907a
2 changed files with 68 additions and 165 deletions

View File

@ -108,51 +108,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
// Add global selection listener // Add global selection listener
console.log('Adding selectionchange listener'); console.log('Adding selectionchange listener');
document.addEventListener('selectionchange', this.selectionChangeHandler); document.addEventListener('selectionchange', this.selectionChangeHandler);
// Set initial content for blocks after a brief delay to ensure DOM is ready
await this.updateComplete;
setTimeout(() => {
this.setBlockContents();
}, 50);
}
updated(changedProperties: Map<string, any>) {
// When blocks change (e.g., from setValue), update DOM content
if (changedProperties.has('blocks')) {
// Wait for render to complete
setTimeout(() => {
this.setBlockContents();
}, 50);
}
}
private setBlockContents() {
// Set content for blocks that aren't being edited and don't already have content
this.blocks.forEach(block => {
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${block.id}"]`);
if (wrapperElement) {
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
if (blockElement && document.activeElement !== blockElement && block.type !== 'divider') {
// Only set content if the element is empty or has placeholder text
const currentContent = blockElement.textContent || '';
const isEmpty = !currentContent || currentContent === 'Type \'/\' for commands...' ||
currentContent === 'Heading 1' || currentContent === 'Heading 2' ||
currentContent === 'Heading 3' || currentContent === 'Quote' ||
currentContent === 'Code block';
if (isEmpty && block.content) {
if (block.type === 'list') {
blockElement.innerHTML = WysiwygBlocks.renderListContent(block.content, block.metadata);
} else if (block.content.includes('<') && block.content.includes('>')) {
// Content contains HTML formatting
blockElement.innerHTML = block.content;
} else {
blockElement.textContent = block.content;
}
}
}
}
});
} }
render(): TemplateResult { render(): TemplateResult {
@ -276,12 +231,17 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
if (listElement) { if (listElement) {
block.metadata = { listType: listElement.tagName.toLowerCase() === 'ol' ? 'ordered' : 'bullet' }; block.metadata = { listType: listElement.tagName.toLowerCase() === 'ol' ? 'ordered' : 'bullet' };
} }
} else { } else if (block.type === 'code') {
// For code blocks, preserve the exact text content
block.content = target.textContent || ''; block.content = target.textContent || '';
} else {
// For other blocks, preserve HTML formatting
block.content = target.innerHTML || '';
} }
// Check for block type change intents // Check for block type change intents (use text content for detection, not HTML)
const detectedType = this.detectBlockTypeIntent(block.content); const textContent = target.textContent || '';
const detectedType = this.detectBlockTypeIntent(textContent);
// Only process if the detected type is different from current type // Only process if the detected type is different from current type
if (detectedType && detectedType.type !== block.type) { if (detectedType && detectedType.type !== block.type) {
@ -311,23 +271,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
block.content = ' '; block.content = ' ';
// Create a new paragraph block after the divider // Create a new paragraph block after the divider
const blockIndex = this.blocks.findIndex(b => b.id === block.id); const newBlock = this.createNewBlock();
const newBlock: IBlock = { this.insertBlockAfter(block, newBlock);
id: WysiwygShortcuts.generateBlockId(),
type: 'paragraph',
content: '',
};
this.blocks = [...this.blocks.slice(0, blockIndex + 1), newBlock, ...this.blocks.slice(blockIndex + 1)];
setTimeout(() => {
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${newBlock.id}"]`);
if (wrapperElement) {
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
if (blockElement) {
blockElement.focus();
}
}
});
this.updateValue(); this.updateValue();
this.requestUpdate(); this.requestUpdate();
@ -366,9 +311,9 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
} }
// Check for slash commands at the beginning of any block // Check for slash commands at the beginning of any block
if (block.content === '/' || (block.content.startsWith('/') && this.showSlashMenu)) { if (textContent === '/' || (textContent.startsWith('/') && this.showSlashMenu)) {
// Only show menu on initial '/', or update filter if already showing // Only show menu on initial '/', or update filter if already showing
if (!this.showSlashMenu && block.content === '/') { if (!this.showSlashMenu && textContent === '/') {
this.showSlashMenu = true; this.showSlashMenu = true;
this.slashMenuSelectedIndex = 0; this.slashMenuSelectedIndex = 0;
const rect = target.getBoundingClientRect(); const rect = target.getBoundingClientRect();
@ -378,8 +323,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
y: rect.bottom - containerRect.top + 4 y: rect.bottom - containerRect.top + 4
}; };
} }
this.slashMenuFilter = block.content.slice(1); this.slashMenuFilter = textContent.slice(1);
} else if (!block.content.startsWith('/')) { } else if (!textContent.startsWith('/')) {
this.closeSlashMenu(); this.closeSlashMenu();
} }
@ -441,28 +386,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
if (e.shiftKey) { if (e.shiftKey) {
// Shift+Enter in code blocks creates a new block // Shift+Enter in code blocks creates a new block
e.preventDefault(); e.preventDefault();
const newBlock = this.createNewBlock();
const blockIndex = this.blocks.findIndex(b => b.id === block.id); this.insertBlockAfter(block, newBlock);
const newBlock: IBlock = {
id: WysiwygShortcuts.generateBlockId(),
type: 'paragraph',
content: '',
};
this.blocks = [...this.blocks.slice(0, blockIndex + 1), newBlock, ...this.blocks.slice(blockIndex + 1)];
this.updateValue();
setTimeout(() => {
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${newBlock.id}"]`);
if (wrapperElement) {
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
if (blockElement) {
blockElement.focus();
WysiwygBlocks.setCursorToStart(blockElement);
}
}
});
} }
// For normal Enter in code blocks, let the browser handle it (creates new line) // For normal Enter in code blocks, let the browser handle it (creates new line)
return; return;
@ -481,25 +406,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
if (currentLi && currentLi.textContent === '') { if (currentLi && currentLi.textContent === '') {
// Empty list item - exit list mode // Empty list item - exit list mode
e.preventDefault(); e.preventDefault();
const blockIndex = this.blocks.findIndex(b => b.id === block.id); const newBlock = this.createNewBlock();
const newBlock: IBlock = { this.insertBlockAfter(block, newBlock);
id: WysiwygShortcuts.generateBlockId(),
type: 'paragraph',
content: '',
};
this.blocks = [...this.blocks.slice(0, blockIndex + 1), newBlock, ...this.blocks.slice(blockIndex + 1)];
setTimeout(() => {
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${newBlock.id}"]`);
if (wrapperElement) {
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
if (blockElement) {
blockElement.focus();
WysiwygBlocks.setCursorToStart(blockElement);
}
}
});
} }
// Otherwise, let the browser handle creating new list items // Otherwise, let the browser handle creating new list items
} }
@ -507,28 +415,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
} }
e.preventDefault(); e.preventDefault();
const newBlock = this.createNewBlock();
const blockIndex = this.blocks.findIndex(b => b.id === block.id); this.insertBlockAfter(block, newBlock);
const newBlock: IBlock = {
id: WysiwygShortcuts.generateBlockId(),
type: 'paragraph',
content: '',
};
this.blocks = [...this.blocks.slice(0, blockIndex + 1), newBlock, ...this.blocks.slice(blockIndex + 1)];
this.updateValue();
setTimeout(() => {
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${newBlock.id}"]`);
if (wrapperElement) {
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
if (blockElement) {
blockElement.focus();
WysiwygBlocks.setCursorToStart(blockElement);
}
}
});
} }
} else if (e.key === 'Backspace' && block.content === '' && this.blocks.length > 1) { } else if (e.key === 'Backspace' && block.content === '' && this.blocks.length > 1) {
e.preventDefault(); e.preventDefault();
@ -584,14 +472,17 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
if (this.showSlashMenu && this.selectedBlockId) { if (this.showSlashMenu && this.selectedBlockId) {
// Clear the slash command from the content if menu is closing without selection // Clear the slash command from the content if menu is closing without selection
const currentBlock = this.blocks.find(b => b.id === this.selectedBlockId); const currentBlock = this.blocks.find(b => b.id === this.selectedBlockId);
if (currentBlock && currentBlock.content.startsWith('/')) { if (currentBlock) {
const blockElement = this.shadowRoot!.querySelector(`[data-block-id="${currentBlock.id}"]`) as HTMLDivElement; const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${currentBlock.id}"]`);
if (blockElement) { if (wrapperElement) {
// Clear the slash command text const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
blockElement.textContent = ''; if (blockElement && (blockElement.textContent || '').startsWith('/')) {
currentBlock.content = ''; // Clear the slash command text
// Ensure cursor stays in the block blockElement.textContent = '';
blockElement.focus(); currentBlock.content = '';
// Ensure cursor stays in the block
blockElement.focus();
}
} }
} }
} }
@ -672,11 +563,40 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
} }
} }
private createNewBlock(type: IBlock['type'] = 'paragraph', content: string = '', metadata?: any): IBlock {
return {
id: WysiwygShortcuts.generateBlockId(),
type,
content,
...(metadata && { metadata })
};
}
private insertBlockAfter(afterBlock: IBlock, newBlock: IBlock, focusNewBlock: boolean = true): void {
const blockIndex = this.blocks.findIndex(b => b.id === afterBlock.id);
this.blocks = [...this.blocks.slice(0, blockIndex + 1), newBlock, ...this.blocks.slice(blockIndex + 1)];
this.updateValue();
if (focusNewBlock) {
setTimeout(() => {
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${newBlock.id}"]`);
if (wrapperElement && newBlock.type !== 'divider') {
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
if (blockElement) {
blockElement.focus();
WysiwygBlocks.setCursorToStart(blockElement);
}
}
}, 50);
}
}
private async insertBlock(type: IBlock['type']) { private async insertBlock(type: IBlock['type']) {
const currentBlockIndex = this.blocks.findIndex(b => b.id === this.selectedBlockId); const currentBlockIndex = this.blocks.findIndex(b => b.id === this.selectedBlockId);
const currentBlock = this.blocks[currentBlockIndex]; const currentBlock = this.blocks[currentBlockIndex];
if (currentBlock && currentBlock.content.startsWith('/')) { if (currentBlock) {
// If it's a code block, ask for language // If it's a code block, ask for language
if (type === 'code') { if (type === 'code') {
const language = await this.showLanguageSelectionModal(); const language = await this.showLanguageSelectionModal();
@ -693,22 +613,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
if (type === 'divider') { if (type === 'divider') {
currentBlock.content = ' '; currentBlock.content = ' ';
const newBlock: IBlock = { const newBlock = this.createNewBlock();
id: WysiwygShortcuts.generateBlockId(), this.insertBlockAfter(currentBlock, newBlock);
type: 'paragraph',
content: '',
};
this.blocks = [...this.blocks.slice(0, currentBlockIndex + 1), newBlock, ...this.blocks.slice(currentBlockIndex + 1)];
setTimeout(() => {
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${newBlock.id}"]`);
if (wrapperElement) {
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
if (blockElement) {
blockElement.focus();
}
}
});
} else if (type === 'list') { } else if (type === 'list') {
// Handle list type specially // Handle list type specially
currentBlock.metadata = { listType: 'bullet' }; // Default to bullet list currentBlock.metadata = { listType: 'bullet' }; // Default to bullet list
@ -982,13 +888,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
} }
} }
private getRootNodeOfNode(node: Node): Node {
let current: Node = node;
while (current.parentNode) {
current = current.parentNode;
}
return current;
}
private updateFormattingMenuPosition(): void { private updateFormattingMenuPosition(): void {
console.log('updateFormattingMenuPosition called'); console.log('updateFormattingMenuPosition called');

View File

@ -74,12 +74,13 @@ export class WysiwygBlocks {
console.log('Block mouseup event fired'); console.log('Block mouseup event fired');
if (handlers.onMouseUp) handlers.onMouseUp(e); if (handlers.onMouseUp) handlers.onMouseUp(e);
}}" }}"
.textContent="${block.content || ''}"
></div> ></div>
</div> </div>
`; `;
} }
return html` const blockElement = html`
<div <div
class="block ${block.type} ${isSelected ? 'selected' : ''}" class="block ${block.type} ${isSelected ? 'selected' : ''}"
contenteditable="true" contenteditable="true"
@ -93,8 +94,11 @@ export class WysiwygBlocks {
console.log('Block mouseup event fired'); console.log('Block mouseup event fired');
if (handlers.onMouseUp) handlers.onMouseUp(e); if (handlers.onMouseUp) handlers.onMouseUp(e);
}}" }}"
.innerHTML="${block.content || ''}"
></div> ></div>
`; `;
return blockElement;
} }
static setCursorToEnd(element: HTMLElement): void { static setCursorToEnd(element: HTMLElement): void {