/** * 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; }