Files
dees-catalog/ts_web/elements/wysiwyg/blocks/MIGRATION-KNOWLEDGE.md

294 lines
8.5 KiB
Markdown
Raw Normal View History

2025-06-24 22:45:50 +00:00
# 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`<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:
```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.