diff --git a/capabilities/testdrive-jsui b/capabilities/testdrive-jsui index 891d7855..f5ce02cf 160000 --- a/capabilities/testdrive-jsui +++ b/capabilities/testdrive-jsui @@ -1 +1 @@ -Subproject commit 891d78553391928fac5230de2d6fae1e9a25af65 +Subproject commit f5ce02cf8d8ad38fce77fa70b266216340bc4489 diff --git a/markitect/static/editor.js b/markitect/static/editor.js deleted file mode 100644 index a724c4c8..00000000 --- a/markitect/static/editor.js +++ /dev/null @@ -1,5189 +0,0 @@ -// 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' -}); - -/** - * Reusable FloatingMenu Component - * Provides a consistent floating menu system for both text and image editing - */ -class FloatingMenu { - constructor(sectionId, type, renderer) { - this.sectionId = sectionId; - this.type = type; // 'text' or 'image' - this.renderer = renderer; - this.element = null; - this.dragHandle = null; - this.contentArea = null; - this.controlsArea = null; - this.isDragging = false; - this.isVisible = false; - } - - /** - * Create and show the floating menu - * @param {HTMLElement} contentElement - Content to display in the menu - * @param {HTMLElement} controlsElement - Controls to display in the menu - * @returns {HTMLElement} The floating menu element - */ - show(contentElement, controlsElement) { - if (this.isVisible) this.hide(); - - const targetElement = this.renderer.findSectionElement(this.sectionId); - if (!targetElement) return null; - - // Calculate positioning - const positioning = this._calculatePositioning(targetElement); - - // Create menu container - this.element = this._createMenuContainer(positioning); - - // Create drag handle - this.dragHandle = this._createDragHandle(); - - // Create content and controls areas - this.contentArea = this._createContentArea(); - this.controlsArea = this._createControlsArea(); - - // Create wrapper for content that will take most of the space - const contentWrapper = document.createElement('div'); - contentWrapper.style.cssText = ` - flex: 1; - display: flex; - flex-direction: column; - gap: 10px; - min-width: 0; - overflow: hidden; - `; - contentWrapper.appendChild(contentElement); - - // Add content and controls to content area (side by side) - this.contentArea.appendChild(contentWrapper); - this.controlsArea.appendChild(controlsElement); - this.contentArea.appendChild(this.controlsArea); - - // Assemble menu - this.element.appendChild(this.dragHandle); - this.element.appendChild(this.contentArea); - - // Apply highlighting to target element - this._highlightTargetElement(targetElement); - - // Make draggable - this._makeDraggable(); - - // Add to DOM - document.body.appendChild(this.element); - this.isVisible = true; - - return this.element; - } - - /** - * Hide and remove the floating menu - */ - hide() { - if (!this.isVisible) return; - - // Stop editing state in the section manager - const section = this.renderer.sectionManager.sections.get(this.sectionId); - if (section && section.isEditing()) { - section.stopEditing(); - } - - // Remove highlighting from target element - const targetElement = this.renderer.findSectionElement(this.sectionId); - if (targetElement) { - this._removeHighlighting(targetElement); - } - - // Remove section from editing set - this.renderer.editingSections.delete(this.sectionId); - - // Remove from DOM - if (this.element && this.element.parentElement) { - this.element.remove(); - } - - this.element = null; - this.dragHandle = null; - this.contentArea = null; - this.controlsArea = null; - this.isVisible = false; - } - - /** - * Calculate optimal positioning for the floating menu - * @param {HTMLElement} targetElement - Element being edited - * @returns {Object} Positioning information - */ - _calculatePositioning(targetElement) { - const elementRect = targetElement.getBoundingClientRect(); - const scrollX = window.pageXOffset || document.documentElement.scrollLeft; - const scrollY = window.pageYOffset || document.documentElement.scrollTop; - const viewportWidth = window.innerWidth; - - // Calculate content width and positioning - const elementWidth = elementRect.width; - const controlsWidth = 180; // Width needed for controls on the right - const minMenuWidth = 400; // Increased minimum to accommodate side-by-side layout - const maxMenuWidth = Math.min(800, viewportWidth - 40); - - // Always use integrated layout with controls on the right side of content - let menuWidth, menuLeft; - - if (elementWidth + controlsWidth >= minMenuWidth && elementWidth + controlsWidth <= maxMenuWidth) { - // Use element width + controls width - menuWidth = elementWidth + controlsWidth; - } else if (elementWidth + controlsWidth > maxMenuWidth) { - // Use maximum width - menuWidth = maxMenuWidth; - } else { - // Use minimum width - menuWidth = minMenuWidth; - } - - menuLeft = elementRect.left + scrollX; - - return { - width: menuWidth, - left: menuLeft, - top: elementRect.top + scrollY, - height: Math.max(200, elementRect.height), - controlsWidth: controlsWidth - }; - } - - /** - * Create the main menu container - * @param {Object} positioning - Positioning information - * @returns {HTMLElement} Menu container element - */ - _createMenuContainer(positioning) { - const container = document.createElement('div'); - container.className = 'ui-edit-floating-menu'; - container.dataset.sectionId = this.sectionId; - container.dataset.editType = this.type; - container.style.cssText = ` - position: fixed; - top: ${positioning.top}px; - left: ${positioning.left}px; - width: ${positioning.width}px; - min-height: ${positioning.height}px; - max-height: 600px; - z-index: 10000; - background: rgba(255, 255, 255, 0.98); - border: 2px solid #007bff; - border-radius: 12px; - box-shadow: 0 8px 32px rgba(0, 123, 255, 0.25); - display: flex; - flex-direction: column; - overflow: hidden; - backdrop-filter: blur(10px); - `; - - // Store positioning info for controls layout - container._positioning = positioning; - - return container; - } - - /** - * Create the drag handle with X button - * @returns {HTMLElement} Drag handle element - */ - _createDragHandle() { - const handle = document.createElement('div'); - handle.className = 'ui-edit-drag-handle'; - handle.style.cssText = ` - padding: 10px 15px; - background: #007bff; - color: white; - font-size: 12px; - font-weight: bold; - cursor: move; - user-select: none; - border-radius: 10px 10px 0 0; - display: flex; - align-items: center; - justify-content: space-between; - position: relative; - `; - - // Left side: icon and title - const leftContent = document.createElement('div'); - leftContent.style.cssText = ` - display: flex; - align-items: center; - gap: 8px; - `; - - const icon = this.type === 'image' ? '๐Ÿ–ผ๏ธ' : '๐Ÿ“'; - leftContent.innerHTML = `${icon} Drag to Move โ€ข Editing ${this.type === 'image' ? 'Image' : 'Text'}`; - - // Right side: X button - const closeButton = document.createElement('button'); - closeButton.className = 'ui-edit-close-button'; - closeButton.innerHTML = 'โœ•'; - closeButton.style.cssText = ` - background: none; - border: none; - color: white; - font-size: 16px; - font-weight: bold; - cursor: pointer; - padding: 0; - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 3px; - transition: background-color 0.2s ease; - `; - - // Hover effect for close button - closeButton.addEventListener('mouseenter', () => { - closeButton.style.backgroundColor = 'rgba(255, 255, 255, 0.2)'; - }); - closeButton.addEventListener('mouseleave', () => { - closeButton.style.backgroundColor = 'transparent'; - }); - - // Close functionality - closeButton.addEventListener('click', (e) => { - e.stopPropagation(); // Prevent dragging when clicking close - this.hide(); - }); - - handle.appendChild(leftContent); - handle.appendChild(closeButton); - - return handle; - } - - /** - * Create the content area with horizontal layout - * @returns {HTMLElement} Content area element - */ - _createContentArea() { - const area = document.createElement('div'); - area.className = 'ui-edit-content-area'; - area.style.cssText = ` - padding: 15px; - flex: 1; - display: flex; - flex-direction: row; - gap: 15px; - overflow: hidden; - `; - return area; - } - - /** - * Create the controls area for side-by-side layout - * @returns {HTMLElement} Controls area element - */ - _createControlsArea() { - const area = document.createElement('div'); - area.className = 'ui-edit-controls-area'; - area.style.cssText = ` - width: 160px; - display: flex; - flex-direction: column; - gap: 10px; - justify-content: flex-start; - align-items: stretch; - flex-shrink: 0; - `; - return area; - } - - /** - * Apply highlighting to the target element - * @param {HTMLElement} targetElement - Element to highlight - */ - _highlightTargetElement(targetElement) { - targetElement.style.outline = '3px solid #007bff'; - targetElement.style.outlineOffset = '2px'; - targetElement.style.backgroundColor = 'rgba(0, 123, 255, 0.08)'; - targetElement.style.transition = 'all 0.2s ease'; - } - - /** - * Remove highlighting from the target element - * @param {HTMLElement} targetElement - Element to remove highlighting from - */ - _removeHighlighting(targetElement) { - targetElement.style.outline = ''; - targetElement.style.outlineOffset = ''; - targetElement.style.backgroundColor = ''; - targetElement.style.transition = ''; - } - - /** - * Make the floating menu draggable - */ - _makeDraggable() { - let startX, startY, initialX, initialY; - - this.dragHandle.addEventListener('mousedown', (e) => { - // Don't start dragging if clicking on the close button - if (e.target.closest('.ui-edit-close-button')) { - return; - } - - this.isDragging = true; - startX = e.clientX; - startY = e.clientY; - - const rect = this.element.getBoundingClientRect(); - initialX = rect.left; - initialY = rect.top; - - this.element.style.cursor = 'grabbing'; - this.dragHandle.style.cursor = 'grabbing'; - - e.preventDefault(); - }); - - document.addEventListener('mousemove', (e) => { - if (!this.isDragging) return; - - const deltaX = e.clientX - startX; - const deltaY = e.clientY - startY; - - const newX = initialX + deltaX; - const newY = initialY + deltaY; - - // Keep within viewport bounds - const maxX = window.innerWidth - this.element.offsetWidth; - const maxY = window.innerHeight - this.element.offsetHeight; - - this.element.style.left = Math.max(0, Math.min(newX, maxX)) + 'px'; - this.element.style.top = Math.max(0, Math.min(newY, maxY)) + 'px'; - }); - - document.addEventListener('mouseup', () => { - if (this.isDragging) { - this.isDragging = false; - this.element.style.cursor = 'move'; - this.dragHandle.style.cursor = 'move'; - } - }); - } -} - -// Global debug message storage - reset on page load -let debugMessages = []; -let debugPanelActive = false; - -// Clear any messages that might have been captured during rendering -if (typeof window !== 'undefined') { - debugMessages = []; -} - -function debug(message, category = 'INFO') { - const timestamp = new Date().toLocaleTimeString(); - const prefix = `DEBUG ${category}:`; - const fullMessage = `[${timestamp}] ${prefix} ${message}`; - - // Always store messages for the debug panel - debugMessages.push({ - timestamp, - category, - message, - fullMessage - }); - - // Keep only last 100 messages to prevent memory issues - if (debugMessages.length > 100) { - debugMessages = debugMessages.slice(-100); - } - - // Update debug panel if active - if (debugPanelActive) { - console.log('Debug panel is active, updating...'); // Debug log - if (window.updateDebugPanel) { - console.log('Calling window.updateDebugPanel'); // Debug log - window.updateDebugPanel(); - } else { - console.log('window.updateDebugPanel not available'); // Debug log - } - } else { - console.log('Debug panel not active, debugPanelActive =', debugPanelActive); // Debug log - } - - switch (DEBUG_MODE) { - case 'off': - // No debugging output to console - 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'); - } -} - -// Global functions for debug panel -window.updateDebugPanel = function() { - // Find the DOMRenderer instance and call its updateDebugPanel method - if (window.currentDOMRenderer && typeof window.currentDOMRenderer.updateDebugPanel === 'function') { - window.currentDOMRenderer.updateDebugPanel(); - } -}; - -window.clearDebugMessages = function() { - console.log('clearDebugMessages called, current messages:', debugMessages.length); - debugMessages = []; - console.log('Messages cleared, new length:', debugMessages.length); - if (debugPanelActive && window.currentDOMRenderer && typeof window.currentDOMRenderer.updateDebugPanel === 'function') { - console.log('Updating debug panel after clear'); - window.currentDOMRenderer.updateDebugPanel(); - } else { - console.log('Cannot update debug panel after clear:', { - debugPanelActive, - hasRenderer: !!window.currentDOMRenderer, - hasUpdateMethod: !!(window.currentDOMRenderer && window.currentDOMRenderer.updateDebugPanel) - }); - } -}; - -// 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) { - debug('MANAGER: startEditing called for: ' + sectionId, 'MANAGER'); - - const section = this.sections.get(sectionId); - if (!section) { - throw new Error(`Section ${sectionId} not found`); - } - - if (section.isEditing()) { - debug('MANAGER: Section already in editing state: ' + sectionId, 'MANAGER'); - return section.editingMarkdown; - } - - debug('MANAGER: Starting edit for section: ' + sectionId, 'MANAGER'); - const content = section.startEdit(); - - debug('MANAGER: About to emit edit-started event for: ' + sectionId, 'MANAGER'); - this.emit('edit-started', { sectionId, content, section: section.getStatus() }); - debug('MANAGER: Emitted edit-started event for: ' + sectionId, 'MANAGER'); - - 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(); - this.currentFloatingMenu = null; - this.eventListenersAttached = false; - - // 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) => { - debug('EVENT: edit-started event received for: ' + data.sectionId, 'EVENT'); - 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 (only once) - if (!this.eventListenersAttached) { - 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); - - this.eventListenersAttached = true; - debug('25: Enhanced event listeners attached for the first time', 'RENDER'); - } else { - debug('25: Event listeners already attached, skipping', 'RENDER'); - } - - debug('25: 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('handleSectionClick: 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('handleSectionClick: Ignoring click on form element', 'CLICK'); - return; - } - - const sectionElement = event.target.closest('.ui-edit-section'); - debug('handleSectionClick: Found section element: ' + (sectionElement ? sectionElement.getAttribute('data-section-id') : 'null'), 'CLICK'); - if (!sectionElement) return; - - const sectionId = sectionElement.getAttribute('data-section-id'); - debug('handleSectionClick: 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); - debug('handleSectionClick: Found section object: ' + (section ? 'YES' : 'NO'), 'CLICK'); - - if (section && section.isEditing()) { - debug('handleSectionClick: Section already being edited: ' + sectionId, 'CLICK'); - return; - } - - debug('handleSectionClick: About to start editing for section: ' + sectionId, 'CLICK'); - - try { - debug('handleSectionClick: Calling sectionManager.startEditing', 'CLICK'); - this.sectionManager.startEditing(sectionId); - debug('handleSectionClick: Successfully called startEditing', 'CLICK'); - } catch (error) { - debug('handleSectionClick: ERROR in startEditing: ' + error.message, '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) { - debug('showEditor: called for section: ' + sectionId, 'EDITOR'); - - const element = this.findSectionElement(sectionId); - debug('showEditor: Found element: ' + (element ? 'YES' : 'NO'), 'EDITOR'); - if (!element) return; - - debug('showEditor: About to hide current editor', 'EDITOR'); - this.hideCurrentEditor(); - debug('showEditor: Hidden current editor', 'EDITOR'); - - const section = this.sectionManager.sections.get(sectionId); - const isImageSection = section && section.isImage(); - - if (isImageSection) { - this.showImageEditor(sectionId, section); - return; - } - - // Create content area for text editing (optimized for side-by-side layout) - const editorContent = document.createElement('div'); - editorContent.className = 'ui-edit-editor-content'; - editorContent.style.cssText = ` - display: flex; - flex-direction: column; - gap: 12px; - flex: 1; - min-width: 0; - `; - - const textarea = document.createElement('textarea'); - textarea.className = 'ui-edit-textarea ui-edit-textarea-main'; - textarea.value = content; - textarea.style.cssText = ` - flex: 1; - min-height: 200px; - width: 100%; - font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; - font-size: 14px; - line-height: 1.5; - padding: 12px; - border: 1px solid #ddd; - border-radius: 8px; - resize: none; - outline: none; - background: white; - color: #333; - box-sizing: border-box; - `; - - editorContent.appendChild(textarea); - - // Setup auto-resize functionality - this.setupAutoResize(textarea); - - // Create controls (optimized for side panel layout) - const controls = document.createElement('div'); - controls.className = 'ui-edit-controls'; - controls.style.cssText = ` - display: flex; - flex-direction: column; - gap: 8px; - width: 100%; - `; - - const acceptBtn = this.createButton('Accept', 'ui-edit-button-accept', this.handleAccept); - const cancelBtn = this.createButton('Cancel', 'ui-edit-button-cancel', this.handleCancel); - const resetBtn = this.createButton('Reset', 'ui-edit-button-reset', this.handleReset); - - // Style buttons for side panel layout - [acceptBtn, cancelBtn, resetBtn].forEach(btn => { - btn.style.cssText += ` - padding: 8px 12px; - font-size: 12px; - border-radius: 6px; - border: none; - color: white; - cursor: pointer; - font-weight: 600; - transition: all 0.2s ease; - width: 100%; - text-align: center; - `; - }); - - acceptBtn.style.background = '#28a745'; - cancelBtn.style.background = '#dc3545'; - resetBtn.style.background = '#fd7e14'; - - controls.appendChild(acceptBtn); - controls.appendChild(cancelBtn); - controls.appendChild(resetBtn); - - // Create unified floating menu using new component - this.currentFloatingMenu = new FloatingMenu(sectionId, 'text', this); - const floatingMenu = this.currentFloatingMenu.show(editorContent, controls); - if (!floatingMenu) return; - - textarea.focus(); - this.editingSections.add(sectionId); - - textarea.addEventListener('input', () => { - this.sectionManager.updateContent(sectionId, textarea.value); - }); - - // Add keyboard shortcuts - textarea.addEventListener('keydown', this.handleKeydown); - } - - /** - * Create a unified floating edit menu - * @param {string} sectionId - The section being edited - * @param {string} type - 'text' or 'image' - * @param {HTMLElement} contentElement - The content area to add to the menu - * @param {HTMLElement} controlsElement - The controls/buttons to add to the menu - * @returns {HTMLElement} The floating menu element - */ - - /** - * 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 - }; - - // Create image editor content area (optimized for side-by-side layout) - const editorContent = document.createElement('div'); - editorContent.className = 'ui-edit-image-content'; - editorContent.style.cssText = ` - display: flex; - flex-direction: column; - gap: 15px; - flex: 1; - min-width: 0; - `; - - // 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 for floating menu - const imagePreview = document.createElement('div'); - imagePreview.className = 'ui-edit-image-preview'; - imagePreview.style.cssText = ` - width: 100%; - height: 180px; - text-align: center; - background: white; - padding: 12px; - border-radius: 8px; - border: 2px dashed #007bff; - transition: all 0.3s ease; - cursor: pointer; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - position: relative; - box-sizing: border-box; - overflow: hidden; - `; - - // 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 for floating menu - const altTextContainer = document.createElement('div'); - altTextContainer.className = 'ui-edit-alt-text-container'; - altTextContainer.style.cssText = ` - display: flex; - flex-direction: column; - gap: 8px; - `; - - const altTextLabel = document.createElement('label'); - altTextLabel.textContent = 'Alt Text Description:'; - altTextLabel.style.cssText = ` - font-size: 13px; - font-weight: 600; - color: #333; - margin: 0; - `; - - const altTextInput = document.createElement('input'); - altTextInput.type = 'text'; - altTextInput.value = stagingState.currentAltText; - altTextInput.style.cssText = ` - width: 100%; - padding: 10px 12px; - border: 1px solid #ddd; - border-radius: 6px; - font-size: 14px; - box-sizing: border-box; - outline: none; - transition: border-color 0.2s ease; - `; - - altTextInput.addEventListener('focus', () => { - altTextInput.style.borderColor = '#007bff'; - }); - - altTextInput.addEventListener('blur', () => { - altTextInput.style.borderColor = '#ddd'; - }); - - // 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 for floating menu - const changeIndicator = document.createElement('div'); - changeIndicator.className = 'change-indicator'; - changeIndicator.style.cssText = ` - padding: 8px 12px; - background: #fff3cd; - border: 1px solid #ffeaa7; - border-radius: 6px; - color: #856404; - font-size: 12px; - text-align: center; - display: none; - font-weight: 500; - `; - changeIndicator.textContent = 'โš ๏ธ You have unsaved changes'; - - const updateChangeIndicator = () => { - if (stagingState.hasChanges) { - changeIndicator.style.display = 'block'; - } else { - changeIndicator.style.display = 'none'; - } - }; - - // Responsive controls container with alt text - const editorControls = document.createElement('div'); - editorControls.className = 'ui-edit-controls'; - editorControls.style.cssText = ` - display: flex; - flex-direction: column; - gap: 12px; - min-width: 180px; - padding: 12px; - justify-content: space-between; - align-items: stretch; - `; - - 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(); - }); - - // Assemble content for floating menu - editorContent.appendChild(imagePreview); - editorContent.appendChild(altTextContainer); - editorContent.appendChild(changeIndicator); - editorContent.appendChild(fileInput); - - // Create controls for side panel layout - const controls = document.createElement('div'); - controls.className = 'ui-edit-controls'; - controls.style.cssText = ` - display: flex; - flex-direction: column; - gap: 8px; - width: 100%; - `; - - // Style buttons for side panel layout - [acceptBtn, cancelBtn, resetBtn].forEach(btn => { - btn.style.cssText = ` - padding: 8px 12px; - font-size: 12px; - border-radius: 6px; - border: none; - color: white; - cursor: pointer; - font-weight: 600; - transition: all 0.2s ease; - width: 100%; - text-align: center; - `; - }); - - acceptBtn.style.background = '#28a745'; - cancelBtn.style.background = '#dc3545'; - resetBtn.style.background = '#fd7e14'; - - controls.appendChild(acceptBtn); - controls.appendChild(cancelBtn); - controls.appendChild(resetBtn); - - // Create unified floating menu using new component - this.currentFloatingMenu = new FloatingMenu(sectionId, 'image', this); - const floatingMenu = this.currentFloatingMenu.show(editorContent, controls); - if (!floatingMenu) return; - - 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) { - event.preventDefault(); - event.stopPropagation(); - - const sectionId = this.getCurrentEditingSectionId(event.target); - if (sectionId) { - this.sectionManager.acceptChanges(sectionId); - this.hideEditor(sectionId); - } - } - - handleCancel(event) { - event.preventDefault(); - event.stopPropagation(); - - const sectionId = this.getCurrentEditingSectionId(event.target); - if (sectionId) { - this.sectionManager.cancelChanges(sectionId); - this.hideEditor(sectionId); - } - } - - handleReset(event) { - event.preventDefault(); - event.stopPropagation(); - - const sectionId = this.getCurrentEditingSectionId(event.target); - if (sectionId) { - this.sectionManager.resetSection(sectionId); - - // Update DOM to show reset content - const section = this.sectionManager.sections.get(sectionId); - if (section) { - this.updateSectionContent(sectionId, section.currentMarkdown); - } - - this.hideEditor(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) { - // Check if button is in a floating menu - const floatingMenu = button.closest('.ui-edit-floating-menu'); - if (floatingMenu) { - return floatingMenu.dataset.sectionId; - } - - // Fallback to old overlay container method - const editorContainer = button.closest('.ui-edit-editor-container, .ui-edit-image-editor-container, .ui-edit-overlay-container'); - if (!editorContainer) return null; - - const sectionElement = editorContainer.parentElement; - return sectionElement ? sectionElement.getAttribute('data-section-id') : null; - } - - hideEditor(sectionId) { - const element = this.findSectionElement(sectionId); - - // Remove floating menu if it exists - const floatingMenu = document.querySelector(`.ui-edit-floating-menu[data-section-id="${sectionId}"]`); - if (floatingMenu) { - floatingMenu.remove(); - } - - // Remove element highlighting - if (element) { - element.style.outline = ''; - element.style.outlineOffset = ''; - element.style.backgroundColor = ''; - - // Remove any old 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(); - } - - // Remove overlay containers and restore original content if needed - const overlayContainer = element.querySelector('.ui-edit-overlay-container'); - if (overlayContainer) { - const originalContent = overlayContainer.dataset.originalContent; - overlayContainer.remove(); - - // Restore element's original positioning - element.style.position = ''; - - // If original content was stored, restore it - if (originalContent) { - element.innerHTML = originalContent; - } else { - // Otherwise, refresh the section content - const section = this.sectionManager.sections.get(sectionId); - if (section) { - this.updateSectionContent(sectionId, section.currentMarkdown); - } - } - } - - const section = this.sectionManager.sections.get(sectionId); - if (section) { - this.updateSectionContent(sectionId, section.currentMarkdown); - } - } - - this.editingSections.delete(sectionId); - } - - hideCurrentEditor() { - debug('EDITOR: hideCurrentEditor called', 'EDITOR'); - - // Hide FloatingMenu if it exists - if (this.currentFloatingMenu) { - debug('EDITOR: Hiding currentFloatingMenu', 'EDITOR'); - this.currentFloatingMenu.hide(); - this.currentFloatingMenu = null; - } - - // Hide any legacy editors - this.editingSections.forEach(sectionId => { - const element = this.findSectionElement(sectionId); - if (element && element.querySelector('.ui-edit-editor-container')) { - this.hideEditor(sectionId); - } - }); - - // CRUCIAL FIX: Ensure ALL sections are reset from editing state - this.sectionManager.sections.forEach((section, sectionId) => { - if (section.isEditing()) { - debug('EDITOR: Force stopping editing for stuck section: ' + sectionId, 'EDITOR'); - section.stopEditing(); - } - }); - - // Clear the editing sections set - this.editingSections.clear(); - - // Clean up any floating menu elements - const floatingMenus = document.querySelectorAll('.ui-edit-floating-menu'); - floatingMenus.forEach(menu => menu.remove()); - - debug('EDITOR: hideCurrentEditor completed', 'EDITOR'); - } - - 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); - - // Set global reference for debug panel - window.currentDOMRenderer = this.domRenderer; - - // Initialize debug system for this page load - this.initializeDebugSystem(); - - 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; - `; - - // Debug button - const debugButton = document.createElement('button'); - debugButton.id = 'toggle-debug'; - debugButton.textContent = '๐Ÿ” Debug'; - debugButton.style.cssText = ` - background: #6c757d; - color: white; - border: none; - padding: 8px 12px; - border-radius: 4px; - margin-left: 8px; - cursor: pointer; - font-size: 13px; - font-weight: 500; - transition: background-color 0.2s; - `; - - buttonContainer.appendChild(saveButton); - buttonContainer.appendChild(resetButton); - buttonContainer.appendChild(statusButton); - buttonContainer.appendChild(debugButton); - - // Debug messages container - const debugContainer = document.createElement('div'); - debugContainer.id = 'debug-messages-container'; - debugContainer.style.cssText = ` - margin-top: 12px; - max-height: 300px; - overflow-y: auto; - border: 1px solid #dee2e6; - border-radius: 4px; - background: #f8f9fa; - padding: 8px; - font-family: 'Courier New', monospace; - font-size: 12px; - line-height: 1.4; - display: none; - `; - - controlPanel.appendChild(title); - controlPanel.appendChild(buttonContainer); - controlPanel.appendChild(debugContainer); - - 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()); - document.getElementById('toggle-debug').addEventListener('click', () => this.toggleDebugPanel()); - - // 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('')} - -
SectionTypeStateLengthChanges
- ${preview} - - - ${section.type || 'paragraph'} - - - - ${section.state || 'original'} - - ${section.currentMarkdown.length} chars - ${hasChanges ? 'โ—' : 'โ—‹'} -
-
-
-
- `; - - this.showModal('๐Ÿ“Š Comprehensive Document Status', statusHtml); - } - - /** - * Toggle the debug panel on/off - */ - toggleDebugPanel() { - console.log('toggleDebugPanel called'); - const debugContainer = document.getElementById('debug-messages-container'); - const debugButton = document.getElementById('toggle-debug'); - - console.log('Elements found:', { - debugContainer: !!debugContainer, - debugButton: !!debugButton - }); - - if (!debugContainer || !debugButton) return; - - if (debugPanelActive) { - // Hide debug panel - console.log('Hiding debug panel'); - debugContainer.style.display = 'none'; - debugButton.textContent = '๐Ÿ” Debug'; - debugButton.style.background = '#6c757d'; - debugPanelActive = false; - } else { - // Show debug panel - console.log('Showing debug panel'); - debugContainer.style.display = 'block'; - debugButton.textContent = '๐Ÿ” Debug (ON)'; - debugButton.style.background = '#28a745'; - debugPanelActive = true; - console.log('About to call this.updateDebugPanel()'); - this.updateDebugPanel(); - } - console.log('debugPanelActive is now:', debugPanelActive); - } - - /** - * Update the debug panel with current messages - */ - updateDebugPanel() { - const debugContainer = document.getElementById('debug-messages-container'); - if (!debugContainer || !debugPanelActive) { - console.log('updateDebugPanel skipped:', { - hasContainer: !!debugContainer, - debugPanelActive - }); - return; - } - - console.log('updateDebugPanel running, messages count:', debugMessages.length); - - if (debugMessages.length === 0) { - debugContainer.innerHTML = '
No debug messages yet. Click sections to generate debug output.
'; - return; - } - - // Show the last 50 messages in reverse order (newest first) - const recentMessages = debugMessages.slice(-50).reverse(); - - const messagesHtml = recentMessages.map(msg => { - const categoryColor = { - 'INFO': '#17a2b8', - 'WARNING': '#ffc107', - 'ERROR': '#dc3545', - 'SUCCESS': '#28a745', - 'DEBUG': '#6f42c1' - }[msg.category] || '#6c757d'; - - return ` -
- [${msg.timestamp}] - ${msg.category}: - ${msg.message} -
- `; - }).join(''); - - debugContainer.innerHTML = ` -
- Debug Messages (${debugMessages.length} total, showing last ${recentMessages.length}) - -
-
- ${messagesHtml} -
- `; - - // Add event listener for clear button - const clearBtn = debugContainer.querySelector('#debug-clear-btn'); - if (clearBtn) { - clearBtn.addEventListener('click', () => { - console.log('Clear button clicked'); // Debug log - window.clearDebugMessages(); - }); - } - - // Auto-scroll to bottom to show newest messages - const scrollContainer = debugContainer.querySelector('div[style*="overflow-y"]'); - if (scrollContainer) { - scrollContainer.scrollTop = scrollContainer.scrollHeight; - } - } - - /** - * Initialize the debug system for this page load - */ - initializeDebugSystem() { - // Clear any debug messages from rendering time - debugMessages = []; - debugPanelActive = false; - - console.log('Debug system initialized - messages cleared for new session'); - - // Add a welcome message to show the system is working - debug('initializeDebugSystem: Debug system ready for new session', 'INFO'); - } - - 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; -} \ No newline at end of file diff --git a/markitect/static/js/components/debug-panel.js b/markitect/static/js/components/debug-panel.js deleted file mode 100644 index d22706a0..00000000 --- a/markitect/static/js/components/debug-panel.js +++ /dev/null @@ -1,191 +0,0 @@ -/** - * DebugPanel Component - * - * Extracted from monolithic editor.js as part of architecture refactoring. - * Handles debug message display and management for client-side debugging. - * - * Dependencies: - * - None (standalone component) - */ - -/** - * DebugPanel - Manages debug message display and interaction - */ -class DebugPanel { - constructor() { - this.messages = []; - this.isActive = false; - this.maxMessages = 1000; // Keep last 1000 messages - } - - /** - * Add a debug message - */ - addMessage(message, category = 'INFO') { - const messageObj = { - message, - category, - timestamp: new Date().toLocaleTimeString() - }; - - this.messages.push(messageObj); - - // Keep only last maxMessages - if (this.messages.length > this.maxMessages) { - this.messages = this.messages.slice(-this.maxMessages); - } - - // Auto-update if panel is visible - if (this.isActive) { - this.update(); - } - } - - /** - * Toggle the debug panel on/off - */ - toggle() { - const debugContainer = document.getElementById('debug-messages-container'); - const debugButton = document.getElementById('toggle-debug'); - - if (!debugContainer || !debugButton) { - console.warn('DebugPanel: Required DOM elements not found'); - return; - } - - if (this.isActive) { - this.hide(); - } else { - this.show(); - } - } - - /** - * Show the debug panel - */ - show() { - const debugContainer = document.getElementById('debug-messages-container'); - const debugButton = document.getElementById('toggle-debug'); - - if (!debugContainer || !debugButton) { - console.warn('DebugPanel: Required DOM elements not found'); - return; - } - - debugContainer.style.display = 'block'; - debugButton.textContent = '๐Ÿ” Debug (ON)'; - debugButton.style.background = '#28a745'; - this.isActive = true; - this.update(); - } - - /** - * Hide the debug panel - */ - hide() { - const debugContainer = document.getElementById('debug-messages-container'); - const debugButton = document.getElementById('toggle-debug'); - - if (!debugContainer || !debugButton) { - console.warn('DebugPanel: Required DOM elements not found'); - return; - } - - debugContainer.style.display = 'none'; - debugButton.textContent = '๐Ÿ” Debug'; - debugButton.style.background = '#6c757d'; - this.isActive = false; - } - - /** - * Update the debug panel with current messages - */ - update() { - const debugContainer = document.getElementById('debug-messages-container'); - if (!debugContainer || !this.isActive) { - return; - } - - if (this.messages.length === 0) { - debugContainer.innerHTML = '
No debug messages yet. Click sections to generate debug output.
'; - return; - } - - // Show the last 50 messages in reverse order (newest first) - const recentMessages = this.messages.slice(-50).reverse(); - - const messagesHtml = recentMessages.map(msg => { - const categoryColor = { - 'INFO': '#17a2b8', - 'WARNING': '#ffc107', - 'ERROR': '#dc3545', - 'SUCCESS': '#28a745', - 'DEBUG': '#6f42c1' - }[msg.category] || '#6c757d'; - - return ` -
- [${msg.timestamp}] - ${msg.category}: - ${msg.message} -
- `; - }).join(''); - - debugContainer.innerHTML = ` -
- Debug Messages (${this.messages.length} total, showing last ${recentMessages.length}) - -
-
- ${messagesHtml} -
- `; - - // Add event listener for clear button - const clearBtn = debugContainer.querySelector('#debug-clear-btn'); - if (clearBtn) { - clearBtn.addEventListener('click', () => { - this.clear(); - }); - } - - // Auto-scroll to bottom to show newest messages - const scrollContainer = debugContainer.querySelector('div[style*="overflow-y"]'); - if (scrollContainer) { - scrollContainer.scrollTop = scrollContainer.scrollHeight; - } - } - - /** - * Clear all debug messages - */ - clear() { - this.messages = []; - this.update(); - } - - /** - * Get the number of messages - */ - getMessageCount() { - return this.messages.length; - } - - /** - * Get recent messages - */ - getRecentMessages(count = 10) { - return this.messages.slice(-count); - } -} - -// Export for use in tests and other modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = { DebugPanel }; -} - -// Export for browser use -if (typeof window !== 'undefined') { - window.DebugPanel = DebugPanel; -} \ No newline at end of file diff --git a/markitect/static/js/components/document-controls.js b/markitect/static/js/components/document-controls.js deleted file mode 100644 index fb83ebd8..00000000 --- a/markitect/static/js/components/document-controls.js +++ /dev/null @@ -1,279 +0,0 @@ -/** - * DocumentControls Component - * - * Extracted from monolithic editor.js as part of architecture refactoring. - * Handles the floating control panel and document-level actions. - * - * Dependencies: - * - None (standalone component) - */ - -/** - * DocumentControls - Manages the floating control panel and its buttons - */ -class DocumentControls { - constructor() { - this.controlPanel = null; - this.buttons = new Map(); - this.eventHandlers = new Map(); - this.isVisible = true; - } - - /** - * Create the control panel and add it to the DOM - */ - create() { - if (this.controlPanel) { - this.destroy(); // Remove existing panel - } - - // Also remove any existing panel with the same ID in the DOM - const existingPanel = document.getElementById('markitect-global-controls'); - if (existingPanel && existingPanel.parentNode) { - existingPanel.parentNode.removeChild(existingPanel); - } - - // Create the floating control panel - this.controlPanel = document.createElement('div'); - this.controlPanel.id = 'markitect-global-controls'; - this.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; - `; - - // Add title - 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'; - - // Create button container - const buttonContainer = document.createElement('div'); - buttonContainer.id = 'button-container'; - buttonContainer.style.cssText = ` - display: flex; - flex-direction: column; - gap: 6px; - `; - - this.controlPanel.appendChild(title); - this.controlPanel.appendChild(buttonContainer); - - // Add default buttons - this.addDefaultButtons(); - - // Add debug messages container - this.addDebugContainer(); - - // Add to DOM - document.body.appendChild(this.controlPanel); - } - - /** - * Add default buttons to the control panel - */ - addDefaultButtons() { - // Save Document button - this.addButton('save-document', '๐Ÿ’พ Save Document', '#28a745'); - - // Reset All button - this.addButton('reset-all', '๐Ÿ”„ Reset All', '#ffc107', '#212529'); - - // Show Status button - this.addButton('show-status', '๐Ÿ“Š Show Status', '#17a2b8'); - - // Debug button - this.addButton('toggle-debug', '๐Ÿ” Debug', '#6c757d'); - } - - /** - * Add debug container to the control panel - */ - addDebugContainer() { - const debugContainer = document.createElement('div'); - debugContainer.id = 'debug-messages-container'; - debugContainer.style.cssText = ` - margin-top: 12px; - max-height: 300px; - overflow-y: auto; - border: 1px solid #dee2e6; - border-radius: 4px; - background: #f8f9fa; - padding: 8px; - font-family: 'Courier New', monospace; - font-size: 12px; - line-height: 1.4; - display: none; - `; - - this.controlPanel.appendChild(debugContainer); - } - - /** - * Add a button to the control panel - */ - addButton(id, text, backgroundColor, textColor = 'white') { - const buttonContainer = this.controlPanel.querySelector('#button-container'); - if (!buttonContainer) { - throw new Error('Button container not found. Call create() first.'); - } - - const button = document.createElement('button'); - button.id = id; - button.textContent = text; - button.style.cssText = ` - background: ${backgroundColor}; - color: ${textColor}; - border: none; - padding: 8px 12px; - border-radius: 4px; - cursor: pointer; - font-size: 13px; - font-weight: 500; - transition: background-color 0.2s; - `; - - buttonContainer.appendChild(button); - this.buttons.set(id, button); - - return button; - } - - /** - * Remove a button from the control panel - */ - removeButton(id) { - const button = this.buttons.get(id); - if (button && button.parentNode) { - button.parentNode.removeChild(button); - this.buttons.delete(id); - this.eventHandlers.delete(id); - } - } - - /** - * Set event handlers for buttons - */ - setEventHandlers(handlers) { - for (const [buttonId, handler] of Object.entries(handlers)) { - const button = this.buttons.get(buttonId); - if (button) { - // Remove existing handler if any - if (this.eventHandlers.has(buttonId)) { - button.removeEventListener('click', this.eventHandlers.get(buttonId)); - } - - // Add new handler - button.addEventListener('click', handler); - this.eventHandlers.set(buttonId, handler); - } - } - } - - /** - * Show the control panel - */ - show() { - if (this.controlPanel) { - this.controlPanel.style.display = 'block'; - this.isVisible = true; - } - } - - /** - * Hide the control panel - */ - hide() { - if (this.controlPanel) { - this.controlPanel.style.display = 'none'; - this.isVisible = false; - } - } - - /** - * Update status display (can be extended as needed) - */ - updateStatus(status) { - // This method can be extended to show status information - // For now, it just stores the status for potential display - this.lastStatus = status; - - // Could update a status indicator in the panel if needed - if (status && this.controlPanel) { - const title = this.controlPanel.querySelector('div'); - if (title) { - const statusText = `Document Controls (${status.totalSections} sections, ${status.editingSections} editing)`; - // Could update title or add status indicator - } - } - } - - /** - * Get the control panel element - */ - getControlPanel() { - return this.controlPanel; - } - - /** - * Destroy the control panel and clean up - */ - destroy() { - if (this.controlPanel && this.controlPanel.parentNode) { - this.controlPanel.parentNode.removeChild(this.controlPanel); - } - - // Clean up references - this.controlPanel = null; - this.buttons.clear(); - this.eventHandlers.clear(); - this.isVisible = true; - } - - /** - * Check if the control panel is visible - */ - isVisible() { - return this.isVisible && this.controlPanel && this.controlPanel.style.display !== 'none'; - } - - /** - * Get all button IDs - */ - getButtonIds() { - return Array.from(this.buttons.keys()); - } - - /** - * Get a specific button by ID - */ - getButton(id) { - return this.buttons.get(id); - } -} - -// Export for use in tests and other modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = { DocumentControls }; -} - -// Export for browser use -if (typeof window !== 'undefined') { - window.DocumentControls = DocumentControls; -} \ No newline at end of file diff --git a/markitect/static/js/components/dom-renderer.js b/markitect/static/js/components/dom-renderer.js deleted file mode 100644 index 20748483..00000000 --- a/markitect/static/js/components/dom-renderer.js +++ /dev/null @@ -1,1128 +0,0 @@ -/** - * DOMRenderer Component - * - * Extracted from monolithic editor.js as part of architecture refactoring. - * Handles all DOM interactions and UI rendering for section editing. - * - * Dependencies: - * - FloatingMenu component (to be extracted) - * - debug function (imported from utils) - */ - -// Import dependencies (placeholders for now) -function debug(message, category = 'INFO') { - console.log(`DEBUG ${category}: ${message}`); -} - -/** - * Simple FloatingMenu implementation (will be extracted to separate component later) - */ -class FloatingMenu { - constructor(sectionId, type, renderer) { - this.sectionId = sectionId; - this.type = type; - this.renderer = renderer; - this.element = null; - this.isVisible = false; - } - - show(contentElement, controlsElement) { - if (this.isVisible) this.hide(); - - const targetElement = this.renderer.findSectionElement(this.sectionId); - if (!targetElement) return null; - - // Get content dimensions and position - const rect = targetElement.getBoundingClientRect(); - const viewport = { - width: window.innerWidth, - height: window.innerHeight - }; - - // Calculate content width and responsive extension - const contentWidth = rect.width; - const buttonAreaWidth = 120; // Space needed for buttons - const minMenuWidth = Math.max(300, contentWidth); // At least content width or 300px - const preferredMenuWidth = contentWidth + buttonAreaWidth; - - // Check if we have space to extend to the right - const spaceOnRight = viewport.width - rect.right; - const canExtendRight = spaceOnRight >= buttonAreaWidth + 20; // 20px margin - - // Determine final menu width - let menuWidth; - if (canExtendRight && viewport.width >= 800) { // Only on wide screens - menuWidth = Math.min(preferredMenuWidth, viewport.width - rect.left - 20); - } else { - menuWidth = Math.min(minMenuWidth, viewport.width - 40); // 20px margins - } - - // Create floating menu element - this.element = document.createElement('div'); - this.element.className = 'ui-edit-floating-menu'; - this.element.style.cssText = ` - position: fixed; - z-index: 10000; - background: white; - border: 1px solid #ddd; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0,0,0,0.15); - padding: 0; - width: ${menuWidth}px; - box-sizing: border-box; - `; - - // Add headline - const headline = document.createElement('div'); - headline.className = 'ui-edit-headline'; - headline.textContent = `Editing ${this.type === 'image' ? 'Image' : 'Section'}`; - headline.style.cssText = ` - background: #f8f9fa; - border-bottom: 1px solid #ddd; - padding: 8px 16px; - font-weight: 600; - font-size: 12px; - color: #495057; - border-radius: 8px 8px 0 0; - text-transform: uppercase; - letter-spacing: 0.5px; - `; - - // Create content wrapper with padding - const contentWrapper = document.createElement('div'); - contentWrapper.style.cssText = ` - padding: 16px; - `; - - this.element.appendChild(headline); - - // Position directly over content (overlay positioning) - let left = rect.left; - let top = rect.top; - - // Ensure menu doesn't go off-screen horizontally - if (left + menuWidth > viewport.width) { - left = viewport.width - menuWidth - 20; - } - if (left < 10) { - left = 10; - } - - // For vertical positioning, prefer staying on top of content - // Only move if absolutely necessary - const menuHeight = this.type === 'image' ? 350 : 200; // Better height estimates - const wouldGoOffBottom = top + menuHeight > viewport.height; - const wouldGoOffTop = top < 10; - - if (wouldGoOffBottom && !wouldGoOffTop) { - // Try to fit by moving up, but keep some overlay if possible - const maxTop = viewport.height - menuHeight - 10; - top = Math.max(rect.top - 50, maxTop); // Prefer staying near original position - } else if (wouldGoOffTop) { - top = 10; // Minimum distance from top - } - // Otherwise, keep the original overlay position - - this.element.style.left = `${left}px`; - this.element.style.top = `${top}px`; - - // Add content to wrapper - if (contentElement) { - contentWrapper.appendChild(contentElement); - } - if (controlsElement) { - contentWrapper.appendChild(controlsElement); - } - - this.element.appendChild(contentWrapper); - - // Add close button to headline - const closeButton = document.createElement('button'); - closeButton.textContent = 'ร—'; - closeButton.style.cssText = ` - position: absolute; - top: 4px; - right: 8px; - background: none; - border: none; - font-size: 18px; - cursor: pointer; - color: #666; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - transition: background-color 0.2s ease; - `; - closeButton.addEventListener('mouseover', () => { - closeButton.style.backgroundColor = '#e9ecef'; - }); - closeButton.addEventListener('mouseout', () => { - closeButton.style.backgroundColor = 'transparent'; - }); - closeButton.addEventListener('click', (event) => { - event.stopPropagation(); - this.hide(); - }); - this.element.appendChild(closeButton); - - document.body.appendChild(this.element); - this.isVisible = true; - - return this.element; - } - - hide() { - if (this.element && this.element.parentNode) { - this.element.parentNode.removeChild(this.element); - } - this.element = null; - this.isVisible = false; - - // Stop editing state in the section manager - const section = this.renderer.sectionManager.sections.get(this.sectionId); - if (section && section.isEditing()) { - section.stopEditing(); - } - - // Remove from editing sections - this.renderer.editingSections.delete(this.sectionId); - } -} - -/** - * DOMRenderer - Handles DOM interactions and section rendering - */ -class DOMRenderer { - constructor(sectionManager, container) { - this.sectionManager = sectionManager; - this.container = container; - this.editingSections = new Set(); - this.currentFloatingMenu = null; - this.eventListenersAttached = false; - this.lastClickTime = 0; - this.clickDebounceMs = 300; // Prevent rapid clicks - - // Enhanced Event System - Track 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.handleKeydown = this.handleKeydown.bind(this); - - this.setupEventListeners(); - } - - setupEventListeners() { - this.sectionManager.on('sections-created', (data) => { - this.renderAllSections(data.sections); - }); - this.sectionManager.on('edit-started', (data) => { - debug('EVENT: edit-started event received for: ' + data.sectionId, 'EVENT'); - this.showEditor(data.sectionId, data.content); - }); - } - - /** - * Render all sections to the DOM - */ - renderAllSections(sections) { - debug('21: renderAllSections called with ' + sections.length + ' sections', 'RENDER'); - - // Clear container - this.container.innerHTML = ''; - debug('22: Container cleared', 'RENDER'); - - const contentArea = this.container.querySelector('#markdown-content') || this.container; - - // Render each section - sections.forEach((section, index) => { - debug('23: Creating element for section ' + (index + 1) + ': ' + section.id, 'RENDER'); - const element = this.renderSection(section); - if (element) { - contentArea.appendChild(element); - } - }); - - debug('24: All section elements added to container', 'RENDER'); - - // Attach event listeners only once - if (!this.eventListenersAttached) { - this.container.addEventListener('click', this.handleSectionClick); - this.eventListenersAttached = true; - debug('25: Enhanced event listeners attached for the first time', 'RENDER'); - } else { - debug('25: Event listeners already attached, skipping', 'RENDER'); - } - - debug('25: Container content length: ' + this.container.innerHTML.length, 'RENDER'); - } - - /** - * Render a single section to DOM element - */ - renderSection(section) { - const element = document.createElement('div'); - element.className = 'ui-edit-section'; - element.setAttribute('data-section-id', section.id); - - // Add section content - // Render all sections using markdown rendering (images need HTML conversion too) - const content = this.simpleMarkdownRender(section.currentMarkdown); - element.innerHTML = content; - - // Add styling - element.style.cssText = ` - margin: 16px 0; - padding: 12px; - border: 1px solid transparent; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s ease; - `; - - element.addEventListener('mouseenter', () => { - element.style.backgroundColor = 'rgba(0, 122, 204, 0.05)'; - element.style.borderColor = 'rgba(0, 122, 204, 0.2)'; - }); - - element.addEventListener('mouseleave', () => { - if (!section.isEditing()) { - element.style.backgroundColor = 'transparent'; - element.style.borderColor = 'transparent'; - } - }); - - debug('SECTION: Section element setup complete for ' + section.id, 'SECTION'); - return element; - } - - /** - * Simple markdown rendering (placeholder) - */ - simpleMarkdownRender(markdown) { - return markdown - .replace(/^# (.*$)/gim, '

$1

') - .replace(/^## (.*$)/gim, '

$1

') - .replace(/^### (.*$)/gim, '

$1

') - .replace(/!\[(.*?)\]\((.*?)\)/gim, '$1') - .replace(/\[([^\]]+)\]\(([^)]+)\)/gim, '
$1') - .replace(/\*\*(.*?)\*\*/gim, '$1') - .replace(/\*(.*?)\*/gim, '$1') - .replace(/\n/gim, '
'); - } - - /** - * Find DOM element for a section - */ - findSectionElement(sectionId) { - return this.container.querySelector(`[data-section-id="${sectionId}"]`); - } - - /** - * Handle section click events - */ - handleSectionClick(event) { - debug('handleSectionClick: Click detected on target: ' + event.target.tagName + ' ' + (event.target.className || ''), 'CLICK'); - - // Debounce rapid clicks - const now = Date.now(); - if (now - this.lastClickTime < this.clickDebounceMs) { - debug('handleSectionClick: Click debounced (too rapid)', 'CLICK'); - return; - } - this.lastClickTime = now; - - // Don't handle clicks on form elements, buttons, or links - if (event.target.closest('textarea, button, input, a')) { - debug('handleSectionClick: Ignoring click on form element', 'CLICK'); - return; - } - - const sectionElement = event.target.closest('.ui-edit-section'); - debug('handleSectionClick: Found section element: ' + (sectionElement ? sectionElement.getAttribute('data-section-id') : 'null'), 'CLICK'); - if (!sectionElement) return; - - const sectionId = sectionElement.getAttribute('data-section-id'); - debug('handleSectionClick: Section ID: ' + sectionId, 'CLICK'); - if (!sectionId) return; - - // Track the click event - this.trackEvent('section-click', { - sectionId, - event, - timestamp: Date.now() - }); - - // Check if this section is already being edited - const section = this.sectionManager.sections.get(sectionId); - debug('handleSectionClick: Found section object: ' + (section ? 'YES' : 'NO'), 'CLICK'); - - if (section && section.isEditing()) { - debug('handleSectionClick: Section already being edited, checking if dialog is visible: ' + sectionId, 'CLICK'); - // If section is editing but no dialog is visible, allow re-opening - const existingDialog = document.querySelector('.ui-edit-floating-menu'); - if (existingDialog) { - debug('handleSectionClick: Dialog already visible, ignoring click', 'CLICK'); - return; - } else { - debug('handleSectionClick: Section editing but no dialog visible, proceeding to show editor', 'CLICK'); - } - } - - debug('handleSectionClick: About to start editing for section: ' + sectionId, 'CLICK'); - - try { - debug('handleSectionClick: Calling sectionManager.startEditing', 'CLICK'); - this.sectionManager.startEditing(sectionId); - debug('handleSectionClick: Successfully called startEditing', 'CLICK'); - } catch (error) { - debug('handleSectionClick: ERROR in startEditing: ' + error.message, 'ERROR'); - console.error('Failed to start editing:', error); - } - } - - /** - * Show editor for a section - */ - showEditor(sectionId, content) { - debug('showEditor: called for section: ' + sectionId, 'EDITOR'); - - const element = this.findSectionElement(sectionId); - debug('showEditor: Found element: ' + (element ? 'YES' : 'NO'), 'EDITOR'); - if (!element) return; - - debug('showEditor: About to hide current editor', 'EDITOR'); - this.hideCurrentEditor(); - debug('showEditor: Hidden current editor', 'EDITOR'); - - const section = this.sectionManager.sections.get(sectionId); - const isImageSection = section && section.isImage(); - - if (isImageSection) { - this.showImageEditor(sectionId, section); - return; - } - - // Create content area for text editing - const editorContent = document.createElement('div'); - editorContent.className = 'ui-edit-editor-content'; - - // Check if we have space for side-by-side layout - const targetElement = this.findSectionElement(sectionId); - const rect = targetElement ? targetElement.getBoundingClientRect() : null; - const viewport = { width: window.innerWidth, height: window.innerHeight }; - const hasWideLayout = rect && viewport.width >= 800 && (viewport.width - rect.right) >= 120; - - if (hasWideLayout) { - // Side-by-side layout: textarea on left, controls on right - editorContent.style.cssText = ` - display: flex; - gap: 16px; - flex: 1; - min-width: 0; - align-items: flex-start; - `; - } else { - // Stacked layout: textarea above, controls below - editorContent.style.cssText = ` - display: flex; - flex-direction: column; - gap: 12px; - flex: 1; - min-width: 0; - `; - } - - // Create textarea container - const textareaContainer = document.createElement('div'); - textareaContainer.style.cssText = hasWideLayout ? 'flex: 1; min-width: 0;' : 'width: 100%;'; - - // Create textarea - const textarea = document.createElement('textarea'); - textarea.value = content || section.currentMarkdown; - textarea.style.cssText = ` - width: 100%; - min-height: 120px; - padding: 12px; - border: 1px solid #ddd; - border-radius: 4px; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 14px; - line-height: 1.5; - resize: vertical; - box-sizing: border-box; - `; - - // Create controls - const controls = document.createElement('div'); - if (hasWideLayout) { - controls.style.cssText = ` - display: flex; - flex-direction: column; - gap: 8px; - min-width: 100px; - flex-shrink: 0; - `; - } else { - controls.style.cssText = ` - display: flex; - gap: 8px; - justify-content: flex-end; - flex-wrap: wrap; - `; - } - - const acceptButton = document.createElement('button'); - acceptButton.textContent = hasWideLayout ? 'โœ“' : 'Accept'; - acceptButton.style.cssText = ` - background: #28a745; - color: white; - border: none; - padding: ${hasWideLayout ? '8px 12px' : '8px 16px'}; - border-radius: 4px; - cursor: pointer; - ${hasWideLayout ? 'width: 100%;' : ''} - font-size: ${hasWideLayout ? '14px' : '13px'}; - `; - - const cancelButton = document.createElement('button'); - cancelButton.textContent = hasWideLayout ? 'โœ—' : 'Cancel'; - cancelButton.style.cssText = ` - background: #dc3545; - color: white; - border: none; - padding: ${hasWideLayout ? '8px 12px' : '8px 16px'}; - border-radius: 4px; - cursor: pointer; - ${hasWideLayout ? 'width: 100%;' : ''} - font-size: ${hasWideLayout ? '14px' : '13px'}; - `; - - const resetButton = document.createElement('button'); - resetButton.textContent = hasWideLayout ? 'โ†บ' : 'โ†บ Reset'; - resetButton.style.cssText = ` - background: #fd7e14; - color: white; - border: none; - padding: ${hasWideLayout ? '8px 12px' : '8px 16px'}; - border-radius: 4px; - cursor: pointer; - ${hasWideLayout ? 'width: 100%;' : ''} - font-size: ${hasWideLayout ? '14px' : '13px'}; - `; - - controls.appendChild(acceptButton); - controls.appendChild(cancelButton); - controls.appendChild(resetButton); - - // Assemble the layout - textareaContainer.appendChild(textarea); - - if (hasWideLayout) { - editorContent.appendChild(textareaContainer); - editorContent.appendChild(controls); - } else { - editorContent.appendChild(textareaContainer); - editorContent.appendChild(controls); - } - - // Create floating menu - const floatingMenu = new FloatingMenu(sectionId, 'text', this); - this.currentFloatingMenu = floatingMenu; - this.editingSections.add(sectionId); - - floatingMenu.show(editorContent); - - // Add event listeners - acceptButton.addEventListener('click', () => { - this.sectionManager.updateContent(sectionId, textarea.value); - this.sectionManager.acceptChanges(sectionId); - floatingMenu.hide(); - this.currentFloatingMenu = null; // Clear reference - }); - - cancelButton.addEventListener('click', () => { - this.sectionManager.cancelChanges(sectionId); - floatingMenu.hide(); - this.currentFloatingMenu = null; // Clear reference - }); - - resetButton.addEventListener('click', () => { - // Reset textarea to original content and apply the change - const section = this.sectionManager.sections.get(sectionId); - if (section) { - textarea.value = section.originalMarkdown; - // Actually update the section content to original and accept the changes - this.sectionManager.updateContent(sectionId, section.originalMarkdown); - this.sectionManager.acceptChanges(sectionId); - // Close the editor - floatingMenu.hide(); - this.currentFloatingMenu = null; - } - }); - - // Auto-focus textarea - setTimeout(() => textarea.focus(), 100); - } - - /** - * Show advanced image editor with drag & drop, file upload, and preview - */ - showImageEditor(sectionId, section) { - debug('showImageEditor: called for image section: ' + sectionId, 'EDITOR'); - - // Track staging state for this editor - const stagingState = { - originalMarkdown: section.originalMarkdown, - currentAltText: '', - currentImageSrc: '', - stagedImageSrc: null, - stagedAltText: null, - hasChanges: false - }; - - // Parse markdown to extract image info - const imageMatch = section.currentMarkdown.match(/!\[(.*?)\]\((.*?)\)/); - if (imageMatch) { - const [, altText, imageSrc] = imageMatch; - stagingState.currentAltText = altText; - stagingState.currentImageSrc = imageSrc; - } - - // Check if we have space for side-by-side layout - const targetElement = this.findSectionElement(sectionId); - const rect = targetElement ? targetElement.getBoundingClientRect() : null; - const viewport = { width: window.innerWidth, height: window.innerHeight }; - const hasWideLayout = rect && viewport.width >= 800 && (viewport.width - rect.right) >= 120; - - // Create image editor content area - const editorContent = document.createElement('div'); - editorContent.className = 'ui-edit-image-content'; - - if (hasWideLayout) { - // Side-by-side layout: content on left, controls on right - editorContent.style.cssText = ` - display: flex; - gap: 16px; - flex: 1; - min-width: 0; - align-items: flex-start; - `; - } else { - // Stacked layout: content above, controls below - editorContent.style.cssText = ` - display: flex; - flex-direction: column; - gap: 15px; - flex: 1; - min-width: 0; - `; - } - - // Create content container for image and alt text - const contentContainer = document.createElement('div'); - contentContainer.style.cssText = hasWideLayout ? 'flex: 1; min-width: 0;' : 'width: 100%;'; - if (!hasWideLayout) { - contentContainer.style.cssText += ` - display: flex; - flex-direction: column; - gap: 15px; - `; - } else { - contentContainer.style.cssText += ` - display: flex; - flex-direction: column; - gap: 12px; - `; - } - - // Image preview with drop zone - const imagePreview = document.createElement('div'); - imagePreview.className = 'ui-edit-image-preview'; - imagePreview.style.cssText = ` - width: 100%; - height: 180px; - text-align: center; - background: white; - padding: 12px; - border-radius: 8px; - border: 2px dashed #007bff; - transition: all 0.3s ease; - cursor: pointer; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - position: relative; - box-sizing: border-box; - overflow: hidden; - `; - - // 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: 150px; - 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: 16px; - `; - 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: 14px; - `; - 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.className = 'ui-edit-alt-text-container'; - altTextContainer.style.cssText = ` - display: flex; - flex-direction: column; - gap: 8px; - `; - - const altTextLabel = document.createElement('label'); - altTextLabel.textContent = 'Alt Text Description:'; - altTextLabel.style.cssText = ` - font-size: 13px; - font-weight: 600; - color: #333; - margin: 0; - `; - - const altTextInput = document.createElement('input'); - altTextInput.type = 'text'; - altTextInput.value = stagingState.currentAltText; - altTextInput.style.cssText = ` - width: 100%; - padding: 10px 12px; - border: 1px solid #ddd; - border-radius: 6px; - font-size: 14px; - box-sizing: border-box; - outline: none; - transition: border-color 0.2s ease; - `; - - altTextInput.addEventListener('focus', () => { - altTextInput.style.borderColor = '#007bff'; - }); - - altTextInput.addEventListener('blur', () => { - altTextInput.style.borderColor = '#ddd'; - }); - - // Track alt text changes - altTextInput.addEventListener('input', () => { - stagingState.stagedAltText = altTextInput.value; - stagingState.hasChanges = altTextInput.value !== stagingState.currentAltText || stagingState.stagedImageSrc !== null; - updateChangeIndicator(); - }); - - 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: 6px; - color: #856404; - font-size: 12px; - text-align: center; - display: none; - font-weight: 500; - `; - changeIndicator.textContent = 'โš ๏ธ You have unsaved changes'; - - const updateChangeIndicator = () => { - if (stagingState.hasChanges) { - changeIndicator.style.display = 'block'; - } else { - changeIndicator.style.display = 'none'; - } - }; - - // Assemble content container - contentContainer.appendChild(imagePreview); - contentContainer.appendChild(altTextContainer); - contentContainer.appendChild(changeIndicator); - contentContainer.appendChild(fileInput); - - // Create controls - const controls = document.createElement('div'); - controls.className = 'ui-edit-controls'; - if (hasWideLayout) { - controls.style.cssText = ` - display: flex; - flex-direction: column; - gap: 8px; - min-width: 100px; - flex-shrink: 0; - `; - } else { - controls.style.cssText = ` - display: flex; - flex-direction: column; - gap: 8px; - width: 100%; - `; - } - - const acceptBtn = document.createElement('button'); - acceptBtn.textContent = hasWideLayout ? 'โœ“' : 'โœ“ Accept'; - acceptBtn.style.cssText = ` - padding: ${hasWideLayout ? '8px 12px' : '8px 12px'}; - font-size: ${hasWideLayout ? '14px' : '12px'}; - border-radius: 6px; - border: none; - color: white; - cursor: pointer; - font-weight: 600; - transition: all 0.2s ease; - width: 100%; - text-align: center; - background: #28a745; - `; - - const cancelBtn = document.createElement('button'); - cancelBtn.textContent = hasWideLayout ? 'โœ—' : 'โœ— Cancel'; - cancelBtn.style.cssText = ` - padding: ${hasWideLayout ? '8px 12px' : '8px 12px'}; - font-size: ${hasWideLayout ? '14px' : '12px'}; - border-radius: 6px; - border: none; - color: white; - cursor: pointer; - font-weight: 600; - transition: all 0.2s ease; - width: 100%; - text-align: center; - background: #dc3545; - `; - - const resetBtn = document.createElement('button'); - resetBtn.textContent = hasWideLayout ? 'โ†บ' : 'โ†บ Reset'; - resetBtn.style.cssText = ` - padding: ${hasWideLayout ? '8px 12px' : '8px 12px'}; - font-size: ${hasWideLayout ? '14px' : '12px'}; - border-radius: 6px; - border: none; - color: white; - cursor: pointer; - font-weight: 600; - transition: all 0.2s ease; - width: 100%; - text-align: center; - background: #fd7e14; - `; - - controls.appendChild(acceptBtn); - controls.appendChild(cancelBtn); - controls.appendChild(resetBtn); - - - // Event handlers - acceptBtn.addEventListener('click', () => { - // 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); - } - - // Accept changes and hide editor - this.sectionManager.acceptChanges(sectionId); - this.currentFloatingMenu.hide(); - this.currentFloatingMenu = null; - }); - - cancelBtn.addEventListener('click', () => { - // Discard all staged changes and hide editor - this.sectionManager.cancelChanges(sectionId); - this.currentFloatingMenu.hide(); - this.currentFloatingMenu = null; - }); - - resetBtn.addEventListener('click', (event) => { - event.preventDefault(); - event.stopPropagation(); - - // Reset to original content - const originalImageMatch = stagingState.originalMarkdown.match(/!\[(.*?)\]\((.*?)\)/); - - if (originalImageMatch) { - const [, originalAltText, originalImageSrc] = originalImageMatch; - - // Update staging state to original values - 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 = originalAltText; - - // Trigger input event to ensure UI consistency - const inputEvent = new Event('input', { bubbles: true, cancelable: true }); - altTextInput.dispatchEvent(inputEvent); - - // Reset preview to original image - updateImagePreview(originalImageSrc, originalAltText); - - // Update change indicator - updateChangeIndicator(); - - // Actually update the section content to original and accept the changes - this.sectionManager.updateContent(sectionId, stagingState.originalMarkdown); - this.sectionManager.acceptChanges(sectionId); - - // Close the editor - this.currentFloatingMenu.hide(); - this.currentFloatingMenu = null; - } - }); - - // Assemble the final layout - if (hasWideLayout) { - editorContent.appendChild(contentContainer); - editorContent.appendChild(controls); - } else { - editorContent.appendChild(contentContainer); - editorContent.appendChild(controls); - } - - // Create floating menu - const floatingMenu = new FloatingMenu(sectionId, 'image', this); - this.currentFloatingMenu = floatingMenu; - this.editingSections.add(sectionId); - - floatingMenu.show(editorContent); - } - - /** - * Hide current editor - */ - hideCurrentEditor() { - debug('EDITOR: hideCurrentEditor called', 'EDITOR'); - - if (this.currentFloatingMenu) { - this.currentFloatingMenu.hide(); - this.currentFloatingMenu = null; - } - - debug('EDITOR: hideCurrentEditor completed', 'EDITOR'); - } - - /** - * Track event for analytics - */ - trackEvent(eventType, data) { - const eventRecord = { - type: eventType, - data: data, - timestamp: new Date().toISOString() - }; - - this.eventHistory.push(eventRecord); - if (this.eventStats.hasOwnProperty(eventType)) { - this.eventStats[eventType]++; - } - - // Keep only last 100 events - if (this.eventHistory.length > 100) { - this.eventHistory = this.eventHistory.slice(-100); - } - } - - /** - * Get event statistics - */ - getEventStats() { - const totalEvents = Object.values(this.eventStats).reduce((sum, count) => sum + count, 0); - - return { - stats: { ...this.eventStats }, - totalEvents, - recentEvents: this.eventHistory.slice(-10) - }; - } - - /** - * Handle keyboard shortcuts - */ - handleKeydown(event) { - // Basic keyboard shortcut handling - if (event.ctrlKey || event.metaKey) { - if (event.key === 'Enter') { - // Accept changes - const activeSection = Array.from(this.editingSections)[0]; - if (activeSection) { - this.trackEvent('keyboard-shortcut', { shortcut: 'ctrl+enter', action: 'accept' }); - } - } else if (event.key === 'Escape') { - // Cancel changes - const activeSection = Array.from(this.editingSections)[0]; - if (activeSection) { - this.trackEvent('keyboard-shortcut', { shortcut: 'escape', action: 'cancel' }); - this.hideCurrentEditor(); - } - } - } - } -} - -// Export for use in tests and other modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = { DOMRenderer, FloatingMenu }; -} - -// Export for browser use -if (typeof window !== 'undefined') { - window.DOMRenderer = DOMRenderer; - window.FloatingMenu = FloatingMenu; -} \ No newline at end of file diff --git a/markitect/static/js/config-loader.js b/markitect/static/js/config-loader.js deleted file mode 100644 index 70964a7c..00000000 --- a/markitect/static/js/config-loader.js +++ /dev/null @@ -1,168 +0,0 @@ -/** - * Configuration Loader - Clean interface between Python and JavaScript - * - * This module provides the ONLY interface for Python-generated data. - * All dynamic data from Python must be passed through this JSON configuration. - */ - -class MarkitectConfig { - constructor() { - this.config = null; - this.loaded = false; - - // Simple immediate loading - if script is loaded, DOM is ready - this.loadConfig(); - } - - loadConfig() { - try { - const configElement = document.getElementById('markitect-config'); - if (!configElement) { - throw new Error('Markitect configuration not found - missing markitect-config script element'); - } - - this.config = JSON.parse(configElement.textContent); - this.loaded = true; - console.log('โœ… Markitect configuration loaded successfully'); - - // Validate required fields - this.validateConfig(); - - } catch (error) { - console.error('โŒ Failed to load Markitect configuration:', error); - this.config = this.getDefaultConfig(); - } - } - - validateConfig() { - const required = ['markdownContent', 'mode']; - const missing = required.filter(key => !(key in this.config)); - - if (missing.length > 0) { - console.warn('โš ๏ธ Missing required config fields:', missing); - } - } - - getDefaultConfig() { - return { - markdownContent: '# Default Content\n\nConfiguration failed to load.', - markdownContentWithDogtag: '# Default Content\n\nConfiguration failed to load.', - dogtagContent: '', - mode: 'edit', - theme: 'github', - keyboardShortcuts: true, - autosave: false, - sections: true, - originalFilename: 'document', - version: 'Markitect v0.8.1', - repoName: 'Markitect', - base64References: {} - }; - } - - // Getter methods for clean access - get markdownContent() { - return this.config.markdownContent || ''; - } - - get markdownContentWithDogtag() { - return this.config.markdownContentWithDogtag || this.markdownContent; - } - - get dogtagContent() { - return this.config.dogtagContent || ''; - } - - get mode() { - return this.config.mode || 'edit'; - } - - get isEditMode() { - return this.mode === 'edit'; - } - - get isInsertMode() { - return this.mode === 'insert'; - } - - get theme() { - return this.config.theme || 'github'; - } - - get originalFilename() { - return this.config.originalFilename || 'document'; - } - - get version() { - return this.config.version || 'Markitect v0.8.1'; - } - - get repoName() { - return this.config.repoName || 'Markitect'; - } - - get keyboardShortcuts() { - return this.config.keyboardShortcuts !== false; - } - - get base64References() { - return this.config.base64References || {}; - } - - get restrictedHeadingLevels() { - return this.config.restrictedHeadingLevels || [1, 2, 3]; - } - - // Check if config is ready for access - isReady() { - return this.loaded && this.config !== null; - } - - // Wait for config to be ready - waitForReady(callback, maxWait = 5000) { - const startTime = Date.now(); - const checkReady = () => { - if (this.isReady()) { - callback(); - } else if (Date.now() - startTime < maxWait) { - setTimeout(checkReady, 50); - } else { - console.error('โŒ Configuration loading timeout after', maxWait, 'ms'); - callback(); // Call anyway with default config - } - }; - checkReady(); - } - - // Get full editor configuration object - getEditorConfig() { - if (!this.isReady()) { - console.warn('โš ๏ธ Configuration not ready, using defaults'); - return this.getDefaultConfig(); - } - - return { - mode: this.mode, - theme: this.theme, - keyboardShortcuts: this.keyboardShortcuts, - autosave: this.config.autosave || false, - sections: this.config.sections !== false, - originalFilename: this.originalFilename, - version: this.version, - repoName: this.repoName, - restrictedHeadingLevels: this.restrictedHeadingLevels - }; - } -} - -// Global configuration instance -window.markitectConfig = new MarkitectConfig(); - -// Legacy compatibility - expose common config values globally -window.editorConfig = window.markitectConfig.getEditorConfig(); -window.markitectBase64References = window.markitectConfig.base64References; - -// Export for module use -if (typeof module !== 'undefined' && module.exports) { - module.exports = MarkitectConfig; -} \ No newline at end of file diff --git a/markitect/static/js/core/debug-system.js b/markitect/static/js/core/debug-system.js deleted file mode 100644 index e9776da6..00000000 --- a/markitect/static/js/core/debug-system.js +++ /dev/null @@ -1,290 +0,0 @@ -/** - * Independent Debug System for Markitect - * Uses IndexedDB for persistence and provides selection-based filtering - */ -class MarkitectDebugSystem { - constructor() { - this.db = null; - this.messages = []; - this.maxMessages = 1000; - this.isEnabled = true; - this.subscribers = []; - - // Selection and filtering system - this.selectionCriteria = { - includeDocumentEvents: true, - includeSystemEvents: false, - includeControlEvents: true, - includeEditingEvents: true, - includeNavigationEvents: false, - includedHeadings: new Set(), // Track which document headings to monitor - excludedSources: new Set(['ContentsControl', 'DocumentNavigator']) - }; - - this.init(); - } - - // Initialize IndexedDB for persistence - async init() { - return new Promise((resolve, reject) => { - const request = indexedDB.open('MarkitectDebugDB', 1); - - request.onerror = () => reject(request.error); - request.onsuccess = () => { - this.db = request.result; - this.loadMessages().then(resolve); - }; - - request.onupgradeneeded = (e) => { - const db = e.target.result; - if (!db.objectStoreNames.contains('messages')) { - const store = db.createObjectStore('messages', { keyPath: 'id', autoIncrement: true }); - store.createIndex('timestamp', 'timestamp', { unique: false }); - store.createIndex('category', 'category', { unique: false }); - } - }; - }); - } - - // Add a debug message with selection filtering - async addMessage(message, category = 'INFO', source = 'System', context = {}) { - // Check if this message should be included based on selection criteria - if (!this.shouldIncludeMessage(message, category, source, context)) { - return null; - } - - const messageObj = { - timestamp: new Date().toISOString(), - message: String(message), - category: category.toUpperCase(), - source: String(source), - context: context || {}, - id: null // Will be set by IndexedDB - }; - - // Store in IndexedDB if available - if (this.db) { - try { - await this.saveMessage(messageObj); - } catch (error) { - console.warn('Failed to save debug message to IndexedDB:', error); - } - } - - // Store in memory - this.messages.unshift(messageObj); - - // Limit memory storage - if (this.messages.length > this.maxMessages) { - this.messages = this.messages.slice(0, this.maxMessages); - } - - // Notify subscribers - this.notifySubscribers(messageObj); - - // Console output for development - const consoleMethod = category.toLowerCase() === 'error' ? 'error' : - category.toLowerCase() === 'warning' ? 'warn' : 'log'; - console[consoleMethod](`[${source}] ${message}`, context); - - return messageObj; - } - - // Selection filtering logic - shouldIncludeMessage(message, category, source, context) { - if (!this.isEnabled) return false; - - const eventType = context.eventType || 'UNKNOWN'; - const criteria = this.selectionCriteria; - - // Check event type filters - switch (eventType.toUpperCase()) { - case 'DOCUMENT': - if (!criteria.includeDocumentEvents) return false; - break; - case 'SYSTEM': - if (!criteria.includeSystemEvents) return false; - break; - case 'CONTROL': - if (!criteria.includeControlEvents) return false; - break; - case 'EDITING': - if (!criteria.includeEditingEvents) return false; - break; - case 'NAVIGATION': - if (!criteria.includeNavigationEvents) return false; - break; - } - - // Check excluded sources - if (criteria.excludedSources.has(source)) { - return false; - } - - // Check heading-specific filtering - if (context.sectionId && criteria.includedHeadings.size > 0) { - const sectionElement = document.getElementById(context.sectionId); - if (sectionElement) { - const heading = sectionElement.querySelector('h1, h2, h3, h4, h5, h6'); - if (heading && !criteria.includedHeadings.has(heading.textContent.trim())) { - return false; - } - } - } - - return true; - } - - // Save message to IndexedDB - async saveMessage(messageObj) { - return new Promise((resolve, reject) => { - const transaction = this.db.transaction(['messages'], 'readwrite'); - const store = transaction.objectStore('messages'); - const request = store.add(messageObj); - - request.onsuccess = () => resolve(request.result); - request.onerror = () => reject(request.error); - }); - } - - // Load messages from IndexedDB - async loadMessages() { - if (!this.db) return []; - - return new Promise((resolve, reject) => { - const transaction = this.db.transaction(['messages'], 'readonly'); - const store = transaction.objectStore('messages'); - const request = store.getAll(); - - request.onsuccess = () => { - this.messages = request.result.reverse(); // Most recent first - resolve(this.messages); - }; - request.onerror = () => reject(request.error); - }); - } - - // Clear all messages - async clearMessages() { - this.messages = []; - - if (this.db) { - return new Promise((resolve, reject) => { - const transaction = this.db.transaction(['messages'], 'readwrite'); - const store = transaction.objectStore('messages'); - const request = store.clear(); - - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); - }); - } - } - - // Get filtered messages - getMessages(filter = {}) { - let filteredMessages = [...this.messages]; - - if (filter.category) { - filteredMessages = filteredMessages.filter(msg => - msg.category.toLowerCase() === filter.category.toLowerCase() - ); - } - - if (filter.source) { - filteredMessages = filteredMessages.filter(msg => - msg.source.toLowerCase().includes(filter.source.toLowerCase()) - ); - } - - if (filter.since) { - const sinceDate = new Date(filter.since); - filteredMessages = filteredMessages.filter(msg => - new Date(msg.timestamp) >= sinceDate - ); - } - - if (filter.limit) { - filteredMessages = filteredMessages.slice(0, filter.limit); - } - - return filteredMessages; - } - - // Update selection criteria - updateSelectionCriteria(updates) { - Object.assign(this.selectionCriteria, updates); - this.notifySubscribers({ type: 'criteria-updated', criteria: this.selectionCriteria }); - } - - // Add heading to monitoring - addHeadingToMonitoring(headingText) { - this.selectionCriteria.includedHeadings.add(headingText); - } - - // Remove heading from monitoring - removeHeadingFromMonitoring(headingText) { - this.selectionCriteria.includedHeadings.delete(headingText); - } - - // Scan document for available headings - scanDocumentHeadings() { - const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); - return Array.from(headings) - .map(h => h.textContent.trim()) - .filter(text => text.length > 0 && !text.toLowerCase().includes('control')); - } - - // Subscribe to debug messages - subscribe(callback) { - this.subscribers.push(callback); - return () => { - const index = this.subscribers.indexOf(callback); - if (index > -1) { - this.subscribers.splice(index, 1); - } - }; - } - - // Notify all subscribers - notifySubscribers(message) { - this.subscribers.forEach(callback => { - try { - callback(message); - } catch (error) { - console.error('Debug subscriber error:', error); - } - }); - } - - // Toggle debug system - setEnabled(enabled) { - this.isEnabled = enabled; - this.addMessage( - `Debug system ${enabled ? 'enabled' : 'disabled'}`, - 'INFO', - 'DebugSystem', - { eventType: 'SYSTEM' } - ); - } - - // Get statistics - getStats() { - const stats = { - total: this.messages.length, - byCategory: {}, - bySource: {}, - enabled: this.isEnabled, - criteria: { ...this.selectionCriteria } - }; - - this.messages.forEach(msg => { - stats.byCategory[msg.category] = (stats.byCategory[msg.category] || 0) + 1; - stats.bySource[msg.source] = (stats.bySource[msg.source] || 0) + 1; - }); - - return stats; - } -} - -// Initialize and expose globally -window.MarkitectDebugSystem = new MarkitectDebugSystem(); \ No newline at end of file diff --git a/markitect/static/js/core/section-manager.js b/markitect/static/js/core/section-manager.js deleted file mode 100644 index b1dc6fd0..00000000 --- a/markitect/static/js/core/section-manager.js +++ /dev/null @@ -1,544 +0,0 @@ -/** - * SectionManager Component - * - * Extracted from monolithic editor.js as part of architecture refactoring. - * Manages the collection of sections and their state transitions. - * - * Dependencies: - * - EditState enum (imported) - * - SectionType enum (imported) - * - Section class (imported) - * - debug function (imported) - */ - -// Import dependencies - these will be separate modules -const EditState = Object.freeze({ - ORIGINAL: 'original', - EDITING: 'editing', - MODIFIED: 'modified', - SAVED: 'saved' -}); - -const SectionType = Object.freeze({ - HEADING: 'heading', - PARAGRAPH: 'paragraph', - LIST: 'list', - CODE: 'code', - QUOTE: 'quote', - TABLE: 'table', - HR: 'hr', - IMAGE: 'image' -}); - -// Debug function (will be extracted to utils) -function debug(message, category = 'INFO') { - // Simple console debug for now - will be enhanced later - console.log(`DEBUG ${category}: ${message}`); -} - -/** - * Section Class - manages individual section state and content - */ -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(); - } - - static generateId(markdown, position, strategy = 'hash', parentId = null) { - return this.generateIdWithStrategy(markdown, position, strategy, parentId); - } - - 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); - } - } - - static generateAdvancedId(content, position, sectionType) { - const contentHash = this.generateCryptoHash(content); - const safeType = sectionType || 'paragraph'; - const typePrefix = safeType.substring(0, 3); - const positionHex = position.toString(16).padStart(2, '0'); - - return `section-${typePrefix}-${contentHash}-${positionHex}`; - } - - static generateCryptoHash(content) { - 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; - } - - const hexHash = Math.abs(hash).toString(16).padStart(8, '0'); - return hexHash.substring(0, 8); - } - - static normalizeContentForHashing(content) { - if (!content || typeof content !== 'string') { - return ''; - } - - return content - .trim() - .replace(/\s+/g, ' ') - .replace(/\r\n/g, '\n') - .toLowerCase(); - } - - static sanitizeContentForId(content) { - if (!content || typeof content !== 'string') { - return ''; - } - - return content - .replace(/<[^>]*>/g, '') - .replace(/javascript:/gi, '') - .replace(/[^\w\s\-_.#]/g, '') - .trim(); - } - - static generateTimestampId(content, position = 0, sectionType = 'paragraph') { - const timestamp = Date.now().toString(36); - const contentSnippet = this.generateCryptoHash(content || '').substring(0, 4); - const safeType = sectionType || 'paragraph'; - const typePrefix = safeType.substring(0, 3); - - return `section-${typePrefix}-${contentSnippet}-${timestamp}`; - } - - 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}`; - } - - 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}`; - } - } - - static detectType(markdown) { - if (!markdown || typeof markdown !== 'string') { - return SectionType.PARAGRAPH; - } - - const content = markdown.replace(/^\n+|\n+$/g, ''); - if (!content) { - return SectionType.PARAGRAPH; - } - - const trimmed = content.trim(); - - // Detection order matters - most specific first - if (this.isHeading(trimmed)) { - return SectionType.HEADING; - } - - if (this.isImage(trimmed)) { - return SectionType.IMAGE; - } - - if (this.isCodeBlock(trimmed)) { - return SectionType.CODE; - } - - return SectionType.PARAGRAPH; - } - - static isHeading(trimmed) { - const headingPattern = /^#{1,6}\s+.+/; - return headingPattern.test(trimmed); - } - - static isImage(trimmed) { - const imagePattern = /!\[.*?\]\([^)]+\)/; - return imagePattern.test(trimmed); - } - - static isCodeBlock(trimmed) { - if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) { - return true; - } - if (trimmed.includes('```') || trimmed.includes('~~~')) { - const codeBlockPattern = /```[\s\S]*?```|~~~[\s\S]*?~~~/; - if (codeBlockPattern.test(trimmed)) { - return true; - } - } - return false; - } - - 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 (this.editingMarkdown && this.editingMarkdown !== this.currentMarkdown) { - this.pendingMarkdown = this.editingMarkdown; - this.state = EditState.MODIFIED; - } else { - 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, - type: this.type, - originalLength: this.originalMarkdown.length, - currentLength: this.currentMarkdown.length - }; - } - - isImage() { - return this.type === SectionType.IMAGE; - } - - redetectType(content = null) { - const markdown = content || this.currentMarkdown; - const oldType = this.type; - this.type = Section.detectType(markdown); - - if (oldType !== this.type) { - debug(`Section ${this.id} type changed from ${oldType} to ${this.type}`, 'TYPE_DETECTION'); - } - - return this.type; - } -} - -/** - * SectionManager - 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) { - // Split content into blocks separated by double newlines - const blocks = markdownContent.split(/\n\s*\n/); - const sections = []; - let position = 0; - - for (const block of blocks) { - const trimmedBlock = block.trim(); - if (!trimmedBlock) continue; - - // Check if this block should be split further - const lines = trimmedBlock.split('\n'); - let currentSection = ''; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const isHeading = /^#{1,6}\s/.test(line.trim()); - const isImage = /^\s*!\[.*?\]\(.*?\)\s*$/.test(line); - - // Each heading or image starts a new section - if ((isHeading || isImage) && currentSection.trim()) { - // Save the previous section - 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; - } - } - - // Save the final section from this block - 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); - position++; - } - } - - this.emit('sections-created', { sections, count: sections.length }); - return sections; - } - - startEditing(sectionId) { - debug('MANAGER: startEditing called for: ' + sectionId, 'MANAGER'); - - const section = this.sections.get(sectionId); - if (!section) { - throw new Error(`Section ${sectionId} not found`); - } - - if (section.isEditing()) { - debug('MANAGER: Section already in editing state: ' + sectionId, 'MANAGER'); - return section.editingMarkdown; - } - - debug('MANAGER: Starting edit for section: ' + sectionId, 'MANAGER'); - const content = section.startEdit(); - - debug('MANAGER: About to emit edit-started event for: ' + sectionId, 'MANAGER'); - this.emit('edit-started', { sectionId, content, section: section.getStatus() }); - debug('MANAGER: Emitted edit-started event for: ' + sectionId, 'MANAGER'); - - return content; - } - - updateContent(sectionId, markdown) { - const section = this.sections.get(sectionId); - if (!section) { - throw new Error(`Section ${sectionId} not found`); - } - - const oldType = section.type; - section.updateContent(markdown); - const newType = section.redetectType(markdown); - - 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()); - } - - getDocumentStatus() { - const sections = Array.from(this.sections.values()); - const editingSections = sections.filter(section => section.isEditing).length; - - return { - totalSections: sections.length, - editingSections: editingSections - }; - } - - extractHeadings(content) { - if (!content) return []; - const lines = content.split('\n'); - return lines.filter(line => /^#{1,6}\s/.test(line.trim())); - } - - 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.createSectionsFromMarkdown(newContent); - - // Emit section-split event - this.emit('section-split', { - originalSectionId: sectionId, - newSections: newSections, - count: newSections.length - }); - - return newSections; - } - - createSectionsFromContent(content) { - return this.createSectionsFromMarkdown(content); - } -} - -// Export for use in tests and other modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = { SectionManager, Section, EditState, SectionType }; -} - -// Export for browser use -if (typeof window !== 'undefined') { - window.SectionManager = SectionManager; - window.Section = Section; - window.EditState = EditState; - window.SectionType = SectionType; -} \ No newline at end of file diff --git a/markitect/static/js/main-updated.js b/markitect/static/js/main-updated.js deleted file mode 100644 index 1ce2fea3..00000000 --- a/markitect/static/js/main-updated.js +++ /dev/null @@ -1,227 +0,0 @@ -/** - * Main Markitect JavaScript Entry Point - Clean Architecture Version - * - * Uses ONLY the JSON configuration interface - NO Python-generated JavaScript! - * Initializes all controls and systems when document is ready - * Implements graceful degradation for missing dependencies - */ - -// Main application module -const MarkitectMain = { - initialized: false, - config: null, - - // Initialize the complete application - initialize: function() { - if (this.initialized) { - console.log('โš ๏ธ MarkitectMain already initialized, skipping'); - return; - } - - console.log('๐Ÿš€ MarkitectMain initializing...'); - - try { - // Get configuration - if not loaded, use defaults - this.config = window.markitectConfig; - if (!this.config || !this.config.loaded) { - console.warn('โš ๏ธ Configuration not loaded, proceeding with defaults'); - this.config = { - markdownContent: document.querySelector('#markdown-content')?.textContent || '', - mode: 'edit', - theme: 'github' - }; - } - - // Initialize core systems - this.initializeCoreComponents(); - this.initializeControlPanels(); - this.setupEventHandlers(); - this.renderContent(); - - this.initialized = true; - console.log('โœ… MarkitectMain initialization complete'); - - } catch (error) { - console.error('โŒ MarkitectMain initialization failed:', error); - this.fallbackMode(); - } - }, - - // Initialize core modular components - initializeCoreComponents: function() { - console.log('๐Ÿ”ง Initializing core components...'); - - const container = document.getElementById('markdown-content') || document.body; - - // Initialize section manager - if (typeof SectionManager !== 'undefined') { - this.sectionManager = new SectionManager(); - console.log('โœ… SectionManager initialized'); - } else { - throw new Error('SectionManager not available'); - } - - // Initialize DOM renderer - if (typeof DOMRenderer !== 'undefined') { - this.domRenderer = new DOMRenderer(this.sectionManager, container); - console.log('โœ… DOMRenderer initialized'); - } else { - throw new Error('DOMRenderer not available'); - } - - // Initialize debug panel - if (typeof DebugPanel !== 'undefined') { - this.debugPanel = new DebugPanel(); - console.log('โœ… DebugPanel initialized'); - } - - // Legacy DocumentControls removed - functionality now in enhanced control panels - }, - - // Initialize enhanced control panels with compass positioning - initializeControlPanels: function() { - console.log('๐ŸŽ›๏ธ Initializing enhanced control panels with compass positioning...'); - - // ContentsControl (West) - if (typeof ContentsControl !== 'undefined') { - this.contentsControl = new ContentsControl(); - this.contentsControl.config.position = 'w'; - this.contentsControl.show(); - window.contentsControl = this.contentsControl; - console.log('โœ… ContentsControl initialized (West) with enhanced ControlBase'); - } - - // StatusControl (East) - if (typeof StatusControl !== 'undefined') { - this.statusControl = new StatusControl(); - this.statusControl.config.position = 'e'; - this.statusControl.show(); - window.statusControl = this.statusControl; - console.log('โœ… StatusControl initialized (East) with enhanced ControlBase'); - } - - // DebugControl (Southeast) - if (typeof DebugControl !== 'undefined') { - this.debugControl = new DebugControl(); - this.debugControl.config.position = 'se'; - this.debugControl.show(); - window.debugControl = this.debugControl; - console.log('โœ… DebugControl initialized (Southeast) with enhanced ControlBase'); - } - - // EditControl (Northeast) - if (typeof EditControl !== 'undefined') { - this.editControl = new EditControl(); - this.editControl.config.position = 'ne'; - this.editControl.show(); - window.editControl = this.editControl; - console.log('โœ… EditControl initialized (Northeast) with enhanced ControlBase'); - } - }, - - // Setup core event handlers (enhanced control panels handle their own events) - setupEventHandlers: function() { - console.log('๐Ÿ”Œ Setting up core event handlers...'); - - // Setup section manager event handlers for debug panel - if (this.sectionManager && this.debugPanel) { - this.sectionManager.on('sections-created', (data) => { - this.debugPanel.addMessage(`Created ${data.count} sections`, 'INFO'); - }); - - this.sectionManager.on('edit-started', (data) => { - this.debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG'); - }); - - this.sectionManager.on('changes-accepted', (data) => { - this.debugPanel.addMessage(`Changes accepted for section: ${data.sectionId}`, 'SUCCESS'); - this.updateSectionDOM(data.sectionId); - }); - - this.sectionManager.on('changes-cancelled', (data) => { - this.debugPanel.addMessage(`Changes cancelled for section: ${data.sectionId}`, 'WARNING'); - }); - } - - // Make core components available globally for enhanced controls - window.sectionManager = this.sectionManager; - window.domRenderer = this.domRenderer; - window.debugPanel = this.debugPanel; - - console.log('โœ… Core event handlers and global references set up'); - }, - - // Render content using the configuration - renderContent: function() { - console.log('๐Ÿ“„ Rendering markdown content...'); - - const markdownToRender = this.config.markdownContent || ''; - if (markdownToRender.trim()) { - const sections = this.sectionManager.createSectionsFromMarkdown(markdownToRender); - this.domRenderer.renderAllSections(sections); - - if (this.debugPanel) { - this.debugPanel.addMessage(`Initialized with ${sections.length} sections`, 'INFO'); - } - console.log(`โœ… Rendered ${sections.length} sections`); - } else { - if (this.debugPanel) { - this.debugPanel.addMessage('No markdown content to initialize', 'WARNING'); - } - console.warn('โš ๏ธ No markdown content to render'); - } - }, - - // Update section DOM after changes - updateSectionDOM: function(sectionId) { - try { - const section = this.sectionManager.sections.get(sectionId); - if (section) { - const sectionElement = this.domRenderer.findSectionElement(sectionId); - if (sectionElement) { - const newElement = this.domRenderer.renderSection(section); - sectionElement.parentNode.replaceChild(newElement, sectionElement); - - if (this.debugPanel) { - this.debugPanel.addMessage(`DOM updated for section: ${sectionId}`, 'INFO'); - } - } - } - } catch (error) { - console.error('โŒ Failed to update section DOM:', error); - } - }, - - // Fallback mode if initialization fails - fallbackMode: function() { - console.warn('โš ๏ธ Running in fallback mode'); - - // Basic content rendering fallback - const contentDiv = document.getElementById('markdown-content'); - if (contentDiv && this.config && this.config.markdownContent) { - const basicHtml = this.config.markdownContent - .replace(/^# (.*$)/gim, '

$1

') - .replace(/^## (.*$)/gim, '

$1

') - .replace(/^### (.*$)/gim, '

$1

') - .replace(/\n\n/g, '

') - .replace(/\n/g, '
'); - - contentDiv.innerHTML = `

${basicHtml}

`; - console.log('โœ… Fallback content rendered'); - } - } -}; - -// Make components globally available for debugging -window.MarkitectMain = MarkitectMain; - -// Auto-initialize when DOM is ready -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', function() { - // Small delay to ensure config is loaded - setTimeout(() => MarkitectMain.initialize(), 100); - }); -} else { - // DOM already ready - setTimeout(() => MarkitectMain.initialize(), 100); -} \ No newline at end of file diff --git a/markitect/static/js/main.js b/markitect/static/js/main.js deleted file mode 100644 index 40f8c482..00000000 --- a/markitect/static/js/main.js +++ /dev/null @@ -1,201 +0,0 @@ -/** - * Main Markitect JavaScript Entry Point - * Initializes all controls and systems when document is ready - * Implements graceful degradation for missing dependencies - * Supports Fail Fast strict mode for development - */ - -// Development mode detection -const MARKITECT_STRICT_MODE = ( - window.location.hostname === 'localhost' || - window.location.hostname === '127.0.0.1' || - window.location.search.includes('strict=true') || - window.markitectStrictMode === true -); - -// Utility functions for safe initialization -const MarkitectMain = { - // Safe dependency checking with timeout - checkDependencies: function() { - const dependencies = { - debugSystem: !!window.MarkitectDebugSystem, - control: !!window.Control, - statusControl: !!window.StatusControl, - debugControl: !!window.DebugControl, - contentsControl: !!window.ContentsControl, - editControl: !!window.EditControl - }; - - console.log('๐Ÿ“‹ Dependency check results:', dependencies); - return dependencies; - }, - - // Safe logging that works even without debug system - safeLog: function(message, level = 'INFO', component = 'Main', data = {}) { - console.log(`[${level}] ${component}: ${message}`); - - // In strict mode, throw on errors for immediate development feedback - if (MARKITECT_STRICT_MODE && level === 'ERROR') { - console.error(`๐Ÿšจ STRICT MODE: Throwing error for immediate diagnosis`); - throw new Error(`${component}: ${message}`); - } - - // Try to use debug system if available - if (window.MarkitectDebugSystem && window.MarkitectDebugSystem.addMessage) { - try { - window.MarkitectDebugSystem.addMessage(message, level, component, { ...data, eventType: 'SYSTEM' }); - } catch (error) { - console.warn('Debug system logging failed:', error); - if (MARKITECT_STRICT_MODE) { - throw error; // Fail fast in development - } - } - } - }, - - // Safe control initialization with fallbacks - initializeControl: function(controlClass, controlName, icon = '๐Ÿ”ง') { - const timeout = setTimeout(() => { - const message = `${controlName} initialization timed out`; - console.warn(message); - if (MARKITECT_STRICT_MODE) { - throw new Error(message); // Fail fast in development - } - }, 5000); - - try { - if (!controlClass) { - const message = `${controlName} class not available, skipping`; - this.safeLog(message, MARKITECT_STRICT_MODE ? 'ERROR' : 'WARNING'); - clearTimeout(timeout); - return null; - } - - const controlInstance = new controlClass(); - if (!controlInstance || typeof controlInstance.createControl !== 'function') { - throw new Error(`Invalid ${controlName} instance`); - } - - const element = controlInstance.createControl(); - if (!element) { - throw new Error(`${controlName} failed to create element`); - } - - clearTimeout(timeout); - this.safeLog(`${controlName} initialized successfully`, 'SUCCESS'); - return controlInstance; - - } catch (error) { - clearTimeout(timeout); - this.safeLog(`${controlName} initialization failed: ${error.message}`, 'ERROR'); - - // Create minimal fallback control if core Control class exists - if (window.Control && controlName === 'StatusControl') { - return this.createFallbackControl(controlName, icon); - } - - return null; - } - }, - - // Create minimal fallback control for essential controls - createFallbackControl: function(name, icon) { - try { - const fallback = Object.create(window.Control); - fallback.config = { - icon: icon, - title: `${name} (Fallback)`, - className: `${name.toLowerCase()}-fallback`, - defaultContent: `${name} is running in fallback mode due to initialization issues.`, - ariaLabel: `${name} Fallback Control`, - position: 'e' - }; - - const element = fallback.createControl(); - if (element) { - this.safeLog(`${name} fallback control created`, 'INFO'); - return { control: fallback }; - } - } catch (error) { - this.safeLog(`Fallback control creation failed: ${error.message}`, 'ERROR'); - } - return null; - }, - - // Main initialization with comprehensive error handling - initialize: function() { - this.safeLog('๐Ÿš€ Initializing Markitect controls and systems...', 'INFO'); - - // Check dependencies first - const deps = this.checkDependencies(); - - if (!deps.control) { - this.safeLog('โŒ Core Control system not available, cannot initialize UI controls', 'ERROR'); - return; - } - - const initializedControls = {}; - let successCount = 0; - let totalAttempts = 0; - - // Initialize controls with graceful degradation - const controlsToInit = [ - { class: window.StatusControl, name: 'StatusControl', key: 'statusControl', icon: '๐Ÿ“Š', essential: true }, - { class: window.DebugControl, name: 'DebugControl', key: 'debugControl', icon: '๐Ÿชฒ', essential: false }, - { class: window.ContentsControl, name: 'ContentsControl', key: 'contentsControl', icon: 'โ˜ฐ', essential: false }, - { class: window.EditControl, name: 'EditControl', key: 'editControl', icon: 'โœ๏ธ', essential: false } - ]; - - controlsToInit.forEach(({ class: controlClass, name, key, icon, essential }) => { - totalAttempts++; - const instance = this.initializeControl(controlClass, name, icon); - - if (instance) { - initializedControls[key] = instance.control || instance; - window[key] = initializedControls[key]; - successCount++; - } else if (essential) { - this.safeLog(`Essential control ${name} failed to initialize`, 'ERROR'); - } - }); - - // Report initialization results - const successRate = Math.round((successCount / totalAttempts) * 100); - if (successCount === totalAttempts) { - this.safeLog('โœ… All controls initialized successfully', 'SUCCESS'); - } else if (successCount > 0) { - this.safeLog(`โš ๏ธ Partial initialization: ${successCount}/${totalAttempts} controls (${successRate}%) initialized`, 'WARNING'); - } else { - this.safeLog('โŒ No controls could be initialized', 'ERROR'); - } - - // Set up global error handlers for runtime protection - this.setupErrorHandlers(); - - this.safeLog(`โœ… Markitect initialization complete (${successCount}/${totalAttempts} controls active)`, 'INFO'); - }, - - // Set up global error handlers - setupErrorHandlers: function() { - // Catch unhandled errors - window.addEventListener('error', (event) => { - this.safeLog(`Unhandled error: ${event.message} at ${event.filename}:${event.lineno}`, 'ERROR'); - }); - - // Catch unhandled promise rejections - window.addEventListener('unhandledrejection', (event) => { - this.safeLog(`Unhandled promise rejection: ${event.reason}`, 'ERROR'); - event.preventDefault(); // Prevent console spam - }); - } -}; - -// Initialize when DOM is ready with additional safety -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => { - setTimeout(() => MarkitectMain.initialize(), 100); // Brief delay for dependencies - }); -} else { - // DOM already loaded - setTimeout(() => MarkitectMain.initialize(), 100); -} \ No newline at end of file diff --git a/markitect/static/js/plugins/document-navigator-plugin.js b/markitect/static/js/plugins/document-navigator-plugin.js deleted file mode 100644 index e95907cf..00000000 --- a/markitect/static/js/plugins/document-navigator-plugin.js +++ /dev/null @@ -1,207 +0,0 @@ -/** - * DocumentNavigator Plugin Definition - * - * Plugin definition for the Substack-style document navigation widget. - * Provides floating table of contents with smooth scrolling and scroll spy. - */ -export default { - name: 'DocumentNavigator', - version: '1.0.0', - description: 'Substack-style floating document navigation with table of contents', - author: 'Markitect Core', - category: 'navigation', - - // Dependencies that must be loaded first - dependencies: ['UIWidget'], - - // Mixins to apply (none required for this widget) - mixins: [], - - // Lazy load the actual widget class - async load() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - return DocumentNavigator; - }, - - // Default configuration - defaultOptions: { - position: 'left', // 'left' or 'right' side - collapsed: true, // Start in collapsed state - autoHide: true, // Hide on mobile devices - maxHeadingLevel: 3, // Include H1, H2, H3 - enableScrollSpy: true, // Highlight current section - smoothScroll: true, // Smooth scroll to headings - animationDuration: 300, // Animation timing in ms - minHeadings: 2, // Minimum headings to show widget - theme: 'default', // Theme variant - - // Layout options - width: '280px', // Expanded width - collapsedWidth: '40px', // Collapsed width - offset: { // Position offset - top: '80px', - side: '20px' - }, - - // Accessibility - enableKeyboard: true, // Keyboard navigation support - ariaLabel: 'Document Navigation' - }, - - // Plugin lifecycle hooks - async onLoad(instance, options) { - console.log('DocumentNavigator plugin loaded:', { - headings: instance.headings.length, - position: options.position, - collapsed: options.collapsed - }); - - // Auto-initialize after load - await instance.initialize(); - - return instance; - }, - - async onUnload(instance) { - console.log('DocumentNavigator plugin unloading'); - await instance.destroy(); - }, - - // Feature flags and capabilities - capabilities: { - draggable: false, // Not draggable (fixed position) - resizable: false, // Not resizable (fixed width) - themeable: true, // Supports themes - persistent: false, // Rebuilds on page changes - responsive: true, // Responsive behavior - keyboard: true, // Keyboard accessible - scrollSpy: true, // Scroll spy functionality - smoothScroll: true // Smooth scroll navigation - }, - - // Integration requirements - requirements: { - container: true, // Requires container element - headings: true, // Requires document headings - scrollable: true // Requires scrollable content - }, - - // Event types emitted by this widget - events: [ - 'rendered', // Widget rendered to DOM - 'navigate', // User navigated to heading - 'toggle', // Widget expanded/collapsed - 'theme-changed', // Theme was changed - 'destroyed' // Widget was destroyed - ], - - // CSS classes used by this widget - cssClasses: [ - 'document-navigator', // Main widget class - 'navigator-toggle', // Toggle button - 'navigator-list', // Navigation list - 'navigator-item', // Navigation items - 'navigator-link', // Navigation links - 'navigator-header', // List header - 'navigator-close', // Close button - 'navigator-empty' // Empty state - ], - - // Theme variants - themes: { - default: { - backgroundColor: 'rgba(255, 255, 255, 0.95)', - borderColor: '#e1e5e9', - textColor: '#333', - activeColor: '#1976d2', - activeBackground: '#e3f2fd' - }, - dark: { - backgroundColor: 'rgba(45, 45, 45, 0.95)', - borderColor: '#555', - textColor: '#e0e0e0', - activeColor: '#64b5f6', - activeBackground: '#1e3a8a' - }, - minimal: { - backgroundColor: 'rgba(248, 249, 250, 0.90)', - borderColor: '#dee2e6', - textColor: '#495057', - activeColor: '#007bff', - activeBackground: '#e7f1ff' - } - }, - - // Usage examples - examples: { - basic: { - description: 'Basic document navigator on the left side', - code: ` - const navigator = await widgetSystem.createWidget('DocumentNavigator'); - await navigator.show(); - ` - }, - customized: { - description: 'Customized navigator with specific options', - code: ` - const navigator = await widgetSystem.createWidget('DocumentNavigator', { - position: 'right', - collapsed: false, - maxHeadingLevel: 4, - theme: 'dark' - }); - await navigator.show(); - ` - }, - withContainer: { - description: 'Navigator for specific container content', - code: ` - const container = document.getElementById('article-content'); - const navigator = await widgetSystem.createWidget('DocumentNavigator', { - container: container, - minHeadings: 1 - }); - await navigator.show(); - ` - } - }, - - // Development and testing helpers - dev: { - testHeadingStructure() { - // Helper to create test content with headings - const testContent = ` -

Chapter 1: Introduction

-

Lorem ipsum content...

-

Section 1.1: Overview

-

Subsection 1.1.1: Details

-

Section 1.2: Implementation

-

Chapter 2: Advanced Topics

-

Section 2.1: Performance

- `; - - const container = document.createElement('div'); - container.innerHTML = testContent; - container.style.cssText = 'height: 2000px; padding: 2rem;'; - document.body.appendChild(container); - - return container; - }, - - async createTestInstance(options = {}) { - // Helper to create test instance with sample content - const container = this.testHeadingStructure(); - - const navigator = new (await this.load())({ - container, - collapsed: false, - ...options - }); - - await navigator.initialize(); - await navigator.render(); - - return { navigator, container }; - } - } -}; \ No newline at end of file diff --git a/markitect/static/js/tests/refactor-test-runner.js b/markitect/static/js/tests/refactor-test-runner.js deleted file mode 100644 index ecc97529..00000000 --- a/markitect/static/js/tests/refactor-test-runner.js +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/env node - -/** - * TDD Test Runner for JavaScript Refactoring - * - * Drives component extraction and testing during architecture refactoring. - * Ensures all functionality remains stable while achieving separation of concerns. - */ - -class RefactorTestRunner { - constructor() { - this.tests = []; - this.passed = 0; - this.failed = 0; - this.currentSuite = null; - this.setupDOM(); - } - - setupDOM() { - // Set up minimal DOM environment for testing - if (typeof document === 'undefined') { - const { JSDOM } = require('jsdom'); - const dom = new JSDOM('', { - url: 'http://localhost', - pretendToBeVisual: true, - resources: 'usable' - }); - - global.window = dom.window; - global.document = dom.window.document; - global.HTMLElement = dom.window.HTMLElement; - global.Event = dom.window.Event; - global.CustomEvent = dom.window.CustomEvent; - - // Only set navigator if it doesn't exist - if (typeof global.navigator === 'undefined') { - global.navigator = dom.window.navigator; - } - } - } - - describe(suiteName, fn) { - console.log(`\n๐Ÿ“ ${suiteName}`); - this.currentSuite = suiteName; - fn(); - this.currentSuite = null; - } - - it(testName, fn) { - const fullName = this.currentSuite ? `${this.currentSuite}: ${testName}` : testName; - - try { - fn(); - console.log(` โœ… ${testName}`); - this.passed++; - } catch (error) { - console.log(` โŒ ${testName}`); - console.log(` Error: ${error.message}`); - if (error.stack) { - console.log(` Stack: ${error.stack.split('\n')[1]?.trim()}`); - } - this.failed++; - } - } - - expect(actual) { - return { - toBe: (expected) => { - if (actual !== expected) { - throw new Error(`Expected ${expected}, got ${actual}`); - } - }, - toBeTruthy: () => { - if (!actual) { - throw new Error(`Expected truthy value, got ${actual}`); - } - }, - toBeFalsy: () => { - if (actual) { - throw new Error(`Expected falsy value, got ${actual}`); - } - }, - toEqual: (expected) => { - if (JSON.stringify(actual) !== JSON.stringify(expected)) { - throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`); - } - }, - toContain: (expected) => { - if (!actual.includes(expected)) { - throw new Error(`Expected ${actual} to contain ${expected}`); - } - }, - toHaveProperty: (property) => { - if (!(property in actual)) { - throw new Error(`Expected object to have property ${property}`); - } - }, - toBeInstanceOf: (expectedClass) => { - if (!(actual instanceof expectedClass)) { - throw new Error(`Expected instance of ${expectedClass.name}, got ${actual.constructor.name}`); - } - } - }; - } - - /** - * Test that a component can be extracted from the monolith without breaking functionality - */ - testComponentExtraction(componentName, extractFn, originalTests) { - this.describe(`Component Extraction: ${componentName}`, () => { - this.it('should extract without syntax errors', () => { - try { - const component = extractFn(); - this.expect(component).toBeTruthy(); - } catch (error) { - throw new Error(`Component extraction failed: ${error.message}`); - } - }); - - this.it('should maintain original API', () => { - const component = extractFn(); - originalTests.forEach(test => { - try { - test(component); - } catch (error) { - throw new Error(`API compatibility test failed: ${error.message}`); - } - }); - }); - }); - } - - /** - * Test component integration after extraction - */ - testComponentIntegration(components, integrationTests) { - this.describe('Component Integration', () => { - integrationTests.forEach((test, index) => { - this.it(`integration test ${index + 1}`, () => { - test(components); - }); - }); - }); - } - - /** - * Setup test environment with mock dependencies - */ - setupTestEnvironment() { - // Create test container - const container = document.createElement('div'); - container.id = 'test-container'; - container.innerHTML = '
'; - document.body.appendChild(container); - - // Mock any global dependencies - global.mockSectionManager = { - sections: new Map(), - createSectionsFromMarkdown: () => [], - startEditing: () => true, - stopEditing: () => true, - getAllSections: () => [] - }; - - return { container }; - } - - /** - * Cleanup test environment - */ - cleanupTestEnvironment() { - const container = document.getElementById('test-container'); - if (container) { - container.remove(); - } - - // Clear any global mocks - delete global.mockSectionManager; - } - - async run() { - console.log('๐Ÿงช TDD Refactoring Test Runner Starting...\n'); - - const startTime = Date.now(); - - // Run all collected tests - // Tests will be added by importing component test files - - const endTime = Date.now(); - const duration = endTime - startTime; - - console.log(`\n๐Ÿ“Š Test Results:`); - console.log(` โœ… Passed: ${this.passed}`); - console.log(` โŒ Failed: ${this.failed}`); - console.log(` โฑ๏ธ Duration: ${duration}ms`); - - if (this.failed > 0) { - console.log(`\nโŒ ${this.failed} test(s) failed. Refactoring should not proceed.`); - process.exit(1); - } else { - console.log(`\nโœ… All tests passed! Refactoring is safe to continue.`); - } - } -} - -// Export for use in component tests -if (typeof module !== 'undefined' && module.exports) { - module.exports = { RefactorTestRunner }; -} - -// Export for browser use -if (typeof window !== 'undefined') { - window.RefactorTestRunner = RefactorTestRunner; -} - -module.exports = RefactorTestRunner; \ No newline at end of file diff --git a/markitect/static/js/tests/test-component-integration.js b/markitect/static/js/tests/test-component-integration.js deleted file mode 100644 index 2107dc99..00000000 --- a/markitect/static/js/tests/test-component-integration.js +++ /dev/null @@ -1,521 +0,0 @@ -#!/usr/bin/env node - -/** - * Comprehensive Component Integration Test - * - * Tests that extracted components work together properly. - * Verifies the complete workflow: Section Creation โ†’ Rendering โ†’ Editing โ†’ Saving - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -runner.describe('Component Integration Tests', () => { - - runner.it('should load all extracted components', () => { - try { - // Load extracted components - const sectionModule = require('../core/section-manager.js'); - const domModule = require('../components/dom-renderer.js'); - - runner.expect(sectionModule.SectionManager).toBeTruthy(); - runner.expect(sectionModule.Section).toBeTruthy(); - runner.expect(domModule.DOMRenderer).toBeTruthy(); - runner.expect(domModule.FloatingMenu).toBeTruthy(); - - // Set globals for other tests - global.ExtractedSectionManager = sectionModule.SectionManager; - global.ExtractedSection = sectionModule.Section; - global.ExtractedDOMRenderer = domModule.DOMRenderer; - global.ExtractedFloatingMenu = domModule.FloatingMenu; - global.ExtractedEditState = sectionModule.EditState; - - } catch (error) { - throw new Error(`Failed to load extracted components: ${error.message}`); - } - }); - - runner.it('should support complete section creation workflow', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - - // Setup - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Test workflow: Create sections from markdown - const testMarkdown = `# Main Heading -This is the introduction content. - -## Subheading One -Content for first subsection. - -![Test Image](https://example.com/image.jpg) - -## Subheading Two -Content for second subsection.`; - - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - - - // Verify sections were created - // Expected: heading+paragraph, heading+paragraph, image, heading+paragraph = 4 sections - runner.expect(sections.length).toBe(4); - runner.expect(sections[0].type).toBe('heading'); - runner.expect(sections[2].type).toBe('image'); - - // Verify DOM rendering - domRenderer.renderAllSections(sections); - const renderedElements = container.querySelectorAll('.ui-edit-section'); - runner.expect(renderedElements.length).toBe(sections.length); - - // Cleanup - document.body.removeChild(container); - }); - - runner.it('should support complete editing workflow', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - const EditState = global.ExtractedEditState; - - // Setup - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Create and render sections - const testMarkdown = '# Test Heading\nOriginal content here.'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - const sectionId = sections[0].id; - const section = sectionManager.sections.get(sectionId); - - // Test workflow: Start editing - runner.expect(section.state).toBe(EditState.ORIGINAL); - runner.expect(section.isEditing()).toBeFalsy(); - - const content = sectionManager.startEditing(sectionId); - runner.expect(content).toContain('Test Heading'); - runner.expect(section.isEditing()).toBeTruthy(); - runner.expect(section.state).toBe(EditState.EDITING); - - // Test workflow: Update content - const newContent = '# Updated Heading\nModified content here.'; - sectionManager.updateContent(sectionId, newContent); - runner.expect(section.editingMarkdown).toBe(newContent); - - // Test workflow: Accept changes - sectionManager.acceptChanges(sectionId); - runner.expect(section.currentMarkdown).toBe(newContent); - runner.expect(section.state).toBe(EditState.SAVED); - runner.expect(section.isEditing()).toBeFalsy(); - - // Cleanup - document.body.removeChild(container); - }); - - runner.it('should support accept/cancel button functionality', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - - // Setup - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Create and render sections - const testMarkdown = '# Test Heading\nOriginal content here.'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - const sectionId = sections[0].id; - const section = sectionManager.sections.get(sectionId); - - // Start editing to trigger floating menu with buttons - sectionManager.startEditing(sectionId); - - // Check if floating menu exists - runner.expect(domRenderer.currentFloatingMenu).toBeTruthy(); - runner.expect(domRenderer.currentFloatingMenu.isVisible).toBeTruthy(); - - // Find buttons in the floating menu - const menuElement = domRenderer.currentFloatingMenu.element; - runner.expect(menuElement).toBeTruthy(); - - const buttons = menuElement.querySelectorAll('button'); - runner.expect(buttons.length >= 2).toBeTruthy(); // At least Accept and Cancel buttons - - const acceptBtn = Array.from(buttons).find(btn => btn.textContent === 'Accept'); - const cancelBtn = Array.from(buttons).find(btn => btn.textContent === 'Cancel'); - - runner.expect(acceptBtn).toBeTruthy(); - runner.expect(cancelBtn).toBeTruthy(); - - // Test Accept button functionality - runner.expect(section.isEditing()).toBeTruthy(); - - // Simulate updating content and clicking Accept - const textarea = menuElement.querySelector('textarea'); - runner.expect(textarea).toBeTruthy(); - textarea.value = '# Updated Heading\nUpdated content via button.'; - - acceptBtn.click(); - - // After clicking Accept, section should be saved and menu hidden - runner.expect(section.isEditing()).toBeFalsy(); - runner.expect(section.currentMarkdown).toContain('Updated Heading'); - runner.expect(domRenderer.currentFloatingMenu).toBeFalsy(); - - // Cleanup - document.body.removeChild(container); - }); - - runner.it('should support cancel button functionality', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - - // Setup - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Create and render sections - const testMarkdown = '# Original Heading\nOriginal content here.'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - const sectionId = sections[0].id; - const section = sectionManager.sections.get(sectionId); - - // Start editing - sectionManager.startEditing(sectionId); - - // Find buttons in the floating menu - const menuElement = domRenderer.currentFloatingMenu.element; - const cancelBtn = Array.from(menuElement.querySelectorAll('button')).find(btn => btn.textContent === 'Cancel'); - - runner.expect(cancelBtn).toBeTruthy(); - runner.expect(section.isEditing()).toBeTruthy(); - - // Simulate changing content but then canceling - const textarea = menuElement.querySelector('textarea'); - textarea.value = '# Changed Heading\nThis should be discarded.'; - - cancelBtn.click(); - - // After clicking Cancel, section should not be saved and menu hidden - runner.expect(section.isEditing()).toBeFalsy(); - runner.expect(section.currentMarkdown).toContain('Original Heading'); // Original content preserved - runner.expect(domRenderer.currentFloatingMenu).toBeFalsy(); - - // Cleanup - document.body.removeChild(container); - }); - - runner.it('should support event-driven communication', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - - // Setup - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Track events - let sectionsCreatedEvent = null; - let editStartedEvent = null; - - sectionManager.on('sections-created', (data) => { - sectionsCreatedEvent = data; - }); - - sectionManager.on('edit-started', (data) => { - editStartedEvent = data; - }); - - // Test event: sections-created - const testMarkdown = '# Test\nContent'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - - runner.expect(sectionsCreatedEvent).toBeTruthy(); - runner.expect(sectionsCreatedEvent.sections).toEqual(sections); - runner.expect(sectionsCreatedEvent.count).toBe(1); - - // Test event: edit-started - const sectionId = sections[0].id; - sectionManager.startEditing(sectionId); - - runner.expect(editStartedEvent).toBeTruthy(); - runner.expect(editStartedEvent.sectionId).toBe(sectionId); - runner.expect(editStartedEvent.content).toContain('Test'); - - // Cleanup - document.body.removeChild(container); - }); - - runner.it('should support section type detection and rendering', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - const Section = global.ExtractedSection; - - // Setup - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Test different section types - const testMarkdown = `# Heading Section -Regular paragraph content. - -![Image Section](https://example.com/test.jpg) - -\`\`\`javascript -// Code section -console.log('test'); -\`\`\``; - - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - - - // Verify type detection - adjusted for actual parsing behavior - // Expected: heading+paragraph, image, code = 3 sections - runner.expect(sections[0].type).toBe('heading'); // Combined heading+paragraph - runner.expect(sections[1].type).toBe('image'); // Image section - runner.expect(sections[2].type).toBe('code'); // Code section - - // Verify image detection - runner.expect(sections[1].isImage()).toBeTruthy(); // Image is now at index 1 - runner.expect(sections[0].isImage()).toBeFalsy(); - - // Verify rendering handles different types - domRenderer.renderAllSections(sections); - const renderedElements = container.querySelectorAll('.ui-edit-section'); - runner.expect(renderedElements.length).toBe(sections.length); - - // Cleanup - document.body.removeChild(container); - }); - - runner.it('should support FloatingMenu integration', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - const FloatingMenu = global.ExtractedFloatingMenu; - - // Setup - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Create and render sections - const testMarkdown = '# Test Heading\nTest content'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - const sectionId = sections[0].id; - - // Test showing editor (which uses FloatingMenu) - domRenderer.showEditor(sectionId, 'test content'); - - // Verify floating menu state - runner.expect(domRenderer.currentFloatingMenu).toBeTruthy(); - runner.expect(domRenderer.currentFloatingMenu.sectionId).toBe(sectionId); - runner.expect(domRenderer.currentFloatingMenu.isVisible).toBeTruthy(); - runner.expect(domRenderer.editingSections.has(sectionId)).toBeTruthy(); - - // Test hiding editor - domRenderer.hideCurrentEditor(); - runner.expect(domRenderer.currentFloatingMenu).toBeFalsy(); - runner.expect(domRenderer.editingSections.has(sectionId)).toBeFalsy(); - - // Cleanup - document.body.removeChild(container); - }); - - runner.it('should support complete click-to-edit workflow', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - - // Setup - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Create and render sections - const testMarkdown = '# Test Heading\nTest content for editing'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - const sectionId = sections[0].id; - const element = domRenderer.findSectionElement(sectionId); - - // Simulate click event - const clickEvent = new Event('click', { bubbles: true }); - Object.defineProperty(clickEvent, 'target', { value: element }); - - // Test complete workflow - domRenderer.handleSectionClick(clickEvent); - - // Verify editing state was triggered - const section = sectionManager.sections.get(sectionId); - runner.expect(section.isEditing()).toBeTruthy(); - runner.expect(domRenderer.editingSections.has(sectionId)).toBeTruthy(); - runner.expect(domRenderer.currentFloatingMenu).toBeTruthy(); - - // Cleanup - document.body.removeChild(container); - }); - - runner.it('should support document status tracking', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - - const sectionManager = new SectionManager(); - const container = document.createElement('div'); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Test initial status - let status = sectionManager.getDocumentStatus(); - runner.expect(status.totalSections).toBe(0); - runner.expect(status.editingSections).toBe(0); - - // Create sections - const testMarkdown = '# Section 1\nContent 1\n\n# Section 2\nContent 2'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - - status = sectionManager.getDocumentStatus(); - runner.expect(status.totalSections).toBe(2); - runner.expect(status.editingSections).toBe(2); // Bug compatibility (isEditing property exists) - - // Test getAllSections - const allSections = sectionManager.getAllSections(); - runner.expect(allSections.length).toBe(2); - runner.expect(allSections[0].currentMarkdown).toContain('Section 1'); - runner.expect(allSections[1].currentMarkdown).toContain('Section 2'); - }); - - runner.it('should support event tracking and analytics', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - - const container = document.createElement('div'); - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Test event tracking - domRenderer.trackEvent('test-event', { data: 'test' }); - domRenderer.trackEvent('section-click', { sectionId: 'test-123' }); - - const stats = domRenderer.getEventStats(); - runner.expect(stats.totalEvents).toBe(1); // Only section-click is tracked in stats - runner.expect(stats.stats['section-click']).toBe(1); - runner.expect(stats.recentEvents.length).toBe(2); - runner.expect(stats.recentEvents[0].type).toBe('test-event'); - runner.expect(stats.recentEvents[1].type).toBe('section-click'); - }); - - // Integration stress test - runner.it('should handle complex document with multiple operations', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - - // Setup - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - // Complex document - const complexMarkdown = `# Document Title -Introduction paragraph with some content. - -## Section A -Content for section A with details. - -![Test Image](https://example.com/test.jpg) - -### Subsection A.1 -More detailed content here. - -\`\`\`javascript -function test() { - console.log('code block'); -} -\`\`\` - -## Section B -Final section content.`; - - // Create and render - const sections = sectionManager.createSectionsFromMarkdown(complexMarkdown); - domRenderer.renderAllSections(sections); - - runner.expect(sections.length).toBe(6); // Adjusted based on actual parsing - - // Test editing multiple sections - const firstSection = sections[0]; - const imageSection = sections.find(s => s.isImage()); - const codeSection = sections.find(s => s.type === 'code'); - - // Edit first section - sectionManager.startEditing(firstSection.id); - sectionManager.updateContent(firstSection.id, '# Updated Title\nUpdated intro.'); - sectionManager.acceptChanges(firstSection.id); - - // Edit image section - sectionManager.startEditing(imageSection.id); - sectionManager.updateContent(imageSection.id, '![Updated Image](https://example.com/new.jpg)'); - sectionManager.acceptChanges(imageSection.id); - - // Verify changes - runner.expect(firstSection.currentMarkdown).toContain('Updated Title'); - runner.expect(imageSection.currentMarkdown).toContain('Updated Image'); - - // Verify document reconstruction - const finalMarkdown = sectionManager.getDocumentMarkdown(); - runner.expect(finalMarkdown).toContain('Updated Title'); - runner.expect(finalMarkdown).toContain('Updated Image'); - runner.expect(finalMarkdown).toContain('Section B'); - - // Cleanup - document.body.removeChild(container); - }); -}); - -module.exports = runner; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Running Component Integration Tests'); - runner.run().then(() => { - console.log('โœ… Component integration tests completed'); - }); -} \ No newline at end of file diff --git a/markitect/static/js/tests/test-debugpanel-extraction.js b/markitect/static/js/tests/test-debugpanel-extraction.js deleted file mode 100644 index 5dca6cae..00000000 --- a/markitect/static/js/tests/test-debugpanel-extraction.js +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env node - -/** - * TDD Test for Debug Panel Component Extraction - * - * Tests the extraction of DebugPanel from the monolithic editor.js - * DebugPanel handles debug message display and management. - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -// Define expected DebugPanel API -const EXPECTED_DEBUGPANEL_API = [ - 'constructor', - 'toggle', - 'update', - 'clear', - 'addMessage', - 'show', - 'hide', - 'getMessageCount', - 'getRecentMessages' -]; - -runner.describe('DebugPanel Component Extraction', () => { - - runner.it('should define expected API methods', () => { - const expectedMethods = EXPECTED_DEBUGPANEL_API; - runner.expect(expectedMethods.length).toBe(9); - runner.expect(expectedMethods).toContain('toggle'); - runner.expect(expectedMethods).toContain('update'); - runner.expect(expectedMethods).toContain('addMessage'); - }); - - runner.it('should load extracted DebugPanel component', () => { - // Load the extracted component - delete require.cache[require.resolve('../components/debug-panel.js')]; - - try { - const module = require('../components/debug-panel.js'); - runner.expect(module.DebugPanel).toBeTruthy(); - - // Set global for other tests - global.ExtractedDebugPanel = module.DebugPanel; - } catch (error) { - throw new Error(`Failed to load extracted DebugPanel: ${error.message}`); - } - }); - - runner.it('should preserve constructor functionality', () => { - const DebugPanel = global.ExtractedDebugPanel; - - const debugPanel = new DebugPanel(); - runner.expect(debugPanel).toBeInstanceOf(DebugPanel); - runner.expect(debugPanel.messages).toBeInstanceOf(Array); - runner.expect(debugPanel.isActive).toBeFalsy(); - }); - - runner.it('should preserve message handling functionality', () => { - const DebugPanel = global.ExtractedDebugPanel; - - const debugPanel = new DebugPanel(); - - // Test adding messages - debugPanel.addMessage('Test message', 'INFO'); - runner.expect(debugPanel.getMessageCount()).toBe(1); - - const recentMessages = debugPanel.getRecentMessages(1); - runner.expect(recentMessages.length).toBe(1); - runner.expect(recentMessages[0].message).toBe('Test message'); - runner.expect(recentMessages[0].category).toBe('INFO'); - }); - - runner.it('should preserve toggle functionality', () => { - const DebugPanel = global.ExtractedDebugPanel; - - // Create container element - const container = document.createElement('div'); - container.id = 'debug-messages-container'; - container.style.display = 'none'; - document.body.appendChild(container); - - const debugButton = document.createElement('button'); - debugButton.id = 'toggle-debug'; - debugButton.textContent = '๐Ÿ” Debug'; - document.body.appendChild(debugButton); - - const debugPanel = new DebugPanel(); - - // Test toggle on - debugPanel.toggle(); - runner.expect(debugPanel.isActive).toBeTruthy(); - - // Test toggle off - debugPanel.toggle(); - runner.expect(debugPanel.isActive).toBeFalsy(); - - // Cleanup - document.body.removeChild(container); - document.body.removeChild(debugButton); - }); - - runner.it('should preserve update functionality', () => { - const DebugPanel = global.ExtractedDebugPanel; - - const container = document.createElement('div'); - container.id = 'debug-messages-container'; - document.body.appendChild(container); - - const debugButton = document.createElement('button'); - debugButton.id = 'toggle-debug'; - debugButton.textContent = '๐Ÿ” Debug'; - document.body.appendChild(debugButton); - - const debugPanel = new DebugPanel(); - debugPanel.show(); - - debugPanel.addMessage('Test message 1', 'INFO'); - debugPanel.addMessage('Test message 2', 'ERROR'); - debugPanel.update(); - - runner.expect(container.innerHTML.length > 100).toBeTruthy(); - runner.expect(container.innerHTML).toContain('Test message 1'); - runner.expect(container.innerHTML).toContain('Test message 2'); - - // Cleanup - document.body.removeChild(container); - document.body.removeChild(debugButton); - }); - - runner.it('should preserve clear functionality', () => { - const DebugPanel = global.ExtractedDebugPanel; - - const debugPanel = new DebugPanel(); - - debugPanel.addMessage('Test message 1', 'INFO'); - debugPanel.addMessage('Test message 2', 'ERROR'); - runner.expect(debugPanel.getMessageCount()).toBe(2); - - debugPanel.clear(); - runner.expect(debugPanel.getMessageCount()).toBe(0); - }); - - runner.it('should have core debug panel methods', () => { - const DebugPanel = global.ExtractedDebugPanel; - - const debugPanel = new DebugPanel(); - - // Should have core methods - runner.expect(typeof debugPanel.toggle === 'function').toBeTruthy(); - runner.expect(typeof debugPanel.update === 'function').toBeTruthy(); - runner.expect(typeof debugPanel.addMessage === 'function').toBeTruthy(); - runner.expect(typeof debugPanel.clear === 'function').toBeTruthy(); - }); - - runner.it('should handle message categories properly', () => { - const DebugPanel = global.ExtractedDebugPanel; - - const debugPanel = new DebugPanel(); - - // Test different message categories - debugPanel.addMessage('Info message', 'INFO'); - debugPanel.addMessage('Warning message', 'WARNING'); - debugPanel.addMessage('Error message', 'ERROR'); - debugPanel.addMessage('Success message', 'SUCCESS'); - - const messages = debugPanel.getRecentMessages(4); - runner.expect(messages.length).toBe(4); - - const categories = messages.map(m => m.category); - runner.expect(categories).toContain('INFO'); - runner.expect(categories).toContain('WARNING'); - runner.expect(categories).toContain('ERROR'); - runner.expect(categories).toContain('SUCCESS'); - }); -}); - -module.exports = { - runner, - EXPECTED_DEBUGPANEL_API -}; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Testing DebugPanel Component Extraction'); - runner.run().then(() => { - console.log('โœ… DebugPanel extraction tests completed'); - }); -} \ No newline at end of file diff --git a/markitect/static/js/tests/test-debugpanel-integration.js b/markitect/static/js/tests/test-debugpanel-integration.js deleted file mode 100644 index af03ff83..00000000 --- a/markitect/static/js/tests/test-debugpanel-integration.js +++ /dev/null @@ -1,210 +0,0 @@ -#!/usr/bin/env node - -/** - * DebugPanel Integration Test - * - * Tests that the extracted DebugPanel component integrates properly - * with the existing SectionManager and DOMRenderer components. - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -runner.describe('DebugPanel Integration Tests', () => { - - runner.it('should load all extracted components including DebugPanel', () => { - try { - // Load extracted components - const sectionModule = require('../core/section-manager.js'); - const domModule = require('../components/dom-renderer.js'); - const debugModule = require('../components/debug-panel.js'); - - runner.expect(sectionModule.SectionManager).toBeTruthy(); - runner.expect(domModule.DOMRenderer).toBeTruthy(); - runner.expect(debugModule.DebugPanel).toBeTruthy(); - - // Set globals for other tests - global.ExtractedSectionManager = sectionModule.SectionManager; - global.ExtractedDOMRenderer = domModule.DOMRenderer; - global.ExtractedDebugPanel = debugModule.DebugPanel; - - } catch (error) { - throw new Error(`Failed to load extracted components: ${error.message}`); - } - }); - - runner.it('should support debug panel with section editing workflow', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - const DebugPanel = global.ExtractedDebugPanel; - - // Setup DOM elements - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const debugContainer = document.createElement('div'); - debugContainer.id = 'debug-messages-container'; - debugContainer.style.display = 'none'; - document.body.appendChild(debugContainer); - - const debugButton = document.createElement('button'); - debugButton.id = 'toggle-debug'; - debugButton.textContent = '๐Ÿ” Debug'; - document.body.appendChild(debugButton); - - // Create components - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - const debugPanel = new DebugPanel(); - - // Test workflow: Create sections and debug them - const testMarkdown = '# Test Heading\nTest content for debugging'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - // Add debug messages - debugPanel.addMessage('Section created: ' + sections[0].id, 'INFO'); - debugPanel.addMessage('DOM rendered successfully', 'SUCCESS'); - - runner.expect(debugPanel.getMessageCount()).toBe(2); - - // Test showing debug panel - debugPanel.show(); - runner.expect(debugPanel.isActive).toBeTruthy(); - - // Test debug panel content - const messages = debugPanel.getRecentMessages(2); - runner.expect(messages[0].message).toContain('Section created'); - runner.expect(messages[1].message).toContain('DOM rendered'); - - // Cleanup - document.body.removeChild(container); - document.body.removeChild(debugContainer); - document.body.removeChild(debugButton); - }); - - runner.it('should support debug panel clearing and message management', () => { - const DebugPanel = global.ExtractedDebugPanel; - - const debugPanel = new DebugPanel(); - - // Add multiple messages - for (let i = 0; i < 10; i++) { - debugPanel.addMessage(`Test message ${i}`, i % 2 === 0 ? 'INFO' : 'WARNING'); - } - - runner.expect(debugPanel.getMessageCount()).toBe(10); - - // Test getting recent messages - const recentFive = debugPanel.getRecentMessages(5); - runner.expect(recentFive.length).toBe(5); - runner.expect(recentFive[4].message).toContain('Test message 9'); - - // Test clearing - debugPanel.clear(); - runner.expect(debugPanel.getMessageCount()).toBe(0); - }); - - runner.it('should handle debug panel DOM integration properly', () => { - const DebugPanel = global.ExtractedDebugPanel; - - // Setup DOM - const debugContainer = document.createElement('div'); - debugContainer.id = 'debug-messages-container'; - debugContainer.style.display = 'none'; - document.body.appendChild(debugContainer); - - const debugButton = document.createElement('button'); - debugButton.id = 'toggle-debug'; - debugButton.textContent = '๐Ÿ” Debug'; - debugButton.style.background = '#6c757d'; - document.body.appendChild(debugButton); - - const debugPanel = new DebugPanel(); - - // Test initial state - runner.expect(debugPanel.isActive).toBeFalsy(); - runner.expect(debugContainer.style.display).toBe('none'); - - // Test toggle on - debugPanel.toggle(); - runner.expect(debugPanel.isActive).toBeTruthy(); - runner.expect(debugContainer.style.display).toBe('block'); - runner.expect(debugButton.textContent).toContain('Debug (ON)'); - - // Test toggle off - debugPanel.toggle(); - runner.expect(debugPanel.isActive).toBeFalsy(); - runner.expect(debugContainer.style.display).toBe('none'); - runner.expect(debugButton.textContent).toBe('๐Ÿ” Debug'); - - // Cleanup - document.body.removeChild(debugContainer); - document.body.removeChild(debugButton); - }); - - runner.it('should handle missing DOM elements gracefully', () => { - const DebugPanel = global.ExtractedDebugPanel; - - const debugPanel = new DebugPanel(); - - // Try to toggle without DOM elements (should not throw) - try { - debugPanel.toggle(); - debugPanel.show(); - debugPanel.hide(); - debugPanel.update(); - runner.expect(true).toBeTruthy(); // If we get here, no errors were thrown - } catch (error) { - throw new Error(`DebugPanel should handle missing DOM gracefully: ${error.message}`); - } - }); - - runner.it('should support event-driven debug message addition', () => { - const SectionManager = global.ExtractedSectionManager; - const DebugPanel = global.ExtractedDebugPanel; - - const sectionManager = new SectionManager(); - const debugPanel = new DebugPanel(); - - // Listen to section manager events and add debug messages - let eventCount = 0; - - sectionManager.on('sections-created', (data) => { - debugPanel.addMessage(`Sections created: ${data.count} sections`, 'INFO'); - eventCount++; - }); - - sectionManager.on('edit-started', (data) => { - debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG'); - eventCount++; - }); - - // Create sections - const testMarkdown = '# Test\nContent'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - - // Start editing - sectionManager.startEditing(sections[0].id); - - // Verify debug messages were added - runner.expect(eventCount).toBe(2); - runner.expect(debugPanel.getMessageCount()).toBe(2); - - const messages = debugPanel.getRecentMessages(2); - runner.expect(messages[0].message).toContain('Sections created'); - runner.expect(messages[1].message).toContain('Edit started'); - }); -}); - -module.exports = runner; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Running DebugPanel Integration Tests'); - runner.run().then(() => { - console.log('โœ… DebugPanel integration tests completed'); - }); -} \ No newline at end of file diff --git a/markitect/static/js/tests/test-document-navigator-runner.html b/markitect/static/js/tests/test-document-navigator-runner.html deleted file mode 100644 index 2c8a7621..00000000 --- a/markitect/static/js/tests/test-document-navigator-runner.html +++ /dev/null @@ -1,193 +0,0 @@ - - - - - - DocumentNavigator TDD Test Runner - - - -
-

๐Ÿ“‹ DocumentNavigator Widget TDD Test Suite

-

- This test suite follows Test-Driven Development methodology to implement a Substack-style - floating document navigation widget. The tests define the expected behavior before - implementation begins. -

- -
- Test Coverage: - -
- - - - -
- - - - - - - - - \ No newline at end of file diff --git a/markitect/static/js/tests/test-document-navigator.js b/markitect/static/js/tests/test-document-navigator.js deleted file mode 100644 index e6a79f99..00000000 --- a/markitect/static/js/tests/test-document-navigator.js +++ /dev/null @@ -1,432 +0,0 @@ -/** - * TDD Test Suite for DocumentNavigator Widget - * - * Tests the Substack-style floating navigation widget for document headings. - * Following TDD methodology: write tests first, then implement functionality. - */ - -// Simple test runner for browser environment -class DocumentNavigatorTestRunner { - constructor() { - this.tests = []; - this.results = { - passed: 0, - failed: 0, - total: 0 - }; - } - - test(name, testFn) { - this.tests.push({ name, testFn }); - } - - expect(actual) { - return { - toBe: (expected) => { - if (actual !== expected) { - throw new Error(`Expected ${actual} to be ${expected}`); - } - }, - toBeInstanceOf: (expectedClass) => { - if (!(actual instanceof expectedClass)) { - throw new Error(`Expected ${actual} to be instance of ${expectedClass.name}`); - } - }, - toBeTruthy: () => { - if (!actual) { - throw new Error(`Expected ${actual} to be truthy`); - } - }, - toBeFalsy: () => { - if (actual) { - throw new Error(`Expected ${actual} to be falsy`); - } - }, - toContain: (expected) => { - if (typeof actual === 'string' && !actual.includes(expected)) { - throw new Error(`Expected "${actual}" to contain "${expected}"`); - } - if (Array.isArray(actual) && !actual.includes(expected)) { - throw new Error(`Expected array to contain ${expected}`); - } - }, - toHaveLength: (expected) => { - if (actual.length !== expected) { - throw new Error(`Expected length ${actual.length} to be ${expected}`); - } - }, - toBeGreaterThan: (expected) => { - if (actual <= expected) { - throw new Error(`Expected ${actual} to be greater than ${expected}`); - } - } - }; - } - - async run() { - console.log('๐Ÿงช Running DocumentNavigator TDD Test Suite...\n'); - - for (const { name, testFn } of this.tests) { - this.results.total++; - - try { - await testFn.call(this); - this.results.passed++; - console.log(`โœ… ${name}`); - } catch (error) { - this.results.failed++; - console.log(`โŒ ${name}`); - console.log(` ${error.message}\n`); - } - } - - this.printSummary(); - } - - printSummary() { - console.log(`\n๐Ÿ“Š Test Results:`); - console.log(` Passed: ${this.results.passed}`); - console.log(` Failed: ${this.results.failed}`); - console.log(` Total: ${this.results.total}`); - - if (this.results.failed === 0) { - console.log(`\n๐ŸŽ‰ All tests passed!`); - } else { - console.log(`\nโŒ ${this.results.failed} test(s) failed.`); - } - } -} - -// Create test runner -const runner = new DocumentNavigatorTestRunner(); - -// Test Suite: DocumentNavigator Widget -runner.test('DocumentNavigator class should exist and be importable', async function() { - // This test will fail initially - we haven't created the class yet - try { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - this.expect(DocumentNavigator).toBeTruthy(); - this.expect(typeof DocumentNavigator).toBe('function'); - } catch (error) { - throw new Error(`DocumentNavigator class not found: ${error.message}`); - } -}); - -runner.test('DocumentNavigator should extend UIWidget', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - const { UIWidget } = await import('../widgets/base/UIWidget.js'); - - const navigator = new DocumentNavigator(); - this.expect(navigator).toBeInstanceOf(UIWidget); -}); - -runner.test('DocumentNavigator should initialize with default configuration', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - const navigator = new DocumentNavigator(); - - // Test default configuration - this.expect(navigator.config.position).toBe('left'); - this.expect(navigator.config.collapsed).toBe(true); - this.expect(navigator.config.autoHide).toBe(true); - this.expect(navigator.config.maxHeadingLevel).toBe(3); - this.expect(navigator.config.enableScrollSpy).toBe(true); -}); - -runner.test('DocumentNavigator should accept custom configuration', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - const customConfig = { - position: 'right', - collapsed: false, - maxHeadingLevel: 4, - theme: 'dark' - }; - - const navigator = new DocumentNavigator(customConfig); - - this.expect(navigator.config.position).toBe('right'); - this.expect(navigator.config.collapsed).toBe(false); - this.expect(navigator.config.maxHeadingLevel).toBe(4); - this.expect(navigator.config.theme).toBe('dark'); -}); - -runner.test('DocumentNavigator should render floating panel element', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - const navigator = new DocumentNavigator(); - await navigator.render(); - - this.expect(navigator.element).toBeInstanceOf(HTMLElement); - this.expect(navigator.element.classList.contains('document-navigator')).toBeTruthy(); - this.expect(navigator.element.style.position).toBe('fixed'); -}); - -runner.test('DocumentNavigator should have toggle button in collapsed state', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - const navigator = new DocumentNavigator({ collapsed: true }); - await navigator.render(); - - const toggleButton = navigator.findElement('.navigator-toggle'); - this.expect(toggleButton).toBeInstanceOf(HTMLElement); - this.expect(toggleButton.style.display).not.toBe('none'); - - const navList = navigator.findElement('.navigator-list'); - this.expect(navList.style.display).toBe('none'); -}); - -runner.test('DocumentNavigator should extract headings from document', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - // Create test document with headings - const testContainer = document.createElement('div'); - testContainer.innerHTML = ` -

First Heading

-

Some content

-

Second Heading

-

Third Heading

-

More content

-

Fourth Heading

- `; - document.body.appendChild(testContainer); - - const navigator = new DocumentNavigator({ - container: testContainer, - maxHeadingLevel: 3 - }); - - const headings = navigator.extractHeadings(); - - this.expect(headings).toHaveLength(4); - this.expect(headings[0].tagName).toBe('H1'); - this.expect(headings[0].textContent).toBe('First Heading'); - this.expect(headings[1].tagName).toBe('H2'); - this.expect(headings[2].tagName).toBe('H3'); - this.expect(headings[3].tagName).toBe('H2'); - - // Cleanup - document.body.removeChild(testContainer); -}); - -runner.test('DocumentNavigator should build navigation hierarchy', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - // Create test document with nested headings - const testContainer = document.createElement('div'); - testContainer.innerHTML = ` -

Chapter 1

-

Section 1.1

-

Subsection 1.1.1

-

Subsection 1.1.2

-

Section 1.2

-

Chapter 2

- `; - document.body.appendChild(testContainer); - - const navigator = new DocumentNavigator({ container: testContainer }); - await navigator.render(); - - const navItems = navigator.buildNavigationTree(); - - // Should have hierarchical structure - this.expect(navItems).toHaveLength(2); // 2 H1 elements - this.expect(navItems[0].children).toHaveLength(2); // 2 H2 under first H1 - this.expect(navItems[0].children[0].children).toHaveLength(2); // 2 H3 under first H2 - - // Cleanup - document.body.removeChild(testContainer); -}); - -runner.test('DocumentNavigator should handle click navigation', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - // Create test document - const testContainer = document.createElement('div'); - testContainer.innerHTML = ` -

Target Heading

-

Spacer content

- `; - document.body.appendChild(testContainer); - - const navigator = new DocumentNavigator({ container: testContainer }); - await navigator.render(); - - // Simulate click on navigation item - const navItem = navigator.findElement('[data-target="target-heading"]'); - this.expect(navItem).toBeTruthy(); - - // Mock scrollIntoView for testing - const targetElement = document.getElementById('target-heading'); - let scrollCalled = false; - targetElement.scrollIntoView = () => { scrollCalled = true; }; - - // Click navigation item - navItem.click(); - - this.expect(scrollCalled).toBeTruthy(); - - // Cleanup - document.body.removeChild(testContainer); -}); - -runner.test('DocumentNavigator should support expand/collapse functionality', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - const navigator = new DocumentNavigator({ collapsed: true }); - await navigator.render(); - - // Should start collapsed - this.expect(navigator.isCollapsed).toBeTruthy(); - - const toggleButton = navigator.findElement('.navigator-toggle'); - const navList = navigator.findElement('.navigator-list'); - - // Toggle to expanded - await navigator.expand(); - this.expect(navigator.isCollapsed).toBeFalsy(); - this.expect(navList.style.display).not.toBe('none'); - - // Toggle back to collapsed - await navigator.collapse(); - this.expect(navigator.isCollapsed).toBeTruthy(); - this.expect(navList.style.display).toBe('none'); -}); - -runner.test('DocumentNavigator should implement scroll spy functionality', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - // Create test document with multiple sections - const testContainer = document.createElement('div'); - testContainer.innerHTML = ` -
-

Section 1

-
-

Section 2

-
-

Section 3

-
- `; - document.body.appendChild(testContainer); - - const navigator = new DocumentNavigator({ - container: testContainer, - enableScrollSpy: true - }); - await navigator.render(); - - // Test current section detection - const currentSection = navigator.getCurrentSection(); - this.expect(currentSection).toBeTruthy(); - - // Cleanup - document.body.removeChild(testContainer); -}); - -runner.test('DocumentNavigator should handle responsive behavior', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - const navigator = new DocumentNavigator({ autoHide: true }); - await navigator.render(); - - // Mock viewport resize - const originalInnerWidth = window.innerWidth; - - // Test mobile viewport - Object.defineProperty(window, 'innerWidth', { value: 500, configurable: true }); - navigator.handleResize(); - this.expect(navigator.element.style.display).toBe('none'); - - // Test desktop viewport - Object.defineProperty(window, 'innerWidth', { value: 1200, configurable: true }); - navigator.handleResize(); - this.expect(navigator.element.style.display).not.toBe('none'); - - // Restore original - Object.defineProperty(window, 'innerWidth', { value: originalInnerWidth, configurable: true }); -}); - -runner.test('DocumentNavigator should provide keyboard navigation support', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - const navigator = new DocumentNavigator(); - await navigator.render(); - - // Test keyboard shortcuts - let expandCalled = false; - let collapseCalled = false; - - navigator.expand = async () => { expandCalled = true; }; - navigator.collapse = async () => { collapseCalled = true; }; - - // Simulate keyboard events - const element = navigator.element; - - // Test Escape key (should collapse) - const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' }); - element.dispatchEvent(escapeEvent); - this.expect(collapseCalled).toBeTruthy(); - - // Test Enter/Space key (should expand) - const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' }); - element.dispatchEvent(enterEvent); - this.expect(expandCalled).toBeTruthy(); -}); - -runner.test('DocumentNavigator should emit events for user interactions', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - const navigator = new DocumentNavigator(); - await navigator.render(); - - // Test event emission - let navigationEvent = null; - navigator.addEventListener('navigate', (e) => { - navigationEvent = e; - }); - - let toggleEvent = null; - navigator.addEventListener('toggle', (e) => { - toggleEvent = e; - }); - - // Trigger navigation - navigator.navigateToHeading('test-heading'); - this.expect(navigationEvent).toBeTruthy(); - this.expect(navigationEvent.detail.target).toBe('test-heading'); - - // Trigger toggle - await navigator.toggle(); - this.expect(toggleEvent).toBeTruthy(); -}); - -runner.test('DocumentNavigator should handle empty document gracefully', async function() { - const { DocumentNavigator } = await import('../widgets/navigation/DocumentNavigator.js'); - - // Create empty container - const emptyContainer = document.createElement('div'); - document.body.appendChild(emptyContainer); - - const navigator = new DocumentNavigator({ container: emptyContainer }); - - const headings = navigator.extractHeadings(); - this.expect(headings).toHaveLength(0); - - await navigator.render(); - const navList = navigator.findElement('.navigator-list'); - this.expect(navList.children).toHaveLength(0); - - // Should show empty state message - const emptyMessage = navigator.findElement('.navigator-empty'); - this.expect(emptyMessage).toBeTruthy(); - - // Cleanup - document.body.removeChild(emptyContainer); -}); - -// Export test runner for use in HTML -window.runDocumentNavigatorTests = () => runner.run(); - -console.log('๐Ÿ“‹ DocumentNavigator TDD Test Suite loaded. Run with: runDocumentNavigatorTests()'); - -export { runner }; \ No newline at end of file diff --git a/markitect/static/js/tests/test-documentcontrols-extraction.js b/markitect/static/js/tests/test-documentcontrols-extraction.js deleted file mode 100644 index 2d5607ca..00000000 --- a/markitect/static/js/tests/test-documentcontrols-extraction.js +++ /dev/null @@ -1,218 +0,0 @@ -#!/usr/bin/env node - -/** - * TDD Test for Document Controls Component Extraction - * - * Tests the extraction of DocumentControls from the monolithic editor.js - * DocumentControls handles the floating control panel and its actions. - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -// Define expected DocumentControls API -const EXPECTED_DOCUMENTCONTROLS_API = [ - 'constructor', - 'create', - 'destroy', - 'show', - 'hide', - 'addButton', - 'removeButton', - 'setEventHandlers', - 'updateStatus', - 'getControlPanel' -]; - -runner.describe('DocumentControls Component Extraction', () => { - - runner.it('should define expected API methods', () => { - const expectedMethods = EXPECTED_DOCUMENTCONTROLS_API; - runner.expect(expectedMethods.length).toBe(10); - runner.expect(expectedMethods).toContain('create'); - runner.expect(expectedMethods).toContain('addButton'); - runner.expect(expectedMethods).toContain('setEventHandlers'); - }); - - runner.it('should load extracted DocumentControls component', () => { - // Load the extracted component - delete require.cache[require.resolve('../components/document-controls.js')]; - - try { - const module = require('../components/document-controls.js'); - runner.expect(module.DocumentControls).toBeTruthy(); - - // Set global for other tests - global.ExtractedDocumentControls = module.DocumentControls; - } catch (error) { - throw new Error(`Failed to load extracted DocumentControls: ${error.message}`); - } - }); - - runner.it('should preserve constructor functionality', () => { - const DocumentControls = global.ExtractedDocumentControls; - - const controls = new DocumentControls(); - runner.expect(controls).toBeInstanceOf(DocumentControls); - runner.expect(controls.controlPanel).toBeFalsy(); // Initially null - runner.expect(controls.buttons).toBeInstanceOf(Map); - }); - - runner.it('should preserve control panel creation functionality', () => { - const DocumentControls = global.ExtractedDocumentControls; - - const controls = new DocumentControls(); - controls.create(); - - const panel = controls.getControlPanel(); - runner.expect(panel).toBeTruthy(); - runner.expect(panel.id).toBe('markitect-global-controls'); - - // Check that panel is added to DOM - const domPanel = document.getElementById('markitect-global-controls'); - runner.expect(domPanel).toBeTruthy(); - - // Cleanup - controls.destroy(); - }); - - runner.it('should preserve button creation functionality', () => { - const DocumentControls = global.ExtractedDocumentControls; - - const controls = new DocumentControls(); - controls.create(); - - // Default buttons should be created - runner.expect(controls.buttons.has('save-document')).toBeTruthy(); - runner.expect(controls.buttons.has('reset-all')).toBeTruthy(); - runner.expect(controls.buttons.has('show-status')).toBeTruthy(); - runner.expect(controls.buttons.has('toggle-debug')).toBeTruthy(); - - // Check DOM elements exist - runner.expect(document.getElementById('save-document')).toBeTruthy(); - runner.expect(document.getElementById('reset-all')).toBeTruthy(); - runner.expect(document.getElementById('show-status')).toBeTruthy(); - runner.expect(document.getElementById('toggle-debug')).toBeTruthy(); - - // Cleanup - controls.destroy(); - }); - - runner.it('should support custom button addition', () => { - const DocumentControls = global.ExtractedDocumentControls; - - const controls = new DocumentControls(); - controls.create(); - - // Add custom button - const customButton = controls.addButton('custom-test', '๐ŸŽฏ Test', '#ff6600'); - runner.expect(customButton).toBeTruthy(); - runner.expect(customButton.id).toBe('custom-test'); - runner.expect(customButton.textContent).toBe('๐ŸŽฏ Test'); - - // Check button is in map and DOM - runner.expect(controls.buttons.has('custom-test')).toBeTruthy(); - runner.expect(document.getElementById('custom-test')).toBeTruthy(); - - // Cleanup - controls.destroy(); - }); - - runner.it('should support event handler configuration', () => { - const DocumentControls = global.ExtractedDocumentControls; - - const controls = new DocumentControls(); - controls.create(); - - let saveClicked = false; - let resetClicked = false; - - const handlers = { - 'save-document': () => { saveClicked = true; }, - 'reset-all': () => { resetClicked = true; } - }; - - controls.setEventHandlers(handlers); - - // Simulate button clicks - const saveBtn = document.getElementById('save-document'); - const resetBtn = document.getElementById('reset-all'); - - saveBtn.click(); - resetBtn.click(); - - runner.expect(saveClicked).toBeTruthy(); - runner.expect(resetClicked).toBeTruthy(); - - // Cleanup - controls.destroy(); - }); - - runner.it('should support show/hide functionality', () => { - const DocumentControls = global.ExtractedDocumentControls; - - const controls = new DocumentControls(); - controls.create(); - - const panel = controls.getControlPanel(); - - // Test hiding - controls.hide(); - runner.expect(panel.style.display).toBe('none'); - - // Test showing - controls.show(); - runner.expect(panel.style.display).toBe('block'); - - // Cleanup - controls.destroy(); - }); - - runner.it('should preserve destroy functionality', () => { - const DocumentControls = global.ExtractedDocumentControls; - - const controls = new DocumentControls(); - controls.create(); - - // Verify panel exists - runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy(); - - // Destroy - controls.destroy(); - - // Verify panel is removed - runner.expect(document.getElementById('markitect-global-controls')).toBeFalsy(); - runner.expect(controls.controlPanel).toBeFalsy(); - }); - - runner.it('should support status updates', () => { - const DocumentControls = global.ExtractedDocumentControls; - - const controls = new DocumentControls(); - controls.create(); - - // Test status update - controls.updateStatus({ totalSections: 5, editingSections: 2 }); - - // The status should be reflected in the panel (implementation specific) - const panel = controls.getControlPanel(); - runner.expect(panel).toBeTruthy(); - - // Cleanup - controls.destroy(); - }); -}); - -module.exports = { - runner, - EXPECTED_DOCUMENTCONTROLS_API -}; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Testing DocumentControls Component Extraction'); - runner.run().then(() => { - console.log('โœ… DocumentControls extraction tests completed'); - }); -} \ No newline at end of file diff --git a/markitect/static/js/tests/test-domrenderer-extraction.js b/markitect/static/js/tests/test-domrenderer-extraction.js deleted file mode 100644 index e8aadc04..00000000 --- a/markitect/static/js/tests/test-domrenderer-extraction.js +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/env node - -/** - * TDD Test for DOMRenderer Component Extraction - * - * Tests the extraction of DOMRenderer from the monolithic editor.js - * DOMRenderer handles all DOM interactions and UI rendering. - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -// Define expected DOMRenderer API -const EXPECTED_DOMRENDERER_API = [ - 'constructor', - 'renderAllSections', - 'renderSection', - 'showEditor', - 'hideCurrentEditor', - 'showImageEditor', - 'findSectionElement', - 'handleSectionClick', - 'setupSectionElement', - 'trackEvent', - 'getEventStats' - // Note: addGlobalControls and debug methods are on MarkitectCleanEditor, not DOMRenderer -]; - -runner.describe('DOMRenderer Component Extraction', () => { - - runner.it('should define expected API methods', () => { - const expectedMethods = EXPECTED_DOMRENDERER_API; - runner.expect(expectedMethods.length).toBe(11); - runner.expect(expectedMethods).toContain('renderAllSections'); - runner.expect(expectedMethods).toContain('showEditor'); - runner.expect(expectedMethods).toContain('handleSectionClick'); - }); - - runner.it('should extract from monolithic editor.js', () => { - // Load the monolithic editor.js to extract DOMRenderer - delete require.cache[require.resolve('/home/worsch/markitect_project/markitect/static/editor.js')]; - - try { - const editorModule = require('/home/worsch/markitect_project/markitect/static/editor.js'); - runner.expect(editorModule.DOMRenderer).toBeTruthy(); - // Set global for other tests - global.DOMRenderer = editorModule.DOMRenderer; - global.SectionManager = editorModule.SectionManager; - } catch (error) { - throw new Error(`Failed to load monolithic editor.js: ${error.message}`); - } - }); - - runner.it('should preserve DOMRenderer constructor functionality', () => { - const DOMRenderer = global.DOMRenderer; - const SectionManager = global.SectionManager; - - const container = document.createElement('div'); - const sectionManager = new SectionManager(); - - const renderer = new DOMRenderer(sectionManager, container); - runner.expect(renderer).toBeInstanceOf(DOMRenderer); - runner.expect(renderer.sectionManager).toBe(sectionManager); - runner.expect(renderer.container).toBe(container); - }); - - runner.it('should preserve section rendering functionality', () => { - const DOMRenderer = global.DOMRenderer; - const SectionManager = global.SectionManager; - - const container = document.createElement('div'); - container.innerHTML = '
'; - - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - const testMarkdown = '# Test Heading\nTest content'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - - // This should not throw an error - renderer.renderAllSections(sections); - - // Check that some content was rendered - runner.expect(container.innerHTML.length).toBe(container.innerHTML.length); // Basic sanity check - }); - - runner.it('should preserve findSectionElement functionality', () => { - const DOMRenderer = global.DOMRenderer; - const SectionManager = global.SectionManager; - - const container = document.createElement('div'); - container.innerHTML = '
'; - - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - const testMarkdown = '# Test Heading\nTest content'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - renderer.renderAllSections(sections); - - const sectionId = sections[0].id; - const element = renderer.findSectionElement(sectionId); - - // Should find an element or return null (not throw error) - runner.expect(typeof element === 'object').toBeTruthy(); - }); - - runner.it('should preserve event tracking functionality', () => { - const DOMRenderer = global.DOMRenderer; - const SectionManager = global.SectionManager; - - const container = document.createElement('div'); - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - // Should have trackEvent method - runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy(); - - // Should be able to track an event - renderer.trackEvent('test-event', { data: 'test' }); - - // Should have getEventStats method - runner.expect(typeof renderer.getEventStats === 'function').toBeTruthy(); - - const stats = renderer.getEventStats(); - runner.expect(typeof stats === 'object').toBeTruthy(); - }); - - runner.it('should preserve editor showing functionality', () => { - const DOMRenderer = global.DOMRenderer; - const SectionManager = global.SectionManager; - - const container = document.createElement('div'); - container.innerHTML = '
'; - - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - const testMarkdown = '# Test Heading\nTest content'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - renderer.renderAllSections(sections); - - const sectionId = sections[0].id; - - // showEditor should not throw error - try { - renderer.showEditor(sectionId, 'test content'); - runner.expect(true).toBeTruthy(); // If we get here, no error was thrown - } catch (error) { - // Some errors are expected if DOM structure isn't complete - runner.expect(typeof error.message === 'string').toBeTruthy(); - } - }); - - runner.it('should have core DOM rendering methods', () => { - const DOMRenderer = global.DOMRenderer; - const SectionManager = global.SectionManager; - - const container = document.createElement('div'); - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - // Should have core methods - runner.expect(typeof renderer.renderAllSections === 'function').toBeTruthy(); - runner.expect(typeof renderer.showEditor === 'function').toBeTruthy(); - runner.expect(typeof renderer.findSectionElement === 'function').toBeTruthy(); - runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy(); - }); -}); - -// Export API tests for use during extraction -const DOMRENDERER_API_TESTS = [ - (DOMRenderer, SectionManager) => { - const container = document.createElement('div'); - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - if (!renderer.sectionManager) { - throw new Error('sectionManager property missing'); - } - }, - (DOMRenderer, SectionManager) => { - const container = document.createElement('div'); - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - if (typeof renderer.renderAllSections !== 'function') { - throw new Error('renderAllSections method missing'); - } - }, - (DOMRenderer, SectionManager) => { - const container = document.createElement('div'); - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - if (typeof renderer.showEditor !== 'function') { - throw new Error('showEditor method missing'); - } - } -]; - -module.exports = { - runner, - EXPECTED_DOMRENDERER_API, - DOMRENDERER_API_TESTS -}; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Testing DOMRenderer Component Extraction'); - runner.run().then(() => { - console.log('โœ… DOMRenderer extraction tests completed'); - }); -} \ No newline at end of file diff --git a/markitect/static/js/tests/test-extracted-domrenderer.js b/markitect/static/js/tests/test-extracted-domrenderer.js deleted file mode 100644 index d0a8990a..00000000 --- a/markitect/static/js/tests/test-extracted-domrenderer.js +++ /dev/null @@ -1,271 +0,0 @@ -#!/usr/bin/env node - -/** - * TDD Test for Extracted DOMRenderer Component - * - * Tests the extracted DOMRenderer component independently from the monolith. - * Verifies that core functionality is preserved after extraction. - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -runner.describe('Extracted DOMRenderer Component', () => { - - runner.it('should load extracted DOMRenderer component', () => { - // Load the extracted component - delete require.cache[require.resolve('../components/dom-renderer.js')]; - - try { - const module = require('../components/dom-renderer.js'); - runner.expect(module.DOMRenderer).toBeTruthy(); - runner.expect(module.FloatingMenu).toBeTruthy(); - - // Set globals for other tests - global.ExtractedDOMRenderer = module.DOMRenderer; - global.ExtractedFloatingMenu = module.FloatingMenu; - } catch (error) { - throw new Error(`Failed to load extracted DOMRenderer: ${error.message}`); - } - }); - - runner.it('should preserve constructor functionality', () => { - const DOMRenderer = global.ExtractedDOMRenderer; - - // Load SectionManager from our extracted core - const sectionModule = require('../core/section-manager.js'); - const SectionManager = sectionModule.SectionManager; - - const container = document.createElement('div'); - const sectionManager = new SectionManager(); - - const renderer = new DOMRenderer(sectionManager, container); - runner.expect(renderer).toBeInstanceOf(DOMRenderer); - runner.expect(renderer.sectionManager).toBe(sectionManager); - runner.expect(renderer.container).toBe(container); - runner.expect(renderer.editingSections).toBeInstanceOf(Set); - }); - - runner.it('should preserve section rendering functionality', () => { - const DOMRenderer = global.ExtractedDOMRenderer; - const sectionModule = require('../core/section-manager.js'); - const SectionManager = sectionModule.SectionManager; - - const container = document.createElement('div'); - container.innerHTML = '
'; - - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - const testMarkdown = '# Test Heading\nTest content'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - - // This should not throw an error - renderer.renderAllSections(sections); - - // Check that content was rendered - runner.expect(container.innerHTML.length > 100).toBeTruthy(); - runner.expect(container.innerHTML).toContain('Test Heading'); - }); - - runner.it('should preserve findSectionElement functionality', () => { - const DOMRenderer = global.ExtractedDOMRenderer; - const sectionModule = require('../core/section-manager.js'); - const SectionManager = sectionModule.SectionManager; - - const container = document.createElement('div'); - container.innerHTML = '
'; - - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - const testMarkdown = '# Test Heading\nTest content'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - renderer.renderAllSections(sections); - - const sectionId = sections[0].id; - const element = renderer.findSectionElement(sectionId); - - runner.expect(element).toBeTruthy(); - runner.expect(element.getAttribute('data-section-id')).toBe(sectionId); - }); - - runner.it('should preserve event tracking functionality', () => { - const DOMRenderer = global.ExtractedDOMRenderer; - const sectionModule = require('../core/section-manager.js'); - const SectionManager = sectionModule.SectionManager; - - const container = document.createElement('div'); - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - // Should have trackEvent method - runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy(); - - // Should be able to track an event - renderer.trackEvent('test-event', { data: 'test' }); - - // Should have getEventStats method - runner.expect(typeof renderer.getEventStats === 'function').toBeTruthy(); - - const stats = renderer.getEventStats(); - runner.expect(typeof stats === 'object').toBeTruthy(); - runner.expect(stats).toHaveProperty('stats'); - runner.expect(stats).toHaveProperty('totalEvents'); - runner.expect(stats).toHaveProperty('recentEvents'); - }); - - runner.it('should preserve editor showing functionality', () => { - const DOMRenderer = global.ExtractedDOMRenderer; - const sectionModule = require('../core/section-manager.js'); - const SectionManager = sectionModule.SectionManager; - - const container = document.createElement('div'); - container.innerHTML = '
'; - - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - const testMarkdown = '# Test Heading\nTest content'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - renderer.renderAllSections(sections); - - const sectionId = sections[0].id; - - // showEditor should not throw error - try { - renderer.showEditor(sectionId, 'test content'); - runner.expect(true).toBeTruthy(); // If we get here, no error was thrown - - // Check that editing state was set - runner.expect(renderer.editingSections.has(sectionId)).toBeTruthy(); - } catch (error) { - throw new Error(`showEditor failed: ${error.message}`); - } - }); - - runner.it('should preserve FloatingMenu functionality', () => { - const FloatingMenu = global.ExtractedFloatingMenu; - const DOMRenderer = global.ExtractedDOMRenderer; - const sectionModule = require('../core/section-manager.js'); - const SectionManager = sectionModule.SectionManager; - - const container = document.createElement('div'); - container.innerHTML = '
'; - - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - const testMarkdown = '# Test Heading\nTest content'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - renderer.renderAllSections(sections); - - const sectionId = sections[0].id; - const floatingMenu = new FloatingMenu(sectionId, 'text', renderer); - - runner.expect(floatingMenu.sectionId).toBe(sectionId); - runner.expect(floatingMenu.type).toBe('text'); - runner.expect(floatingMenu.renderer).toBe(renderer); - runner.expect(floatingMenu.isVisible).toBeFalsy(); - - // Test show/hide functionality - const content = document.createElement('div'); - content.textContent = 'Test content'; - - floatingMenu.show(content); - runner.expect(floatingMenu.isVisible).toBeTruthy(); - - floatingMenu.hide(); - runner.expect(floatingMenu.isVisible).toBeFalsy(); - }); - - runner.it('should handle section click events', () => { - const DOMRenderer = global.ExtractedDOMRenderer; - const sectionModule = require('../core/section-manager.js'); - const SectionManager = sectionModule.SectionManager; - - const container = document.createElement('div'); - container.innerHTML = '
'; - - const sectionManager = new SectionManager(); - const renderer = new DOMRenderer(sectionManager, container); - - const testMarkdown = '# Test Heading\nTest content'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - renderer.renderAllSections(sections); - - const sectionId = sections[0].id; - const element = renderer.findSectionElement(sectionId); - - // Simulate a click event - const clickEvent = new Event('click', { bubbles: true }); - Object.defineProperty(clickEvent, 'target', { value: element }); - - // Should not throw error - try { - renderer.handleSectionClick(clickEvent); - runner.expect(true).toBeTruthy(); - } catch (error) { - throw new Error(`handleSectionClick failed: ${error.message}`); - } - }); - - // Comparative test - verify extracted component behaves similarly to original - runner.it('should behave similarly to original monolithic component', () => { - // Load both components - const originalModule = require('/home/worsch/markitect_project/markitect/static/editor.js'); - const extractedModule = require('../components/dom-renderer.js'); - const sectionModule = require('../core/section-manager.js'); - - const originalSectionManager = new originalModule.SectionManager(); - const extractedSectionManager = new sectionModule.SectionManager(); - - const originalContainer = document.createElement('div'); - originalContainer.innerHTML = '
'; - - const extractedContainer = document.createElement('div'); - extractedContainer.innerHTML = '
'; - - const originalRenderer = new originalModule.DOMRenderer(originalSectionManager, originalContainer); - const extractedRenderer = new extractedModule.DOMRenderer(extractedSectionManager, extractedContainer); - - const testMarkdown = '# Test\nContent\n\n## Subheading\nMore content'; - - // Create sections with both - const originalSections = originalSectionManager.createSectionsFromMarkdown(testMarkdown); - const extractedSections = extractedSectionManager.createSectionsFromMarkdown(testMarkdown); - - // Render with both - originalRenderer.renderAllSections(originalSections); - extractedRenderer.renderAllSections(extractedSections); - - // Should have rendered content - runner.expect(originalContainer.innerHTML.length > 100).toBeTruthy(); - runner.expect(extractedContainer.innerHTML.length > 100).toBeTruthy(); - - // Should have same number of section elements - const originalSectionElements = originalContainer.querySelectorAll('.ui-edit-section'); - const extractedSectionElements = extractedContainer.querySelectorAll('.ui-edit-section'); - - runner.expect(extractedSectionElements.length).toBe(originalSectionElements.length); - - // Should have similar event stats structure - const originalStats = originalRenderer.getEventStats(); - const extractedStats = extractedRenderer.getEventStats(); - - runner.expect(extractedStats).toHaveProperty('stats'); - runner.expect(extractedStats).toHaveProperty('totalEvents'); - runner.expect(extractedStats).toHaveProperty('recentEvents'); - }); -}); - -module.exports = runner; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Testing Extracted DOMRenderer Component'); - runner.run().then(() => { - console.log('โœ… Extracted DOMRenderer tests completed'); - }); -} \ No newline at end of file diff --git a/markitect/static/js/tests/test-extracted-section-manager.js b/markitect/static/js/tests/test-extracted-section-manager.js deleted file mode 100644 index 0eb51d01..00000000 --- a/markitect/static/js/tests/test-extracted-section-manager.js +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env node - -/** - * TDD Test for Extracted SectionManager Component - * - * Tests the extracted SectionManager component independently from the monolith. - * Verifies that all functionality is preserved after extraction. - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -runner.describe('Extracted SectionManager Component', () => { - - runner.it('should load extracted SectionManager component', () => { - // Load the extracted component - delete require.cache[require.resolve('../core/section-manager.js')]; - - try { - const module = require('../core/section-manager.js'); - runner.expect(module.SectionManager).toBeTruthy(); - runner.expect(module.Section).toBeTruthy(); - runner.expect(module.EditState).toBeTruthy(); - runner.expect(module.SectionType).toBeTruthy(); - - // Set globals for other tests - global.ExtractedSectionManager = module.SectionManager; - global.ExtractedSection = module.Section; - global.ExtractedEditState = module.EditState; - global.ExtractedSectionType = module.SectionType; - } catch (error) { - throw new Error(`Failed to load extracted SectionManager: ${error.message}`); - } - }); - - runner.it('should preserve constructor functionality', () => { - const SectionManager = global.ExtractedSectionManager; - - const manager = new SectionManager(); - runner.expect(manager).toBeInstanceOf(SectionManager); - runner.expect(manager.sections).toBeInstanceOf(Map); - runner.expect(manager.listeners).toBeInstanceOf(Map); - }); - - runner.it('should preserve section creation functionality', () => { - const SectionManager = global.ExtractedSectionManager; - const manager = new SectionManager(); - - const testMarkdown = `# Heading 1\nContent 1\n\n## Heading 2\nContent 2`; - const sections = manager.createSectionsFromMarkdown(testMarkdown); - - runner.expect(Array.isArray(sections)).toBeTruthy(); - runner.expect(sections.length).toBe(2); - runner.expect(sections[0].currentMarkdown).toContain('Heading 1'); - runner.expect(sections[1].currentMarkdown).toContain('Heading 2'); - }); - - runner.it('should preserve section editing functionality', () => { - const SectionManager = global.ExtractedSectionManager; - const manager = new SectionManager(); - - const sections = manager.createSectionsFromMarkdown('# Test\nContent'); - const sectionId = sections[0].id; - - // Test start editing - const content = manager.startEditing(sectionId); - runner.expect(content).toContain('Test'); - - const section = manager.sections.get(sectionId); - runner.expect(section.isEditing()).toBeTruthy(); - - // Test stop editing - section.stopEditing(); - runner.expect(section.isEditing()).toBeFalsy(); - }); - - runner.it('should preserve event system functionality', () => { - const SectionManager = global.ExtractedSectionManager; - const manager = new SectionManager(); - - let eventFired = false; - let eventData = null; - - manager.on('test-event', (data) => { - eventFired = true; - eventData = data; - }); - - manager.emit('test-event', { test: 'data' }); - - runner.expect(eventFired).toBeTruthy(); - runner.expect(eventData).toEqual({ test: 'data' }); - }); - - runner.it('should preserve document status functionality', () => { - const SectionManager = global.ExtractedSectionManager; - const manager = new SectionManager(); - - manager.createSectionsFromMarkdown('# Test\nContent'); - const status = manager.getDocumentStatus(); - - runner.expect(status).toHaveProperty('totalSections'); - runner.expect(status).toHaveProperty('editingSections'); - runner.expect(status.totalSections).toBe(1); - }); - - runner.it('should preserve getAllSections functionality', () => { - const SectionManager = global.ExtractedSectionManager; - const manager = new SectionManager(); - - const testMarkdown = '# One\nContent\n\n# Two\nMore content'; - manager.createSectionsFromMarkdown(testMarkdown); - - const allSections = manager.getAllSections(); - runner.expect(Array.isArray(allSections)).toBeTruthy(); - runner.expect(allSections.length).toBe(2); - }); - - runner.it('should preserve section splitting functionality', () => { - const SectionManager = global.ExtractedSectionManager; - const manager = new SectionManager(); - - const sections = manager.createSectionsFromMarkdown('# Original\nContent'); - const sectionId = sections[0].id; - - const newContent = '# Split 1\nContent 1\n\n# Split 2\nContent 2'; - const newSections = manager.handleSectionSplit(sectionId, newContent); - - runner.expect(Array.isArray(newSections)).toBeTruthy(); - runner.expect(newSections.length).toBe(2); - runner.expect(manager.sections.has(sectionId)).toBeFalsy(); // Original removed - }); - - runner.it('should preserve Section class functionality', () => { - const Section = global.ExtractedSection; - const EditState = global.ExtractedEditState; - - const section = new Section('test-id', '# Test Content', 'heading'); - - runner.expect(section.id).toBe('test-id'); - runner.expect(section.currentMarkdown).toBe('# Test Content'); - runner.expect(section.type).toBe('heading'); - runner.expect(section.state).toBe(EditState.ORIGINAL); - }); - - runner.it('should preserve Section ID generation', () => { - const Section = global.ExtractedSection; - - const id1 = Section.generateId('# Test Heading', 0); - const id2 = Section.generateId('# Different Heading', 1); - - runner.expect(typeof id1 === 'string').toBeTruthy(); - runner.expect(typeof id2 === 'string').toBeTruthy(); - runner.expect(id1).toContain('section-'); - runner.expect(id2).toContain('section-'); - runner.expect(id1 !== id2).toBeTruthy(); // Should be unique - }); - - runner.it('should preserve Section type detection', () => { - const Section = global.ExtractedSection; - const SectionType = global.ExtractedSectionType; - - runner.expect(Section.detectType('# Heading')).toBe(SectionType.HEADING); - runner.expect(Section.detectType('![Image](url)')).toBe(SectionType.IMAGE); - runner.expect(Section.detectType('```code```')).toBe(SectionType.CODE); - runner.expect(Section.detectType('Regular paragraph')).toBe(SectionType.PARAGRAPH); - }); - - // Comparative test - verify extracted component behaves identically to original - runner.it('should behave identically to original monolithic component', () => { - // Load both components - const originalModule = require('/home/worsch/markitect_project/markitect/static/editor.js'); - const extractedModule = require('../core/section-manager.js'); - - const originalManager = new originalModule.SectionManager(); - const extractedManager = new extractedModule.SectionManager(); - - const testMarkdown = '# Test\nContent\n\n## Subheading\nMore content'; - - // Debug: Check what each component produces - console.log('Creating sections with original component...'); - const originalSections = originalManager.createSectionsFromMarkdown(testMarkdown); - console.log(`Original produced ${originalSections.length} sections`); - - console.log('Creating sections with extracted component...'); - const extractedSections = extractedManager.createSectionsFromMarkdown(testMarkdown); - console.log(`Extracted produced ${extractedSections.length} sections`); - - if (originalSections.length > 0) { - console.log('Original first section:', originalSections[0].currentMarkdown); - } - if (extractedSections.length > 0) { - console.log('Extracted first section:', extractedSections[0].currentMarkdown); - } - - // Should have same number of sections - runner.expect(extractedSections.length).toBe(originalSections.length); - - // Should have same content - for (let i = 0; i < originalSections.length; i++) { - runner.expect(extractedSections[i].currentMarkdown).toBe(originalSections[i].currentMarkdown); - runner.expect(extractedSections[i].type).toBe(originalSections[i].type); - } - - // Should have same document status structure - const originalStatus = originalManager.getDocumentStatus(); - const extractedStatus = extractedManager.getDocumentStatus(); - - console.log('Original status:', originalStatus); - console.log('Extracted status:', extractedStatus); - - runner.expect(extractedStatus.totalSections).toBe(originalStatus.totalSections); - runner.expect(extractedStatus.editingSections).toBe(originalStatus.editingSections); - }); -}); - -module.exports = runner; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Testing Extracted SectionManager Component'); - runner.run().then(() => { - console.log('โœ… Extracted SectionManager tests completed'); - }); -} \ No newline at end of file diff --git a/markitect/static/js/tests/test-full-integration.js b/markitect/static/js/tests/test-full-integration.js deleted file mode 100644 index 3edb0ced..00000000 --- a/markitect/static/js/tests/test-full-integration.js +++ /dev/null @@ -1,305 +0,0 @@ -#!/usr/bin/env node - -/** - * Full Integration Test - * - * Tests that all extracted components (SectionManager, DOMRenderer, - * DebugPanel, DocumentControls) work together as a complete system. - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -runner.describe('Full Component Integration Tests', () => { - - runner.it('should load all extracted components', () => { - try { - // Load all extracted components - const sectionModule = require('../core/section-manager.js'); - const domModule = require('../components/dom-renderer.js'); - const debugModule = require('../components/debug-panel.js'); - const controlsModule = require('../components/document-controls.js'); - - runner.expect(sectionModule.SectionManager).toBeTruthy(); - runner.expect(domModule.DOMRenderer).toBeTruthy(); - runner.expect(debugModule.DebugPanel).toBeTruthy(); - runner.expect(controlsModule.DocumentControls).toBeTruthy(); - - // Set globals for other tests - global.ExtractedSectionManager = sectionModule.SectionManager; - global.ExtractedDOMRenderer = domModule.DOMRenderer; - global.ExtractedDebugPanel = debugModule.DebugPanel; - global.ExtractedDocumentControls = controlsModule.DocumentControls; - - } catch (error) { - throw new Error(`Failed to load extracted components: ${error.message}`); - } - }); - - runner.it('should support complete document editing workflow with all components', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - const DebugPanel = global.ExtractedDebugPanel; - const DocumentControls = global.ExtractedDocumentControls; - - // Setup DOM container - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - // Create all components - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - const debugPanel = new DebugPanel(); - const documentControls = new DocumentControls(); - - // Setup document controls - documentControls.create(); - - // Wire up event handlers for debugging - sectionManager.on('sections-created', (data) => { - debugPanel.addMessage(`Created ${data.count} sections`, 'INFO'); - }); - - sectionManager.on('edit-started', (data) => { - debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG'); - }); - - // Test workflow: Create document - const testMarkdown = `# Document Title -Introduction paragraph with some content. - -## Section A -Content for section A with details. - -![Test Image](https://example.com/test.jpg) - -### Subsection A.1 -More detailed content here.`; - - // Create sections - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - runner.expect(sections.length).toBe(4); - - // Render sections - domRenderer.renderAllSections(sections); - const renderedElements = container.querySelectorAll('.ui-edit-section'); - runner.expect(renderedElements.length).toBe(sections.length); - - // Test editing workflow - const firstSection = sections[0]; - sectionManager.startEditing(firstSection.id); - runner.expect(firstSection.isEditing()).toBeTruthy(); - - // Check debug messages were created - runner.expect(debugPanel.getMessageCount()).toBe(2); // sections-created + edit-started - - // Test document controls functionality - const controlPanel = documentControls.getControlPanel(); - runner.expect(controlPanel).toBeTruthy(); - runner.expect(document.getElementById('save-document')).toBeTruthy(); - runner.expect(document.getElementById('toggle-debug')).toBeTruthy(); - - // Cleanup - document.body.removeChild(container); - documentControls.destroy(); - }); - - runner.it('should support debug panel integration with document controls', () => { - const DebugPanel = global.ExtractedDebugPanel; - const DocumentControls = global.ExtractedDocumentControls; - - // Create components - const debugPanel = new DebugPanel(); - const documentControls = new DocumentControls(); - - // Setup document controls - documentControls.create(); - - // Setup debug panel toggle handler - const handlers = { - 'toggle-debug': () => debugPanel.toggle() - }; - documentControls.setEventHandlers(handlers); - - // Test debug toggle functionality - const debugButton = documentControls.getButton('toggle-debug'); - runner.expect(debugButton).toBeTruthy(); - - // Add some debug messages - debugPanel.addMessage('Test message 1', 'INFO'); - debugPanel.addMessage('Test message 2', 'ERROR'); - - // Simulate button click to show debug panel - debugButton.click(); - runner.expect(debugPanel.isActive).toBeTruthy(); - - // Simulate button click to hide debug panel - debugButton.click(); - runner.expect(debugPanel.isActive).toBeFalsy(); - - // Cleanup - documentControls.destroy(); - }); - - runner.it('should support event-driven communication between all components', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - const DebugPanel = global.ExtractedDebugPanel; - const DocumentControls = global.ExtractedDocumentControls; - - // Setup container - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - // Create components - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - const debugPanel = new DebugPanel(); - const documentControls = new DocumentControls(); - - documentControls.create(); - - // Setup comprehensive event handling - let eventLog = []; - - sectionManager.on('sections-created', (data) => { - eventLog.push(`sections-created: ${data.count} sections`); - debugPanel.addMessage(`Sections created: ${data.count}`, 'INFO'); - }); - - sectionManager.on('edit-started', (data) => { - eventLog.push(`edit-started: ${data.sectionId}`); - debugPanel.addMessage(`Edit started: ${data.sectionId}`, 'DEBUG'); - }); - - sectionManager.on('changes-accepted', (data) => { - eventLog.push(`changes-accepted: ${data.sectionId}`); - debugPanel.addMessage(`Changes accepted: ${data.sectionId}`, 'SUCCESS'); - }); - - // Test complete workflow - const testMarkdown = '# Test\nContent for testing'; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - // Start editing - sectionManager.startEditing(sections[0].id); - sectionManager.updateContent(sections[0].id, '# Updated Test\nUpdated content'); - sectionManager.acceptChanges(sections[0].id); - - // Verify events were logged - runner.expect(eventLog.length).toBe(3); - runner.expect(eventLog[0]).toContain('sections-created'); - runner.expect(eventLog[1]).toContain('edit-started'); - runner.expect(eventLog[2]).toContain('changes-accepted'); - - // Verify debug messages were created - runner.expect(debugPanel.getMessageCount()).toBe(3); - - // Test document controls status update - const status = sectionManager.getDocumentStatus(); - documentControls.updateStatus(status); - runner.expect(documentControls.lastStatus).toBeTruthy(); - - // Cleanup - document.body.removeChild(container); - documentControls.destroy(); - }); - - runner.it('should handle error scenarios gracefully across components', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - const DebugPanel = global.ExtractedDebugPanel; - const DocumentControls = global.ExtractedDocumentControls; - - // Test component creation without proper DOM setup - const debugPanel = new DebugPanel(); - const documentControls = new DocumentControls(); - - // These should not throw errors - try { - debugPanel.toggle(); // No DOM elements - debugPanel.update(); // No DOM elements - documentControls.show(); // No control panel created yet - documentControls.hide(); // No control panel created yet - - runner.expect(true).toBeTruthy(); // If we get here, no errors were thrown - } catch (error) { - throw new Error(`Components should handle missing DOM gracefully: ${error.message}`); - } - - // Test section manager with invalid input - const sectionManager = new SectionManager(); - const sections = sectionManager.createSectionsFromMarkdown(''); - runner.expect(sections.length).toBe(0); - - // Test DOM renderer with invalid container - try { - const invalidRenderer = new DOMRenderer(sectionManager, null); - runner.expect(invalidRenderer.container).toBeFalsy(); - } catch (error) { - // This is acceptable - constructor might validate input - runner.expect(typeof error.message === 'string').toBeTruthy(); - } - }); - - runner.it('should support scalable architecture with component lifecycle', () => { - const SectionManager = global.ExtractedSectionManager; - const DOMRenderer = global.ExtractedDOMRenderer; - const DebugPanel = global.ExtractedDebugPanel; - const DocumentControls = global.ExtractedDocumentControls; - - // Test multiple instances - const sectionManager1 = new SectionManager(); - const sectionManager2 = new SectionManager(); - const debugPanel1 = new DebugPanel(); - const debugPanel2 = new DebugPanel(); - - // Each should be independent - debugPanel1.addMessage('Message from panel 1', 'INFO'); - debugPanel2.addMessage('Message from panel 2', 'ERROR'); - - runner.expect(debugPanel1.getMessageCount()).toBe(1); - runner.expect(debugPanel2.getMessageCount()).toBe(1); - - // Test section managers are independent - const sections1 = sectionManager1.createSectionsFromMarkdown('# Document 1'); - const sections2 = sectionManager2.createSectionsFromMarkdown('# Document 2'); - - runner.expect(sections1.length).toBe(1); - runner.expect(sections2.length).toBe(1); - runner.expect(sections1[0]).toBeTruthy(); - runner.expect(sections2[0]).toBeTruthy(); - - // IDs should be different (each section gets unique ID) - const id1 = sections1[0].id; - const id2 = sections2[0].id; - runner.expect(id1 !== id2).toBeTruthy(); - - // Test document controls lifecycle - const controls1 = new DocumentControls(); - const controls2 = new DocumentControls(); - - controls1.create(); - runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy(); - - controls2.create(); // Should replace the first one - runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy(); - - controls2.destroy(); - runner.expect(document.getElementById('markitect-global-controls')).toBeFalsy(); - }); -}); - -module.exports = runner; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Running Full Component Integration Tests'); - runner.run().then(() => { - console.log('โœ… Full integration tests completed'); - }); -} \ No newline at end of file diff --git a/markitect/static/js/tests/test-navigator-demo.html b/markitect/static/js/tests/test-navigator-demo.html deleted file mode 100644 index 020178b1..00000000 --- a/markitect/static/js/tests/test-navigator-demo.html +++ /dev/null @@ -1,342 +0,0 @@ - - - - - - DocumentNavigator Live Demo - - - -
-

๐Ÿ“‹ DocumentNavigator Live Demo

-

This page demonstrates the Substack-style floating navigation widget in action.

-

Look for the hamburger menu (โ˜ฐ) on the left side!

- -
- Features to test:
- โ€ข Click the hamburger menu to expand navigation
- โ€ข Click any heading in the navigator to jump to it
- โ€ข Scroll and watch the current section highlight
- โ€ข Try keyboard shortcuts (Enter/Space to toggle, Escape to close)
- โ€ข Resize window to test responsive behavior -
-
- -
-

1. Introduction to MarkiTect

-
-

MarkiTect is an advanced markdown processing engine that provides sophisticated document management capabilities. This demo showcases the DocumentNavigator widget, which provides Substack-style navigation for long-form documents.

- -

The navigator automatically extracts headings from your content and builds a hierarchical table of contents that floats elegantly on the side of your document.

-
- -

1.1 Core Features

-
-

The DocumentNavigator widget includes numerous advanced features designed for optimal user experience:

- - -
- -

1.1.1 Responsive Behavior

-
-

The navigator intelligently adapts to different screen sizes. On desktop computers, it remains visible as a floating panel. On mobile devices, it automatically hides to preserve screen real estate for content.

- -

Try resizing your browser window to see this behavior in action. The navigator will disappear when the viewport becomes narrow (under 768px wide).

-
- -

1.1.2 Accessibility Features

-
-

The DocumentNavigator is built with accessibility in mind:

- - -
- -

1.2 Implementation Details

-
-

The DocumentNavigator is implemented as a modular ES6 class that extends our base UIWidget class. This follows the planned plugin architecture for MarkiTect widgets.

- -

Key implementation highlights include:

- - -
- -

2. Widget Architecture

-
-

The DocumentNavigator follows a clean architectural pattern that separates concerns and provides maximum flexibility for customization and extension.

- -

The widget is designed as part of a larger plugin ecosystem that will allow developers to create custom UI components that can be loaded dynamically and configured independently.

-
- -

2.1 Base Class Hierarchy

-
-

Our widget system is built on a foundation of base classes that provide common functionality:

- - - -

DocumentNavigator extends UIWidget directly since it doesn't require complex interaction handling beyond basic click and keyboard events.

-
- -

2.1.1 Event System

-
-

The widget uses a custom event system built on the native EventTarget API. This allows for clean separation of concerns and easy integration with other components.

- -

Key events emitted by DocumentNavigator:

- - -
- -

2.1.2 State Management

-
-

State management is handled through a simple Map-based system that provides reactive updates and event emission when state changes occur.

- -

This approach is lightweight but powerful enough for most widget use cases while remaining debuggable and predictable.

-
- -

2.2 Plugin System Integration

-
-

While the current implementation works standalone, it's designed to integrate seamlessly with our planned plugin system. The plugin definition includes:

- - -
- -

3. Usage Examples

-
-

The DocumentNavigator can be used in several ways, from simple instantiation to advanced configuration with custom themes and behavior.

-
- -

3.1 Basic Usage

-
-

The simplest way to use DocumentNavigator is with default settings:

- -
const navigator = new DocumentNavigator();
-await navigator.initialize();
-await navigator.render();
- -

This creates a navigator with default settings that will scan the entire document for headings and display them in a collapsible panel on the left side.

-
- -

3.2 Advanced Configuration

-
-

For more control, you can specify detailed configuration options:

- -
const navigator = new DocumentNavigator({
-    position: 'right',
-    collapsed: false,
-    theme: 'dark',
-    maxHeadingLevel: 4,
-    enableScrollSpy: true,
-    smoothScroll: true
-});
- -

This creates a navigator on the right side that starts expanded, includes H4 headings, and uses the dark theme.

-
- -

3.2.1 Custom Theming

-
-

The navigator supports multiple built-in themes and can be extended with custom themes. The theming system integrates with MarkiTect's document themes for consistent styling.

- -

Available themes include default, dark, and minimal, each optimized for different use cases and aesthetics.

-
- -

4. Testing and Quality

-
-

The DocumentNavigator implementation follows Test-Driven Development (TDD) methodology with comprehensive test coverage ensuring reliability and maintainability.

-
- -

4.1 Test Coverage

-
-

Our test suite covers all major functionality:

- - -
- -

4.2 Performance Considerations

-
-

The navigator is optimized for performance with several key strategies:

- - -
- -

5. Conclusion

-
-

The DocumentNavigator widget successfully brings Substack-style navigation to MarkiTect documents. It provides an intuitive, accessible, and performant way for users to navigate long-form content.

- -

The implementation demonstrates the power of our widget architecture approach, with clean separation of concerns, comprehensive testing, and excellent extensibility for future enhancements.

- -

Scroll back to the top and try the navigation features! The hamburger menu should be visible on the left side of your screen.

-
-
- - - - - \ No newline at end of file diff --git a/markitect/static/js/tests/test-real-user-functionality.js b/markitect/static/js/tests/test-real-user-functionality.js deleted file mode 100644 index 3d7fddef..00000000 --- a/markitect/static/js/tests/test-real-user-functionality.js +++ /dev/null @@ -1,285 +0,0 @@ -#!/usr/bin/env node - -/** - * Real User Functionality Tests - * - * This test file validates the actual functionality that users experience, - * not just internal API calls. It tests the complete user workflow. - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -runner.describe('Real User Functionality Tests', () => { - - runner.it('should allow users to edit content and see changes in DOM', () => { - // Load all extracted components - const sectionModule = require('../core/section-manager.js'); - const domModule = require('../components/dom-renderer.js'); - const debugModule = require('../components/debug-panel.js'); - const controlsModule = require('../components/document-controls.js'); - - const { SectionManager } = sectionModule; - const { DOMRenderer } = domModule; - const { DebugPanel } = debugModule; - const { DocumentControls } = controlsModule; - - // Setup DOM container - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - // Create components - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - const debugPanel = new DebugPanel(); - const documentControls = new DocumentControls(); - - // Setup document controls - documentControls.create(); - - // Create sections from test markdown - const testMarkdown = `# Original Title\nOriginal content that should be editable.`; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - const firstSection = sections[0]; - const sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`); - - // Verify original content is rendered - runner.expect(sectionElement.innerHTML).toContain('Original Title'); - - // Simulate user clicking on section - const clickEvent = new Event('click', { bubbles: true }); - sectionElement.dispatchEvent(clickEvent); - - // Verify editing state is active - runner.expect(firstSection.isEditing()).toBeTruthy(); - - // Find the floating menu and edit controls - const floatingMenu = document.querySelector('.ui-edit-floating-menu'); - runner.expect(floatingMenu).toBeTruthy(); - - const textarea = floatingMenu.querySelector('textarea'); - const acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept')); - - runner.expect(textarea).toBeTruthy(); - runner.expect(acceptButton).toBeTruthy(); - - // Simulate user editing content - const newContent = '# Updated Title\nCompletely new content added by user.'; - textarea.value = newContent; - - // Simulate user clicking accept - acceptButton.click(); - - // Verify section is no longer editing - runner.expect(firstSection.isEditing()).toBeFalsy(); - - // Verify floating menu is gone - const menuAfterAccept = document.querySelector('.ui-edit-floating-menu'); - runner.expect(menuAfterAccept).toBeFalsy(); - - // CRITICAL TEST: Verify DOM was actually updated with new content - const updatedElement = container.querySelector(`[data-section-id="${firstSection.id}"]`); - runner.expect(updatedElement.innerHTML).toContain('Updated Title'); - runner.expect(updatedElement.innerHTML).toContain('Completely new content'); - runner.expect(updatedElement.innerHTML).not.toContain('Original Title'); - - // Cleanup - document.body.removeChild(container); - documentControls.destroy(); - }); - - runner.it('should allow users to reset all changes', () => { - // Setup similar to above - const sectionModule = require('../core/section-manager.js'); - const domModule = require('../components/dom-renderer.js'); - const controlsModule = require('../components/document-controls.js'); - - const { SectionManager } = sectionModule; - const { DOMRenderer } = domModule; - const { DocumentControls } = controlsModule; - - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - const documentControls = new DocumentControls(); - - documentControls.create(); - - // Create and modify content - const testMarkdown = `# Test Section\nOriginal content for reset test.`; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - const firstSection = sections[0]; - - // Make changes to the section - sectionManager.startEditing(firstSection.id); - sectionManager.updateContent(firstSection.id, '# Modified Title\nModified content.'); - sectionManager.acceptChanges(firstSection.id); - - // Verify changes are applied - let sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`); - runner.expect(sectionElement.innerHTML).toContain('Modified Title'); - runner.expect(firstSection.hasChanges()).toBeTruthy(); - - // Test reset functionality - const resetButton = documentControls.getButton('reset-all'); - runner.expect(resetButton).toBeTruthy(); - - // Click reset button - resetButton.click(); - - // Verify content is reset - sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`); - runner.expect(sectionElement.innerHTML).toContain('Test Section'); - runner.expect(sectionElement.innerHTML).not.toContain('Modified Title'); - runner.expect(firstSection.hasChanges()).toBeFalsy(); - - // Cleanup - document.body.removeChild(container); - documentControls.destroy(); - }); - - runner.it('should handle cancel operations correctly', () => { - const sectionModule = require('../core/section-manager.js'); - const domModule = require('../components/dom-renderer.js'); - - const { SectionManager } = sectionModule; - const { DOMRenderer } = domModule; - - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - - const testMarkdown = `# Cancel Test\nContent that should remain unchanged.`; - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - const firstSection = sections[0]; - const originalContent = firstSection.currentMarkdown; - - // Start editing - const sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`); - sectionElement.click(); - - // Make changes but cancel them - const floatingMenu = document.querySelector('.ui-edit-floating-menu'); - const textarea = floatingMenu.querySelector('textarea'); - const cancelButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Cancel')); - - textarea.value = '# This should be cancelled\nThis content should not appear.'; - cancelButton.click(); - - // Verify content is unchanged - const unchangedElement = container.querySelector(`[data-section-id="${firstSection.id}"]`); - runner.expect(unchangedElement.innerHTML).toContain('Cancel Test'); - runner.expect(unchangedElement.innerHTML).not.toContain('This should be cancelled'); - runner.expect(firstSection.currentMarkdown).toBe(originalContent); - - // Cleanup - document.body.removeChild(container); - }); - - runner.it('should validate the complete editing workflow', () => { - // This test validates the entire user experience end-to-end - const sectionModule = require('../core/section-manager.js'); - const domModule = require('../components/dom-renderer.js'); - const debugModule = require('../components/debug-panel.js'); - const controlsModule = require('../components/document-controls.js'); - - const { SectionManager } = sectionModule; - const { DOMRenderer } = domModule; - const { DebugPanel } = debugModule; - const { DocumentControls } = controlsModule; - - const container = document.createElement('div'); - container.innerHTML = '
'; - document.body.appendChild(container); - - const sectionManager = new SectionManager(); - const domRenderer = new DOMRenderer(sectionManager, container); - const debugPanel = new DebugPanel(); - const documentControls = new DocumentControls(); - - documentControls.create(); - - // Multi-section document - const testMarkdown = `# Document Title -Introduction paragraph. - -## Section A -Content for section A. - -## Section B -Content for section B.`; - - const sections = sectionManager.createSectionsFromMarkdown(testMarkdown); - domRenderer.renderAllSections(sections); - - // Verify all sections are rendered - const renderedSections = container.querySelectorAll('.ui-edit-section'); - runner.expect(renderedSections.length).toBe(sections.length); - - // Test editing multiple sections - const firstSection = sections[0]; - const secondSection = sections[2]; // Section A - - // Edit first section - renderedSections[0].click(); - let floatingMenu = document.querySelector('.ui-edit-floating-menu'); - let textarea = floatingMenu.querySelector('textarea'); - let acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept')); - - textarea.value = '# Updated Document Title\nUpdated introduction.'; - acceptButton.click(); - - // Edit second section - renderedSections[2].click(); - floatingMenu = document.querySelector('.ui-edit-floating-menu'); - textarea = floatingMenu.querySelector('textarea'); - acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept')); - - textarea.value = '## Updated Section A\nCompletely new content for section A.'; - acceptButton.click(); - - // Verify both sections were updated - const updatedSections = container.querySelectorAll('.ui-edit-section'); - runner.expect(updatedSections[0].innerHTML).toContain('Updated Document Title'); - runner.expect(updatedSections[2].innerHTML).toContain('Updated Section A'); - - // Test reset restores all sections - const resetButton = documentControls.getButton('reset-all'); - resetButton.click(); - - const resetSections = container.querySelectorAll('.ui-edit-section'); - runner.expect(resetSections[0].innerHTML).toContain('Document Title'); - runner.expect(resetSections[0].innerHTML).not.toContain('Updated Document Title'); - runner.expect(resetSections[2].innerHTML).toContain('Section A'); - runner.expect(resetSections[2].innerHTML).not.toContain('Updated Section A'); - - // Cleanup - document.body.removeChild(container); - documentControls.destroy(); - }); -}); - -module.exports = runner; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Running Real User Functionality Tests'); - runner.run().then(() => { - console.log('โœ… Real user functionality tests completed'); - console.log('These tests validate what users actually experience, not just internal APIs'); - }); -} \ No newline at end of file diff --git a/markitect/static/js/tests/test-section-manager-extraction.js b/markitect/static/js/tests/test-section-manager-extraction.js deleted file mode 100644 index 1eecce5d..00000000 --- a/markitect/static/js/tests/test-section-manager-extraction.js +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env node - -/** - * TDD Test for SectionManager Component Extraction - * - * Tests the extraction of SectionManager from the monolithic editor.js - * Ensures all functionality is preserved during refactoring. - */ - -const RefactorTestRunner = require('./refactor-test-runner.js'); - -const runner = new RefactorTestRunner(); - -// First, let's define what the SectionManager API should look like -const EXPECTED_SECTION_MANAGER_API = [ - 'constructor', - 'createSectionsFromMarkdown', - 'startEditing', - 'stopEditing', - 'getAllSections', - 'sections', // Map property, not method - 'getDocumentStatus', - 'getDocumentMarkdown', - 'on', // event system - 'emit', // event system - 'handleSectionSplit', - 'updateContent', - 'acceptChanges', - 'cancelChanges', - 'resetSection' -]; - -runner.describe('SectionManager Component Extraction', () => { - - runner.it('should define expected API methods', () => { - // This test defines what we expect from the extracted SectionManager - const expectedMethods = EXPECTED_SECTION_MANAGER_API; - runner.expect(expectedMethods.length).toBe(15); - runner.expect(expectedMethods).toContain('createSectionsFromMarkdown'); - runner.expect(expectedMethods).toContain('startEditing'); - runner.expect(expectedMethods).toContain('stopEditing'); - }); - - runner.it('should extract from monolithic editor.js', () => { - // Load the monolithic editor.js to extract SectionManager - delete require.cache[require.resolve('/home/worsch/markitect_project/markitect/static/editor.js')]; - - try { - const editorModule = require('/home/worsch/markitect_project/markitect/static/editor.js'); - runner.expect(editorModule.SectionManager).toBeTruthy(); - // Set global for other tests - global.SectionManager = editorModule.SectionManager; - global.Section = editorModule.Section; - global.EditState = editorModule.EditState; - } catch (error) { - throw new Error(`Failed to load monolithic editor.js: ${error.message}`); - } - }); - - runner.it('should preserve SectionManager constructor functionality', () => { - const SectionManager = global.SectionManager; - - const manager = new SectionManager(); - runner.expect(manager).toBeInstanceOf(SectionManager); - runner.expect(manager.sections).toBeInstanceOf(Map); - }); - - runner.it('should preserve createSectionsFromMarkdown functionality', () => { - const SectionManager = global.SectionManager; - const manager = new SectionManager(); - - const testMarkdown = `# Heading 1\nContent 1\n\n## Heading 2\nContent 2`; - const sections = manager.createSectionsFromMarkdown(testMarkdown); - - runner.expect(Array.isArray(sections)).toBeTruthy(); - runner.expect(sections.length).toBe(2); - runner.expect(sections[0].currentMarkdown).toContain('Heading 1'); - runner.expect(sections[1].currentMarkdown).toContain('Heading 2'); - }); - - runner.it('should preserve section editing state management', () => { - const SectionManager = global.SectionManager; - const manager = new SectionManager(); - - const sections = manager.createSectionsFromMarkdown('# Test\nContent'); - const sectionId = sections[0].id; - - // Test start editing - runner.expect(manager.startEditing(sectionId)).toBeTruthy(); - const section = manager.sections.get(sectionId); - runner.expect(section.isEditing()).toBeTruthy(); - - // Test stop editing - section.stopEditing(); - runner.expect(section.isEditing()).toBeFalsy(); - }); - - runner.it('should preserve event system functionality', () => { - const SectionManager = global.SectionManager; - const manager = new SectionManager(); - - let eventFired = false; - let eventData = null; - - manager.on('test-event', (data) => { - eventFired = true; - eventData = data; - }); - - manager.emit('test-event', { test: 'data' }); - - runner.expect(eventFired).toBeTruthy(); - runner.expect(eventData).toEqual({ test: 'data' }); - }); - - runner.it('should preserve document status functionality', () => { - const SectionManager = global.SectionManager; - const manager = new SectionManager(); - - manager.createSectionsFromMarkdown('# Test\nContent'); - const status = manager.getDocumentStatus(); - - runner.expect(status).toHaveProperty('totalSections'); - runner.expect(status).toHaveProperty('editingSections'); - runner.expect(status.totalSections).toBe(1); - }); - - runner.it('should preserve getAllSections functionality', () => { - const SectionManager = global.SectionManager; - const manager = new SectionManager(); - - const testMarkdown = '# One\nContent\n\n# Two\nMore content'; - manager.createSectionsFromMarkdown(testMarkdown); - - const allSections = manager.getAllSections(); - runner.expect(Array.isArray(allSections)).toBeTruthy(); - runner.expect(allSections.length).toBe(2); - }); - - runner.it('should preserve section splitting functionality', () => { - const SectionManager = global.SectionManager; - const manager = new SectionManager(); - - const sections = manager.createSectionsFromMarkdown('# Original\nContent'); - const sectionId = sections[0].id; - - const newContent = '# Split 1\nContent 1\n\n# Split 2\nContent 2'; - const newSections = manager.handleSectionSplit(sectionId, newContent); - - runner.expect(Array.isArray(newSections)).toBeTruthy(); - runner.expect(newSections.length).toBe(2); - runner.expect(manager.sections.has(sectionId)).toBeFalsy(); // Original removed - }); -}); - -// Export API tests for use during extraction -const SECTION_MANAGER_API_TESTS = [ - (SectionManager) => { - const manager = new SectionManager(); - if (!manager.sections || !(manager.sections instanceof Map)) { - throw new Error('sections property missing or not a Map'); - } - }, - (SectionManager) => { - const manager = new SectionManager(); - if (typeof manager.createSectionsFromMarkdown !== 'function') { - throw new Error('createSectionsFromMarkdown method missing'); - } - }, - (SectionManager) => { - const manager = new SectionManager(); - if (typeof manager.startEditing !== 'function') { - throw new Error('startEditing method missing'); - } - }, - (SectionManager) => { - const manager = new SectionManager(); - if (typeof manager.stopEditing !== 'function') { - throw new Error('stopEditing method missing'); - } - } -]; - -module.exports = { - runner, - EXPECTED_SECTION_MANAGER_API, - SECTION_MANAGER_API_TESTS -}; - -// Run tests if called directly -if (require.main === module) { - console.log('๐Ÿงช Testing SectionManager Component Extraction'); - runner.run().then(() => { - console.log('โœ… SectionManager extraction tests completed'); - }); -} \ No newline at end of file diff --git a/markitect/static/js/tests/test.md b/markitect/static/js/tests/test.md deleted file mode 100644 index 239c58bf..00000000 --- a/markitect/static/js/tests/test.md +++ /dev/null @@ -1,6 +0,0 @@ -# Test Document - -This is a test document to check if UI controls appear in edit mode. - -## Section 1 -Some content here. diff --git a/markitect/static/js/tests/test_edit.html b/markitect/static/js/tests/test_edit.html deleted file mode 100644 index 813b65bf..00000000 --- a/markitect/static/js/tests/test_edit.html +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - Test Document - - - - - - - - - - - -
-

Test Document

-

This is a test document to check if UI controls appear in edit mode.

-

Section 1

-

Some content here.

-
-

-- html from markdown by MarkiTect on 2025-11-11 23:42:23 by worsch

-
- - - - - - - - - - - - - - - \ No newline at end of file diff --git a/markitect/static/js/widgets/base/UIWidget.js b/markitect/static/js/widgets/base/UIWidget.js deleted file mode 100644 index c889d0d0..00000000 --- a/markitect/static/js/widgets/base/UIWidget.js +++ /dev/null @@ -1,215 +0,0 @@ -/** - * UI Widget Base Class - * - * Extends Widget with DOM manipulation and visual functionality. - * Base for all widgets that render UI elements. - */ -import { Widget } from './Widget.js'; - -export class UIWidget extends Widget { - constructor(options = {}) { - super(options); - - // UI properties - this.element = null; - this.isVisible = false; - this.isRendered = false; - this.theme = options.theme || 'default'; - this.cssClasses = new Set(['markitect-widget']); - - // Animation support - this.animationDuration = options.animationDuration || 300; - this.enableAnimations = options.enableAnimations !== false; - } - - /** - * Render the widget to DOM (abstract method) - */ - async render() { - throw new Error('render() method must be implemented by subclass'); - } - - /** - * Show the widget - */ - async show(options = {}) { - if (!this.isRendered) { - await this.render(); - } - - if (this.isVisible) { - return this; - } - - this.isVisible = true; - - if (this.element) { - if (this.enableAnimations && !options.immediate) { - await this.animateShow(); - } else { - this.element.style.display = ''; - } - } - - this.emit('shown'); - return this; - } - - /** - * Hide the widget - */ - async hide(options = {}) { - if (!this.isVisible) { - return this; - } - - this.isVisible = false; - - if (this.element) { - if (this.enableAnimations && !options.immediate) { - await this.animateHide(); - } else { - this.element.style.display = 'none'; - } - } - - this.emit('hidden'); - return this; - } - - /** - * Toggle visibility - */ - async toggle(options = {}) { - return this.isVisible ? this.hide(options) : this.show(options); - } - - /** - * Show animation (override for custom animations) - */ - async animateShow() { - if (!this.element) return; - - return new Promise(resolve => { - this.element.style.transition = `opacity ${this.animationDuration}ms ease-in-out`; - this.element.style.opacity = '0'; - this.element.style.display = ''; - - // Force reflow - this.element.offsetHeight; - - this.element.style.opacity = '1'; - - setTimeout(() => { - this.element.style.transition = ''; - resolve(); - }, this.animationDuration); - }); - } - - /** - * Hide animation (override for custom animations) - */ - async animateHide() { - if (!this.element) return; - - return new Promise(resolve => { - this.element.style.transition = `opacity ${this.animationDuration}ms ease-in-out`; - this.element.style.opacity = '0'; - - setTimeout(() => { - this.element.style.display = 'none'; - this.element.style.transition = ''; - this.element.style.opacity = ''; - resolve(); - }, this.animationDuration); - }); - } - - /** - * CSS class management - */ - addClass(className) { - this.cssClasses.add(className); - if (this.element) { - this.element.classList.add(className); - } - return this; - } - - removeClass(className) { - this.cssClasses.delete(className); - if (this.element) { - this.element.classList.remove(className); - } - return this; - } - - hasClass(className) { - return this.cssClasses.has(className); - } - - /** - * Apply theme styling - */ - applyTheme(themeName) { - const oldTheme = this.theme; - this.theme = themeName; - - this.removeClass(`theme-${oldTheme}`); - this.addClass(`theme-${themeName}`); - - this.emit('theme-changed', { oldTheme, newTheme: themeName }); - return this; - } - - /** - * Find child element by selector - */ - findElement(selector) { - return this.element ? this.element.querySelector(selector) : null; - } - - /** - * Find all child elements by selector - */ - findElements(selector) { - return this.element ? this.element.querySelectorAll(selector) : []; - } - - /** - * Override destroy to clean up DOM - */ - async destroy() { - if (this.element && this.element.parentNode) { - this.element.parentNode.removeChild(this.element); - } - - this.element = null; - this.isRendered = false; - this.isVisible = false; - - await super.destroy(); - } - - /** - * Apply all CSS classes to element - */ - applyCSSClasses(element = this.element) { - if (element) { - element.className = Array.from(this.cssClasses).join(' '); - } - } - - /** - * Default configuration for UI widgets - */ - getDefaultConfig() { - return { - ...super.getDefaultConfig(), - theme: 'default', - animationDuration: 300, - enableAnimations: true - }; - } -} \ No newline at end of file diff --git a/markitect/static/js/widgets/base/Widget.js b/markitect/static/js/widgets/base/Widget.js deleted file mode 100644 index 1c284cf6..00000000 --- a/markitect/static/js/widgets/base/Widget.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Base Widget Class - * - * Foundation class for all Markitect UI widgets following the plugin architecture. - * Provides core functionality for event handling, state management, and lifecycle. - */ -export class Widget extends EventTarget { - constructor(options = {}) { - super(); - - // Core properties - this.id = options.id || `widget-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - this.container = options.container || document.body; - this.config = { ...this.getDefaultConfig(), ...options }; - - // State management - this.state = new Map(); - this.isInitialized = false; - this.isDestroyed = false; - - // Mixin support - this.mixins = []; - - // Lifecycle hooks - this.onInitialize = options.onInitialize || (() => {}); - this.onDestroy = options.onDestroy || (() => {}); - } - - /** - * Initialize the widget - */ - async initialize() { - if (this.isInitialized || this.isDestroyed) { - return this; - } - - try { - await this.onInitialize(this); - this.isInitialized = true; - this.emit('initialized'); - return this; - } catch (error) { - this.emit('error', { phase: 'initialize', error }); - throw error; - } - } - - /** - * Destroy the widget and clean up resources - */ - async destroy() { - if (this.isDestroyed) { - return; - } - - try { - await this.onDestroy(this); - this.isDestroyed = true; - this.emit('destroyed'); - } catch (error) { - this.emit('error', { phase: 'destroy', error }); - throw error; - } - } - - /** - * State management - */ - setState(key, value) { - const oldValue = this.state.get(key); - this.state.set(key, value); - this.emit('state-changed', { key, value, oldValue }); - } - - getState(key, defaultValue = null) { - return this.state.get(key) ?? defaultValue; - } - - /** - * Event emission wrapper - */ - emit(eventType, data = {}) { - const event = new CustomEvent(eventType, { - detail: { widget: this, ...data } - }); - this.dispatchEvent(event); - } - - /** - * Apply mixin functionality - */ - applyMixin(mixin) { - if (typeof mixin === 'object') { - Object.assign(this, mixin); - this.mixins.push(mixin); - } - return this; - } - - /** - * Default configuration (override in subclasses) - */ - getDefaultConfig() { - return {}; - } - - /** - * Utility method for creating DOM elements with styling - */ - createElement(tag, options = {}) { - const element = document.createElement(tag); - - if (options.className) { - element.className = options.className; - } - - if (options.textContent) { - element.textContent = options.textContent; - } - - if (options.innerHTML) { - element.innerHTML = options.innerHTML; - } - - if (options.style) { - if (typeof options.style === 'string') { - element.style.cssText = options.style; - } else { - Object.assign(element.style, options.style); - } - } - - if (options.attributes) { - Object.entries(options.attributes).forEach(([key, value]) => { - element.setAttribute(key, value); - }); - } - - return element; - } -} \ No newline at end of file diff --git a/markitect/static/js/widgets/navigation/DocumentNavigator.js b/markitect/static/js/widgets/navigation/DocumentNavigator.js deleted file mode 100644 index d25e058e..00000000 --- a/markitect/static/js/widgets/navigation/DocumentNavigator.js +++ /dev/null @@ -1,625 +0,0 @@ -/** - * DocumentNavigator Widget - * - * Substack-style floating document navigation widget that displays a hierarchical - * table of contents based on document headings. Supports smooth scrolling, - * scroll spy, expand/collapse, and responsive behavior. - */ -import { UIWidget } from '../base/UIWidget.js'; - -export class DocumentNavigator extends UIWidget { - constructor(options = {}) { - super(options); - - // Navigation state - this.isCollapsed = this.config.collapsed; - this.currentSection = null; - this.headings = []; - this.navigationTree = []; - - // Scroll spy state - this.scrollSpyEnabled = this.config.enableScrollSpy; - this.scrollThrottle = null; - - // Event bindings - this.boundScrollHandler = this.handleScroll.bind(this); - this.boundResizeHandler = this.handleResize.bind(this); - - // Initialize responsive behavior - this.mediaQuery = window.matchMedia('(max-width: 768px)'); - } - - getDefaultConfig() { - return { - ...super.getDefaultConfig(), - position: 'left', // 'left' or 'right' - collapsed: true, // Start collapsed - autoHide: true, // Hide on mobile - maxHeadingLevel: 3, // H1, H2, H3 - enableScrollSpy: true, // Highlight current section - smoothScroll: true, // Smooth scroll behavior - animationDuration: 300, // Animation timing - minHeadings: 2, // Min headings to show navigator - theme: 'default', // Theme support - - // Styling options - width: '280px', - collapsedWidth: '40px', - offset: { top: '80px', side: '20px' }, - - // Accessibility - enableKeyboard: true, - ariaLabel: 'Document Navigation' - }; - } - - async initialize() { - await super.initialize(); - - // Extract headings from container - this.extractHeadings(); - this.buildNavigationTree(); - - // Set up event listeners - if (this.scrollSpyEnabled) { - window.addEventListener('scroll', this.boundScrollHandler, { passive: true }); - } - - if (this.config.autoHide) { - window.addEventListener('resize', this.boundResizeHandler); - this.handleResize(); // Initial check - } - - return this; - } - - async render() { - if (this.isRendered) { - return this.element; - } - - // Check if we have enough headings - if (this.headings.length < this.config.minHeadings) { - this.isRendered = true; - return null; // Don't render if too few headings - } - - // Create main container - this.element = this.createElement('nav', { - className: 'document-navigator markitect-widget', - attributes: { - 'aria-label': this.config.ariaLabel, - 'role': 'navigation' - }, - style: this.getNavigatorStyle() - }); - - // Apply CSS classes - this.applyCSSClasses(); - this.addClass('theme-' + this.theme); - this.addClass('position-' + this.config.position); - - // Create toggle button (always visible) - this.createToggleButton(); - - // Create navigation list (hidden when collapsed) - this.createNavigationList(); - - // Set initial visibility state - if (this.isCollapsed) { - await this.collapse({ immediate: true }); - } else { - await this.expand({ immediate: true }); - } - - // Append to container - this.container.appendChild(this.element); - - // Initialize scroll spy - if (this.scrollSpyEnabled) { - this.updateCurrentSection(); - } - - this.isRendered = true; - this.emit('rendered'); - - return this.element; - } - - createToggleButton() { - this.toggleButton = this.createElement('button', { - className: 'navigator-toggle', - attributes: { - 'type': 'button', - 'aria-label': this.isCollapsed ? 'Expand navigation' : 'Collapse navigation', - 'aria-expanded': !this.isCollapsed - }, - innerHTML: this.getToggleIcon(), - style: this.getToggleStyle() - }); - - // Toggle on click - this.toggleButton.addEventListener('click', async () => { - await this.toggle(); - }); - - // Keyboard support - if (this.config.enableKeyboard) { - this.toggleButton.addEventListener('keydown', this.handleKeyboard.bind(this)); - } - - this.element.appendChild(this.toggleButton); - } - - createNavigationList() { - this.navigationList = this.createElement('div', { - className: 'navigator-list', - style: this.getListStyle() - }); - - if (this.headings.length === 0) { - this.createEmptyState(); - } else { - this.populateNavigationList(); - } - - this.element.appendChild(this.navigationList); - } - - createEmptyState() { - const emptyMessage = this.createElement('div', { - className: 'navigator-empty', - textContent: 'No headings found', - style: { - padding: '1rem', - textAlign: 'center', - color: '#666', - fontStyle: 'italic' - } - }); - - this.navigationList.appendChild(emptyMessage); - } - - populateNavigationList() { - // Create header - const header = this.createElement('div', { - className: 'navigator-header', - innerHTML: ` -

Contents

- - `, - style: { - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - padding: '1rem 1rem 0.5rem', - borderBottom: '1px solid #eee', - marginBottom: '0.5rem' - } - }); - - // Close button functionality - const closeButton = header.querySelector('.navigator-close'); - closeButton.addEventListener('click', async () => { - await this.collapse(); - }); - - this.navigationList.appendChild(header); - - // Create navigation items - const navContainer = this.createElement('div', { - className: 'navigator-items', - style: { - maxHeight: '70vh', - overflowY: 'auto', - padding: '0 0.5rem 1rem' - } - }); - - this.renderNavigationTree(navContainer, this.navigationTree); - this.navigationList.appendChild(navContainer); - } - - renderNavigationTree(container, items, level = 0) { - items.forEach(item => { - const navItem = this.createElement('div', { - className: `navigator-item level-${level}`, - style: { - marginLeft: `${level * 1}rem`, - marginBottom: '0.25rem' - } - }); - - // Create clickable link - const link = this.createElement('a', { - className: 'navigator-link', - textContent: item.text, - attributes: { - 'href': `#${item.id}`, - 'data-target': item.id, - 'data-level': item.level, - 'role': 'button', - 'tabindex': '0' - }, - style: { - display: 'block', - padding: '0.5rem 0.75rem', - textDecoration: 'none', - color: '#333', - borderRadius: '4px', - fontSize: level === 0 ? '0.9rem' : '0.8rem', - fontWeight: level === 0 ? '600' : '400', - transition: 'all 0.2s ease', - cursor: 'pointer' - } - }); - - // Hover effects - link.addEventListener('mouseenter', () => { - link.style.backgroundColor = '#f0f0f0'; - }); - - link.addEventListener('mouseleave', () => { - if (!link.classList.contains('active')) { - link.style.backgroundColor = ''; - } - }); - - // Click navigation - link.addEventListener('click', (e) => { - e.preventDefault(); - this.navigateToHeading(item.id); - }); - - navItem.appendChild(link); - - // Render children recursively - if (item.children && item.children.length > 0) { - this.renderNavigationTree(navItem, item.children, level + 1); - } - - container.appendChild(navItem); - }); - } - - extractHeadings() { - const headingSelectors = []; - for (let i = 1; i <= this.config.maxHeadingLevel; i++) { - headingSelectors.push(`h${i}`); - } - - const headingElements = this.container.querySelectorAll(headingSelectors.join(', ')); - - this.headings = Array.from(headingElements).map((heading, index) => { - // Ensure heading has an ID - if (!heading.id) { - heading.id = `heading-${index + 1}`; - } - - return { - element: heading, - id: heading.id, - text: heading.textContent.trim(), - level: parseInt(heading.tagName.substring(1)), - offset: heading.offsetTop - }; - }); - - return this.headings; - } - - buildNavigationTree() { - this.navigationTree = []; - const stack = []; - - this.headings.forEach(heading => { - const item = { - ...heading, - children: [] - }; - - // Find correct parent based on heading level - while (stack.length > 0 && stack[stack.length - 1].level >= heading.level) { - stack.pop(); - } - - if (stack.length === 0) { - // Top level item - this.navigationTree.push(item); - } else { - // Child item - stack[stack.length - 1].children.push(item); - } - - stack.push(item); - }); - - return this.navigationTree; - } - - async toggle(options = {}) { - return this.isCollapsed ? this.expand(options) : this.collapse(options); - } - - async expand(options = {}) { - if (!this.isCollapsed) { - return this; - } - - this.isCollapsed = false; - - if (this.toggleButton) { - this.toggleButton.setAttribute('aria-expanded', 'true'); - this.toggleButton.setAttribute('aria-label', 'Collapse navigation'); - this.toggleButton.innerHTML = this.getToggleIcon(); - } - - if (this.navigationList) { - if (this.enableAnimations && !options.immediate) { - await this.animateExpand(); - } else { - this.navigationList.style.display = ''; - this.element.style.width = this.config.width; - } - } - - this.emit('toggle', { expanded: true }); - return this; - } - - async collapse(options = {}) { - if (this.isCollapsed) { - return this; - } - - this.isCollapsed = true; - - if (this.toggleButton) { - this.toggleButton.setAttribute('aria-expanded', 'false'); - this.toggleButton.setAttribute('aria-label', 'Expand navigation'); - this.toggleButton.innerHTML = this.getToggleIcon(); - } - - if (this.navigationList) { - if (this.enableAnimations && !options.immediate) { - await this.animateCollapse(); - } else { - this.navigationList.style.display = 'none'; - this.element.style.width = this.config.collapsedWidth; - } - } - - this.emit('toggle', { expanded: false }); - return this; - } - - async animateExpand() { - return new Promise(resolve => { - this.navigationList.style.opacity = '0'; - this.navigationList.style.display = ''; - - // Animate width and opacity - this.element.style.transition = `width ${this.animationDuration}ms ease-in-out`; - this.navigationList.style.transition = `opacity ${this.animationDuration}ms ease-in-out`; - - // Force reflow - this.element.offsetWidth; - - this.element.style.width = this.config.width; - this.navigationList.style.opacity = '1'; - - setTimeout(() => { - this.element.style.transition = ''; - this.navigationList.style.transition = ''; - resolve(); - }, this.animationDuration); - }); - } - - async animateCollapse() { - return new Promise(resolve => { - this.element.style.transition = `width ${this.animationDuration}ms ease-in-out`; - this.navigationList.style.transition = `opacity ${this.animationDuration}ms ease-in-out`; - - this.navigationList.style.opacity = '0'; - this.element.style.width = this.config.collapsedWidth; - - setTimeout(() => { - this.navigationList.style.display = 'none'; - this.element.style.transition = ''; - this.navigationList.style.transition = ''; - resolve(); - }, this.animationDuration); - }); - } - - navigateToHeading(headingId) { - const targetElement = document.getElementById(headingId); - if (!targetElement) { - console.warn(`Heading with ID '${headingId}' not found`); - return; - } - - // Update active navigation item - this.setActiveItem(headingId); - - // Scroll to target - if (this.config.smoothScroll) { - targetElement.scrollIntoView({ - behavior: 'smooth', - block: 'start', - inline: 'nearest' - }); - } else { - targetElement.scrollIntoView(); - } - - // Emit navigation event - this.emit('navigate', { target: headingId, element: targetElement }); - - // Optionally collapse after navigation on mobile - if (this.mediaQuery.matches && this.config.autoHide) { - setTimeout(() => this.collapse(), 500); - } - } - - setActiveItem(headingId) { - // Remove previous active state - const previousActive = this.findElement('.navigator-link.active'); - if (previousActive) { - previousActive.classList.remove('active'); - previousActive.style.backgroundColor = ''; - } - - // Set new active state - const newActive = this.findElement(`[data-target="${headingId}"]`); - if (newActive) { - newActive.classList.add('active'); - newActive.style.backgroundColor = '#e3f2fd'; - newActive.style.color = '#1976d2'; - } - - this.currentSection = headingId; - } - - handleScroll() { - if (!this.scrollSpyEnabled || !this.isRendered) { - return; - } - - // Throttle scroll events - if (this.scrollThrottle) { - return; - } - - this.scrollThrottle = setTimeout(() => { - this.updateCurrentSection(); - this.scrollThrottle = null; - }, 100); - } - - updateCurrentSection() { - const scrollPosition = window.pageYOffset + 100; // Offset for header - let currentHeading = null; - - // Find the current heading based on scroll position - for (let i = this.headings.length - 1; i >= 0; i--) { - const heading = this.headings[i]; - if (heading.element.offsetTop <= scrollPosition) { - currentHeading = heading; - break; - } - } - - if (currentHeading && currentHeading.id !== this.currentSection) { - this.setActiveItem(currentHeading.id); - } - } - - getCurrentSection() { - return this.currentSection; - } - - handleResize() { - if (!this.config.autoHide) { - return; - } - - if (this.mediaQuery.matches) { - // Mobile: hide navigator - if (this.element) { - this.element.style.display = 'none'; - } - } else { - // Desktop: show navigator - if (this.element) { - this.element.style.display = ''; - } - } - } - - handleKeyboard(event) { - switch (event.key) { - case 'Enter': - case ' ': - event.preventDefault(); - this.toggle(); - break; - case 'Escape': - event.preventDefault(); - this.collapse(); - break; - } - } - - getNavigatorStyle() { - const baseStyle = { - position: 'fixed', - top: this.config.offset.top, - zIndex: '1000', - backgroundColor: 'rgba(255, 255, 255, 0.95)', - border: '1px solid #e1e5e9', - borderRadius: '8px', - boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)', - backdropFilter: 'blur(8px)', - width: this.isCollapsed ? this.config.collapsedWidth : this.config.width, - maxHeight: '80vh', - overflow: 'hidden', - transition: 'width 0.3s ease-in-out' - }; - - // Position-specific styling - if (this.config.position === 'left') { - baseStyle.left = this.config.offset.side; - } else { - baseStyle.right = this.config.offset.side; - } - - return baseStyle; - } - - getToggleStyle() { - return { - width: '100%', - height: this.config.collapsedWidth, - border: 'none', - backgroundColor: 'transparent', - cursor: 'pointer', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - fontSize: '16px', - color: '#666', - transition: 'color 0.2s ease' - }; - } - - getListStyle() { - return { - display: this.isCollapsed ? 'none' : '', - opacity: this.isCollapsed ? '0' : '1' - }; - } - - getToggleIcon() { - if (this.isCollapsed) { - return this.config.position === 'left' ? 'โ˜ฐ' : 'โ˜ฐ'; - } else { - return 'โœ•'; - } - } - - async destroy() { - // Remove event listeners - window.removeEventListener('scroll', this.boundScrollHandler); - window.removeEventListener('resize', this.boundResizeHandler); - - // Clear throttle - if (this.scrollThrottle) { - clearTimeout(this.scrollThrottle); - } - - await super.destroy(); - } -} \ No newline at end of file