562 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			562 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 
								 | 
							
								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 `
							 | 
						||
| 
								 | 
							
								      <div class="markdown-block-container${isSelected ? ' selected' : ''}" 
							 | 
						||
| 
								 | 
							
								           data-block-id="${block.id}"
							 | 
						||
| 
								 | 
							
								           data-edit-mode="${isEditMode}">
							 | 
						||
| 
								 | 
							
								        <div class="markdown-header">
							 | 
						||
| 
								 | 
							
								          <div class="markdown-icon">M↓</div>
							 | 
						||
| 
								 | 
							
								          <div class="markdown-title">Markdown</div>
							 | 
						||
| 
								 | 
							
								          <button class="markdown-toggle-mode" title="${isEditMode ? 'Preview' : 'Edit'}">
							 | 
						||
| 
								 | 
							
								            ${isEditMode ? '👁️' : '✏️'}
							 | 
						||
| 
								 | 
							
								          </button>
							 | 
						||
| 
								 | 
							
								        </div>
							 | 
						||
| 
								 | 
							
								        <div class="markdown-content">
							 | 
						||
| 
								 | 
							
								          ${isEditMode ? this.renderEditor(content) : this.renderPreview(content)}
							 | 
						||
| 
								 | 
							
								        </div>
							 | 
						||
| 
								 | 
							
								      </div>
							 | 
						||
| 
								 | 
							
								    `;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  private renderEditor(content: string): string {
							 | 
						||
| 
								 | 
							
								    return `
							 | 
						||
| 
								 | 
							
								      <textarea class="markdown-editor" 
							 | 
						||
| 
								 | 
							
								                placeholder="Enter markdown content..."
							 | 
						||
| 
								 | 
							
								                spellcheck="false">${this.escapeHtml(content)}</textarea>
							 | 
						||
| 
								 | 
							
								    `;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  private renderPreview(content: string): string {
							 | 
						||
| 
								 | 
							
								    const html = this.parseMarkdown(content);
							 | 
						||
| 
								 | 
							
								    return `
							 | 
						||
| 
								 | 
							
								      <div class="markdown-preview">
							 | 
						||
| 
								 | 
							
								        ${html || '<div class="preview-empty">No content to preview</div>'}
							 | 
						||
| 
								 | 
							
								      </div>
							 | 
						||
| 
								 | 
							
								    `;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  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, '<h3>$1</h3>');
							 | 
						||
| 
								 | 
							
								    html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
							 | 
						||
| 
								 | 
							
								    html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    // Bold
							 | 
						||
| 
								 | 
							
								    html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
							 | 
						||
| 
								 | 
							
								    html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    // Italic
							 | 
						||
| 
								 | 
							
								    html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
							 | 
						||
| 
								 | 
							
								    html = html.replace(/_(.+?)_/g, '<em>$1</em>');
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    // Code blocks
							 | 
						||
| 
								 | 
							
								    html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    // Inline code
							 | 
						||
| 
								 | 
							
								    html = html.replace(/`(.+?)`/g, '<code>$1</code>');
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    // Links
							 | 
						||
| 
								 | 
							
								    html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    // Lists
							 | 
						||
| 
								 | 
							
								    html = html.replace(/^\* (.+)$/gm, '<li>$1</li>');
							 | 
						||
| 
								 | 
							
								    html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
							 | 
						||
| 
								 | 
							
								    html = html.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    // Wrap consecutive list items
							 | 
						||
| 
								 | 
							
								    html = html.replace(/(<li>.*<\/li>\n?)+/g, (match) => {
							 | 
						||
| 
								 | 
							
								      return '<ul>' + match + '</ul>';
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    // Paragraphs
							 | 
						||
| 
								 | 
							
								    html = html.replace(/\n\n/g, '</p><p>');
							 | 
						||
| 
								 | 
							
								    html = '<p>' + html + '</p>';
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    // Clean up empty paragraphs
							 | 
						||
| 
								 | 
							
								    html = html.replace(/<p><\/p>/g, '');
							 | 
						||
| 
								 | 
							
								    html = html.replace(/<p>(<h[1-3]>)/g, '$1');
							 | 
						||
| 
								 | 
							
								    html = html.replace(/(<\/h[1-3]>)<\/p>/g, '$1');
							 | 
						||
| 
								 | 
							
								    html = html.replace(/<p>(<ul>)/g, '$1');
							 | 
						||
| 
								 | 
							
								    html = html.replace(/(<\/ul>)<\/p>/g, '$1');
							 | 
						||
| 
								 | 
							
								    html = html.replace(/<p>(<pre>)/g, '$1');
							 | 
						||
| 
								 | 
							
								    html = html.replace(/(<\/pre>)<\/p>/g, '$1');
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    return html;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  private escapeHtml(text: string): string {
							 | 
						||
| 
								 | 
							
								    const div = document.createElement('div');
							 | 
						||
| 
								 | 
							
								    div.textContent = text;
							 | 
						||
| 
								 | 
							
								    return div.innerHTML;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  getContent(element: HTMLElement): string {
							 | 
						||
| 
								 | 
							
								    const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
							 | 
						||
| 
								 | 
							
								    if (editor) {
							 | 
						||
| 
								 | 
							
								      return editor.value;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    // If in preview mode, return the stored content
							 | 
						||
| 
								 | 
							
								    const container = element.querySelector('.markdown-block-container');
							 | 
						||
| 
								 | 
							
								    const blockId = container?.getAttribute('data-block-id');
							 | 
						||
| 
								 | 
							
								    // In real implementation, would need access to block data
							 | 
						||
| 
								 | 
							
								    return '';
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  setContent(element: HTMLElement, content: string): void {
							 | 
						||
| 
								 | 
							
								    const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
							 | 
						||
| 
								 | 
							
								    if (editor) {
							 | 
						||
| 
								 | 
							
								      editor.value = content;
							 | 
						||
| 
								 | 
							
								      this.autoResize(editor);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  getCursorPosition(element: HTMLElement): number | null {
							 | 
						||
| 
								 | 
							
								    const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
							 | 
						||
| 
								 | 
							
								    return editor ? editor.selectionStart : null;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  setCursorToStart(element: HTMLElement): void {
							 | 
						||
| 
								 | 
							
								    const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
							 | 
						||
| 
								 | 
							
								    if (editor) {
							 | 
						||
| 
								 | 
							
								      editor.selectionStart = editor.selectionEnd = 0;
							 | 
						||
| 
								 | 
							
								      editor.focus();
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      this.focus(element);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  setCursorToEnd(element: HTMLElement): void {
							 | 
						||
| 
								 | 
							
								    const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
							 | 
						||
| 
								 | 
							
								    if (editor) {
							 | 
						||
| 
								 | 
							
								      const length = editor.value.length;
							 | 
						||
| 
								 | 
							
								      editor.selectionStart = editor.selectionEnd = length;
							 | 
						||
| 
								 | 
							
								      editor.focus();
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      this.focus(element);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  focus(element: HTMLElement): void {
							 | 
						||
| 
								 | 
							
								    const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
							 | 
						||
| 
								 | 
							
								    if (editor) {
							 | 
						||
| 
								 | 
							
								      editor.focus();
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      const preview = element.querySelector('.markdown-preview') as HTMLElement;
							 | 
						||
| 
								 | 
							
								      preview?.focus();
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end'): void {
							 | 
						||
| 
								 | 
							
								    const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
							 | 
						||
| 
								 | 
							
								    if (editor) {
							 | 
						||
| 
								 | 
							
								      if (position === 'start') {
							 | 
						||
| 
								 | 
							
								        this.setCursorToStart(element);
							 | 
						||
| 
								 | 
							
								      } else if (position === 'end') {
							 | 
						||
| 
								 | 
							
								        this.setCursorToEnd(element);
							 | 
						||
| 
								 | 
							
								      } else if (typeof position === 'number') {
							 | 
						||
| 
								 | 
							
								        editor.selectionStart = editor.selectionEnd = position;
							 | 
						||
| 
								 | 
							
								        editor.focus();
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      this.focus(element);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  getSplitContent(element: HTMLElement): { before: string; after: string } | null {
							 | 
						||
| 
								 | 
							
								    const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
							 | 
						||
| 
								 | 
							
								    if (!editor) return null;
							 | 
						||
| 
								 | 
							
								    
							 | 
						||
| 
								 | 
							
								    const cursorPos = editor.selectionStart;
							 | 
						||
| 
								 | 
							
								    return {
							 | 
						||
| 
								 | 
							
								      before: editor.value.substring(0, cursorPos),
							 | 
						||
| 
								 | 
							
								      after: editor.value.substring(cursorPos)
							 | 
						||
| 
								 | 
							
								    };
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  
							 | 
						||
| 
								 | 
							
								  getStyles(): string {
							 | 
						||
| 
								 | 
							
								    return `
							 | 
						||
| 
								 | 
							
								      /* Markdown Block Container */
							 | 
						||
| 
								 | 
							
								      .markdown-block-container {
							 | 
						||
| 
								 | 
							
								        position: relative;
							 | 
						||
| 
								 | 
							
								        margin: 12px 0;
							 | 
						||
| 
								 | 
							
								        border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
							 | 
						||
| 
								 | 
							
								        border-radius: 6px;
							 | 
						||
| 
								 | 
							
								        overflow: hidden;
							 | 
						||
| 
								 | 
							
								        transition: all 0.15s ease;
							 | 
						||
| 
								 | 
							
								        background: ${cssManager.bdTheme('#ffffff', '#111827')};
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .markdown-block-container.selected {
							 | 
						||
| 
								 | 
							
								        border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      /* Header */
							 | 
						||
| 
								 | 
							
								      .markdown-header {
							 | 
						||
| 
								 | 
							
								        display: flex;
							 | 
						||
| 
								 | 
							
								        align-items: center;
							 | 
						||
| 
								 | 
							
								        gap: 8px;
							 | 
						||
| 
								 | 
							
								        padding: 8px 12px;
							 | 
						||
| 
								 | 
							
								        border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
							 | 
						||
| 
								 | 
							
								        background: ${cssManager.bdTheme('#f9fafb', '#0a0a0a')};
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .markdown-icon {
							 | 
						||
| 
								 | 
							
								        font-size: 14px;
							 | 
						||
| 
								 | 
							
								        font-weight: 600;
							 | 
						||
| 
								 | 
							
								        opacity: 0.8;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .markdown-title {
							 | 
						||
| 
								 | 
							
								        flex: 1;
							 | 
						||
| 
								 | 
							
								        font-size: 13px;
							 | 
						||
| 
								 | 
							
								        font-weight: 500;
							 | 
						||
| 
								 | 
							
								        color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .markdown-toggle-mode {
							 | 
						||
| 
								 | 
							
								        padding: 4px 8px;
							 | 
						||
| 
								 | 
							
								        background: transparent;
							 | 
						||
| 
								 | 
							
								        border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
							 | 
						||
| 
								 | 
							
								        border-radius: 4px;
							 | 
						||
| 
								 | 
							
								        font-size: 14px;
							 | 
						||
| 
								 | 
							
								        cursor: pointer;
							 | 
						||
| 
								 | 
							
								        transition: all 0.15s ease;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .markdown-toggle-mode:hover {
							 | 
						||
| 
								 | 
							
								        background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
							 | 
						||
| 
								 | 
							
								        border-color: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      /* Content */
							 | 
						||
| 
								 | 
							
								      .markdown-content {
							 | 
						||
| 
								 | 
							
								        position: relative;
							 | 
						||
| 
								 | 
							
								        min-height: 120px;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      /* Editor */
							 | 
						||
| 
								 | 
							
								      .markdown-editor {
							 | 
						||
| 
								 | 
							
								        width: 100%;
							 | 
						||
| 
								 | 
							
								        min-height: 120px;
							 | 
						||
| 
								 | 
							
								        padding: 12px;
							 | 
						||
| 
								 | 
							
								        background: transparent;
							 | 
						||
| 
								 | 
							
								        border: none;
							 | 
						||
| 
								 | 
							
								        outline: none;
							 | 
						||
| 
								 | 
							
								        resize: none;
							 | 
						||
| 
								 | 
							
								        font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
							 | 
						||
| 
								 | 
							
								        font-size: 13px;
							 | 
						||
| 
								 | 
							
								        line-height: 1.6;
							 | 
						||
| 
								 | 
							
								        color: ${cssManager.bdTheme('#1f2937', '#f3f4f6')};
							 | 
						||
| 
								 | 
							
								        overflow: hidden;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .markdown-editor::placeholder {
							 | 
						||
| 
								 | 
							
								        color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      /* Preview */
							 | 
						||
| 
								 | 
							
								      .markdown-preview {
							 | 
						||
| 
								 | 
							
								        padding: 12px;
							 | 
						||
| 
								 | 
							
								        min-height: 96px;
							 | 
						||
| 
								 | 
							
								        outline: none;
							 | 
						||
| 
								 | 
							
								        font-size: 14px;
							 | 
						||
| 
								 | 
							
								        line-height: 1.6;
							 | 
						||
| 
								 | 
							
								        color: ${cssManager.bdTheme('#1f2937', '#f3f4f6')};
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .preview-empty {
							 | 
						||
| 
								 | 
							
								        color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
							 | 
						||
| 
								 | 
							
								        font-style: italic;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      /* Markdown preview styles */
							 | 
						||
| 
								 | 
							
								      .markdown-preview h1 {
							 | 
						||
| 
								 | 
							
								        font-size: 24px;
							 | 
						||
| 
								 | 
							
								        font-weight: 600;
							 | 
						||
| 
								 | 
							
								        margin: 16px 0 8px 0;
							 | 
						||
| 
								 | 
							
								        color: ${cssManager.bdTheme('#111827', '#f9fafb')};
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .markdown-preview h2 {
							 | 
						||
| 
								 | 
							
								        font-size: 20px;
							 | 
						||
| 
								 | 
							
								        font-weight: 600;
							 | 
						||
| 
								 | 
							
								        margin: 14px 0 6px 0;
							 | 
						||
| 
								 | 
							
								        color: ${cssManager.bdTheme('#111827', '#f9fafb')};
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .markdown-preview h3 {
							 | 
						||
| 
								 | 
							
								        font-size: 18px;
							 | 
						||
| 
								 | 
							
								        font-weight: 600;
							 | 
						||
| 
								 | 
							
								        margin: 12px 0 4px 0;
							 | 
						||
| 
								 | 
							
								        color: ${cssManager.bdTheme('#111827', '#f9fafb')};
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .markdown-preview p {
							 | 
						||
| 
								 | 
							
								        margin: 8px 0;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .markdown-preview ul,
							 | 
						||
| 
								 | 
							
								      .markdown-preview ol {
							 | 
						||
| 
								 | 
							
								        margin: 8px 0;
							 | 
						||
| 
								 | 
							
								        padding-left: 24px;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .markdown-preview li {
							 | 
						||
| 
								 | 
							
								        margin: 4px 0;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .markdown-preview code {
							 | 
						||
| 
								 | 
							
								        background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
							 | 
						||
| 
								 | 
							
								        padding: 2px 4px;
							 | 
						||
| 
								 | 
							
								        border-radius: 3px;
							 | 
						||
| 
								 | 
							
								        font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
							 | 
						||
| 
								 | 
							
								        font-size: 0.9em;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .markdown-preview pre {
							 | 
						||
| 
								 | 
							
								        background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
							 | 
						||
| 
								 | 
							
								        padding: 12px;
							 | 
						||
| 
								 | 
							
								        border-radius: 4px;
							 | 
						||
| 
								 | 
							
								        overflow-x: auto;
							 | 
						||
| 
								 | 
							
								        margin: 8px 0;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .markdown-preview pre code {
							 | 
						||
| 
								 | 
							
								        background: transparent;
							 | 
						||
| 
								 | 
							
								        padding: 0;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .markdown-preview strong {
							 | 
						||
| 
								 | 
							
								        font-weight: 600;
							 | 
						||
| 
								 | 
							
								        color: ${cssManager.bdTheme('#111827', '#f9fafb')};
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .markdown-preview em {
							 | 
						||
| 
								 | 
							
								        font-style: italic;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .markdown-preview a {
							 | 
						||
| 
								 | 
							
								        color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
							 | 
						||
| 
								 | 
							
								        text-decoration: none;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .markdown-preview a:hover {
							 | 
						||
| 
								 | 
							
								        text-decoration: underline;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      
							 | 
						||
| 
								 | 
							
								      .markdown-preview blockquote {
							 | 
						||
| 
								 | 
							
								        border-left: 3px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
							 | 
						||
| 
								 | 
							
								        padding-left: 12px;
							 | 
						||
| 
								 | 
							
								        margin: 8px 0;
							 | 
						||
| 
								 | 
							
								        color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    `;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 |