Files
dees-catalog/ts_web/elements/wysiwyg/blocks/MIGRATION-KNOWLEDGE.md
Juergen Kunz e9541da8ff refactor
2025-06-24 22:45:50 +00:00

8.5 KiB

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:

// In dees-wysiwyg-block.ts
render(): TemplateResult {
  if (!this.block) return html``;
  // Render empty container - content set in firstUpdated
  return html`<div class="wysiwyg-block-container"></div>`;
}

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:

// 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:

// 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:

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:

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:

// 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:

// 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:

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.