diff --git a/TODO.md b/TODO.md index b7089e3a..b1e5ed8e 100644 --- a/TODO.md +++ b/TODO.md @@ -12,7 +12,18 @@ The structure organizes **future tasks** by their impact, just as a changelog or This section is for tasks currently being discussed with or worked on by the coding assistant. These are the ephemeral, flow-of-thought tasks. -**📊 STATUS UPDATE (2025-11-02)**: Systematic JavaScript functionality recovery using TDD methodology has made excellent progress. **5 major features** have been successfully implemented and tested: +**🏗️ MAJOR ARCHITECTURE REFACTORING (2025-11-03)**: Critical architecture issues identified requiring comprehensive JavaScript refactoring: + +**PROBLEMS IDENTIFIED**: +1. **Monolithic Architecture**: Single 5,188-line `editor.js` file violates separation of concerns +2. **Server-Side Debug Generation**: Debug messages captured during Python HTML generation instead of client-side interaction +3. **Architectural Boundary Violations**: JavaScript editing infrastructure affecting Python md-render code +4. **Tight Coupling**: UI components interdependent and untestable independently +5. **Generic Editor Compromise**: Debug system entangled with rendering instead of being purely client-side + +**SOLUTION**: Modular JavaScript Architecture with component separation and proper client-side debugging. + +**📊 PREVIOUS STATUS (2025-11-02)**: Systematic JavaScript functionality recovery using TDD methodology had made excellent progress. **5 major features** were successfully implemented and tested: 1. **Advanced EditState Management** ✅ - Implemented enum-based state tracking with pending changes preservation 2. **Keyboard Shortcuts** ✅ - Added Ctrl+Enter (accept) and Escape (cancel) functionality @@ -22,17 +33,73 @@ This section is for tasks currently being discussed with or worked on by the cod All implementations include comprehensive TDD test suites and are fully integrated into the existing codebase. The recovery approach has proven highly effective for restoring sophisticated lost functionality. +## 🏗️ JAVASCRIPT ARCHITECTURE REFACTORING ROADMAP + +### **Phase 1: Preparation & Backup (CRITICAL)** +* 🚧 Update TODO.md with comprehensive refactoring plan - IN PROGRESS +* ⏳ Commit current monolithic state for rollback safety - PENDING +* ⏳ Create modular directory structure `markitect/static/js/` - PENDING +* ⏳ Set up component template files with proper exports/imports - PENDING + +### **Phase 2: Core System Extraction (HIGH)** +* ⏳ Extract SectionManager to `core/section-manager.js` - PENDING +* ⏳ Extract EventSystem to `core/event-system.js` - PENDING +* ⏳ Create EditorCore orchestrator in `core/editor-core.js` - PENDING + +### **Phase 3: Component Separation (HIGH)** +* ⏳ Document Controls → `components/document-controls.js` - PENDING +* ⏳ Status Panel → `components/status-panel.js` - PENDING +* ⏳ Debug Panel → `components/debug-panel.js` (pure client-side) - PENDING +* ⏳ Floating Menu → `components/floating-menu.js` - PENDING +* ⏳ Text/Image Editors → separate component files - PENDING + +### **Phase 4: Testing Infrastructure (MEDIUM)** +* ⏳ Standalone test runner that doesn't require md-render - PENDING +* ⏳ Component unit tests for individual functionality - PENDING +* ⏳ Integration tests for component interaction - PENDING +* ⏳ Browser-based test runner for direct UI testing - PENDING + +### **Phase 5: Integration & Cleanup (MEDIUM)** +* ⏳ Update HTML template to load modular components - PENDING +* ⏳ Remove monolithic editor.js - PENDING +* ⏳ Ensure Python code unchanged - no md-render modifications - PENDING +* ⏳ Validate all functionality works with new architecture - PENDING + +### **Directory Structure Plan:** +``` +markitect/static/js/ +├── core/ +│ ├── editor-core.js # Main editor initialization +│ ├── section-manager.js # Section state management +│ └── event-system.js # Event handling system +├── components/ +│ ├── document-controls.js # Document controls panel +│ ├── status-panel.js # Status display component +│ ├── debug-panel.js # Debug panel (client-side only) +│ ├── floating-menu.js # Generic floating menu system +│ ├── text-editor.js # Text section editor +│ └── image-editor.js # Image section editor +├── utils/ +│ ├── dom-utils.js # DOM manipulation utilities +│ └── positioning.js # Layout positioning logic +└── tests/ + ├── test-runner.js # Standalone test framework + ├── component-tests.js # UI component tests + └── integration-tests.js # Full workflow tests +``` + +### **PREVIOUS COMPLETED FEATURES (Now requiring refactoring):** * **To Add:** * ✅ Advanced state management with EditState enum and pending changes (CRITICAL) - COMPLETED * ✅ Keyboard shortcuts (Ctrl+Enter accept, Escape cancel) (CRITICAL) - COMPLETED * ✅ Section splitting functionality for dynamic heading detection (HIGH) - COMPLETED * ✅ Real-time status tracking with periodic updates (HIGH) - COMPLETED * ✅ Intelligent save filename generation with 4-method fallback (MEDIUM) - COMPLETED - * 🚧 Professional message system with color-coded positioning (MEDIUM) - IN PROGRESS - * Multiple concurrent editing sessions support (MEDIUM) - * Enhanced DOM event system with 6 event types (LOW) - * Automatic section type detection (heading, code, list, etc) (LOW) - * Sophisticated section ID generation with hash-based algorithm (LOW) + * 🔄 Professional message system with color-coded positioning (MEDIUM) - NEEDS REFACTORING + * 🔄 Multiple concurrent editing sessions support (MEDIUM) - NEEDS REFACTORING + * 🔄 Enhanced DOM event system with 6 event types (LOW) - NEEDS REFACTORING + * 🔄 Automatic section type detection (heading, code, list, etc) (LOW) - NEEDS REFACTORING + * 🔄 Sophisticated section ID generation with hash-based algorithm (LOW) - NEEDS REFACTORING * **To Fix:** * Comprehensive status reporting dialog with detailed stats (HIGH) diff --git a/markitect/static/editor.js b/markitect/static/editor.js index 02253a7d..a724c4c8 100644 --- a/markitect/static/editor.js +++ b/markitect/static/editor.js @@ -17,12 +17,420 @@ const EditState = Object.freeze({ 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 + // No debugging output to console break; case 'console': console.log(prefix, message); @@ -36,6 +444,30 @@ function debug(message, category = 'INFO') { } } +// 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({ @@ -868,18 +1300,25 @@ class SectionManager { } 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()) { - console.log('Section already in editing state:', sectionId); + 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; } @@ -1301,6 +1740,8 @@ class DOMRenderer { 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 = []; @@ -1338,6 +1779,7 @@ class DOMRenderer { 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) => { @@ -1412,19 +1854,26 @@ class DOMRenderer { debug('24: All section elements added to container', 'RENDER'); - // Enhanced DOM Event System - Setup all 6 event types with delegation - this.container.addEventListener('click', this.handleSectionClick); - this.container.addEventListener('mouseenter', this.handleSectionHover, true); - this.container.addEventListener('mouseleave', this.handleSectionHover, true); - this.container.addEventListener('keydown', this.handleKeydown); - this.container.addEventListener('dragstart', this.handleDragStart); - this.container.addEventListener('dragover', this.handleDragOver); - this.container.addEventListener('drop', this.handleDrop); - this.container.addEventListener('focusin', this.handleFocus); - this.container.addEventListener('focusout', this.handleFocus); - this.container.addEventListener('contextmenu', this.handleContextMenu); + // 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); - debug('25: Enhanced event listeners attached - container content length: ' + this.container.innerHTML.length, 'RENDER'); + 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) { @@ -1446,20 +1895,20 @@ class DOMRenderer { } handleSectionClick(event) { - debug('CLICK: Click detected on target: ' + event.target.tagName + ' ' + (event.target.className || ''), 'CLICK'); + 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('CLICK: Ignoring click on form element', 'CLICK'); + debug('handleSectionClick: Ignoring click on form element', 'CLICK'); return; } const sectionElement = event.target.closest('.ui-edit-section'); - debug('CLICK: Found section element: ' + (sectionElement ? sectionElement.getAttribute('data-section-id') : 'null'), 'CLICK'); + debug('handleSectionClick: Found section element: ' + (sectionElement ? sectionElement.getAttribute('data-section-id') : 'null'), 'CLICK'); if (!sectionElement) return; const sectionId = sectionElement.getAttribute('data-section-id'); - debug('CLICK: Section ID: ' + sectionId, 'CLICK'); + debug('handleSectionClick: Section ID: ' + sectionId, 'CLICK'); if (!sectionId) return; // Track the click event @@ -1472,15 +1921,21 @@ class DOMRenderer { // 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()) { - console.log('Section already being edited:', sectionId); + debug('handleSectionClick: Section already being edited: ' + sectionId, 'CLICK'); return; } + debug('handleSectionClick: About to start editing for section: ' + sectionId, 'CLICK'); + try { - console.log('Starting edit for section:', sectionId); + 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); } } @@ -1745,10 +2200,15 @@ class DOMRenderer { } 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(); @@ -1758,50 +2218,84 @@ class DOMRenderer { return; } - const editorContainer = document.createElement('div'); - editorContainer.className = 'ui-edit-editor-container'; + // 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: 100px; + min-height: 200px; + width: 100%; font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; font-size: 14px; line-height: 1.5; padding: 12px; - border: 2px solid #007bff; - border-radius: 6px; - resize: vertical; + 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; - justify-content: flex-end; - margin-top: 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); - editorContainer.appendChild(textarea); - editorContainer.appendChild(controls); - - element.innerHTML = ''; - element.appendChild(editorContainer); + // 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); @@ -1814,6 +2308,15 @@ class DOMRenderer { 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 @@ -1835,17 +2338,15 @@ class DOMRenderer { hasChanges: false }; - const editorContainer = document.createElement('div'); - editorContainer.className = 'ui-edit-image-editor-container'; - editorContainer.style.cssText = ` + // 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: 12px; - margin-top: 12px; - padding: 16px; - background: #f8f9fa; - border-radius: 8px; - border: 2px solid #007bff; + gap: 15px; + flex: 1; + min-width: 0; `; // Parse markdown to extract image info @@ -1856,24 +2357,26 @@ class DOMRenderer { stagingState.currentImageSrc = imageSrc; } - // Image preview with drop zone + // Image preview with drop zone for floating menu const imagePreview = document.createElement('div'); imagePreview.className = 'ui-edit-image-preview'; imagePreview.style.cssText = ` - max-width: 100%; + width: 100%; + height: 180px; text-align: center; background: white; - padding: 16px; - border-radius: 6px; + padding: 12px; + border-radius: 8px; border: 2px dashed #007bff; transition: all 0.3s ease; cursor: pointer; - min-height: 200px; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; + box-sizing: border-box; + overflow: hidden; `; // Function to update image preview @@ -1993,24 +2496,46 @@ class DOMRenderer { } }); - // Alt text editor + // Alt text editor for floating menu const altTextContainer = document.createElement('div'); - altTextContainer.style.cssText = `margin-bottom: 16px;`; + 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:'; - altTextLabel.style.cssText = `display: block; margin-bottom: 4px; font-weight: bold;`; + 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: 8px; - border: 1px solid #ccc; - border-radius: 4px; + 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; @@ -2024,18 +2549,19 @@ class DOMRenderer { altTextContainer.appendChild(altTextLabel); altTextContainer.appendChild(altTextInput); - // Change indicator + // 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: 4px; + border-radius: 6px; color: #856404; - font-size: 14px; + font-size: 12px; text-align: center; display: none; + font-weight: 500; `; changeIndicator.textContent = '⚠️ You have unsaved changes'; @@ -2047,14 +2573,17 @@ class DOMRenderer { } }; - // Standard editor controls + // Responsive controls container with alt text const editorControls = document.createElement('div'); editorControls.className = 'ui-edit-controls'; editorControls.style.cssText = ` display: flex; - gap: 8px; - justify-content: flex-end; - margin-top: 12px; + 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) => { @@ -2132,22 +2661,51 @@ class DOMRenderer { 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'; - editorControls.appendChild(acceptBtn); - editorControls.appendChild(cancelBtn); - editorControls.appendChild(resetBtn); + controls.appendChild(acceptBtn); + controls.appendChild(cancelBtn); + controls.appendChild(resetBtn); - // Assemble the editor - editorContainer.appendChild(imagePreview); - editorContainer.appendChild(altTextContainer); - editorContainer.appendChild(changeIndicator); - editorContainer.appendChild(editorControls); - editorContainer.appendChild(fileInput); + // Create unified floating menu using new component + this.currentFloatingMenu = new FloatingMenu(sectionId, 'image', this); + const floatingMenu = this.currentFloatingMenu.show(editorContent, controls); + if (!floatingMenu) return; - element.appendChild(editorContainer); altTextInput.focus(); this.editingSections.add(sectionId); } @@ -2207,23 +2765,42 @@ class DOMRenderer { } 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); } } @@ -2295,7 +2872,14 @@ class DOMRenderer { } getCurrentEditingSectionId(button) { - const editorContainer = button.closest('.ui-edit-editor-container, .ui-edit-image-editor-container'); + // 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; @@ -2304,34 +2888,94 @@ class DOMRenderer { hideEditor(sectionId) { const element = this.findSectionElement(sectionId); - if (!element) return; - // Remove any editor UI containers from the DOM - const textEditorContainer = element.querySelector('.ui-edit-editor-container'); - if (textEditorContainer) { - textEditorContainer.remove(); + // Remove floating menu if it exists + const floatingMenu = document.querySelector(`.ui-edit-floating-menu[data-section-id="${sectionId}"]`); + if (floatingMenu) { + floatingMenu.remove(); } - const imageEditorContainer = element.querySelector('.ui-edit-image-editor-container'); - if (imageEditorContainer) { - imageEditorContainer.remove(); - } + // Remove element highlighting + if (element) { + element.style.outline = ''; + element.style.outlineOffset = ''; + element.style.backgroundColor = ''; - const section = this.sectionManager.sections.get(sectionId); - if (section) { - this.updateSectionContent(sectionId, section.currentMarkdown); + // 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) { @@ -3012,6 +3656,13 @@ class MarkitectCleanEditor { 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; @@ -3178,12 +3829,48 @@ class MarkitectCleanEditor { 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); @@ -3191,6 +3878,7 @@ class MarkitectCleanEditor { 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; @@ -4112,6 +4800,122 @@ class MarkitectCleanEditor { 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(); }