8.6 KiB
8.6 KiB
WYSIWYG Editor Refactoring Plan
Current Status
The dees-wysiwyg-block.ts
file has grown to over 2100 lines and contains:
- Main component logic
- CSS styles for all block types
- Rendering logic for each block type
- Setup methods for each block type
- Helper methods for various functionality
This makes the file difficult to maintain and extend.
Refactoring Goals
- Modularity: Each block type should be self-contained
- Extensibility: Adding new block types should be straightforward
- Maintainability: Code should be organized by responsibility
- Type Safety: Strong interfaces to ensure consistent implementation
- Performance: Enable potential lazy loading of block types
Proposed File Structure
ts_web/elements/wysiwyg/
dees-wysiwyg-block.ts (main component - simplified)
blocks/
index.ts (exports all block types)
block.base.ts (base class/interface)
block.registry.ts (block type registry)
block.styles.ts (common block styles)
text/
paragraph.block.ts
heading.block.ts
quote.block.ts
code.block.ts
list.block.ts
media/
image.block.ts
youtube.block.ts
attachment.block.ts
content/
markdown.block.ts
html.block.ts
divider.block.ts
utils/
file.utils.ts
media.utils.ts
markdown.utils.ts
Implementation Details
1. Block Handler Interface
// block.base.ts
export interface IBlockHandler {
type: string;
render(block: IBlock, isSelected: boolean): string;
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void;
getStyles(): string;
getPlaceholder?(): string;
}
export interface IBlockEventHandlers {
onInput: (e: InputEvent) => void;
onKeyDown: (e: KeyboardEvent) => void;
onFocus: () => void;
onBlur: () => void;
onCompositionStart: () => void;
onCompositionEnd: () => void;
onMouseUp?: (e: MouseEvent) => void;
}
export abstract class BaseBlockHandler implements IBlockHandler {
abstract type: string;
abstract render(block: IBlock, isSelected: boolean): string;
// Default implementation for common setup
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
// Common setup logic
}
// Common styles can be defined here
getStyles(): string {
return '';
}
}
2. Block Registry Pattern
// block.registry.ts
export class BlockRegistry {
private static handlers = new Map<string, IBlockHandler>();
static register(type: string, handler: IBlockHandler): void {
this.handlers.set(type, handler);
}
static getHandler(type: string): IBlockHandler | undefined {
return this.handlers.get(type);
}
static getAllTypes(): string[] {
return Array.from(this.handlers.keys());
}
}
3. Example Block Implementation
// media/image.block.ts
export class ImageBlockHandler extends BaseBlockHandler {
type = 'image';
render(block: IBlock, isSelected: boolean): string {
const selectedClass = isSelected ? ' selected' : '';
const imageUrl = block.metadata?.url || '';
const isLoading = block.metadata?.loading || false;
return `
<div class="block image${selectedClass}" data-block-id="${block.id}" data-block-type="${block.type}" tabindex="0">
${isLoading ? `<div class="image-loading">Uploading image...</div>` : ''}
${imageUrl ? this.renderImage(imageUrl, block.content) : this.renderPlaceholder()}
</div>
`;
}
private renderImage(url: string, alt: string): string {
return `
<div class="image-container">
<img src="${url}" alt="${alt || 'Uploaded image'}" />
</div>
`;
}
private renderPlaceholder(): string {
return `
<div class="image-upload-placeholder">
<div class="upload-icon">=<3D></div>
<div class="upload-text">Click to upload an image</div>
<div class="upload-hint">or drag and drop</div>
</div>
<input type="file" accept="image/*" style="display: none;" />
`;
}
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const imageBlock = element.querySelector('.block.image') as HTMLDivElement;
if (!imageBlock) return;
// Setup click handlers
imageBlock.addEventListener('click', (e) => {
if ((e.target as HTMLElement).tagName !== 'INPUT') {
e.stopPropagation();
imageBlock.focus();
handlers.onFocus();
}
});
// Setup file upload
this.setupFileUpload(imageBlock, block, handlers);
// Setup drag and drop
this.setupDragDrop(imageBlock, block, handlers);
// Setup keyboard events
this.setupKeyboardEvents(imageBlock, handlers);
}
getStyles(): string {
return `
.block.image {
min-height: 200px;
padding: 0;
margin: 16px 0;
border-radius: 8px;
overflow: hidden;
position: relative;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.15s ease;
}
.block.image:focus {
outline: none;
}
.block.image.selected {
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.3);
}
/* ... rest of image-specific styles ... */
`;
}
}
Migration Strategy
Phase 1: Infrastructure (Week 1)
- Create
blocks/
directory structure - Implement
IBlockHandler
interface andBaseBlockHandler
class - Implement
BlockRegistry
with registration mechanism - Create
block.styles.ts
for common styles - Set up exports in
blocks/index.ts
Phase 2: Proof of Concept (Week 1) ✅
- Migrate
divider
block as the simplest example - Update main component to use registry for divider blocks
- Test that divider blocks work identically to before
- Document any issues or adjustments needed
Phase 3: Text Blocks Migration (Week 2) ✅
- Migrate
paragraph
block - Migrate
heading
blocks (h1, h2, h3) - Migrate
quote
block - Migrate
code
block - Migrate
list
block - Extract common text formatting utilities
Phase 4: Media Blocks Migration (Week 2)
- Migrate
image
block - Migrate
youtube
block - Migrate
attachment
block - Extract file handling utilities to
utils/file.utils.ts
- Extract media utilities to
utils/media.utils.ts
Phase 5: Content Blocks Migration (Week 3)
- Migrate
markdown
block - Migrate
html
block - Extract markdown utilities to
utils/markdown.utils.ts
Phase 6: Cleanup (Week 3)
- Remove old code from main component
- Optimize imports and exports
- Update documentation
- Performance testing
- Bundle size analysis
Technical Considerations
1. Backwards Compatibility
- Ensure all existing functionality remains intact
- No breaking changes to the public API
- Maintain existing event handling patterns
2. Performance
- Monitor bundle size impact
- Consider dynamic imports for heavy block types:
// Lazy load markdown handler const { MarkdownBlockHandler } = await import('./content/markdown.block.js');
3. Type Safety
- All block handlers must implement
IBlockHandler
- Proper typing for block metadata
- Type guards for block-specific operations
4. Testing Strategy
- Unit tests for each block handler
- Integration tests for the registry
- E2E tests to ensure no regressions
5. Style Management
- Common styles in
block.styles.ts
- Block-specific styles returned by
getStyles()
- Theme variables properly utilized
Success Criteria
- Code Organization: Each file under 300 lines
- Single Responsibility: Each class handles one block type
- No Regressions: All existing functionality works identically
- Improved Developer Experience: Adding new blocks is straightforward
- Performance: No significant increase in bundle size or runtime overhead
Future Enhancements
- Plugin System: Allow external block types to be registered
- Lazy Loading: Dynamic imports for rarely used block types
- Block Templates: Predefined configurations for common use cases
- Block Transformations: Convert between compatible block types
- Custom Block API: Allow users to define their own block types
Notes
- This refactoring should be done incrementally to minimize risk
- Each phase should be fully tested before moving to the next
- Regular code reviews to ensure consistency
- Documentation should be updated as we go