/** * 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; // 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: 16px; min-width: 300px; `; // Position the menu const rect = targetElement.getBoundingClientRect(); this.element.style.left = `${rect.left}px`; this.element.style.top = `${rect.bottom + 10}px`; // Add content if (contentElement) { this.element.appendChild(contentElement); } if (controlsElement) { this.element.appendChild(controlsElement); } // Add close button const closeButton = document.createElement('button'); closeButton.textContent = '×'; closeButton.style.cssText = ` position: absolute; top: 8px; right: 8px; background: none; border: none; font-size: 18px; cursor: pointer; color: #666; `; closeButton.addEventListener('click', () => 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; // 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 if (section.isImage()) { element.innerHTML = section.currentMarkdown; } else { // Simple markdown rendering (can be enhanced later) 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(/\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'); // 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: ' + 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); } } /** * 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'; editorContent.style.cssText = ` display: flex; flex-direction: column; gap: 12px; flex: 1; min-width: 0; `; // 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; `; // Create controls const controls = document.createElement('div'); controls.style.cssText = ` display: flex; gap: 8px; justify-content: flex-end; `; const acceptButton = document.createElement('button'); acceptButton.textContent = 'Accept'; acceptButton.style.cssText = ` background: #28a745; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; `; const cancelButton = document.createElement('button'); cancelButton.textContent = 'Cancel'; cancelButton.style.cssText = ` background: #dc3545; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; `; controls.appendChild(acceptButton); controls.appendChild(cancelButton); editorContent.appendChild(textarea); 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(); }); cancelButton.addEventListener('click', () => { this.sectionManager.cancelChanges(sectionId); floatingMenu.hide(); }); // Auto-focus textarea setTimeout(() => textarea.focus(), 100); } /** * Show editor for image sections */ showImageEditor(sectionId, section) { debug('showImageEditor: called for image section: ' + sectionId, 'EDITOR'); const editorContent = document.createElement('div'); editorContent.innerHTML = `
Image Editor
Edit the markdown for this image:
`; const floatingMenu = new FloatingMenu(sectionId, 'image', this); this.currentFloatingMenu = floatingMenu; this.editingSections.add(sectionId); floatingMenu.show(editorContent); // Add event listeners const textarea = editorContent.querySelector('textarea'); const acceptBtn = editorContent.querySelector('#accept-image'); const cancelBtn = editorContent.querySelector('#cancel-image'); acceptBtn.addEventListener('click', () => { this.sectionManager.updateContent(sectionId, textarea.value); this.sectionManager.acceptChanges(sectionId); floatingMenu.hide(); }); cancelBtn.addEventListener('click', () => { this.sectionManager.cancelChanges(sectionId); floatingMenu.hide(); }); } /** * 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; }