import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js'; import type { IBlock } from '../../wysiwyg.types.js'; import { cssManager } from '@design.estate/dees-element'; /** * MarkdownBlockHandler - Handles markdown content with preview/edit toggle * * Features: * - Live markdown preview * - Edit/preview mode toggle * - Syntax highlighting in edit mode * - Common markdown shortcuts * - Auto-save on mode switch */ export class MarkdownBlockHandler extends BaseBlockHandler { type = 'markdown'; render(block: IBlock, isSelected: boolean): string { const isEditMode = block.metadata?.isEditMode ?? true; const content = block.content || ''; return `
M↓
Markdown
${isEditMode ? this.renderEditor(content) : this.renderPreview(content)}
`; } private renderEditor(content: string): string { return ` `; } private renderPreview(content: string): string { const html = this.parseMarkdown(content); return `
${html || '
No content to preview
'}
`; } setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void { const container = element.querySelector('.markdown-block-container') as HTMLElement; const toggleBtn = element.querySelector('.markdown-toggle-mode') as HTMLButtonElement; if (!container || !toggleBtn) { console.error('MarkdownBlockHandler: Could not find required elements'); return; } // Initialize metadata if (!block.metadata) block.metadata = {}; if (block.metadata.isEditMode === undefined) block.metadata.isEditMode = true; // Toggle mode button toggleBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); // Save current content if in edit mode if (block.metadata.isEditMode) { const editor = container.querySelector('.markdown-editor') as HTMLTextAreaElement; if (editor) { block.content = editor.value; } } // Toggle mode block.metadata.isEditMode = !block.metadata.isEditMode; // Request UI update handlers.onRequestUpdate?.(); }); // Setup based on mode if (block.metadata.isEditMode) { this.setupEditor(element, block, handlers); } else { this.setupPreview(element, block, handlers); } } private setupEditor(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void { const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement; if (!editor) return; // Focus handling editor.addEventListener('focus', () => handlers.onFocus()); editor.addEventListener('blur', () => handlers.onBlur()); // Content changes editor.addEventListener('input', () => { block.content = editor.value; }); // Keyboard shortcuts editor.addEventListener('keydown', (e) => { // Tab handling for indentation if (e.key === 'Tab') { e.preventDefault(); const start = editor.selectionStart; const end = editor.selectionEnd; const value = editor.value; if (e.shiftKey) { // Unindent const beforeCursor = value.substring(0, start); const lastNewline = beforeCursor.lastIndexOf('\n'); const lineStart = lastNewline + 1; const lineContent = value.substring(lineStart, start); if (lineContent.startsWith(' ')) { editor.value = value.substring(0, lineStart) + lineContent.substring(2) + value.substring(start); editor.selectionStart = editor.selectionEnd = start - 2; } } else { // Indent editor.value = value.substring(0, start) + ' ' + value.substring(end); editor.selectionStart = editor.selectionEnd = start + 2; } block.content = editor.value; return; } // Bold shortcut (Ctrl/Cmd + B) if ((e.ctrlKey || e.metaKey) && e.key === 'b') { e.preventDefault(); this.wrapSelection(editor, '**', '**'); block.content = editor.value; return; } // Italic shortcut (Ctrl/Cmd + I) if ((e.ctrlKey || e.metaKey) && e.key === 'i') { e.preventDefault(); this.wrapSelection(editor, '_', '_'); block.content = editor.value; return; } // Link shortcut (Ctrl/Cmd + K) if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); this.insertLink(editor); block.content = editor.value; return; } // Pass other key events to handlers handlers.onKeyDown(e); }); // Auto-resize this.autoResize(editor); editor.addEventListener('input', () => this.autoResize(editor)); } private setupPreview(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void { const container = element.querySelector('.markdown-block-container') as HTMLElement; const preview = element.querySelector('.markdown-preview') as HTMLElement; if (!container || !preview) return; // Make preview focusable preview.setAttribute('tabindex', '0'); // Focus handling preview.addEventListener('focus', () => handlers.onFocus()); preview.addEventListener('blur', () => handlers.onBlur()); // Keyboard navigation preview.addEventListener('keydown', (e) => { // Switch to edit mode on Enter if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); block.metadata.isEditMode = true; handlers.onRequestUpdate?.(); return; } handlers.onKeyDown(e); }); } private wrapSelection(editor: HTMLTextAreaElement, before: string, after: string): void { const start = editor.selectionStart; const end = editor.selectionEnd; const selectedText = editor.value.substring(start, end); const replacement = before + (selectedText || 'text') + after; editor.value = editor.value.substring(0, start) + replacement + editor.value.substring(end); if (selectedText) { editor.selectionStart = start; editor.selectionEnd = start + replacement.length; } else { editor.selectionStart = start + before.length; editor.selectionEnd = start + before.length + 4; // 'text'.length } editor.focus(); } private insertLink(editor: HTMLTextAreaElement): void { const start = editor.selectionStart; const end = editor.selectionEnd; const selectedText = editor.value.substring(start, end); const linkText = selectedText || 'link text'; const replacement = `[${linkText}](url)`; editor.value = editor.value.substring(0, start) + replacement + editor.value.substring(end); // Select the URL part editor.selectionStart = start + linkText.length + 3; // '[linktext]('.length editor.selectionEnd = start + linkText.length + 6; // '[linktext](url'.length editor.focus(); } private autoResize(editor: HTMLTextAreaElement): void { editor.style.height = 'auto'; editor.style.height = editor.scrollHeight + 'px'; } private parseMarkdown(markdown: string): string { // Basic markdown parsing - in production, use a proper markdown parser let html = this.escapeHtml(markdown); // Headers html = html.replace(/^### (.+)$/gm, '

$1

'); html = html.replace(/^## (.+)$/gm, '

$1

'); html = html.replace(/^# (.+)$/gm, '

$1

'); // Bold html = html.replace(/\*\*(.+?)\*\*/g, '$1'); html = html.replace(/__(.+?)__/g, '$1'); // Italic html = html.replace(/\*(.+?)\*/g, '$1'); html = html.replace(/_(.+?)_/g, '$1'); // Code blocks html = html.replace(/```([\s\S]*?)```/g, '
$1
'); // Inline code html = html.replace(/`(.+?)`/g, '$1'); // Links html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); // Lists html = html.replace(/^\* (.+)$/gm, '
  • $1
  • '); html = html.replace(/^- (.+)$/gm, '
  • $1
  • '); html = html.replace(/^\d+\. (.+)$/gm, '
  • $1
  • '); // Wrap consecutive list items html = html.replace(/(
  • .*<\/li>\n?)+/g, (match) => { return ''; }); // Paragraphs html = html.replace(/\n\n/g, '

    '); html = '

    ' + html + '

    '; // Clean up empty paragraphs html = html.replace(/

    <\/p>/g, ''); html = html.replace(/

    ()/g, '$1'); html = html.replace(/(<\/h[1-3]>)<\/p>/g, '$1'); html = html.replace(/

    (