// Clean Editor Architecture /** * Test-Driven Section Editor Implementation * * A clean, object-oriented approach to handling section editing * that can be tested independently of the DOM. */ // Debug system - choose one of: 'off', 'console', 'alerts' const DEBUG_MODE = 'console'; // Advanced State Management const EditState = Object.freeze({ ORIGINAL: 'original', EDITING: 'editing', MODIFIED: 'modified', SAVED: 'saved' }); function debug(message, category = 'INFO') { const prefix = `DEBUG ${category}:`; switch (DEBUG_MODE) { case 'off': // No debugging output break; case 'console': console.log(prefix, message); break; case 'alerts': alert(`${prefix} ${message}`); console.log(prefix, message); // Also log to console for reference break; default: console.warn('Invalid DEBUG_MODE. Use: off, console, or alerts'); } } // Enums for clear state management (already defined above) const SectionType = Object.freeze({ HEADING: 'heading', PARAGRAPH: 'paragraph', LIST: 'list', CODE: 'code', QUOTE: 'quote', IMAGE: 'image', TABLE: 'table', HR: 'hr', OTHER: 'other' }); /** * Section class - Represents a single editable section */ class Section { constructor(id, markdown, type) { this.id = id; this.originalMarkdown = markdown; this.currentMarkdown = markdown; this.editingMarkdown = markdown; this.pendingMarkdown = null; this.type = type; this.state = EditState.ORIGINAL; this.domElement = null; this.lastSaved = null; this.created = new Date(); } /** * Generate sophisticated section ID with hash-based algorithm * @param {string} markdown - Section content * @param {number} position - Position in document * @param {string} strategy - ID generation strategy ('hash', 'timestamp', 'sequential', 'hierarchical') * @param {string} parentId - Parent section ID for hierarchical strategy * @returns {string} Generated section ID */ static generateId(markdown, position, strategy = 'hash', parentId = null) { return this.generateIdWithStrategy(markdown, position, strategy, parentId); } /** * Generate ID with specific strategy * @param {string} markdown - Section content * @param {number} position - Position in document * @param {string} strategy - Generation strategy * @param {string} parentId - Parent ID for hierarchical * @returns {string} Generated ID */ static generateIdWithStrategy(markdown, position, strategy = 'hash', parentId = null) { const sanitizedContent = this.sanitizeContentForId(markdown); const normalizedContent = this.normalizeContentForHashing(sanitizedContent); const sectionType = this.detectType(markdown); switch (strategy) { case 'timestamp': return this.generateTimestampId(normalizedContent, position, sectionType); case 'sequential': return this.generateSequentialId(normalizedContent, position, sectionType); case 'hierarchical': return this.generateHierarchicalId(normalizedContent, position, parentId); case 'hash': default: return this.generateAdvancedId(normalizedContent, position, sectionType); } } /** * Generate advanced hash-based ID with section type * @param {string} content - Normalized content * @param {number} position - Position * @param {string} sectionType - Detected section type * @returns {string} Advanced ID */ static generateAdvancedId(content, position, sectionType) { const contentHash = this.generateCryptoHash(content); const safeType = sectionType || 'paragraph'; const typePrefix = safeType.substring(0, 3); // First 3 chars of type const positionHex = position.toString(16).padStart(2, '0'); return `section-${typePrefix}-${contentHash}-${positionHex}`; } /** * Generate cryptographic hash for content fingerprinting * @param {string} content - Content to hash * @returns {string} Hex hash string */ static generateCryptoHash(content) { // Simple but effective hash function for browser compatibility let hash = 0; if (content.length === 0) return '00000000'; for (let i = 0; i < content.length; i++) { const char = content.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } // Convert to positive hex string const hexHash = Math.abs(hash).toString(16).padStart(8, '0'); return hexHash.substring(0, 8); } /** * Normalize content for consistent hashing * @param {string} content - Raw content * @returns {string} Normalized content */ static normalizeContentForHashing(content) { if (!content || typeof content !== 'string') { return ''; } return content .trim() // Remove leading/trailing whitespace .replace(/\s+/g, ' ') // Normalize whitespace .replace(/\r\n/g, '\n') // Normalize line endings .toLowerCase(); // Case insensitive } /** * Sanitize content for safe ID generation * @param {string} content - Raw content * @returns {string} Sanitized content */ static sanitizeContentForId(content) { if (!content || typeof content !== 'string') { return ''; } // Remove potentially dangerous characters and HTML return content .replace(/<[^>]*>/g, '') // Remove HTML tags .replace(/javascript:/gi, '') // Remove javascript: protocol .replace(/[^\w\s\-_.#]/g, '') // Keep only safe characters .trim(); } /** * Generate timestamp-based ID for temporal uniqueness * @param {string} content - Content * @param {number} position - Position * @param {string} sectionType - Section type * @returns {string} Timestamp-based ID */ static generateTimestampId(content, position = 0, sectionType = 'paragraph') { const timestamp = Date.now().toString(36); // Base-36 timestamp const contentSnippet = this.generateCryptoHash(content || '').substring(0, 4); const safeType = sectionType || 'paragraph'; const typePrefix = safeType.substring(0, 3); return `section-${typePrefix}-${contentSnippet}-${timestamp}`; } /** * Generate sequential ID * @param {string} content - Content * @param {number} position - Position * @param {string} sectionType - Section type * @returns {string} Sequential ID */ static generateSequentialId(content, position, sectionType = 'paragraph') { const safeType = sectionType || 'paragraph'; const typePrefix = safeType.substring(0, 3); const seqNumber = (position || 0).toString().padStart(3, '0'); const contentHash = this.generateCryptoHash(content || '').substring(0, 4); return `section-${typePrefix}-seq${seqNumber}-${contentHash}`; } /** * Generate hierarchical ID for nested sections * @param {string} content - Content * @param {number} position - Position * @param {string} parentId - Parent section ID * @returns {string} Hierarchical ID */ static generateHierarchicalId(content, position, parentId = null) { const contentHash = this.generateCryptoHash(content || '').substring(0, 6); if (parentId) { const childIndex = (position || 0).toString().padStart(2, '0'); return `${parentId}-child-${childIndex}-${contentHash}`; } else { return `section-root-${position || 0}-${contentHash}`; } } /** * Detect ID collision * @param {string} id - Proposed ID * @param {Set} existingIds - Set of existing IDs * @returns {boolean} True if collision detected */ static detectIdCollision(id, existingIds) { return existingIds && existingIds.has(id); } /** * Resolve ID collision by generating alternative * @param {string} id - Colliding ID * @param {Set} existingIds - Set of existing IDs * @returns {string} Resolved unique ID */ static resolveIdCollision(id, existingIds) { let counter = 1; let resolvedId = `${id}-${counter}`; while (existingIds.has(resolvedId)) { counter++; resolvedId = `${id}-${counter}`; } return resolvedId; } /** * Analyze section ID and extract metadata * @param {string} id - Section ID to analyze * @returns {Object} ID metadata */ static analyzeId(id) { if (!id || !id.startsWith('section-')) { return { id, valid: false }; } const parts = id.split('-'); const analysis = { id, valid: true, prefix: parts[0], // 'section' format: 'unknown' }; if (parts.length >= 4) { // Advanced format: section-{type}-{hash}-{position} analysis.format = 'advanced'; analysis.type = parts[1]; analysis.hash = parts[2]; analysis.position = parseInt(parts[3], 16) || 0; } else if (parts.length === 3 && parts[1] === 'root') { // Hierarchical root format analysis.format = 'hierarchical-root'; analysis.level = 'root'; analysis.position = parseInt(parts[2]) || 0; } else if (id.includes('child')) { // Hierarchical child format analysis.format = 'hierarchical-child'; analysis.level = 'child'; } else if (id.includes('seq')) { // Sequential format analysis.format = 'sequential'; } else { // Basic or legacy format analysis.format = 'basic'; analysis.hash = parts.slice(1).join('-'); } return analysis; } static detectType(markdown) { if (!markdown || typeof markdown !== 'string') { return SectionType.PARAGRAPH; } // Don't trim the entire markdown - preserve leading indentation const content = markdown.replace(/^\n+|\n+$/g, ''); // Only remove leading/trailing newlines if (!content) { return SectionType.PARAGRAPH; } const rawLines = content.split('\n'); const lines = rawLines.map(line => line.trim()).filter(line => line.length > 0); if (lines.length === 0) { return SectionType.PARAGRAPH; } // For other detection methods, use trimmed content const trimmed = content.trim(); // Detection order matters - most specific first // 1. Heading detection (must start with # and have space) if (this.isHeading(trimmed)) { return SectionType.HEADING; } // 2. Code block detection if (this.isCodeBlock(trimmed, lines, rawLines)) { return SectionType.CODE; } // 3. Table detection if (this.isTable(trimmed, lines)) { return SectionType.TABLE; } // 4. Horizontal rule detection if (this.isHorizontalRule(trimmed, lines)) { return SectionType.HR; } // 5. List detection if (this.isList(trimmed, lines)) { return SectionType.LIST; } // 6. Quote detection if (this.isQuote(trimmed, lines)) { return SectionType.QUOTE; } // 7. Image detection if (this.isImage(trimmed)) { return SectionType.IMAGE; } // 8. Default to paragraph return SectionType.PARAGRAPH; } /** * Advanced type detection with confidence scores * @param {string} markdown - Markdown content * @returns {Object} Detection result with type, confidence, and alternatives */ static detectTypeWithConfidence(markdown) { const scores = { [SectionType.HEADING]: 0, [SectionType.CODE]: 0, [SectionType.TABLE]: 0, [SectionType.HR]: 0, [SectionType.LIST]: 0, [SectionType.QUOTE]: 0, [SectionType.IMAGE]: 0, [SectionType.PARAGRAPH]: 0 }; if (!markdown || typeof markdown !== 'string') { scores[SectionType.PARAGRAPH] = 1.0; return this.formatDetectionResult(scores); } const content = markdown.replace(/^\n+|\n+$/g, ''); const trimmed = content.trim(); const rawLines = content.split('\n'); const lines = rawLines.map(line => line.trim()).filter(line => line.length > 0); // Calculate confidence scores for each type scores[SectionType.HEADING] = this.calculateHeadingScore(trimmed); scores[SectionType.CODE] = this.calculateCodeScore(trimmed, lines, rawLines); scores[SectionType.TABLE] = this.calculateTableScore(trimmed, lines); scores[SectionType.HR] = this.calculateHRScore(trimmed, lines); scores[SectionType.LIST] = this.calculateListScore(trimmed, lines); scores[SectionType.QUOTE] = this.calculateQuoteScore(trimmed, lines); scores[SectionType.IMAGE] = this.calculateImageScore(trimmed); // Base paragraph score scores[SectionType.PARAGRAPH] = 0.1; return this.formatDetectionResult(scores); } /** * Format detection result with primary type and alternatives * @param {Object} scores - Confidence scores for each type * @returns {Object} Formatted result */ static formatDetectionResult(scores) { const sortedTypes = Object.entries(scores) .sort(([,a], [,b]) => b - a) .map(([type, score]) => ({ type, confidence: score })); const primaryType = sortedTypes[0]; const alternatives = sortedTypes.slice(1, 4).filter(alt => alt.confidence > 0.1); return { type: primaryType.type, confidence: primaryType.confidence, alternatives: alternatives }; } // Specific detection methods static isHeading(trimmed) { const headingPattern = /^#{1,6}\s+.+/; return headingPattern.test(trimmed); } static calculateHeadingScore(trimmed) { if (/^#{1,6}\s+.+/.test(trimmed)) { const hashCount = (trimmed.match(/^#+/) || [''])[0].length; if (hashCount <= 6) { return 0.95 - (hashCount - 1) * 0.05; // Higher score for lower level headings } } return 0; } static isCodeBlock(trimmed, lines, rawLines) { // Fenced code blocks - check if starts with fence OR contains fence blocks if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) { return true; } // Check for code blocks anywhere in the content for mixed content if (trimmed.includes('```') || trimmed.includes('~~~')) { const codeBlockPattern = /```[\s\S]*?```|~~~[\s\S]*?~~~/; if (codeBlockPattern.test(trimmed)) { return true; } } // Indented code blocks (4+ spaces or 1+ tabs at start of every line) if (rawLines && rawLines.length > 0) { const nonEmptyRawLines = rawLines.filter(line => line.trim().length > 0); const indentedLines = nonEmptyRawLines.filter(line => line.startsWith(' ') || line.startsWith('\t') ); return indentedLines.length === nonEmptyRawLines.length && nonEmptyRawLines.length > 0; } return false; } static calculateCodeScore(trimmed, lines, rawLines) { let score = 0; // Fenced code blocks at start if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) { score = 0.95; } // Code blocks anywhere in content (mixed content) else if (trimmed.includes('```') || trimmed.includes('~~~')) { const codeBlockPattern = /```[\s\S]*?```|~~~[\s\S]*?~~~/; if (codeBlockPattern.test(trimmed)) { // Lower score for mixed content, but still significant score = 0.7; } } // Indented code blocks else if (rawLines && rawLines.length > 0) { const nonEmptyRawLines = rawLines.filter(line => line.trim().length > 0); const indentedLines = nonEmptyRawLines.filter(line => line.startsWith(' ') || line.startsWith('\t') ); if (indentedLines.length === nonEmptyRawLines.length && nonEmptyRawLines.length > 0) { score = 0.8; } else if (indentedLines.length > 0) { score = 0.3; } } // Boost score for code-like content if (/[{}();]/.test(trimmed)) { score += 0.1; } return Math.min(score, 1.0); } static isTable(trimmed, lines) { if (lines.length < 2) return false; // Look for table structure with pipes const hasTableSeparator = lines.some(line => /^\s*\|?.*?\|.*?\|?\s*$/.test(line) && line.includes('|') ); const hasSeparatorRow = lines.some(line => /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(line) ); return hasTableSeparator && (hasSeparatorRow || this.looksLikeSimpleTable(lines)); } static looksLikeSimpleTable(lines) { if (lines.length < 2) return false; const pipeLines = lines.filter(line => line.includes('|')); if (pipeLines.length < 2) return false; // Check if lines have similar number of pipes const pipeCounts = pipeLines.map(line => (line.match(/\|/g) || []).length); const avgPipes = pipeCounts.reduce((a, b) => a + b, 0) / pipeCounts.length; return pipeCounts.every(count => Math.abs(count - avgPipes) <= 1); } static calculateTableScore(trimmed, lines) { if (lines.length < 2) return 0; let score = 0; const pipeLines = lines.filter(line => line.includes('|')); if (pipeLines.length >= 2) { score = 0.3 + (pipeLines.length / lines.length) * 0.4; // Boost for separator row if (lines.some(line => /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(line))) { score += 0.3; } } return Math.min(score, 1.0); } static isHorizontalRule(trimmed, lines) { if (lines.length !== 1) return false; const line = lines[0]; // Three or more hyphens, asterisks, or underscores const hrPatterns = [ /^-{3,}$/, /^\*{3,}$/, /^_{3,}$/, /^- -( -)*$/, /^\* \*( \*)*$/, /^_ _( _)*$/ ]; return hrPatterns.some(pattern => pattern.test(line)); } static calculateHRScore(trimmed, lines) { if (lines.length === 1) { const line = lines[0]; const hrPatterns = [ /^-{3,}$/, /^\*{3,}$/, /^_{3,}$/, /^- -( -)*$/, /^\* \*( \*)*$/, /^_ _( _)*$/ ]; if (hrPatterns.some(pattern => pattern.test(line))) { return 0.9; } } return 0; } static isList(trimmed, lines) { if (lines.length === 0) return false; const listItemPatterns = [ /^[-*+]\s+/, // Bullet lists /^\d+[\.)]\s+/, // Numbered lists /^[-*+]\s*\[([ x])\]\s+/ // Task lists ]; const listLines = lines.filter(line => listItemPatterns.some(pattern => pattern.test(line)) ); return listLines.length > 0 && listLines.length >= lines.length * 0.5; } static calculateListScore(trimmed, lines) { if (lines.length === 0) return 0; const listItemPatterns = [ /^[-*+]\s+/, /^\d+[\.)]\s+/, /^[-*+]\s*\[([ x])\]\s+/ ]; const listLines = lines.filter(line => listItemPatterns.some(pattern => pattern.test(line)) ); if (listLines.length === 0) return 0; const ratio = listLines.length / lines.length; return ratio * 0.9; } static isQuote(trimmed, lines) { if (lines.length === 0) return false; const quoteLines = lines.filter(line => line.startsWith('>')); return quoteLines.length > 0 && quoteLines.length >= lines.length * 0.5; } static calculateQuoteScore(trimmed, lines) { if (lines.length === 0) return 0; const quoteLines = lines.filter(line => line.startsWith('>')); if (quoteLines.length === 0) return 0; const ratio = quoteLines.length / lines.length; return ratio * 0.85; } static isImage(trimmed) { // Look for image syntax anywhere in the content const imagePattern = /!\[.*?\]\([^)]+\)/; return imagePattern.test(trimmed); } static calculateImageScore(trimmed) { const imagePattern = /!\[.*?\]\([^)]+\)/g; const matches = trimmed.match(imagePattern); if (matches) { // Higher score for standalone images const isStandalone = trimmed.replace(imagePattern, '').trim().length < 20; return isStandalone ? 0.9 : 0.6; } return 0; } startEdit() { if (this.state === EditState.EDITING) { throw new Error(`Section ${this.id} is already being edited`); } this.editingMarkdown = this.pendingMarkdown || this.currentMarkdown; this.state = EditState.EDITING; return this.editingMarkdown; } updateContent(markdown) { if (this.state !== EditState.EDITING) { throw new Error(`Section ${this.id} is not in editing state`); } this.editingMarkdown = markdown; } acceptChanges() { if (this.state !== EditState.EDITING) { throw new Error(`Section ${this.id} is not in editing state`); } this.currentMarkdown = this.editingMarkdown; this.editingMarkdown = null; this.pendingMarkdown = null; this.state = EditState.SAVED; this.lastSaved = new Date(); return this.currentMarkdown; } cancelChanges() { if (this.state !== EditState.EDITING) { throw new Error(`Section ${this.id} is not in editing state`); } this.editingMarkdown = null; if (this.pendingMarkdown !== null) { this.state = EditState.MODIFIED; return this.pendingMarkdown; } else if (this.lastSaved !== null) { this.state = EditState.SAVED; return this.currentMarkdown; } else { this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL; return this.currentMarkdown; } } stopEditing() { if (this.state !== EditState.EDITING) { return this.state; } // If we have editing changes that differ from current content, preserve them as pending if (this.editingMarkdown && this.editingMarkdown !== this.currentMarkdown) { this.pendingMarkdown = this.editingMarkdown; this.state = EditState.MODIFIED; // Has pending changes } else { // No changes made during this edit session this.pendingMarkdown = null; if (this.lastSaved !== null) { this.state = EditState.SAVED; } else { this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL; } } this.editingMarkdown = null; return this.state; } resetToOriginal() { this.currentMarkdown = this.originalMarkdown; this.editingMarkdown = this.originalMarkdown; this.pendingMarkdown = null; this.state = EditState.ORIGINAL; return this.originalMarkdown; } isEditing() { return this.state === EditState.EDITING; } hasChanges() { return this.currentMarkdown !== this.originalMarkdown; } getStatus() { return { id: this.id, state: this.state, hasChanges: this.hasChanges(), isEditing: this.isEditing(), contentLength: this.currentMarkdown.length, lastSaved: this.lastSaved, sectionType: this.sectionType, // Legacy compatibility type: this.type, originalLength: this.originalMarkdown.length, currentLength: this.currentMarkdown.length }; } isImage() { return this.type === SectionType.IMAGE; } isProtectedHeading() { // In insert mode, headings 1-3 are protected if (this.type === SectionType.HEADING) { const headingMatch = this.originalMarkdown.match(/^(#{1,3})\s/); if (headingMatch) { this.headingLevel = headingMatch[1].length; return this.headingLevel <= 3; } } return false; } getHeadingContent() { if (this.type === SectionType.HEADING) { const lines = this.currentMarkdown.split('\n'); return lines.slice(1).join('\n').trim(); } return this.currentMarkdown; } /** * Re-detect and update the section type based on current content * @param {string} content - Optional content to use for detection (defaults to currentMarkdown) * @returns {string} The detected section type */ redetectType(content = null) { const markdown = content || this.currentMarkdown; const oldType = this.type; this.type = Section.detectType(markdown); // Emit type change event if type changed if (oldType !== this.type) { debug(`Section ${this.id} type changed from ${oldType} to ${this.type}`, 'TYPE_DETECTION'); } return this.type; } /** * Get detailed type detection information * @param {string} content - Optional content to analyze * @returns {Object} Detection result with confidence and alternatives */ getTypeAnalysis(content = null) { const markdown = content || this.currentMarkdown; return Section.detectTypeWithConfidence(markdown); } } /** * SectionManager class - Manages the collection of sections */ class SectionManager { constructor() { this.sections = new Map(); this.listeners = new Map(); this.statusInterval = null; this.lastStatusUpdate = new Date().toISOString(); } on(event, callback) { if (!this.listeners.has(event)) { this.listeners.set(event, []); } this.listeners.get(event).push(callback); } emit(event, data) { if (this.listeners.has(event)) { this.listeners.get(event).forEach(callback => callback(data)); } } createSectionsFromMarkdown(markdownContent) { const lines = markdownContent.split('\n'); const sections = []; let currentSection = ''; let position = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const isHeading = /^#{1,6}\s/.test(line); const isNewParagraph = line.trim() && i > 0 && !lines[i-1].trim(); const isNewSection = isHeading || isNewParagraph; if (isNewSection && currentSection.trim()) { const sectionId = Section.generateId(currentSection, position); const sectionType = Section.detectType(currentSection); const section = new Section(sectionId, currentSection.trim(), sectionType); sections.push(section); this.sections.set(sectionId, section); position++; currentSection = line; } else { if (currentSection) currentSection += '\n'; currentSection += line; } } if (currentSection.trim()) { const sectionId = Section.generateId(currentSection, position); const sectionType = Section.detectType(currentSection); const section = new Section(sectionId, currentSection.trim(), sectionType); sections.push(section); this.sections.set(sectionId, section); } this.emit('sections-created', { sections, count: sections.length }); return sections; } startEditing(sectionId) { const section = this.sections.get(sectionId); if (!section) { throw new Error(`Section ${sectionId} not found`); } if (section.isEditing()) { console.log('Section already in editing state:', sectionId); return section.editingMarkdown; } const content = section.startEdit(); this.emit('edit-started', { sectionId, content, section: section.getStatus() }); return content; } updateContent(sectionId, markdown) { const section = this.sections.get(sectionId); if (!section) { throw new Error(`Section ${sectionId} not found`); } // Store old type for comparison const oldType = section.type; // Update content section.updateContent(markdown); // Automatically redetect type if content changed significantly const newType = section.redetectType(markdown); // Emit events const eventData = { sectionId, markdown, section: section.getStatus(), typeChanged: oldType !== newType, oldType, newType }; this.emit('content-updated', eventData); if (oldType !== newType) { this.emit('section-type-changed', { sectionId, oldType, newType, section: section.getStatus() }); } } acceptChanges(sectionId) { const section = this.sections.get(sectionId); if (!section) { throw new Error(`Section ${sectionId} not found`); } const content = section.acceptChanges(); this.emit('changes-accepted', { sectionId, content, section: section.getStatus() }); return content; } cancelChanges(sectionId) { const section = this.sections.get(sectionId); if (!section) { throw new Error(`Section ${sectionId} not found`); } const content = section.cancelChanges(); this.emit('changes-cancelled', { sectionId, content, section: section.getStatus() }); return content; } resetSection(sectionId) { const section = this.sections.get(sectionId); if (!section) { throw new Error(`Section ${sectionId} not found`); } const content = section.resetToOriginal(); this.emit('section-reset', { sectionId, content, section: section.getStatus() }); return content; } getDocumentMarkdown() { const sortedSections = Array.from(this.sections.values()) .sort((a, b) => a.created - b.created); return sortedSections.map(section => section.currentMarkdown).join('\n\n'); } getAllSections() { return Array.from(this.sections.values()); } getSectionStatus() { return Array.from(this.sections.values()).map(section => section.getStatus()); } getDocumentStatus() { const sections = Array.from(this.sections.values()); const editingSections = sections.filter(section => section.isEditing).length; return { totalSections: sections.length, editingSections: editingSections }; } escapeRegex(str) { return str.replace(/[.*+?^${}()|\[\]\\]/g, '\\\\$&'); } /** * Check if new content contains new headings that would require section splitting * @param {string} newContent - The new content to check * @param {string} originalContent - The original content to compare against * @returns {boolean} True if section splitting is needed */ checkForSectionSplits(newContent, originalContent) { const originalHeadings = this.extractHeadings(originalContent); const newHeadings = this.extractHeadings(newContent); // If new content has more headings than original, we need to split return newHeadings.length > originalHeadings.length; } /** * Extract heading lines from markdown content * @param {string} content - Markdown content * @returns {Array} Array of heading lines */ extractHeadings(content) { if (!content) return []; const lines = content.split('\n'); return lines.filter(line => /^#{1,6}\s/.test(line.trim())); } /** * Handle splitting a section when new headings are detected * @param {string} sectionId - ID of the section to split * @param {string} newContent - New content with multiple headings */ handleSectionSplit(sectionId, newContent) { const section = this.sections.get(sectionId); if (!section) { throw new Error(`Section ${sectionId} not found`); } // Remove the original section this.sections.delete(sectionId); // Create new sections from the content const newSections = this.createSectionsFromContent(newContent); // Emit section-split event this.emit('section-split', { originalSectionId: sectionId, newSections: newSections, count: newSections.length }); return newSections; } /** * Create sections from content (alias for createSectionsFromMarkdown) * @param {string} content - Markdown content * @returns {Array} Array of created sections */ createSectionsFromContent(content) { return this.createSectionsFromMarkdown(content); } /** * Get global status information about all sections * @returns {Object} Status object with global information */ getGlobalStatus() { const sections = this.getAllSections(); const editingSections = sections.filter(s => s.isEditing()).map(s => s.id); const modifiedSections = sections.filter(s => s.hasChanges()); const hasModifications = modifiedSections.length > 0 || editingSections.length > 0; let state = 'ready'; if (editingSections.length > 0) { state = 'editing'; } else if (hasModifications) { state = 'modified'; } return { totalSections: sections.length, editingSections: editingSections, modifiedSections: modifiedSections.length, hasModifications: hasModifications, state: state, lastUpdate: this.lastStatusUpdate }; } /** * Update global status and emit status-updated event */ updateGlobalStatus() { this.lastStatusUpdate = new Date().toISOString(); const status = this.getGlobalStatus(); this.emit('status-updated', status); return status; } /** * Start periodic status tracking * @param {number} intervalMs - Update interval in milliseconds (default: 2000) */ startStatusTracking(intervalMs = 2000) { if (this.statusInterval) { clearInterval(this.statusInterval); } this.statusInterval = setInterval(() => { this.updateGlobalStatus(); }, intervalMs); // Emit initial status this.updateGlobalStatus(); } /** * Stop periodic status tracking */ stopStatusTracking() { if (this.statusInterval) { clearInterval(this.statusInterval); this.statusInterval = null; } } /** * Bulk Operations for Concurrent Editing Sessions */ /** * Accept changes for all currently editing sessions * @returns {Array} Array of results from accept operations */ acceptAllEditingSessions() { const editingSections = this.getAllSections().filter(section => section.isEditing()); const results = []; editingSections.forEach(section => { try { const content = this.acceptChanges(section.id); results.push({ sectionId: section.id, success: true, content }); } catch (error) { results.push({ sectionId: section.id, success: false, error: error.message }); } }); this.emit('bulk-accept-completed', { results, count: results.length }); return results; } /** * Cancel changes for all currently editing sessions * @returns {Array} Array of results from cancel operations */ cancelAllEditingSessions() { const editingSections = this.getAllSections().filter(section => section.isEditing()); const results = []; editingSections.forEach(section => { try { const content = this.cancelChanges(section.id); results.push({ sectionId: section.id, success: true, content }); } catch (error) { results.push({ sectionId: section.id, success: false, error: error.message }); } }); this.emit('bulk-cancel-completed', { results, count: results.length }); return results; } /** * Stop editing for all currently editing sessions (preserve changes as pending) * @returns {Array} Array of results from stop operations */ stopAllEditingSessions() { const editingSections = this.getAllSections().filter(section => section.isEditing()); const results = []; editingSections.forEach(section => { try { const finalState = section.stopEditing(); results.push({ sectionId: section.id, success: true, finalState }); } catch (error) { results.push({ sectionId: section.id, success: false, error: error.message }); } }); this.emit('bulk-stop-completed', { results, count: results.length }); return results; } /** * Get detailed status of all concurrent editing sessions * @returns {Object} Detailed concurrent session information */ getConcurrentEditingStatus() { const sections = this.getAllSections(); const editingSections = sections.filter(s => s.isEditing()); const modifiedSections = sections.filter(s => s.hasChanges()); const pendingSections = sections.filter(s => s.pendingMarkdown !== null); return { totalSections: sections.length, concurrentSessions: { editing: editingSections.map(s => ({ id: s.id, type: s.type, hasUnsavedChanges: s.editingMarkdown !== s.currentMarkdown, editingLength: s.editingMarkdown ? s.editingMarkdown.length : 0, originalLength: s.originalMarkdown.length })), editingCount: editingSections.length, pendingCount: pendingSections.length, modifiedCount: modifiedSections.length }, systemState: { allowsConcurrentEditing: true, maxConcurrentSessions: null, // No limit activeSessionCount: editingSections.length } }; } /** * Handle conflicts when multiple sections are edited simultaneously * @param {string} sectionId - Primary section ID * @param {Array} conflictingSectionIds - IDs of sections with potential conflicts * @returns {Object} Conflict resolution result */ resolveEditingConflicts(sectionId, conflictingSectionIds = []) { const primarySection = this.sections.get(sectionId); if (!primarySection) { throw new Error(`Primary section ${sectionId} not found`); } const conflicts = []; const resolutions = []; conflictingSectionIds.forEach(conflictId => { const conflictSection = this.sections.get(conflictId); if (conflictSection && conflictSection.isEditing()) { // Check for content overlap or dependency conflicts const hasConflict = this.detectContentConflict(primarySection, conflictSection); if (hasConflict) { conflicts.push({ sectionId: conflictId, conflictType: 'content-overlap', description: 'Sections may have overlapping content changes' }); // Auto-resolve by preserving both as separate sections resolutions.push({ sectionId: conflictId, resolution: 'preserve-separate', action: 'maintain-current-state' }); } } }); this.emit('conflicts-resolved', { sectionId, conflicts, resolutions }); return { conflicts, resolutions, resolved: true }; } /** * Detect potential content conflicts between two sections * @param {Section} section1 - First section * @param {Section} section2 - Second section * @returns {boolean} True if conflict detected */ detectContentConflict(section1, section2) { // Simple conflict detection - check for similar content or headings if (section1.type === 'heading' && section2.type === 'heading') { const heading1 = section1.editingMarkdown || section1.currentMarkdown; const heading2 = section2.editingMarkdown || section2.currentMarkdown; // Check if headings are similar (potential duplicate) const similarity = this.calculateContentSimilarity(heading1, heading2); return similarity > 0.8; // 80% similarity threshold } return false; // No conflicts detected for non-heading sections } /** * Calculate content similarity between two markdown strings * @param {string} content1 - First content * @param {string} content2 - Second content * @returns {number} Similarity score (0-1) */ calculateContentSimilarity(content1, content2) { if (!content1 || !content2) return 0; const clean1 = content1.toLowerCase().replace(/[^a-z0-9\s]/g, ''); const clean2 = content2.toLowerCase().replace(/[^a-z0-9\s]/g, ''); const words1 = clean1.split(/\s+/).filter(w => w.length > 0); const words2 = clean2.split(/\s+/).filter(w => w.length > 0); if (words1.length === 0 && words2.length === 0) return 1; if (words1.length === 0 || words2.length === 0) return 0; const commonWords = words1.filter(word => words2.includes(word)); const totalWords = Math.max(words1.length, words2.length); return commonWords.length / totalWords; } } /** * DOM Renderer - Handles DOM interactions with Enhanced Event System */ class DOMRenderer { constructor(sectionManager, container) { this.sectionManager = sectionManager; this.container = container; this.editingSections = new Set(); // Enhanced Event System - Track 6 event types this.eventHistory = []; this.eventStats = { 'section-click': 0, 'section-hover-enter': 0, 'section-hover-leave': 0, 'keyboard-shortcut': 0, 'section-drag-start': 0, 'section-drag-over': 0, 'section-drop': 0, 'section-focus-in': 0, 'section-focus-out': 0, 'section-context-menu': 0 }; // Bind event handlers this.handleSectionClick = this.handleSectionClick.bind(this); this.handleSectionHover = this.handleSectionHover.bind(this); this.handleAccept = this.handleAccept.bind(this); this.handleCancel = this.handleCancel.bind(this); this.handleReset = this.handleReset.bind(this); this.handleKeydown = this.handleKeydown.bind(this); this.handleDragStart = this.handleDragStart.bind(this); this.handleDragOver = this.handleDragOver.bind(this); this.handleDrop = this.handleDrop.bind(this); this.handleFocus = this.handleFocus.bind(this); this.handleContextMenu = this.handleContextMenu.bind(this); this.setupEventListeners(); } setupEventListeners() { this.sectionManager.on('sections-created', (data) => { this.renderAllSections(data.sections); }); this.sectionManager.on('edit-started', (data) => { this.showEditor(data.sectionId, data.content); }); this.sectionManager.on('edit-stopped', (data) => { this.hideEditor(data.sectionId); }); this.sectionManager.on('changes-accepted', (data) => { this.hideEditor(data.sectionId); this.updateSectionContent(data.sectionId, data.content); }); this.sectionManager.on('changes-cancelled', (data) => { this.hideEditor(data.sectionId); this.updateSectionContent(data.sectionId, data.content); }); this.sectionManager.on('section-reset', (data) => { this.hideEditor(data.sectionId); this.updateSectionContent(data.sectionId, data.content); }); } /** * Track and log DOM events for analytics and debugging * @param {string} eventType - Type of event * @param {Object} eventData - Event data */ trackEvent(eventType, eventData) { const timestamp = new Date().toISOString(); const eventRecord = { type: eventType, timestamp, data: eventData }; // Add to history (keep last 1000 events) this.eventHistory.push(eventRecord); if (this.eventHistory.length > 1000) { this.eventHistory.shift(); } // Update stats if (this.eventStats.hasOwnProperty(eventType)) { this.eventStats[eventType]++; } // Emit to section manager for broader handling this.sectionManager.emit(eventType, eventData); } /** * Get event statistics for debugging * @returns {Object} Event statistics */ getEventStats() { return { stats: { ...this.eventStats }, recentEvents: this.eventHistory.slice(-10), totalEvents: this.eventHistory.length }; } renderAllSections(sections) { debug('21: renderAllSections called with ' + sections.length + ' sections', 'RENDER'); this.container.innerHTML = ''; debug('22: Container cleared', 'RENDER'); sections.forEach((section, index) => { debug('23: Creating element for section ' + (index + 1) + ': ' + section.id, 'RENDER'); const element = this.createSectionElement(section); section.domElement = element; this.container.appendChild(element); }); debug('24: All section elements added to container', 'RENDER'); // Enhanced DOM Event System - Setup all 6 event types with delegation this.container.addEventListener('click', this.handleSectionClick); this.container.addEventListener('mouseenter', this.handleSectionHover, true); this.container.addEventListener('mouseleave', this.handleSectionHover, true); this.container.addEventListener('keydown', this.handleKeydown); this.container.addEventListener('dragstart', this.handleDragStart); this.container.addEventListener('dragover', this.handleDragOver); this.container.addEventListener('drop', this.handleDrop); this.container.addEventListener('focusin', this.handleFocus); this.container.addEventListener('focusout', this.handleFocus); this.container.addEventListener('contextmenu', this.handleContextMenu); debug('25: Enhanced event listeners attached - container content length: ' + this.container.innerHTML.length, 'RENDER'); } createSectionElement(section) { const element = document.createElement('div'); element.setAttribute('data-section-id', section.id); debug('SECTION: Creating element for section ' + section.id + ' with content: ' + section.currentMarkdown.substring(0, 50) + '...', 'SECTION'); if (typeof marked !== 'undefined') { const html = marked.parse(section.currentMarkdown); const htmlWithTargetBlank = html.replace(/]*)>/g, ''); element.innerHTML = htmlWithTargetBlank; } else { element.innerHTML = `

${section.currentMarkdown}

`; } this.setupSectionElement(element); debug('SECTION: Section element setup complete for ' + section.id, 'SECTION'); return element; } handleSectionClick(event) { debug('CLICK: Click detected on target: ' + event.target.tagName + ' ' + (event.target.className || ''), 'CLICK'); // Don't handle clicks on form elements, buttons, or links if (event.target.closest('textarea, button, input, a')) { debug('CLICK: Ignoring click on form element', 'CLICK'); return; } const sectionElement = event.target.closest('.ui-edit-section'); debug('CLICK: Found section element: ' + (sectionElement ? sectionElement.getAttribute('data-section-id') : 'null'), 'CLICK'); if (!sectionElement) return; const sectionId = sectionElement.getAttribute('data-section-id'); debug('CLICK: Section ID: ' + sectionId, 'CLICK'); if (!sectionId) return; // Track the click event this.trackEvent('section-click', { sectionId, target: event.target.tagName.toLowerCase(), event, timestamp: Date.now() }); // Check if this section is already being edited const section = this.sectionManager.sections.get(sectionId); if (section && section.isEditing()) { console.log('Section already being edited:', sectionId); return; } try { console.log('Starting edit for section:', sectionId); this.sectionManager.startEditing(sectionId); } catch (error) { console.error('Failed to start editing:', error); } } /** * Handle hover events (mouseenter/mouseleave) * @param {Event} event - Mouse event */ handleSectionHover(event) { const sectionElement = event.target.closest('.ui-edit-section'); if (!sectionElement) return; const sectionId = sectionElement.getAttribute('data-section-id'); if (!sectionId) return; const eventType = event.type === 'mouseenter' ? 'section-hover-enter' : 'section-hover-leave'; // Track the hover event this.trackEvent(eventType, { sectionId, event, timestamp: Date.now() }); } /** * Handle drag and drop events for section reordering * @param {Event} event - Drag event */ handleDragStart(event) { const sectionElement = event.target.closest('.ui-edit-section'); if (!sectionElement) return; const sectionId = sectionElement.getAttribute('data-section-id'); if (!sectionId) return; // Store the section ID being dragged event.dataTransfer.setData('text/plain', sectionId); event.dataTransfer.effectAllowed = 'move'; // Track the drag start event this.trackEvent('section-drag-start', { sectionId, event, timestamp: Date.now() }); } handleDragOver(event) { const sectionElement = event.target.closest('.ui-edit-section'); if (!sectionElement) return; const sectionId = sectionElement.getAttribute('data-section-id'); if (!sectionId) return; event.preventDefault(); // Allow drop event.dataTransfer.dropEffect = 'move'; // Track the drag over event this.trackEvent('section-drag-over', { sectionId, event, timestamp: Date.now() }); } handleDrop(event) { const sectionElement = event.target.closest('.ui-edit-section'); if (!sectionElement) return; const targetSectionId = sectionElement.getAttribute('data-section-id'); const draggedSectionId = event.dataTransfer.getData('text/plain'); if (!targetSectionId || !draggedSectionId || targetSectionId === draggedSectionId) return; event.preventDefault(); // Track the drop event this.trackEvent('section-drop', { draggedSectionId, targetSectionId, event, timestamp: Date.now() }); // Emit section reorder event this.sectionManager.emit('section-reorder', { draggedSectionId, targetSectionId, action: 'reorder' }); } /** * Handle focus events for accessibility * @param {Event} event - Focus event */ handleFocus(event) { const sectionElement = event.target.closest('.ui-edit-section'); if (!sectionElement) return; const sectionId = sectionElement.getAttribute('data-section-id'); if (!sectionId) return; const eventType = event.type === 'focusin' ? 'section-focus-in' : 'section-focus-out'; // Track the focus event this.trackEvent(eventType, { sectionId, target: event.target.tagName.toLowerCase(), event, timestamp: Date.now() }); } /** * Handle context menu events for right-click operations * @param {Event} event - Context menu event */ handleContextMenu(event) { const sectionElement = event.target.closest('.ui-edit-section'); if (!sectionElement) return; const sectionId = sectionElement.getAttribute('data-section-id'); if (!sectionId) return; // Track the context menu event this.trackEvent('section-context-menu', { sectionId, x: event.clientX, y: event.clientY, event, timestamp: Date.now() }); // Prevent default context menu for sections event.preventDefault(); // Show custom context menu this.showSectionContextMenu(sectionId, event.clientX, event.clientY); } /** * Show custom context menu for section operations * @param {string} sectionId - Section ID * @param {number} x - X coordinate * @param {number} y - Y coordinate */ showSectionContextMenu(sectionId, x, y) { // Remove existing context menu const existingMenu = document.querySelector('.ui-edit-context-menu'); if (existingMenu) { existingMenu.remove(); } const menu = document.createElement('div'); menu.className = 'ui-edit-context-menu'; menu.style.cssText = ` position: fixed; left: ${x}px; top: ${y}px; background: white; border: 1px solid #ccc; border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; min-width: 150px; `; const section = this.sectionManager.sections.get(sectionId); const isEditing = section && section.isEditing(); const menuItems = [ { text: 'Edit Section', action: () => this.sectionManager.startEditing(sectionId), disabled: isEditing }, { text: 'Copy Section', action: () => this.copySectionToClipboard(sectionId), disabled: false }, { text: 'Delete Section', action: () => this.deleteSection(sectionId), disabled: isEditing }, { text: '—', action: null, disabled: false }, // Separator { text: 'Move Up', action: () => this.moveSectionUp(sectionId), disabled: false }, { text: 'Move Down', action: () => this.moveSectionDown(sectionId), disabled: false } ]; menuItems.forEach(item => { if (item.text === '—') { const separator = document.createElement('div'); separator.style.cssText = 'height: 1px; background: #eee; margin: 4px 0;'; menu.appendChild(separator); } else { const menuItem = document.createElement('div'); menuItem.textContent = item.text; menuItem.style.cssText = ` padding: 8px 12px; cursor: ${item.disabled ? 'not-allowed' : 'pointer'}; color: ${item.disabled ? '#999' : '#333'}; background: ${item.disabled ? 'transparent' : 'white'}; `; if (!item.disabled) { menuItem.addEventListener('mouseenter', () => { menuItem.style.background = '#f0f0f0'; }); menuItem.addEventListener('mouseleave', () => { menuItem.style.background = 'white'; }); menuItem.addEventListener('click', () => { item.action(); menu.remove(); }); } menu.appendChild(menuItem); } }); document.body.appendChild(menu); // Remove menu when clicking elsewhere const removeMenu = (e) => { if (!menu.contains(e.target)) { menu.remove(); document.removeEventListener('click', removeMenu); } }; setTimeout(() => document.addEventListener('click', removeMenu), 100); } /** * Context menu actions */ copySectionToClipboard(sectionId) { const section = this.sectionManager.sections.get(sectionId); if (section) { navigator.clipboard.writeText(section.currentMarkdown).then(() => { console.log('Section copied to clipboard'); }); } } deleteSection(sectionId) { if (confirm('Are you sure you want to delete this section?')) { this.sectionManager.sections.delete(sectionId); const element = this.findSectionElement(sectionId); if (element) { element.remove(); } } } moveSectionUp(sectionId) { const element = this.findSectionElement(sectionId); if (element && element.previousElementSibling) { element.parentNode.insertBefore(element, element.previousElementSibling); } } moveSectionDown(sectionId) { const element = this.findSectionElement(sectionId); if (element && element.nextElementSibling) { element.parentNode.insertBefore(element.nextElementSibling, element); } } showEditor(sectionId, content) { const element = this.findSectionElement(sectionId); if (!element) return; this.hideCurrentEditor(); const section = this.sectionManager.sections.get(sectionId); const isImageSection = section && section.isImage(); if (isImageSection) { this.showImageEditor(sectionId, section); return; } const editorContainer = document.createElement('div'); editorContainer.className = 'ui-edit-editor-container'; const textarea = document.createElement('textarea'); textarea.className = 'ui-edit-textarea ui-edit-textarea-main'; textarea.value = content; textarea.style.cssText = ` flex: 1; min-height: 100px; font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; font-size: 14px; line-height: 1.5; padding: 12px; border: 2px solid #007bff; border-radius: 6px; resize: vertical; outline: none; background: white; color: #333; `; // Setup auto-resize functionality this.setupAutoResize(textarea); const controls = document.createElement('div'); controls.className = 'ui-edit-controls'; controls.style.cssText = ` display: flex; gap: 8px; justify-content: flex-end; margin-top: 8px; `; const acceptBtn = this.createButton('Accept', 'ui-edit-button-accept', this.handleAccept); const cancelBtn = this.createButton('Cancel', 'ui-edit-button-cancel', this.handleCancel); controls.appendChild(acceptBtn); controls.appendChild(cancelBtn); editorContainer.appendChild(textarea); editorContainer.appendChild(controls); element.innerHTML = ''; element.appendChild(editorContainer); textarea.focus(); this.editingSections.add(sectionId); textarea.addEventListener('input', () => { this.sectionManager.updateContent(sectionId, textarea.value); }); // Add keyboard shortcuts textarea.addEventListener('keydown', this.handleKeydown); } /** * Show image editor with manipulation controls * @param {string} sectionId - The section ID * @param {Section} section - The section object */ showImageEditor(sectionId, section) { const element = this.findSectionElement(sectionId); if (!element) return; this.hideCurrentEditor(); // Track staging state for this editor const stagingState = { originalMarkdown: section.currentMarkdown, currentAltText: '', currentImageSrc: '', stagedImageSrc: null, stagedAltText: null, hasChanges: false }; const editorContainer = document.createElement('div'); editorContainer.className = 'ui-edit-image-editor-container'; editorContainer.style.cssText = ` display: flex; flex-direction: column; gap: 12px; margin-top: 12px; padding: 16px; background: #f8f9fa; border-radius: 8px; border: 2px solid #007bff; `; // Parse markdown to extract image info const imageMatch = section.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/); if (imageMatch) { const [, altText, imageSrc] = imageMatch; stagingState.currentAltText = altText; stagingState.currentImageSrc = imageSrc; } // Image preview with drop zone const imagePreview = document.createElement('div'); imagePreview.className = 'ui-edit-image-preview'; imagePreview.style.cssText = ` max-width: 100%; text-align: center; background: white; padding: 16px; border-radius: 6px; border: 2px dashed #007bff; transition: all 0.3s ease; cursor: pointer; min-height: 200px; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; `; // Function to update image preview const updateImagePreview = (imageSrc, altText) => { imagePreview.innerHTML = ''; if (imageSrc) { const img = document.createElement('img'); img.src = imageSrc; img.alt = altText || ''; img.style.cssText = ` max-width: 100%; max-height: 250px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); `; imagePreview.appendChild(img); // Add overlay for drop zone const overlay = document.createElement('div'); overlay.className = 'drop-overlay'; overlay.style.cssText = ` position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 123, 255, 0.1); border-radius: 6px; display: none; align-items: center; justify-content: center; color: #007bff; font-weight: bold; font-size: 18px; `; overlay.textContent = '📁 Drop new image here'; imagePreview.appendChild(overlay); } else { // Show drop zone placeholder const placeholder = document.createElement('div'); placeholder.style.cssText = ` text-align: center; color: #6c757d; font-size: 16px; `; placeholder.innerHTML = `
📁
Drop image here or click to select
Supports JPG, PNG, GIF, WebP
`; imagePreview.appendChild(placeholder); } }; // Initialize preview updateImagePreview(stagingState.currentImageSrc, stagingState.currentAltText); // File input for image selection const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'image/*'; fileInput.style.display = 'none'; // Function to handle image file selection const handleImageFile = (file) => { if (file && file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = (event) => { stagingState.stagedImageSrc = event.target.result; stagingState.hasChanges = true; updateImagePreview(stagingState.stagedImageSrc, altTextInput.value); updateChangeIndicator(); }; reader.readAsDataURL(file); } }; // Drag and drop functionality imagePreview.addEventListener('dragover', (e) => { e.preventDefault(); imagePreview.style.borderColor = '#28a745'; imagePreview.style.backgroundColor = '#f8fff8'; const overlay = imagePreview.querySelector('.drop-overlay'); if (overlay) overlay.style.display = 'flex'; }); imagePreview.addEventListener('dragleave', (e) => { e.preventDefault(); imagePreview.style.borderColor = '#007bff'; imagePreview.style.backgroundColor = 'white'; const overlay = imagePreview.querySelector('.drop-overlay'); if (overlay) overlay.style.display = 'none'; }); imagePreview.addEventListener('drop', (e) => { e.preventDefault(); imagePreview.style.borderColor = '#007bff'; imagePreview.style.backgroundColor = 'white'; const overlay = imagePreview.querySelector('.drop-overlay'); if (overlay) overlay.style.display = 'none'; const files = e.dataTransfer.files; if (files.length > 0) { handleImageFile(files[0]); } }); // Click to select file imagePreview.addEventListener('click', () => { fileInput.click(); }); fileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) { handleImageFile(e.target.files[0]); } }); // Alt text editor const altTextContainer = document.createElement('div'); altTextContainer.style.cssText = `margin-bottom: 16px;`; const altTextLabel = document.createElement('label'); altTextLabel.textContent = 'Alt Text:'; altTextLabel.style.cssText = `display: block; margin-bottom: 4px; font-weight: bold;`; const altTextInput = document.createElement('input'); altTextInput.type = 'text'; altTextInput.value = stagingState.currentAltText; altTextInput.style.cssText = ` width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; `; // Track alt text changes altTextInput.addEventListener('input', () => { stagingState.stagedAltText = altTextInput.value; stagingState.hasChanges = altTextInput.value !== stagingState.currentAltText || stagingState.stagedImageSrc !== null; updateChangeIndicator(); }); // Add keyboard shortcuts to alt text input altTextInput.addEventListener('keydown', this.handleKeydown); altTextContainer.appendChild(altTextLabel); altTextContainer.appendChild(altTextInput); // Change indicator const changeIndicator = document.createElement('div'); changeIndicator.className = 'change-indicator'; changeIndicator.style.cssText = ` padding: 8px 12px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 4px; color: #856404; font-size: 14px; text-align: center; display: none; `; changeIndicator.textContent = '⚠️ You have unsaved changes'; const updateChangeIndicator = () => { if (stagingState.hasChanges) { changeIndicator.style.display = 'block'; } else { changeIndicator.style.display = 'none'; } }; // Standard editor controls const editorControls = document.createElement('div'); editorControls.className = 'ui-edit-controls'; editorControls.style.cssText = ` display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; `; const acceptBtn = this.createButton('✓ Accept', 'ui-edit-accept', (e) => { // Apply staged changes only when accept is clicked if (stagingState.hasChanges) { let newMarkdown = stagingState.originalMarkdown; // Apply image source change if staged if (stagingState.stagedImageSrc !== null) { const currentImageMatch = newMarkdown.match(/!\[(.*?)\]\((.*?)\)/); if (currentImageMatch) { newMarkdown = newMarkdown.replace( /!\[(.*?)\]\((.*?)\)/, `![${currentImageMatch[1]}](${stagingState.stagedImageSrc})` ); } } // Apply alt text change if staged if (stagingState.stagedAltText !== null) { newMarkdown = newMarkdown.replace( /!\[(.*?)\]/, `![${stagingState.stagedAltText}]` ); } // Update section with final changes this.sectionManager.updateContent(sectionId, newMarkdown); this.updateSectionContent(sectionId, newMarkdown); } // Accept changes and hide editor this.sectionManager.acceptChanges(sectionId); this.hideEditor(sectionId); }); const cancelBtn = this.createButton('✗ Cancel', 'ui-edit-cancel', (e) => { // Discard all staged changes and hide editor this.sectionManager.cancelChanges(sectionId); this.hideEditor(sectionId); }); const resetBtn = this.createButton('↺ Reset', 'ui-edit-reset', (e) => { // Reset section to original content (like reset all does) this.sectionManager.resetSection(sectionId); // Get the reset section to update staging state with original content const resetSection = this.sectionManager.sections.get(sectionId); if (resetSection) { // Update DOM immediately to show reset content this.updateSectionContent(sectionId, resetSection.currentMarkdown); // Parse original image info from reset content const originalImageMatch = resetSection.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/); if (originalImageMatch) { const [, originalAltText, originalImageSrc] = originalImageMatch; // Update staging state to reflect original content stagingState.originalMarkdown = resetSection.currentMarkdown; stagingState.currentAltText = originalAltText; stagingState.currentImageSrc = originalImageSrc; } } // Clear any staged changes stagingState.stagedImageSrc = null; stagingState.stagedAltText = null; stagingState.hasChanges = false; // Reset alt text input to original altTextInput.value = stagingState.currentAltText; // Reset preview to original updateImagePreview(stagingState.currentImageSrc, stagingState.currentAltText); updateChangeIndicator(); }); acceptBtn.style.background = '#28a745'; cancelBtn.style.background = '#dc3545'; resetBtn.style.background = '#fd7e14'; editorControls.appendChild(acceptBtn); editorControls.appendChild(cancelBtn); editorControls.appendChild(resetBtn); // Assemble the editor editorContainer.appendChild(imagePreview); editorContainer.appendChild(altTextContainer); editorContainer.appendChild(changeIndicator); editorContainer.appendChild(editorControls); editorContainer.appendChild(fileInput); element.appendChild(editorContainer); altTextInput.focus(); this.editingSections.add(sectionId); } /** * Image manipulation methods * Note: Image replacement is now integrated into the main image editor with drag & drop */ resizeImage(sectionId) { const section = this.sectionManager.sections.get(sectionId); const size = prompt('Enter image width (e.g., 300px, 50%, or leave empty for auto):', ''); if (size !== null) { const imageMatch = section.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/); if (imageMatch) { const style = size ? ` style="width: ${size};"` : ''; const newMarkdown = section.currentMarkdown.replace( /!\[(.*?)\]\((.*?)\)/, `${imageMatch[1]}` ); this.sectionManager.updateContent(sectionId, newMarkdown); } } } addImageCaption(sectionId) { const section = this.sectionManager.sections.get(sectionId); const caption = prompt('Enter image caption:', ''); if (caption) { const newMarkdown = section.currentMarkdown + `\n\n*${caption}*`; this.sectionManager.updateContent(sectionId, newMarkdown); } } removeImage(sectionId) { if (confirm('Are you sure you want to remove this image?')) { this.sectionManager.updateContent(sectionId, ''); this.hideEditor(sectionId); } } createButton(text, className, handler) { const btn = document.createElement('button'); btn.textContent = text; btn.className = className; btn.style.cssText = ` background: #007bff; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 14px; `; btn.addEventListener('click', handler); return btn; } handleAccept(event) { const sectionId = this.getCurrentEditingSectionId(event.target); if (sectionId) { this.sectionManager.acceptChanges(sectionId); } } handleCancel(event) { const sectionId = this.getCurrentEditingSectionId(event.target); if (sectionId) { this.sectionManager.cancelChanges(sectionId); } } handleReset(event) { const sectionId = this.getCurrentEditingSectionId(event.target); if (sectionId) { this.sectionManager.resetSection(sectionId); } } handleKeydown(event) { // Enhanced keyboard shortcuts for section editing const textarea = event.target.closest('textarea, input'); if (!textarea) return; // Find the section being edited const editorContainer = textarea.closest('.ui-edit-editor-container, .ui-edit-image-editor-container'); if (!editorContainer) return; const sectionElement = editorContainer.parentElement; const sectionId = sectionElement ? sectionElement.getAttribute('data-section-id') : null; if (!sectionId) return; let shortcutKey = ''; let shortcutAction = ''; // Handle keyboard shortcuts if (event.ctrlKey || event.metaKey) { switch (event.key) { case 'Enter': event.preventDefault(); shortcutKey = (event.ctrlKey ? 'ctrl' : 'cmd') + '+enter'; shortcutAction = 'accept'; this.sectionManager.acceptChanges(sectionId); debug('Keyboard shortcut: Ctrl+Enter - accepted changes for section ' + sectionId, 'KEYBOARD'); break; case 'Escape': event.preventDefault(); shortcutKey = (event.ctrlKey ? 'ctrl' : 'cmd') + '+escape'; shortcutAction = 'cancel'; this.sectionManager.cancelChanges(sectionId); debug('Keyboard shortcut: Ctrl+Escape - cancelled changes for section ' + sectionId, 'KEYBOARD'); break; case 's': event.preventDefault(); shortcutKey = (event.ctrlKey ? 'ctrl' : 'cmd') + '+s'; shortcutAction = 'save'; this.sectionManager.acceptChanges(sectionId); debug('Keyboard shortcut: Ctrl+S - saved changes for section ' + sectionId, 'KEYBOARD'); break; } } // Handle plain Escape (without Ctrl) if (event.key === 'Escape' && !event.ctrlKey && !event.metaKey) { event.preventDefault(); shortcutKey = 'escape'; shortcutAction = 'cancel'; this.sectionManager.cancelChanges(sectionId); debug('Keyboard shortcut: Escape - cancelled changes for section ' + sectionId, 'KEYBOARD'); } // Track keyboard shortcut events if (shortcutKey && shortcutAction) { this.trackEvent('keyboard-shortcut', { sectionId, shortcut: shortcutKey, action: shortcutAction, key: event.key, ctrlKey: event.ctrlKey, metaKey: event.metaKey, event, timestamp: Date.now() }); } } getCurrentEditingSectionId(button) { const editorContainer = button.closest('.ui-edit-editor-container, .ui-edit-image-editor-container'); if (!editorContainer) return null; const sectionElement = editorContainer.parentElement; return sectionElement ? sectionElement.getAttribute('data-section-id') : null; } hideEditor(sectionId) { const element = this.findSectionElement(sectionId); if (!element) return; // Remove any editor UI containers from the DOM const textEditorContainer = element.querySelector('.ui-edit-editor-container'); if (textEditorContainer) { textEditorContainer.remove(); } const imageEditorContainer = element.querySelector('.ui-edit-image-editor-container'); if (imageEditorContainer) { imageEditorContainer.remove(); } const section = this.sectionManager.sections.get(sectionId); if (section) { this.updateSectionContent(sectionId, section.currentMarkdown); } this.editingSections.delete(sectionId); } hideCurrentEditor() { this.editingSections.forEach(sectionId => { const element = this.findSectionElement(sectionId); if (element && element.querySelector('.ui-edit-editor-container')) { this.hideEditor(sectionId); } }); } updateSectionContent(sectionId, content) { const element = this.findSectionElement(sectionId); if (!element) return; if (typeof marked !== 'undefined') { const html = marked.parse(content); const htmlWithTargetBlank = html.replace(/
]*)>/g, ''); element.innerHTML = htmlWithTargetBlank; } else { element.innerHTML = `

${content}

`; } this.setupSectionElement(element); } findSectionElement(sectionId) { return this.container.querySelector(`[data-section-id="${sectionId}"]`); } setupSectionElement(element) { element.className = 'ui-edit-section'; element.draggable = true; // Enable drag and drop element.tabIndex = 0; // Make focusable for accessibility element.style.cssText = ` margin: 16px 0; padding: 12px; border-radius: 6px; cursor: pointer; transition: all 0.2s ease; border: 2px solid transparent; position: relative; `; // Add drag handle indicator const dragHandle = document.createElement('div'); dragHandle.className = 'ui-edit-drag-handle'; dragHandle.innerHTML = '⋮⋮'; dragHandle.style.cssText = ` position: absolute; left: -8px; top: 50%; transform: translateY(-50%); color: #ccc; font-size: 16px; line-height: 1; cursor: grab; opacity: 0; transition: opacity 0.2s ease; user-select: none; `; element.appendChild(dragHandle); element.removeEventListener('mouseenter', element._mouseenterHandler); element.removeEventListener('mouseleave', element._mouseleaveHandler); element._mouseenterHandler = () => { element.style.backgroundColor = 'rgba(0, 0, 0, 0.02)'; element.style.borderColor = 'rgba(0, 0, 0, 0.1)'; dragHandle.style.opacity = '1'; }; element._mouseleaveHandler = () => { element.style.backgroundColor = ''; element.style.borderColor = 'transparent'; dragHandle.style.opacity = '0'; }; element.addEventListener('mouseenter', element._mouseenterHandler); element.addEventListener('mouseleave', element._mouseleaveHandler); // Enhanced accessibility element.setAttribute('role', 'article'); element.setAttribute('aria-label', 'Editable section'); element.setAttribute('title', 'Click to edit, right-click for options, drag to reorder'); // Apply comprehensive styling enhancements this.applyComprehensiveStyling(element); } /** * Apply comprehensive styling enhancements to section elements * @param {HTMLElement} element - The section element */ applyComprehensiveStyling(element) { if (!element || !element.dataset.sectionId) return; const sectionId = element.dataset.sectionId; const section = this.sectionManager.sections.get(sectionId); if (!section) return; // Apply type-specific styling this.applyTypeSpecificStyling(element, section); // Apply state-based styling this.applyStateStyling(element, section); // Apply responsive design this.applyResponsiveStyling(element); // Apply accessibility enhancements this.enhanceAccessibility(element, section); // Apply length-based styling this.applyLengthBasedStyling(element, section); // Apply performance-optimized transitions this.applyOptimizedTransitions(element); // Apply CSS custom properties this.applyCSSCustomProperties(element, section); // Apply theme support this.applySectionTheme(element, 'light'); // Default theme // Apply content analysis styling this.analyzeContentForStyling(element, section); // Apply CSS reset and normalization this.applyCSSReset(element); // Setup animation support this.setupAnimationSupport(element); // Apply print-friendly styling this.applyPrintStyling(element); // Integrate with existing systems this.integrateWithMessageSystem(element); this.integrateWithControlPanel(element); } /** * Apply type-specific styling to section elements * @param {HTMLElement} element - Section element * @param {Section} section - Section object */ applyTypeSpecificStyling(element, section) { // Add base class element.classList.add('markitect-section-editable'); // Add type-specific class const typeClass = `markitect-section-${section.type || 'paragraph'}`; element.classList.add(typeClass); // Set data attributes element.dataset.sectionType = section.type || 'paragraph'; element.dataset.sectionId = section.id; // Type-specific styling const typeStyles = { heading: { borderLeft: '4px solid #007acc', backgroundColor: 'rgba(0, 122, 204, 0.02)', fontWeight: '600' }, code: { borderLeft: '4px solid #28a745', backgroundColor: 'rgba(40, 167, 69, 0.02)', fontFamily: 'monospace' }, list: { borderLeft: '4px solid #ffc107', backgroundColor: 'rgba(255, 193, 7, 0.02)' }, quote: { borderLeft: '4px solid #6f42c1', backgroundColor: 'rgba(111, 66, 193, 0.02)', fontStyle: 'italic' }, image: { borderLeft: '4px solid #fd7e14', backgroundColor: 'rgba(253, 126, 20, 0.02)', textAlign: 'center' }, table: { borderLeft: '4px solid #20c997', backgroundColor: 'rgba(32, 201, 151, 0.02)' }, hr: { borderLeft: '4px solid #6c757d', backgroundColor: 'rgba(108, 117, 125, 0.02)', minHeight: '20px' } }; const styles = typeStyles[section.type] || typeStyles.paragraph || {}; Object.assign(element.style, styles); } /** * Apply state-based styling * @param {HTMLElement} element - Section element * @param {Section} section - Section object */ applyStateStyling(element, section) { // Remove existing state classes element.classList.remove('section-original', 'section-editing', 'section-modified', 'section-saved'); // Apply current state class if (section.isEditing()) { element.classList.add('section-editing'); element.style.backgroundColor = 'rgba(0, 122, 204, 0.1)'; element.style.borderColor = '#007acc'; } else if (section.hasChanges()) { element.classList.add('section-modified'); element.style.backgroundColor = 'rgba(255, 193, 7, 0.1)'; element.style.borderColor = '#ffc107'; } else if (section.state === 'saved') { element.classList.add('section-saved'); element.style.backgroundColor = 'rgba(40, 167, 69, 0.1)'; element.style.borderColor = '#28a745'; } else { element.classList.add('section-original'); } } /** * Apply responsive design styling * @param {HTMLElement} element - Section element */ applyResponsiveStyling(element) { element.classList.add('section-responsive'); // Responsive styles element.style.cssText += ` max-width: 100%; min-width: 0; overflow-wrap: break-word; word-wrap: break-word; `; // Responsive behavior based on viewport const updateResponsiveStyles = () => { const width = window.innerWidth; if (width < 768) { // Mobile styles element.style.margin = '8px 0'; element.style.padding = '8px'; element.style.fontSize = '14px'; } else if (width < 1024) { // Tablet styles element.style.margin = '12px 0'; element.style.padding = '10px'; element.style.fontSize = '15px'; } else { // Desktop styles element.style.margin = '16px 0'; element.style.padding = '12px'; element.style.fontSize = '16px'; } }; updateResponsiveStyles(); window.addEventListener('resize', updateResponsiveStyles); } /** * Enhance accessibility features * @param {HTMLElement} element - Section element * @param {Section} section - Section object */ enhanceAccessibility(element, section) { // Enhanced ARIA attributes element.setAttribute('aria-describedby', `section-desc-${section.id}`); element.setAttribute('aria-labelledby', `section-label-${section.id}`); // Screen reader support const srOnly = document.createElement('span'); srOnly.className = 'sr-only'; srOnly.id = `section-desc-${section.id}`; srOnly.textContent = `${section.type} section with ${section.currentMarkdown.length} characters`; srOnly.style.cssText = ` position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; `; element.appendChild(srOnly); // Enhanced keyboard navigation element.tabIndex = 0; element.setAttribute('aria-keyshortcuts', 'Enter Space'); // Focus enhancement element.addEventListener('focus', () => { element.style.outline = '2px solid #007acc'; element.style.outlineOffset = '2px'; }); element.addEventListener('blur', () => { element.style.outline = ''; element.style.outlineOffset = ''; }); } /** * Apply length-based visual indicators * @param {HTMLElement} element - Section element * @param {Section} section - Section object */ applyLengthBasedStyling(element, section) { const length = section.currentMarkdown.length; // Remove existing length classes element.classList.remove('section-short', 'section-medium', 'section-long'); if (length < 100) { element.classList.add('section-short'); element.style.minHeight = '40px'; } else if (length < 500) { element.classList.add('section-medium'); element.style.minHeight = '60px'; } else { element.classList.add('section-long'); element.style.minHeight = '80px'; element.style.maxHeight = '400px'; element.style.overflowY = 'auto'; } // Word count indicator const wordCount = section.currentMarkdown.split(/\s+/).length; element.dataset.wordCount = wordCount.toString(); element.setAttribute('title', element.getAttribute('title') + ` (${wordCount} words)`); } /** * Apply performance-optimized CSS transitions * @param {HTMLElement} element - Section element */ applyOptimizedTransitions(element) { // GPU-accelerated transitions element.style.willChange = 'transform, opacity'; element.style.transform = 'translateZ(0)'; // Force GPU layer element.style.transition = 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)'; // Hover enhancements element.classList.add('section-hoverable'); const optimizedMouseEnter = () => { element.style.transform = 'translateZ(0) scale(1.01)'; element.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.1)'; }; const optimizedMouseLeave = () => { element.style.transform = 'translateZ(0) scale(1)'; element.style.boxShadow = ''; }; element.addEventListener('mouseenter', optimizedMouseEnter); element.addEventListener('mouseleave', optimizedMouseLeave); } /** * Apply CSS custom properties for advanced styling * @param {HTMLElement} element - Section element * @param {Section} section - Section object */ applyCSSCustomProperties(element, section) { // CSS variables for dynamic theming element.style.setProperty('--section-primary-color', '#007acc'); element.style.setProperty('--section-background', 'rgba(248, 249, 250, 0.5)'); element.style.setProperty('--section-border-radius', '6px'); element.style.setProperty('--section-padding', '12px'); element.style.setProperty('--section-margin', '16px 0'); element.style.setProperty('--section-transition', 'all 0.2s ease'); // Type-specific CSS variables const typeColors = { heading: '#007acc', code: '#28a745', list: '#ffc107', quote: '#6f42c1', image: '#fd7e14', table: '#20c997', hr: '#6c757d' }; element.style.setProperty('--section-type-color', typeColors[section.type] || '#6c757d'); element.dataset.cssVariables = 'true'; } /** * Apply theme-based styling * @param {HTMLElement} element - Section element * @param {string} theme - Theme name ('light', 'dark', 'high-contrast') */ applySectionTheme(element, theme) { element.dataset.theme = theme; element.classList.remove('theme-light', 'theme-dark', 'theme-high-contrast'); element.classList.add(`theme-${theme}`); const themes = { light: { '--section-background': 'rgba(248, 249, 250, 0.5)', '--section-text-color': '#212529', '--section-border-color': 'rgba(0, 0, 0, 0.1)', '--section-hover-bg': 'rgba(0, 0, 0, 0.02)' }, dark: { '--section-background': 'rgba(33, 37, 41, 0.8)', '--section-text-color': '#f8f9fa', '--section-border-color': 'rgba(255, 255, 255, 0.2)', '--section-hover-bg': 'rgba(255, 255, 255, 0.05)' }, 'high-contrast': { '--section-background': '#ffffff', '--section-text-color': '#000000', '--section-border-color': '#000000', '--section-hover-bg': '#f0f0f0' } }; const themeStyles = themes[theme] || themes.light; Object.entries(themeStyles).forEach(([property, value]) => { element.style.setProperty(property, value); }); } /** * Analyze content for styling purposes * @param {HTMLElement} element - Section element * @param {Section} section - Section object */ analyzeContentForStyling(element, section) { const content = section.currentMarkdown.toLowerCase(); // Content-based classes if (content.includes('```') || content.includes('`')) { element.classList.add('contains-code'); } if (content.includes('$$') || content.includes('\\(') || content.includes('\\[')) { element.classList.add('contains-math'); } if (content.includes('http') || content.includes('[') && content.includes(']')) { element.classList.add('contains-links'); } if (content.includes('![')) { element.classList.add('contains-images'); } if (content.includes('|') && content.includes('-')) { element.classList.add('contains-tables'); } // Priority content indicators if (content.includes('important') || content.includes('warning') || content.includes('!')) { element.classList.add('priority-high'); element.style.borderLeftWidth = '6px'; } } /** * Apply CSS reset and normalization * @param {HTMLElement} element - Section element */ applyCSSReset(element) { element.classList.add('css-reset'); // Normalize box model element.style.boxSizing = 'border-box'; element.style.margin = '16px 0'; element.style.padding = '12px'; // Reset browser defaults const allChildren = element.querySelectorAll('*'); allChildren.forEach(child => { child.style.boxSizing = 'border-box'; }); } /** * Setup animation support for state transitions * @param {HTMLElement} element - Section element */ setupAnimationSupport(element) { element.classList.add('animation-ready'); // Animation utility method element._animate = (animationType) => { this.animateSectionTransition(element, animationType); }; } /** * Animate section transitions * @param {HTMLElement} element - Section element * @param {string} animationType - Type of animation */ animateSectionTransition(element, animationType) { element.classList.add('section-animating'); switch (animationType) { case 'enter': element.classList.add('transition-entering'); element.style.opacity = '0'; element.style.transform = 'translateY(-10px)'; setTimeout(() => { element.style.opacity = '1'; element.style.transform = 'translateY(0)'; }, 50); break; case 'leave': element.classList.add('transition-leaving'); element.style.opacity = '0'; element.style.transform = 'translateY(10px)'; break; case 'highlight': element.style.backgroundColor = 'rgba(255, 193, 7, 0.3)'; setTimeout(() => { element.style.backgroundColor = ''; }, 1000); break; } setTimeout(() => { element.classList.remove('section-animating', 'transition-entering', 'transition-leaving'); }, 300); } /** * Apply print-friendly styling * @param {HTMLElement} element - Section element */ applyPrintStyling(element) { element.classList.add('print-friendly'); element.dataset.printOptimized = 'true'; // Print-specific styles via CSS const printStyles = document.createElement('style'); printStyles.textContent = ` @media print { .print-friendly { break-inside: avoid; margin: 8px 0; padding: 8px; border: 1px solid #000; background: white !important; color: black !important; box-shadow: none !important; transform: none !important; } .ui-edit-drag-handle, .sr-only { display: none !important; } } `; if (!document.querySelector('#section-print-styles')) { printStyles.id = 'section-print-styles'; document.head.appendChild(printStyles); } } /** * Update dynamic styles based on section state * @param {HTMLElement} element - Section element * @param {Object} newStyles - Style updates */ updateSectionDynamicStyles(element, newStyles) { Object.entries(newStyles).forEach(([property, value]) => { if (property.startsWith('--')) { element.style.setProperty(property, value); } else { element.style[property] = value; } }); } /** * Integrate with message system styling * @param {HTMLElement} element - Section element */ integrateWithMessageSystem(element) { // Add message system integration class element.classList.add('message-system-ready'); // Store reference for message positioning element.dataset.messageAnchor = 'true'; } /** * Integrate with control panel styling * @param {HTMLElement} element - Section element */ integrateWithControlPanel(element) { // Add control panel integration class element.classList.add('control-panel-aware'); // Adjust positioning to avoid overlap with control panel const adjustForControlPanel = () => { const controlPanel = document.getElementById('markitect-global-controls'); if (controlPanel) { const rect = controlPanel.getBoundingClientRect(); const elementRect = element.getBoundingClientRect(); // Avoid overlap with control panel if (elementRect.right > rect.left && elementRect.top < rect.bottom) { element.style.marginRight = `${rect.width + 20}px`; } } }; // Check for overlap periodically setTimeout(adjustForControlPanel, 100); } /** * Setup auto-resize for textarea * @param {HTMLTextAreaElement} textarea - The textarea element */ setupAutoResize(textarea) { const autoResize = () => { const transition = textarea.style.transition; textarea.style.transition = 'none'; textarea.style.height = 'auto'; const contentHeight = textarea.scrollHeight; const padding = 24; const lineCount = textarea.value.split('\n').length; const minHeight = Math.max(60, lineCount * 24 + padding); const maxHeight = 360; const newHeight = Math.max(60, Math.min(maxHeight, Math.max(minHeight, contentHeight + 4))); textarea.style.height = newHeight + 'px'; textarea.style.transition = transition; }; textarea.addEventListener('input', autoResize); textarea.addEventListener('paste', () => setTimeout(autoResize, 10)); // Initial sizing setTimeout(autoResize, 20); } /** * Create status panel for real-time status display * @returns {HTMLElement} Status panel element */ // createStatusPanel method removed - floating status panel no longer needed // Status information is displayed in the control panel menu instead /** * Update status display with current status information * @param {Object} status - Status object from SectionManager */ updateStatusDisplay(status) { // Status information is now only displayed in the control panel menu // No floating status panel needed since status is integrated into the menu return; } } /** * Main Editor Integration */ class MarkitectCleanEditor { constructor(markdownContent, containerElement, options = {}) { debug('10: MarkitectCleanEditor constructor called', 'EDITOR'); this.options = { theme: 'github', keyboardShortcuts: true, autosave: false, originalFilename: null, ...options }; debug('11: Creating SectionManager', 'EDITOR'); this.sectionManager = new SectionManager(); debug('12: Creating DOMRenderer', 'EDITOR'); this.domRenderer = new DOMRenderer(this.sectionManager, containerElement); this.originalMarkdown = markdownContent; this.cleanMarkdownContent = options.cleanMarkdownContent || markdownContent; this.dogtagContent = options.dogtagContent || ''; debug('13: About to call initialize()', 'EDITOR'); this.initialize(); debug('14: initialize() completed', 'EDITOR'); } initialize() { try { debug('15: Starting initialize() - markdown length: ' + this.originalMarkdown.length, 'INIT'); const sections = this.sectionManager.createSectionsFromMarkdown(this.originalMarkdown); debug('16: Created ' + sections.length + ' sections', 'INIT'); // Mark the dogtag section as protected if we have a dogtag if (this.dogtagContent) { debug('17: Marking dogtag section', 'INIT'); this.markDogtagSection(sections); } // Mark base64 reference sections as protected debug('18: Marking base64 reference sections', 'INIT'); this.markBase64ReferenceSections(sections); console.log(`✓ Initialized clean editor with ${sections.length} sections`); // Add global control panel debug('19: Adding global controls', 'INIT'); this.addGlobalControls(); // Setup status tracking debug('19.5: Setting up status tracking', 'INIT'); this.setupStatusTracking(); debug('20: Initialize completed successfully', 'INIT'); return true; } catch (error) { debug('ERROR in initialize: ' + error.message, 'ERROR'); console.error('Failed to initialize clean editor:', error); return false; } } markDogtagSection(sections) { // Find the section that contains the dogtag content const dogtagText = this.dogtagContent.trim(); if (!dogtagText) return; for (const section of sections) { if (section.currentMarkdown.includes(dogtagText)) { section.isDogtagSection = true; console.log('Marked dogtag section as protected:', section.id); break; } } } markBase64ReferenceSections(sections) { // Find sections that contain base64 image references if (!window.markitectBase64References) return; const refIds = Object.keys(window.markitectBase64References); if (refIds.length === 0) return; for (const section of sections) { const markdown = section.currentMarkdown; // Check if this section contains base64 reference syntax for (const refId of refIds) { if (markdown.includes(`[${refId}]:`)) { section.isBase64RefSection = true; console.log('Marked base64 reference section as protected:', section.id); break; } } } } addGlobalControls() { // Create a floating control panel const controlPanel = document.createElement('div'); controlPanel.id = 'markitect-global-controls'; controlPanel.style.cssText = ` position: fixed; top: 20px; right: 20px; background: rgba(248, 249, 250, 0.95); border: 1px solid #dee2e6; border-radius: 8px; padding: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 1000; backdrop-filter: blur(8px); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; min-width: 200px; `; const title = document.createElement('div'); title.style.cssText = ` font-weight: 600; margin-bottom: 8px; color: #495057; border-bottom: 1px solid #dee2e6; padding-bottom: 4px; `; title.textContent = 'Document Controls'; const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; flex-direction: column; gap: 6px; `; // Save Document button const saveButton = document.createElement('button'); saveButton.id = 'save-document'; saveButton.textContent = '💾 Save Document'; saveButton.style.cssText = ` background: #28a745; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 500; transition: background-color 0.2s; `; // Reset All button const resetButton = document.createElement('button'); resetButton.id = 'reset-all'; resetButton.textContent = '🔄 Reset All'; resetButton.style.cssText = ` background: #ffc107; color: #212529; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 500; transition: background-color 0.2s; `; // Show Status button const statusButton = document.createElement('button'); statusButton.id = 'show-status'; statusButton.textContent = '📊 Show Status'; statusButton.style.cssText = ` background: #17a2b8; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 500; transition: background-color 0.2s; `; buttonContainer.appendChild(saveButton); buttonContainer.appendChild(resetButton); buttonContainer.appendChild(statusButton); controlPanel.appendChild(title); controlPanel.appendChild(buttonContainer); document.body.appendChild(controlPanel); // Add event listeners document.getElementById('save-document').addEventListener('click', () => this.saveDocument()); document.getElementById('reset-all').addEventListener('click', () => this.resetAllSections()); document.getElementById('show-status').addEventListener('click', () => this.showDocumentStatus()); // Store reference for enhanced control panel methods this.controlPanel = controlPanel; // Initialize enhanced features this.setupControlPanelEnhancements(); } /** * Enhanced method to create floating control panel (for TDD compatibility) * @returns {HTMLElement} The control panel element */ createFloatingControlPanel() { // If control panel already exists, return it if (this.controlPanel) { return this.controlPanel; } // Create enhanced control panel this.addGlobalControls(); return this.controlPanel; } /** * Setup enhanced control panel features */ setupControlPanelEnhancements() { if (!this.controlPanel) return; // Add draggable functionality this.makeControlPanelDraggable(); // Add collapsible functionality this.addCollapsibleFeature(); // Add statistics display this.addStatisticsDisplay(); // Setup keyboard shortcuts this.setupControlPanelKeyboard(); // Apply responsive design this.setupResponsiveControlPanel(); // Load user preferences this.loadControlPanelPreferences(); // Setup animations this.setupControlPanelAnimations(); // Apply default theme this.setControlPanelTheme('light'); } /** * Make control panel draggable */ makeControlPanelDraggable() { if (!this.controlPanel) return; const title = this.controlPanel.querySelector('div'); if (title) { title.style.cursor = 'move'; title.draggable = true; let isDragging = false; let startX, startY, initialX, initialY; title.addEventListener('mousedown', (e) => { isDragging = true; startX = e.clientX; startY = e.clientY; initialX = this.controlPanel.offsetLeft; initialY = this.controlPanel.offsetTop; title.style.userSelect = 'none'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; this.controlPanel.style.left = `${initialX + deltaX}px`; this.controlPanel.style.top = `${initialY + deltaY}px`; this.controlPanel.style.right = 'auto'; // Override right positioning }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; title.style.userSelect = ''; this.saveControlPanelPreferences(); } }); } } /** * Add collapsible/expandable functionality */ addCollapsibleFeature() { if (!this.controlPanel) return; const title = this.controlPanel.querySelector('div'); const buttonContainer = this.controlPanel.querySelector('div:last-child'); if (title && buttonContainer) { // Add toggle button const toggleBtn = document.createElement('span'); toggleBtn.textContent = '▼'; toggleBtn.className = 'panel-toggle'; toggleBtn.style.cssText = ` float: right; cursor: pointer; font-size: 12px; transition: transform 0.3s ease; `; title.appendChild(toggleBtn); // Toggle functionality let isCollapsed = false; toggleBtn.addEventListener('click', () => { this.toggleControlPanel(); }); } } /** * Toggle control panel collapsed state */ toggleControlPanel() { if (!this.controlPanel) return; const buttonContainer = this.controlPanel.querySelector('div:last-child'); const toggleBtn = this.controlPanel.querySelector('.panel-toggle'); if (buttonContainer && toggleBtn) { const isCollapsed = buttonContainer.style.display === 'none'; if (isCollapsed) { buttonContainer.style.display = 'flex'; toggleBtn.textContent = '▼'; toggleBtn.style.transform = 'rotate(0deg)'; this.controlPanel.classList.remove('collapsed'); } else { buttonContainer.style.display = 'none'; toggleBtn.textContent = '▶'; toggleBtn.style.transform = 'rotate(-90deg)'; this.controlPanel.classList.add('collapsed'); } this.saveControlPanelPreferences(); } } /** * Add real-time statistics display */ addStatisticsDisplay() { if (!this.controlPanel) return; const statsDiv = document.createElement('div'); statsDiv.className = 'control-panel-stats'; statsDiv.style.cssText = ` margin-top: 8px; padding-top: 8px; border-top: 1px solid #dee2e6; font-size: 12px; color: #6c757d; `; // Insert before button container const buttonContainer = this.controlPanel.querySelector('div:last-child'); this.controlPanel.insertBefore(statsDiv, buttonContainer); // Update stats periodically this.updateControlPanelStats(); setInterval(() => this.updateControlPanelStats(), 3000); } /** * Update control panel statistics */ updateControlPanelStats() { const statsDiv = this.controlPanel?.querySelector('.control-panel-stats'); if (!statsDiv) return; const status = this.sectionManager.getDocumentStatus(); const eventStats = this.domRenderer.getEventStats(); statsDiv.innerHTML = `
${status.totalSections} sections
${status.editingSections} editing
${eventStats.totalEvents} events
`; } /** * Setup keyboard shortcuts for control panel */ setupControlPanelKeyboard() { document.addEventListener('keydown', (e) => { this.handleControlPanelKeyboard(e); }); } /** * Handle control panel keyboard shortcuts * @param {KeyboardEvent} e - Keyboard event */ handleControlPanelKeyboard(e) { // Ctrl+P: Toggle panel if (e.ctrlKey && e.key === 'p') { e.preventDefault(); this.toggleControlPanel(); this.domRenderer.trackEvent('keyboard-shortcut', { shortcut: 'ctrl+p', action: 'toggle-panel' }); } // Ctrl+S: Save document if (e.ctrlKey && e.key === 's') { e.preventDefault(); this.saveDocument(); this.domRenderer.trackEvent('keyboard-shortcut', { shortcut: 'ctrl+s', action: 'save' }); } // Ctrl+Shift+S: Show status if (e.ctrlKey && e.shiftKey && e.key === 'S') { e.preventDefault(); this.showDocumentStatus(); this.domRenderer.trackEvent('keyboard-shortcut', { shortcut: 'ctrl+shift+s', action: 'status' }); } } /** * Setup responsive design for control panel */ setupResponsiveControlPanel() { this.adjustControlPanelForViewport(window.innerWidth); window.addEventListener('resize', () => { this.adjustControlPanelForViewport(window.innerWidth); }); } /** * Adjust control panel for different viewport sizes * @param {number} width - Viewport width */ adjustControlPanelForViewport(width) { if (!this.controlPanel) return; if (width < 768) { // Mobile layout this.controlPanel.style.cssText += ` top: 10px; right: 10px; left: auto; min-width: 150px; font-size: 12px; `; } else { // Desktop layout this.controlPanel.style.cssText += ` top: 20px; right: 20px; left: auto; min-width: 200px; font-size: 14px; `; } } /** * Save control panel preferences to localStorage */ saveControlPanelPreferences() { if (!this.controlPanel) return; try { const preferences = { top: this.controlPanel.style.top, left: this.controlPanel.style.left, right: this.controlPanel.style.right, collapsed: this.controlPanel.classList.contains('collapsed'), theme: this.controlPanel.dataset.theme || 'light' }; localStorage.setItem('markitect-control-panel-prefs', JSON.stringify(preferences)); } catch (error) { console.warn('Could not save control panel preferences:', error); } } /** * Load control panel preferences from localStorage */ loadControlPanelPreferences() { if (!this.controlPanel) return; try { const saved = localStorage.getItem('markitect-control-panel-prefs'); if (saved) { const preferences = JSON.parse(saved); if (preferences.top) this.controlPanel.style.top = preferences.top; if (preferences.left) this.controlPanel.style.left = preferences.left; if (preferences.right) this.controlPanel.style.right = preferences.right; if (preferences.collapsed) { this.toggleControlPanel(); } if (preferences.theme) { this.setControlPanelTheme(preferences.theme); } } } catch (error) { console.warn('Could not load control panel preferences:', error); } } /** * Setup control panel animations */ setupControlPanelAnimations() { if (!this.controlPanel) return; this.controlPanel.style.transition = 'all 0.3s ease'; this.animateControlPanel('fadeIn'); } /** * Animate control panel * @param {string} animation - Animation type */ animateControlPanel(animation) { if (!this.controlPanel) return; switch (animation) { case 'fadeIn': this.controlPanel.style.opacity = '0'; setTimeout(() => { this.controlPanel.style.opacity = '1'; }, 100); break; case 'slideIn': this.controlPanel.style.transform = 'translateX(100%)'; setTimeout(() => { this.controlPanel.style.transform = 'translateX(0)'; }, 100); break; } } /** * Set control panel theme * @param {string} theme - Theme name ('light', 'dark') */ setControlPanelTheme(theme) { if (!this.controlPanel) return; this.controlPanel.dataset.theme = theme; const themes = { light: { background: 'rgba(248, 249, 250, 0.95)', border: '#dee2e6', text: '#495057', buttonColors: { save: '#28a745', reset: '#ffc107', status: '#17a2b8' } }, dark: { background: 'rgba(33, 37, 41, 0.95)', border: '#495057', text: '#f8f9fa', buttonColors: { save: '#198754', reset: '#fd7e14', status: '#0dcaf0' } } }; const selectedTheme = themes[theme] || themes.light; this.controlPanel.style.background = selectedTheme.background; this.controlPanel.style.borderColor = selectedTheme.border; this.controlPanel.style.color = selectedTheme.text; // Update button colors const buttons = this.controlPanel.querySelectorAll('button'); buttons.forEach((button, index) => { const colors = Object.values(selectedTheme.buttonColors); if (colors[index]) { button.style.backgroundColor = colors[index]; } }); this.saveControlPanelPreferences(); } saveDocument() { try { const markdown = this.sectionManager.getDocumentMarkdown(); // Generate intelligent filename const filename = this.generateSaveFilename(); // Create a download link const blob = new Blob([markdown], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.showMessage(`Document saved as ${filename}!`, 'success'); console.log('Document saved:', filename, markdown.length, 'characters'); } catch (error) { this.showMessage('Failed to save document: ' + error.message, 'error'); console.error('Save failed:', error); } } resetAllSections() { if (!confirm('Reset all sections to their original content? This will lose all changes and cannot be undone.')) { return; } try { const sections = this.sectionManager.getAllSections(); sections.forEach(section => { this.sectionManager.resetSection(section.id); }); this.showMessage('All sections reset to original content', 'info'); console.log('All sections reset'); } catch (error) { this.showMessage('Failed to reset sections: ' + error.message, 'error'); console.error('Reset failed:', error); } } showStatus() { const sections = this.sectionManager.getSectionStatus(); const totalSections = sections.length; const editedSections = sections.filter(s => s.hasChanges).length; const currentlyEditing = sections.filter(s => s.isEditing).length; const statusHtml = `

Document Status

Total Sections: ${totalSections}

Modified Sections: ${editedSections}

Currently Editing: ${currentlyEditing}


Section Details:

`; this.showModal('Document Status', statusHtml); } showMessage(message, type = 'info', options = {}) { // Enhanced professional message system with color-coded positioning const { position = 'top-center', duration = 3000, dismissible = true, icon = true, animation = true } = options; const messageDiv = document.createElement('div'); messageDiv.className = `markitect-message markitect-message-${type}`; // Get positioning styles const positionStyles = this.getMessagePositionStyles(position); // Get color scheme for message type const colors = this.getMessageColors(type); // Create icon if enabled const iconHtml = icon ? this.getMessageIcon(type) : ''; messageDiv.style.cssText = ` position: fixed; ${positionStyles} background: ${colors.background}; color: ${colors.text}; border: 1px solid ${colors.border}; border-left: 4px solid ${colors.accent}; border-radius: 8px; padding: 16px 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; font-weight: 500; z-index: 10001; box-shadow: 0 8px 24px rgba(0,0,0,0.12), 0 4px 8px rgba(0,0,0,0.08); backdrop-filter: blur(8px); max-width: 400px; min-width: 200px; transform: ${animation ? 'translateY(-20px)' : 'none'}; opacity: ${animation ? '0' : '1'}; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); cursor: ${dismissible ? 'pointer' : 'default'}; `; // Set message content with icon messageDiv.innerHTML = `
${iconHtml}
${message}
${dismissible ? '
×
' : ''}
`; // Add to DOM and handle stacking document.body.appendChild(messageDiv); this.stackMessages(); // Animation entrance if (animation) { requestAnimationFrame(() => { messageDiv.style.transform = 'translateY(0)'; messageDiv.style.opacity = '1'; }); } // Auto-dismiss functionality const autoRemove = () => { if (messageDiv.parentNode) { messageDiv.style.transform = 'translateY(-20px)'; messageDiv.style.opacity = '0'; setTimeout(() => { if (messageDiv.parentNode) { messageDiv.parentNode.removeChild(messageDiv); this.stackMessages(); } }, 300); } }; // Manual dismiss functionality if (dismissible) { messageDiv.addEventListener('click', autoRemove); } // Auto-dismiss timer if (duration > 0) { setTimeout(autoRemove, duration); } return messageDiv; } /** * Get position styles for message positioning * @param {string} position - Position identifier * @returns {string} CSS positioning styles */ getMessagePositionStyles(position) { const positions = { 'top-left': 'top: 20px; left: 20px;', 'top-center': 'top: 20px; left: 50%; transform: translateX(-50%);', 'top-right': 'top: 20px; right: 20px;', 'center': 'top: 50%; left: 50%; transform: translate(-50%, -50%);', 'bottom-left': 'bottom: 20px; left: 20px;', 'bottom-center': 'bottom: 20px; left: 50%; transform: translateX(-50%);', 'bottom-right': 'bottom: 20px; right: 20px;' }; return positions[position] || positions['top-center']; } /** * Get color scheme for message type * @param {string} type - Message type * @returns {Object} Color scheme object */ getMessageColors(type) { const schemes = { success: { background: '#d4edda', text: '#155724', border: '#c3e6cb', accent: '#28a745' }, error: { background: '#f8d7da', text: '#721c24', border: '#f5c6cb', accent: '#dc3545' }, warning: { background: '#fff3cd', text: '#856404', border: '#ffeaa7', accent: '#ffc107' }, info: { background: '#d1ecf1', text: '#0c5460', border: '#bee5eb', accent: '#17a2b8' }, debug: { background: '#e2e3e5', text: '#383d41', border: '#d6d8db', accent: '#6c757d' } }; return schemes[type] || schemes.info; } /** * Get icon HTML for message type * @param {string} type - Message type * @returns {string} Icon HTML */ getMessageIcon(type) { const icons = { success: '
', error: '
', warning: '
', info: '
', debug: '
🐛
' }; return icons[type] || icons.info; } /** * Stack messages to prevent overlap */ stackMessages() { const messages = Array.from(document.querySelectorAll('.markitect-message')).filter(el => el.style.display !== 'none' && el.parentNode ); // Group messages by position const messageGroups = { 'top': [], 'center': [], 'bottom': [] }; messages.forEach(msg => { const styles = msg.style; if (styles.top && styles.top !== 'auto' && styles.top !== '') { messageGroups.top.push(msg); } else if (styles.bottom && styles.bottom !== 'auto' && styles.bottom !== '') { messageGroups.bottom.push(msg); } else { messageGroups.center.push(msg); } }); // Stack top messages downward let topOffset = 20; messageGroups.top.forEach(msg => { msg.style.top = `${topOffset}px`; topOffset += msg.offsetHeight + 12; }); // Stack bottom messages upward let bottomOffset = 20; messageGroups.bottom.forEach(msg => { msg.style.bottom = `${bottomOffset}px`; bottomOffset += msg.offsetHeight + 12; }); } showModal(title, content) { // Remove existing modal if present const existingModal = document.getElementById('markitect-modal'); if (existingModal) { existingModal.remove(); } const modalOverlay = document.createElement('div'); modalOverlay.id = 'markitect-modal'; modalOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 10000; `; const modal = document.createElement('div'); modal.style.cssText = ` background: white; border-radius: 8px; padding: 24px; max-width: 600px; max-height: 80vh; overflow-y: auto; box-shadow: 0 8px 32px rgba(0,0,0,0.3); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; `; const closeBtn = document.createElement('button'); closeBtn.textContent = '×'; closeBtn.style.cssText = ` float: right; background: none; border: none; font-size: 24px; cursor: pointer; color: #6c757d; margin: -8px -8px 0 0; `; const modalContent = document.createElement('div'); modalContent.innerHTML = `

${title}

${content}`; function closeModal() { modalOverlay.remove(); } closeBtn.addEventListener('click', closeModal); modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) closeModal(); }); modal.appendChild(closeBtn); modal.appendChild(modalContent); modalOverlay.appendChild(modal); document.body.appendChild(modalOverlay); } /** * Show comprehensive document status dialog with detailed statistics */ showDocumentStatus() { const status = this.sectionManager.getDocumentStatus(); const eventStats = this.domRenderer.getEventStats(); const sections = this.sectionManager.getAllSections(); // Calculate additional statistics const sectionTypes = {}; const sectionSizes = { small: 0, medium: 0, large: 0 }; let totalCharacters = 0; let averageLength = 0; sections.forEach(section => { const type = section.type || 'paragraph'; sectionTypes[type] = (sectionTypes[type] || 0) + 1; const length = section.currentMarkdown.length; totalCharacters += length; if (length < 100) sectionSizes.small++; else if (length < 500) sectionSizes.medium++; else sectionSizes.large++; }); if (sections.length > 0) { averageLength = Math.round(totalCharacters / sections.length); } // Create comprehensive status HTML const statusHtml = `

📄 Document Overview

Total Sections: ${status.totalSections}
Total Characters: ${totalCharacters.toLocaleString()}
Average Section Length: ${averageLength} chars
Document Status: ${status.hasUnsavedChanges ? '⚠️ Has Changes' : '✅ All Saved'}

📝 Section States

Currently Editing: ${status.editingSections} sections
Modified (Unsaved): ${status.modifiedSections} sections
Saved: ${status.savedSections} sections
Original: ${status.totalSections - status.modifiedSections - status.savedSections} sections

🏷️ Section Types

${Object.entries(sectionTypes).map(([type, count]) => `
${type.charAt(0).toUpperCase() + type.slice(1)}: ${count}
` ).join('')}

📏 Section Sizes

Small (<100 chars): ${sectionSizes.small}
Medium (100-500 chars): ${sectionSizes.medium}
Large (>500 chars): ${sectionSizes.large}

⚡ Event Statistics

Total Events: ${eventStats.totalEvents}
Section Clicks: ${eventStats.stats['section-click'] || 0}
Hover Events: ${(eventStats.stats['section-hover-enter'] || 0) + (eventStats.stats['section-hover-leave'] || 0)}
Keyboard Shortcuts: ${eventStats.stats['keyboard-shortcut'] || 0}
Context Menus: ${eventStats.stats['section-context-menu'] || 0}
Drag Events: ${(eventStats.stats['section-drag-start'] || 0) + (eventStats.stats['section-drag-over'] || 0)}
${eventStats.recentEvents.length > 0 ? `

🕒 Recent Activity (Last 10 Events)

${eventStats.recentEvents.slice(-10).reverse().map(event => `
${event.type} - ${event.timestamp} ${event.data.sectionId ? `
Section: ${event.data.sectionId.substring(0, 12)}...` : ''} ${event.data.shortcut ? `
Shortcut: ${event.data.shortcut}` : ''}
`).join('')}
` : ''}

📋 Section Details

${sections.map((section, index) => { const hasChanges = section.editingMarkdown !== section.currentMarkdown; const stateColor = section.state === 'editing' ? '#3498db' : section.state === 'modified' ? '#f39c12' : section.state === 'saved' ? '#27ae60' : '#95a5a6'; const preview = section.currentMarkdown.substring(0, 40).replace(/\n/g, ' ') + (section.currentMarkdown.length > 40 ? '...' : ''); return ` `; }).join('')}
Section Type State Length Changes
${preview} ${section.type || 'paragraph'} ${section.state || 'original'} ${section.currentMarkdown.length} chars ${hasChanges ? '●' : '○'}
`; this.showModal('📊 Comprehensive Document Status', statusHtml); } getDocumentMarkdown() { return this.sectionManager.getDocumentMarkdown(); } escapeRegex(str) { return str.replace(/[.*+?^${}()|[]\]/g, '\\\\$&'); } convertDataUrlToReference(markdown) { if (!window.markitectBase64References) { return markdown; } let convertedMarkdown = markdown; Object.entries(window.markitectBase64References).forEach(([refId, refData]) => { const dataUrl = refData.full_data_url; const escapedDataUrl = this.escapeRegex(dataUrl); const dataUrlPattern = new RegExp(`!\\[([^\\]]*)\\]\\(${escapedDataUrl}\\)`, 'g'); convertedMarkdown = convertedMarkdown.replace(dataUrlPattern, `![$1][${refId}]`); }); return convertedMarkdown; } /** * Setup real-time status tracking */ setupStatusTracking() { // Listen for status updates from SectionManager this.sectionManager.on('status-updated', (status) => { this.domRenderer.updateStatusDisplay(status); }); // Start periodic status tracking this.sectionManager.startStatusTracking(2000); // Update every 2 seconds // Initial status display this.sectionManager.updateGlobalStatus(); console.log('✓ Real-time status tracking initialized'); } /** * Generate intelligent save filename using 4-method fallback system * @returns {string} Generated filename with .md extension */ generateSaveFilename() { // Method 1: Original filename from options if (this.options.originalFilename) { return this.sanitizeFilename(this.options.originalFilename); } // Method 2: Page title extraction const titleFilename = this.extractFilenameFromTitle(); if (titleFilename) { return this.sanitizeFilename(titleFilename + '.md'); } // Method 3: URL pathname analysis const urlFilename = this.extractFilenameFromUrl(); if (urlFilename) { return this.sanitizeFilename(urlFilename + '.md'); } // Method 4: First heading extraction const headingFilename = this.extractFilenameFromHeading(); if (headingFilename) { return this.sanitizeFilename(headingFilename + '.md'); } // Method 5: Timestamp generation (fallback) return this.generateTimestampFilename(); } /** * Sanitize filename to be filesystem-safe * @param {string} filename - Raw filename * @returns {string} Sanitized filename */ sanitizeFilename(filename) { if (!filename) return 'document.md'; // Remove or replace filesystem-unsafe characters let sanitized = filename .replace(/[\/\\:*?"<>|]/g, '-') // Replace unsafe chars with dashes .replace(/\s+/g, '-') // Replace spaces with dashes .replace(/-+/g, '-') // Replace multiple dashes with single dash .replace(/^-|-$/g, '') // Remove leading/trailing dashes .trim(); // Ensure it ends with .md if (!sanitized.endsWith('.md')) { sanitized += '.md'; } // Ensure it's not empty if (sanitized === '.md') { sanitized = 'document.md'; } return sanitized; } /** * Extract filename from page title * @returns {string|null} Filename or null if not suitable */ extractFilenameFromTitle() { if (typeof document === 'undefined' || !document.title) { return null; } let title = document.title.trim(); // Remove common website suffixes title = title .split('|')[0] // Remove " | Website" .split('-')[0] // Remove " - Website" .split('•')[0] // Remove " • Website" .trim(); if (title.length < 3 || title.length > 100) { return null; } return title; } /** * Extract filename from URL pathname * @returns {string|null} Filename or null if not suitable */ extractFilenameFromUrl() { if (typeof window === 'undefined' || !window.location) { return null; } const pathname = window.location.pathname; if (!pathname || pathname === '/') { return null; } // Get the last segment of the path const segments = pathname.split('/').filter(s => s.length > 0); if (segments.length === 0) { return null; } const lastSegment = segments[segments.length - 1]; // Remove file extensions const filenameBase = lastSegment.replace(/\.[^.]*$/, ''); if (filenameBase.length < 3 || filenameBase.length > 100) { return null; } return filenameBase; } /** * Extract filename from first heading in markdown * @returns {string|null} Filename or null if no heading found */ extractFilenameFromHeading() { if (!this.originalMarkdown) { return null; } const lines = this.originalMarkdown.split('\n'); // Find first heading line for (const line of lines) { const trimmed = line.trim(); if (/^#{1,6}\s/.test(trimmed)) { // Extract heading text (remove # symbols and trim) const headingText = trimmed.replace(/^#{1,6}\s*/, '').trim(); if (headingText.length >= 3 && headingText.length <= 100) { return headingText; } } } return null; } /** * Generate timestamp-based filename as final fallback * @returns {string} Timestamp-based filename */ generateTimestampFilename() { const now = new Date(); const timestamp = now.getFullYear().toString() + (now.getMonth() + 1).toString().padStart(2, '0') + now.getDate().toString().padStart(2, '0') + '-' + now.getHours().toString().padStart(2, '0') + now.getMinutes().toString().padStart(2, '0'); return `document-${timestamp}.md`; } /** * Cleanup method to stop status tracking */ destroy() { if (this.sectionManager) { this.sectionManager.stopStatusTracking(); } } } // Initialize the clean editor system let markitectCleanEditor; function initializeCleanEditor() { debug('1: initializeCleanEditor called', 'INIT'); const container = document.getElementById('markdown-content'); if (!container) { debug('2: FAILED - Markdown content container not found', 'ERROR'); return; } debug('3: Container found', 'INIT'); if (typeof window.MarkitectEditor === 'undefined') { debug('4: FAILED - MarkitectEditor not found', 'ERROR'); return; } debug('5: MarkitectEditor found', 'INIT'); debug('6: Creating editor with content length: ' + markdownContentWithDogtag.length, 'INIT'); try { markitectCleanEditor = new window.MarkitectEditor.MarkitectCleanEditor(markdownContentWithDogtag, container, { cleanMarkdownContent: markdownContent, dogtagContent: dogtagContent }); debug('7: Editor instance created', 'INIT'); window.markitectCleanEditor = markitectCleanEditor; // Make globally available debug('8: Editor made globally available', 'INIT'); const contentAfterInit = container.innerHTML; debug('9: Container has content: ' + (contentAfterInit.length > 0 ? 'YES (' + contentAfterInit.length + ' chars)' : 'NO'), 'INIT'); console.log('✅ Clean section editor initialized successfully'); } catch (error) { debug('ERROR in editor creation: ' + error.message, 'ERROR'); throw error; } } // Document scroll indicators function initializeScrollIndicators() { console.log('✅ Document scroll indicators initialized'); } // Export for module systems if (typeof module !== 'undefined' && module.exports) { module.exports = { EditState, SectionType, Section, SectionManager, DOMRenderer, MarkitectCleanEditor }; global.EditState = EditState; global.SectionType = SectionType; global.Section = Section; } else { window.MarkitectEditor = { EditState, SectionType, Section, SectionManager, DOMRenderer, MarkitectCleanEditor }; window.EditState = EditState; window.SectionType = SectionType; window.Section = Section; }