refactor
This commit is contained in:
294
ts_web/elements/wysiwyg/blocks/MIGRATION-KNOWLEDGE.md
Normal file
294
ts_web/elements/wysiwyg/blocks/MIGRATION-KNOWLEDGE.md
Normal file
@ -0,0 +1,294 @@
|
||||
# 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.
|
Reference in New Issue
Block a user