# Critical WYSIWYG Knowledge - DO NOT LOSE This document captures all the hard-won knowledge from our WYSIWYG editor development. These patterns and solutions took significant effort to discover and MUST be preserved during refactoring. ## 1. Static Rendering to Prevent Focus Loss ### Problem When using Lit's reactive rendering, every state change would cause a re-render, which would: - Lose cursor position - Lose focus state - Interrupt typing - Break IME (Input Method Editor) support ### Solution We render blocks **statically** and manage updates imperatively: ```typescript // In dees-wysiwyg-block.ts render(): TemplateResult { if (!this.block) return html``; // Render empty container - content set in firstUpdated return html`
`; } firstUpdated(): void { const container = this.shadowRoot?.querySelector('.wysiwyg-block-container'); if (container && this.block) { container.innerHTML = this.renderBlockContent(); } } ``` ### Critical Pattern - NEVER use reactive properties that would trigger re-renders during typing - Use `shouldUpdate()` to prevent unnecessary renders - Manage content updates imperatively through event handlers ## 2. Shadow DOM Selection Handling ### Problem The Web Selection API doesn't work across Shadow DOM boundaries by default. This broke: - Text selection - Cursor position tracking - Formatting detection - Content splitting for Enter key ### Solution Use the `getComposedRanges` API with all relevant shadow roots: ```typescript // From paragraph.block.ts const wysiwygBlock = element.closest('dees-wysiwyg-block'); const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg'); const parentShadowRoot = parentComponent?.shadowRoot; const blockShadowRoot = (wysiwygBlock as any)?.shadowRoot; const shadowRoots: ShadowRoot[] = []; if (parentShadowRoot) shadowRoots.push(parentShadowRoot); if (blockShadowRoot) shadowRoots.push(blockShadowRoot); const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots); ``` ### Critical Pattern - ALWAYS collect all shadow roots in the hierarchy - Use `WysiwygSelection` utility methods that handle shadow DOM - Never use raw `window.getSelection()` without shadow root context ## 3. Cursor Position Tracking ### Problem Cursor position would be lost during various operations, making it impossible to: - Split content at the right position for Enter key - Restore cursor after operations - Track position for formatting ### Solution Track cursor position through multiple events and maintain `lastKnownCursorPosition`: ```typescript // Track on every relevant event private lastKnownCursorPosition: number = 0; // In event handlers: const pos = this.getCursorPosition(element); if (pos !== null) { this.lastKnownCursorPosition = pos; } // Fallback when selection not available: if (!selectionInfo && this.lastKnownCursorPosition !== null) { // Use last known position } ``` ### Critical Events to Track - `input` - After text changes - `keydown` - Before key press - `keyup` - After key press - `mouseup` - After mouse selection - `click` - With setTimeout(0) for browser to set cursor ## 4. Content Splitting for Enter Key ### Problem Splitting content at cursor position while preserving HTML formatting was complex due to: - Need to preserve formatting tags - Shadow DOM complications - Cursor position accuracy ### Solution Use Range API to split content while preserving HTML: ```typescript getSplitContent(): { before: string; after: string } | null { // Create ranges for before and after cursor const beforeRange = document.createRange(); const afterRange = document.createRange(); beforeRange.setStart(element, 0); beforeRange.setEnd(selectionInfo.startContainer, selectionInfo.startOffset); afterRange.setStart(selectionInfo.startContainer, selectionInfo.startOffset); afterRange.setEnd(element, element.childNodes.length); // Extract HTML content const beforeFragment = beforeRange.cloneContents(); const afterFragment = afterRange.cloneContents(); // Convert to HTML strings 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 }; } ``` ## 5. Focus Management ### Problem Focus would be lost or not properly set due to: - Timing issues with DOM updates - Shadow DOM complications - Browser inconsistencies ### Solution Use defensive focus management with fallbacks: ```typescript focus(element: HTMLElement): void { const block = element.querySelector('.block'); if (!block) return; // Ensure focusable if (!block.hasAttribute('contenteditable')) { block.setAttribute('contenteditable', 'true'); } block.focus(); // Fallback with microtask if focus failed if (document.activeElement !== block && element.shadowRoot?.activeElement !== block) { Promise.resolve().then(() => { block.focus(); }); } } ``` ## 6. Selection Event Handling for Formatting ### Problem Need to show formatting menu when text is selected, but selection events don't bubble across Shadow DOM. ### Solution Dispatch custom events with selection information: ```typescript // Listen for selection changes document.addEventListener('selectionchange', () => { const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots); if (selectedText !== this.lastSelectedText) { const range = WysiwygSelection.createRangeFromInfo(selectionInfo); const rect = range.getBoundingClientRect(); // Dispatch custom event this.dispatchSelectionEvent(element, { text: selectedText, blockId: block.id, range: range, rect: rect, hasSelection: true }); } }); // Custom event bubbles through Shadow DOM const event = new CustomEvent('block-text-selected', { detail, bubbles: true, composed: true }); ``` ## 7. IME (Input Method Editor) Support ### Problem Composition events for non-Latin input methods would break without proper handling. ### Solution Track composition state and handle events: ```typescript // In dees-input-wysiwyg.ts public isComposing: boolean = false; // In block handlers element.addEventListener('compositionstart', () => { handlers.onCompositionStart(); // Sets isComposing = true }); element.addEventListener('compositionend', () => { handlers.onCompositionEnd(); // Sets isComposing = false }); // Don't process certain operations during composition if (this.isComposing) return; ``` ## 8. Programmatic Rendering ### Problem Lit's declarative rendering would cause focus loss and performance issues with many blocks. ### Solution Render blocks programmatically: ```typescript public renderBlocksProgrammatically() { if (!this.editorContentRef) return; // Clear existing blocks this.editorContentRef.innerHTML = ''; // Create and append block elements this.blocks.forEach(block => { const blockWrapper = this.createBlockElement(block); this.editorContentRef.appendChild(blockWrapper); }); } ``` ## 9. Block Handler Architecture Requirements When creating new block handlers, they MUST: 1. **Implement all cursor/selection methods** even if not applicable 2. **Use Shadow DOM-aware selection utilities** 3. **Track cursor position through events** 4. **Handle focus with fallbacks** 5. **Preserve HTML content when getting/setting** 6. **Dispatch selection events for formatting** 7. **Support IME composition events** 8. **Clean up event listeners on disconnect** ## 10. Testing Considerations ### webhelpers.fixture() Issue The test helper `webhelpers.fixture()` triggers property changes during initialization that can cause null reference errors. Always: 1. Check for null/undefined before accessing nested properties 2. Set required properties in specific order when testing 3. Consider manual element creation for complex test scenarios ## Summary These patterns represent hours of debugging and problem-solving. When refactoring: 1. **NEVER** remove static rendering approach 2. **ALWAYS** use Shadow DOM-aware selection utilities 3. **MAINTAIN** cursor position tracking through all events 4. **PRESERVE** the complex content splitting logic 5. **KEEP** all focus management fallbacks 6. **ENSURE** selection events bubble through Shadow DOM 7. **SUPPORT** IME composition events 8. **TEST** thoroughly with actual typing, not just unit tests Any changes that break these patterns will result in a degraded user experience that took significant effort to achieve.