diff --git a/markitect/clean_document_manager.py b/markitect/clean_document_manager.py index d9b71ad9..975bde44 100644 --- a/markitect/clean_document_manager.py +++ b/markitect/clean_document_manager.py @@ -1177,268 +1177,412 @@ class CleanDocumentManager: console.error("Scroll indicators failed to initialize:", error); }} - // Step 4: Initialize DocumentNavigator (lazy loading for all modes) - try {{ - const documentNavigator = {{ - navElement: null, - isExpanded: false, - isDragging: false, - dragOffset: {{ x: 0, y: 0 }}, - originalPosition: {{ top: '80px', left: '20px' }}, + // Step 4: Define abstract Control class for UI controls + const Control = {{ + // Abstract control properties + element: null, + isExpanded: false, + isDragging: false, + dragOffset: {{ x: 0, y: 0 }}, + originalPosition: {{ top: '80px', left: '20px' }}, - createControl: function() {{ - console.log("📋 Creating DocumentNavigator control for view mode..."); + // Configuration properties (to be overridden by subclasses) + config: {{ + icon: '?', + title: 'Control', + className: 'control', + defaultContent: 'Template only', + ariaLabel: 'Control', + position: 'w' // Default compass position: west (middle-left) + }}, - this.navElement = document.createElement('nav'); - this.navElement.className = 'document-navigator'; - this.navElement.innerHTML = ` - -
${{this.config.defaultContent}}
`; + }}, + + // Concrete methods (shared by all controls) + createControl: function() {{ + console.log(`🎛️ Creating ${{this.config.title}} control...`); + + this.element = document.createElement('div'); + this.element.className = this.config.className; + this.element.innerHTML = ` + +No headings found
'; - }} else {{ - let navHtml = ''; - headings.forEach((heading, index) => {{ - if (!heading.id) {{ - heading.id = `heading-${{index + 1}}`; - }} - const level = parseInt(heading.tagName.substring(1)); - const indent = (level - 1) * 1; - navHtml += ` - - ${{heading.textContent.trim()}} - - `; - }}); - content.innerHTML = navHtml; - }} - - // Style the content area to work with compact header - content.style.cssText = ` - padding: 0.5rem; - max-height: calc(80vh - 40px); - overflow-y: auto; - `; - - // Show panel - this.expand(); - }}, - - expand: function() {{ - this.isExpanded = true; - const panel = this.navElement.querySelector('.navigator-panel'); - const toggleBtn = this.navElement.querySelector('.navigator-toggle'); - this.navElement.style.width = '280px'; - panel.style.display = 'block'; - toggleBtn.style.display = 'none'; - }}, - - collapse: function() {{ - this.isExpanded = false; - const panel = this.navElement.querySelector('.navigator-panel'); - const toggleBtn = this.navElement.querySelector('.navigator-toggle'); - panel.style.display = 'none'; - this.navElement.style.width = '40px'; - toggleBtn.style.display = 'block'; - - // Reset position to original location - this.navElement.style.top = this.originalPosition.top; - this.navElement.style.left = this.originalPosition.left; - }}, - - setupDragHandlers: function(dragElement) {{ - dragElement.addEventListener('mousedown', (e) => {{ - this.isDragging = true; - const rect = this.navElement.getBoundingClientRect(); - this.dragOffset.x = e.clientX - rect.left; - this.dragOffset.y = e.clientY - rect.top; - - dragElement.style.cursor = 'grabbing'; - - e.preventDefault(); - }}); - - document.addEventListener('mousemove', (e) => {{ - if (!this.isDragging || !this.isExpanded) return; - - const newX = e.clientX - this.dragOffset.x; - const newY = e.clientY - this.dragOffset.y; - - // Keep within viewport bounds - const maxX = window.innerWidth - this.navElement.offsetWidth; - const maxY = window.innerHeight - this.navElement.offsetHeight; - - const boundedX = Math.max(0, Math.min(newX, maxX)); - const boundedY = Math.max(0, Math.min(newY, maxY)); - - this.navElement.style.left = boundedX + 'px'; - this.navElement.style.top = boundedY + 'px'; - }}); - - document.addEventListener('mouseup', () => {{ - if (this.isDragging) {{ - this.isDragging = false; - dragElement.style.cursor = 'grab'; - }} - }}); }} + }}, + + styleContent: function() {{ + const content = this.element.querySelector('.control-content'); + const expansion = this.getExpansionDirection(); + + // Style the content area based on expansion direction + let contentStyles = ` + padding: 0.5rem; + overflow-y: auto; + `; + + if (expansion.body === 'up') {{ + // Body expands upward (for bottom border positions) + contentStyles += ` + max-height: calc(80vh - 40px); + `; + content.parentElement.style.flexDirection = 'column-reverse'; + }} else {{ + // Body expands downward (default) + contentStyles += ` + max-height: calc(80vh - 40px); + `; + content.parentElement.style.flexDirection = 'column'; + }} + + content.style.cssText = contentStyles; + }}, + + expand: function() {{ + this.isExpanded = true; + const panel = this.element.querySelector('.control-panel'); + const toggleBtn = this.element.querySelector('.control-toggle'); + + // Get expansion direction based on compass position + const expansion = this.getExpansionDirection(); + + // Apply expansion styling based on direction + if (expansion.header === 'left') {{ + // Header expands to the left (for right border positions) + this.element.style.width = '280px'; + this.element.style.transformOrigin = 'top right'; + }} else {{ + // Header expands to the right (default) + this.element.style.width = '280px'; + this.element.style.transformOrigin = 'top left'; + }} + + panel.style.display = 'block'; + toggleBtn.style.display = 'none'; + + this.styleHeader(); + this.styleContent(); + }}, + + collapse: function() {{ + this.isExpanded = false; + const panel = this.element.querySelector('.control-panel'); + const toggleBtn = this.element.querySelector('.control-toggle'); + panel.style.display = 'none'; + this.element.style.width = '40px'; + toggleBtn.style.display = 'block'; + + // Reset position to original compass location + this.element.style.top = this.originalPosition.top; + this.element.style.right = this.originalPosition.right; + this.element.style.bottom = this.originalPosition.bottom; + this.element.style.left = this.originalPosition.left; + this.element.style.transform = this.originalPosition.transform; + }}, + + setupDragHandlers: function(dragElement) {{ + dragElement.addEventListener('mousedown', (e) => {{ + this.isDragging = true; + const rect = this.element.getBoundingClientRect(); + const iconRect = dragElement.getBoundingClientRect(); + + // Calculate offset relative to the icon position, not the element + this.dragOffset.x = e.clientX - rect.left; + this.dragOffset.y = iconRect.top - rect.top + (iconRect.height / 2); // Keep mouse at icon center + + dragElement.style.cursor = 'grabbing'; + + e.preventDefault(); + }}); + + document.addEventListener('mousemove', (e) => {{ + if (!this.isDragging || !this.isExpanded) return; + + const newX = e.clientX - this.dragOffset.x; + const newY = e.clientY - this.dragOffset.y; + + // Keep within viewport bounds + const maxX = window.innerWidth - this.element.offsetWidth; + const maxY = window.innerHeight - this.element.offsetHeight; + + const boundedX = Math.max(0, Math.min(newX, maxX)); + const boundedY = Math.max(0, Math.min(newY, maxY)); + + this.element.style.left = boundedX + 'px'; + this.element.style.top = boundedY + 'px'; + }}); + + document.addEventListener('mouseup', () => {{ + if (this.isDragging) {{ + this.isDragging = false; + dragElement.style.cursor = 'grab'; + }} + }}); + }} + }}; + + // Step 5: Initialize ContentsControl (new implementation based on Control class) + try {{ + const contentsControl = Object.create(Control); + + // Configure for contents navigation + contentsControl.config = {{ + icon: '☰', + title: 'Contents', + className: 'contents-control', + defaultContent: 'No headings found', + ariaLabel: 'Document Navigation', + position: 'wnw' // West-north-west positioning }}; - // Initialize the DocumentNavigator control - documentNavigator.createControl(); + // Override buildContent method for navigation functionality + contentsControl.buildContent = function() {{ + const content = this.element.querySelector('.control-content'); + + // Build navigation content from current DOM + const allHeadings = document.querySelectorAll('h1, h2, h3'); + // Filter out headings that contain "Contents" or similar navigation-related text + const headings = Array.from(allHeadings).filter(heading => {{ + const text = heading.textContent.trim().toLowerCase(); + return !text.includes('contents') && !text.includes('table of contents') && !text.includes('navigation'); + }}); + console.log("📋 Found headings for navigation:", headings.length); + + if (headings.length === 0) {{ + content.innerHTML = 'No headings found
'; + }} else {{ + let navHtml = ''; + headings.forEach((heading, index) => {{ + if (!heading.id) {{ + heading.id = `heading-${{index + 1}}`; + }} + const level = parseInt(heading.tagName.substring(1)); + const indent = (level - 1) * 1; + navHtml += ` + + ${{heading.textContent.trim()}} + + `; + }}); + content.innerHTML = navHtml; + }} + + // Show panel + this.expand(); + }}; + + // Initialize the ContentsControl + contentsControl.createControl(); // Make globally available for mobile collapse - window.documentNavigator = documentNavigator; + window.contentsControl = contentsControl; }} catch (error) {{ - console.error("DocumentNavigator failed to initialize:", error); + console.error("ContentsControl failed to initialize:", error); }} + }}); // Handle CDN loading errors @@ -1500,49 +1644,133 @@ document.addEventListener('DOMContentLoaded', function() { // Create document controls documentControls.create(); - // Create DocumentNavigator for edit mode (lazy loading) - const documentNavigator = { - navElement: null, + // Define abstract Control class for UI controls (same as viewing mode) + const Control = { + // Abstract control properties + element: null, isExpanded: false, isDragging: false, dragOffset: { x: 0, y: 0 }, originalPosition: { top: '80px', left: '20px' }, - createControl: function() { - console.log("📋 Creating DocumentNavigator control for edit mode..."); + // Configuration properties (to be overridden by subclasses) + config: { + icon: '?', + title: 'Control', + className: 'control', + defaultContent: 'Template only', + ariaLabel: 'Control', + position: 'w' // Default compass position: west (middle-left) + }, - this.navElement = document.createElement('nav'); - this.navElement.className = 'document-navigator edit-mode'; - this.navElement.innerHTML = ` - -${this.config.defaultContent}
`; + }, + + // Concrete methods (shared by all controls) + createControl: function() { + console.log(`🎛️ Creating ${this.config.title} control...`); + + this.element = document.createElement('div'); + this.element.className = this.config.className; + this.element.innerHTML = ` + +