fix(wysiwyg): Implement programmatic rendering to eliminate focus loss during typing

- Convert parent component to use static rendering with programmatic DOM manipulation
- Remove all reactive state that could trigger re-renders during editing
- Delay content sync to avoid interference with typing (2s auto-save)
- Update all block operations to use manual DOM updates instead of Lit re-renders
- Fix specific issue where typing + arrow keys caused focus loss
- Add comprehensive focus management documentation
This commit is contained in:
Juergen Kunz
2025-06-24 11:06:02 +00:00
parent 8b02c5aea3
commit 1c76ade150
6 changed files with 401 additions and 95 deletions

View File

@ -2,6 +2,7 @@ import { type IBlock } from './wysiwyg.types.js';
import { WysiwygShortcuts } from './wysiwyg.shortcuts.js';
import { WysiwygBlocks } from './wysiwyg.blocks.js';
import { WysiwygBlockOperations } from './wysiwyg.blockoperations.js';
import { WysiwygModalManager } from './wysiwyg.modalmanager.js';
export class WysiwygInputHandler {
private component: any;
@ -20,10 +21,7 @@ export class WysiwygInputHandler {
const target = e.target as HTMLDivElement;
const textContent = target.textContent || '';
// Update block content based on type
this.updateBlockContent(block, target);
// Check for block type transformations
// Check for block type transformations BEFORE updating content
const detectedType = this.detectBlockTypeIntent(textContent);
if (detectedType && detectedType.type !== block.type) {
e.preventDefault();
@ -34,7 +32,10 @@ export class WysiwygInputHandler {
// Handle slash commands
this.handleSlashCommand(textContent, target);
// Schedule auto-save
// Don't update block content immediately - let the block handle its own content
// This prevents re-renders during typing
// Schedule auto-save (which will sync content later)
this.scheduleAutoSave();
}
@ -48,7 +49,11 @@ export class WysiwygInputHandler {
if (blockComponent) {
// Use the block component's getContent method for consistency
block.content = blockComponent.getContent();
const newContent = blockComponent.getContent();
// Only update if content actually changed to avoid unnecessary updates
if (block.content !== newContent) {
block.content = newContent;
}
// Update list metadata if needed
if (block.type === 'list') {
@ -133,7 +138,11 @@ export class WysiwygInputHandler {
target.innerHTML = `<${listTag}><li></li></${listTag}>`;
this.component.updateValue();
this.component.requestUpdate();
// Update the block element programmatically
if (this.component.editorContentRef) {
this.component.updateBlockElement(block.id);
}
setTimeout(() => {
WysiwygBlocks.focusListItem(target);
@ -142,13 +151,17 @@ export class WysiwygInputHandler {
block.type = 'divider';
block.content = ' ';
// Update the block element programmatically
if (this.component.editorContentRef) {
this.component.updateBlockElement(block.id);
}
const newBlock = blockOps.createBlock();
blockOps.insertBlockAfter(block, newBlock);
this.component.updateValue();
this.component.requestUpdate();
} else if (detectedType.type === 'code') {
const language = await this.component.showLanguageSelectionModal();
const language = await WysiwygModalManager.showLanguageSelectionModal();
if (language) {
block.type = 'code';
block.content = '';
@ -156,7 +169,11 @@ export class WysiwygInputHandler {
target.textContent = '';
this.component.updateValue();
this.component.requestUpdate();
// Update the block element programmatically
if (this.component.editorContentRef) {
this.component.updateBlockElement(block.id);
}
}
} else {
block.type = detectedType.type;
@ -164,7 +181,11 @@ export class WysiwygInputHandler {
target.textContent = '';
this.component.updateValue();
this.component.requestUpdate();
// Update the block element programmatically
if (this.component.editorContentRef) {
this.component.updateBlockElement(block.id);
}
}
}
@ -233,8 +254,30 @@ export class WysiwygInputHandler {
return;
}
this.saveTimeout = setTimeout(() => {
// Sync all block content from DOM before saving
this.syncAllBlockContent();
// Only update value, don't trigger any re-renders
this.component.updateValue();
}, 1000);
// Don't call requestUpdate() as it's not needed
}, 2000); // Increased delay to reduce interference with typing
}
/**
* Syncs content from all block DOMs to the data model
*/
private syncAllBlockContent(): void {
this.component.blocks.forEach((block: IBlock) => {
const wrapperElement = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`);
const blockComponent = wrapperElement?.querySelector('dees-wysiwyg-block') as any;
if (blockComponent && blockComponent.getContent) {
const newContent = blockComponent.getContent();
// Only update if content actually changed
if (block.content !== newContent) {
block.content = newContent;
}
}
});
}
/**