From 3264517c91c9c5654edbab90af8322c5a48793ea Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 14 Nov 2025 23:37:17 +0100 Subject: [PATCH] refactor: eliminate duplicate control files and consolidate to capabilities/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed duplicate control files from testdrive-jsui/static/js/controls/ - Removed duplicate control files from markitect/static/js/controls/ - Updated all references to point to capabilities/testdrive-jsui/js/controls/ - Fixed relative paths in test files and templates - Consolidated to single source of truth in capabilities directory - Updated plugin configuration and documentation references This eliminates confusion and ensures all systems use the most recent control implementations from the capabilities directory. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../testdrive-jsui/tests/test_complete.html | 10 +- .../tests/test_guardrail_js.html | 4 +- .../tests/test_integration.html | 4 +- markitect/plugins/testdrive_jsui.py | 10 +- .../static/js/controls/contents-control.js | 336 ------- markitect/static/js/controls/control-base.js | 631 ------------- markitect/static/js/controls/debug-control.js | 52 -- markitect/static/js/controls/edit-control.js | 568 ------------ .../static/js/controls/status-control.js | 368 -------- markitect/static/js/tests/test_edit.html | 4 +- markitect/templates/document.html | 10 +- markitect/templates/edit-mode-fixed.html | 10 +- testdrive-jsui/README.md | 2 +- .../static/js/controls/contents-control.js | 287 ------ .../static/js/controls/control-base.js | 847 ------------------ .../static/js/controls/debug-control.js | 483 ---------- .../static/js/controls/edit-control.js | 573 ------------ .../static/js/controls/status-control.js | 371 -------- testdrive-jsui/static/js/tests/test_edit.html | 4 +- testdrive-jsui/test.html | 10 +- 20 files changed, 34 insertions(+), 4550 deletions(-) delete mode 100644 markitect/static/js/controls/contents-control.js delete mode 100644 markitect/static/js/controls/control-base.js delete mode 100644 markitect/static/js/controls/debug-control.js delete mode 100644 markitect/static/js/controls/edit-control.js delete mode 100644 markitect/static/js/controls/status-control.js delete mode 100644 testdrive-jsui/static/js/controls/contents-control.js delete mode 100644 testdrive-jsui/static/js/controls/control-base.js delete mode 100644 testdrive-jsui/static/js/controls/debug-control.js delete mode 100644 testdrive-jsui/static/js/controls/edit-control.js delete mode 100644 testdrive-jsui/static/js/controls/status-control.js diff --git a/capabilities/testdrive-jsui/tests/test_complete.html b/capabilities/testdrive-jsui/tests/test_complete.html index a7431280..dd154e72 100644 --- a/capabilities/testdrive-jsui/tests/test_complete.html +++ b/capabilities/testdrive-jsui/tests/test_complete.html @@ -170,11 +170,11 @@ - - - - - + + + + + diff --git a/capabilities/testdrive-jsui/tests/test_guardrail_js.html b/capabilities/testdrive-jsui/tests/test_guardrail_js.html index 357851d9..8122916c 100644 --- a/capabilities/testdrive-jsui/tests/test_guardrail_js.html +++ b/capabilities/testdrive-jsui/tests/test_guardrail_js.html @@ -87,10 +87,10 @@ function testFunction() { - + - + diff --git a/capabilities/testdrive-jsui/tests/test_integration.html b/capabilities/testdrive-jsui/tests/test_integration.html index 8603a47a..050c14fb 100644 --- a/capabilities/testdrive-jsui/tests/test_integration.html +++ b/capabilities/testdrive-jsui/tests/test_integration.html @@ -195,8 +195,8 @@ - - + + diff --git a/markitect/plugins/testdrive_jsui.py b/markitect/plugins/testdrive_jsui.py index 21465c94..7017418a 100644 --- a/markitect/plugins/testdrive_jsui.py +++ b/markitect/plugins/testdrive_jsui.py @@ -43,11 +43,11 @@ class TestDriveJSUIEngine(RenderingEnginePlugin): "static/js/core/section-manager.js", "static/js/components/debug-panel.js", "static/js/components/dom-renderer.js", - "static/js/controls/control-base.js", - "static/js/controls/contents-control.js", - "static/js/controls/status-control.js", - "static/js/controls/debug-control.js", - "static/js/controls/edit-control.js", + "../capabilities/testdrive-jsui/js/controls/control-base.js", + "../capabilities/testdrive-jsui/js/controls/contents-control.js", + "../capabilities/testdrive-jsui/js/controls/status-control.js", + "../capabilities/testdrive-jsui/js/controls/debug-control.js", + "../capabilities/testdrive-jsui/js/controls/edit-control.js", "static/js/config-loader.js", "static/js/main-updated.js" ], diff --git a/markitect/static/js/controls/contents-control.js b/markitect/static/js/controls/contents-control.js deleted file mode 100644 index b4351052..00000000 --- a/markitect/static/js/controls/contents-control.js +++ /dev/null @@ -1,336 +0,0 @@ -/** - * ContentsControl - Table of Contents Display Control - * - * Provides an interactive table of contents for document navigation. - * Extracts headings from the document and displays them in a hierarchical - * structure with clickable links for quick navigation. - * - * Features: - * - Automatic heading extraction from document - * - Hierarchical display with proper indentation - * - Clickable navigation links with smooth scrolling - * - Real-time updates when document structure changes - * - Collapsible sections for better organization - * - Search functionality within the table of contents - * - * Dependencies: - * - ControlBase (base control functionality) - */ - -/** - * ContentsControl - Interactive table of contents control - * - * This control scans the document for headings (h1-h6) and presents them - * in a navigable tree structure. Users can click on any heading to jump - * directly to that section with smooth scrolling. - */ -class ContentsControl extends ControlBase { - constructor() { - super(); - - // Configure for contents functionality - this.config = { - icon: 'πŸ“‹', - title: 'Contents', - className: 'contents-control', - defaultContent: 'Loading table of contents...', - ariaLabel: 'Table of Contents Control', - position: 'w' // West positioning - }; - - // Contents-specific state - this.headings = []; - this.lastScanTime = null; - this.updateInterval = null; - this.searchQuery = ''; - } - - /** - * Extract all headings from the document - * Creates a hierarchical structure of the document's heading elements - */ - extractHeadings() { - return this.safeOperation(() => { - const headingSelectors = 'h1, h2, h3, h4, h5, h6'; - const headingElements = document.querySelectorAll(headingSelectors); - const extractedHeadings = []; - - headingElements.forEach((heading, index) => { - const level = parseInt(heading.tagName.charAt(1)); - const text = heading.textContent.trim(); - - // Generate or use existing ID for anchor links - let id = heading.id; - if (!id) { - id = text.toLowerCase() - .replace(/[^\w\s-]/g, '') - .replace(/\s+/g, '-') - .substring(0, 50); - - // Ensure uniqueness - let counter = 1; - let uniqueId = id; - while (document.getElementById(uniqueId)) { - uniqueId = `${id}-${counter}`; - counter++; - } - - heading.id = uniqueId; - id = uniqueId; - } - - extractedHeadings.push({ - id, - text, - level, - element: heading, - index - }); - }); - - this.headings = extractedHeadings; - this.lastScanTime = Date.now(); - return extractedHeadings; - - }, [], 'extractHeadings'); - } - - /** - * Filter headings based on search query - */ - filterHeadings(headings, query) { - if (!query || query.trim() === '') { - return headings; - } - - const normalizedQuery = query.toLowerCase().trim(); - return headings.filter(heading => - heading.text.toLowerCase().includes(normalizedQuery) - ); - } - - /** - * Generate HTML for the table of contents - */ - generateContentsHTML(headings = null) { - return this.safeOperation(() => { - const displayHeadings = headings || this.headings; - - if (displayHeadings.length === 0) { - return ` -
-

No headings found in document

- -
- `; - } - - const searchHTML = ` -
- -
- `; - - const contentsHTML = displayHeadings.map(heading => { - const indentLevel = Math.max(0, heading.level - 1); - const indentPx = indentLevel * 15; - - return ` - - `; - }).join(''); - - return ` -
- ${searchHTML} -
-
- Found ${displayHeadings.length} heading${displayHeadings.length !== 1 ? 's' : ''} -
- ${contentsHTML} -
-
- -
-
- `; - - }, '

Error generating contents

', 'generateContentsHTML'); - } - - /** - * Navigate to a specific heading with smooth scrolling - */ - navigateToHeading(headingId) { - return this.safeOperation(() => { - const targetElement = document.getElementById(headingId); - if (targetElement) { - targetElement.scrollIntoView({ - behavior: 'smooth', - block: 'start' - }); - - // Highlight the target temporarily - const originalStyle = targetElement.style.backgroundColor; - targetElement.style.backgroundColor = '#fff3cd'; - targetElement.style.transition = 'background-color 0.3s ease'; - - setTimeout(() => { - targetElement.style.backgroundColor = originalStyle; - setTimeout(() => { - targetElement.style.transition = ''; - }, 300); - }, 1500); - - return true; - } - return false; - }, false, 'navigateToHeading'); - } - - /** - * Handle search input - */ - handleSearch(query) { - this.searchQuery = query; - const filteredHeadings = this.filterHeadings(this.headings, query); - this.updateContentsDisplay(filteredHeadings); - } - - /** - * Update the contents display with new headings - */ - updateContentsDisplay(headings) { - return this.safeOperation(() => { - const content = this.element?.querySelector('.control-content'); - if (content) { - content.innerHTML = this.generateContentsHTML(headings); - } - }, null, 'updateContentsDisplay'); - } - - /** - * Refresh the contents by re-scanning the document - */ - refreshContents() { - return this.safeOperation(() => { - this.extractHeadings(); - const filteredHeadings = this.filterHeadings(this.headings, this.searchQuery); - this.updateContentsDisplay(filteredHeadings); - - // Show success feedback - const refreshBtn = this.element?.querySelector('button'); - if (refreshBtn) { - const originalText = refreshBtn.innerHTML; - refreshBtn.innerHTML = 'βœ… Updated'; - refreshBtn.style.background = '#28a745'; - - setTimeout(() => { - refreshBtn.innerHTML = originalText; - refreshBtn.style.background = '#28a745'; - }, 1000); - } - }, null, 'refreshContents'); - } - - /** - * Build the control content - * Override of base class method to provide contents-specific functionality - */ - buildContent() { - return this.safeOperation(() => { - // Extract headings on first build - this.extractHeadings(); - - // Generate and set content - const content = this.element?.querySelector('.control-content'); - if (content) { - content.innerHTML = this.generateContentsHTML(); - - // Store reference to this control for onclick handlers - this.element.contentsControl = this; - } - - // Set up auto-refresh for dynamic content - if (this.updateInterval) { - clearInterval(this.updateInterval); - } - - this.updateInterval = setInterval(() => { - const currentHeadingCount = document.querySelectorAll('h1, h2, h3, h4, h5, h6').length; - if (currentHeadingCount !== this.headings.length) { - this.refreshContents(); - } - }, 5000); // Check every 5 seconds - - }, null, 'buildContent'); - } - - /** - * Get statistics about the document structure - */ - getDocumentStats() { - return this.safeOperation(() => { - const stats = { - totalHeadings: this.headings.length, - byLevel: {}, - deepestLevel: 1, - structure: 'flat' - }; - - // Count headings by level - this.headings.forEach(heading => { - stats.byLevel[heading.level] = (stats.byLevel[heading.level] || 0) + 1; - stats.deepestLevel = Math.max(stats.deepestLevel, heading.level); - }); - - // Determine structure type - const levels = Object.keys(stats.byLevel).map(Number).sort(); - if (levels.length > 1) { - const hasSequentialLevels = levels.every((level, index) => - index === 0 || level <= levels[index - 1] + 1 - ); - stats.structure = hasSequentialLevels ? 'hierarchical' : 'mixed'; - } - - return stats; - }, {}, 'getDocumentStats'); - } - - /** - * Clean up resources when control is destroyed - */ - destroy() { - if (this.updateInterval) { - clearInterval(this.updateInterval); - this.updateInterval = null; - } - super.destroy(); - } -} - -// Export for module systems or attach to global for direct usage -if (typeof module !== 'undefined' && module.exports) { - module.exports = ContentsControl; -} else { - window.ContentsControl = ContentsControl; -} \ No newline at end of file diff --git a/markitect/static/js/controls/control-base.js b/markitect/static/js/controls/control-base.js deleted file mode 100644 index 0706c4b7..00000000 --- a/markitect/static/js/controls/control-base.js +++ /dev/null @@ -1,631 +0,0 @@ -/** - * Base Control Class for TestDrive-JSUI Controls - * - * Provides common functionality for positioning, drag, resize, expand/collapse operations. - * This is the foundation class that all UI controls inherit from to ensure consistent - * behavior across the TestDrive-JSUI component system. - * - * Key Features: - * - Drag and drop positioning with compass-based anchoring - * - Resize handles with hover-based visibility - * - Expand/collapse state management - * - Safe operation wrappers with error handling - * - Development mode with strict error checking - * - Accessibility support with proper ARIA labels - * - * Dependencies: - * - None (standalone base class) - * - * Usage: - * Controls inherit from this base by using Object.create(Control) and - * implementing their specific buildContent() methods. - */ - -// Development mode detection for enhanced error reporting -const MARKITECT_STRICT_MODE = ( - window.location.hostname === 'localhost' || - window.location.hostname === '127.0.0.1' || - window.location.search.includes('strict=true') || - window.markitectStrictMode === true -); - -/** - * ControlBase - Foundation class for all TestDrive-JSUI controls - * - * Provides the base functionality that all controls inherit: - * - DOM element management - * - Positioning and drag behavior - * - Resize handle management - * - State persistence - * - Error handling with strict mode support - */ -class ControlBase { - constructor() { - // Default configuration that controls can override - this.config = { - icon: 'πŸ”§', - title: 'Control', - className: 'base-control', - defaultContent: 'Control content', - ariaLabel: 'Base Control', - position: 'w', // Compass position: west (middle-left) - footer: null // Custom footer text - }; - - // Internal state - this.element = null; - this.isExpanded = false; - this.isHeaderOnly = false; // New state for header-only visibility - this.isDragging = false; - this.isResizing = false; - this.position = { x: 0, y: 0 }; - this.size = { width: 300, height: 200 }; - this.originalPosition = null; // Store original position for collapse - - // Event handlers storage - this.eventHandlers = new Map(); - } - - /** - * Safe operation wrapper with error handling - * Provides consistent error handling across all control operations - */ - safeOperation(operation, fallback = null, context = 'Unknown') { - try { - return operation(); - } catch (error) { - console.error(`Control operation failed in ${context}:`, error); - - if (MARKITECT_STRICT_MODE) { - throw error; // Re-throw in strict mode for debugging - } - - return fallback; - } - } - - /** - * Create and initialize the control element - * This method sets up the basic DOM structure that all controls use - */ - createElement() { - return this.safeOperation(() => { - if (this.element) { - this.destroy(); // Clean up existing element - } - - const control = document.createElement('div'); - control.className = `control-panel ${this.config.className}`; - control.setAttribute('role', 'dialog'); - control.setAttribute('aria-label', this.config.ariaLabel); - - control.innerHTML = ` - - - `; - - this.element = control; - this.setupStyles(); - this.setupEventListeners(); - return control; - - }, null, 'createElement'); - } - - /** - * Set up base styles for the control - */ - setupStyles() { - if (!this.element) return; - - // Position the element - this.element.style.position = 'fixed'; - this.element.style.zIndex = '1000'; - - // Store original position for collapse - this.storeOriginalPosition(); - - // Style the icon-only toggle button - const toggleBtn = this.element.querySelector('.control-toggle'); - if (toggleBtn) { - toggleBtn.style.cssText = ` - width: 40px; - height: 40px; - border: none; - background: rgba(248, 249, 250, 0.95); - border: 1px solid #dee2e6; - border-radius: 8px; - cursor: pointer; - font-size: 16px; - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 2px 8px rgba(0,0,0,0.1); - transition: all 0.2s ease; - `; - } - } - - /** - * Set up event listeners for control interaction - * Handles dragging, resizing, and toggle functionality - */ - setupEventListeners() { - if (!this.element) return; - - // Icon toggle to expand - const toggleBtn = this.element.querySelector('.control-toggle'); - if (toggleBtn) { - this.addEventListener(toggleBtn, 'click', () => this.expand()); - } - - // Close button to collapse back to icon - const closeBtn = this.element.querySelector('.control-close'); - if (closeBtn) { - this.addEventListener(closeBtn, 'click', () => this.collapse()); - } - - // Header title click to toggle content visibility - const title = this.element.querySelector('.control-title'); - if (title) { - this.addEventListener(title, 'click', () => this.toggleHeaderOnly()); - } - - // Drag functionality on header when expanded - const header = this.element.querySelector('.control-header'); - if (header) { - this.addEventListener(header, 'mousedown', (e) => { - if (this.isExpanded && e.target !== title && e.target !== closeBtn) { - this.startDrag(e); - } - }); - } - } - - /** - * Add event listener with automatic cleanup tracking - */ - addEventListener(element, event, handler) { - const key = `${element.className}_${event}`; - - // Remove existing handler if it exists - if (this.eventHandlers.has(key)) { - const [oldElement, oldEvent, oldHandler] = this.eventHandlers.get(key); - oldElement.removeEventListener(oldEvent, oldHandler); - } - - // Add new handler - element.addEventListener(event, handler); - this.eventHandlers.set(key, [element, event, handler]); - } - - /** - * Store original position for collapse restoration - */ - storeOriginalPosition() { - if (!this.element) return; - - const positionStyles = this.getCompassPosition(); - this.originalPosition = { - top: positionStyles.top, - left: positionStyles.left, - right: positionStyles.right, - bottom: positionStyles.bottom, - transform: positionStyles.transform - }; - - // Apply original position - Object.assign(this.element.style, positionStyles); - } - - /** - * Get compass-based positioning styles - */ - getCompassPosition() { - const positions = { - 'n': { top: '20px', left: '50%', transform: 'translateX(-50%)' }, - 'ne': { top: '20px', right: '20px' }, - 'e': { right: '20px', top: '50%', transform: 'translateY(-50%)' }, - 'se': { bottom: '20px', right: '20px' }, - 's': { bottom: '20px', left: '50%', transform: 'translateX(-50%)' }, - 'sw': { bottom: '20px', left: '20px' }, - 'w': { left: '20px', top: '50%', transform: 'translateY(-50%)' }, - 'nw': { top: '20px', left: '20px' } - }; - - return positions[this.config.position] || positions['w']; - } - - /** - * Expand the control from icon-only state - */ - expand() { - return this.safeOperation(() => { - this.isExpanded = true; - const panel = this.element?.querySelector('.control-panel-expanded'); - const toggleBtn = this.element?.querySelector('.control-toggle'); - - if (panel && toggleBtn) { - panel.style.display = 'block'; - toggleBtn.style.display = 'none'; - - // Style expanded panel - panel.style.cssText = ` - background: rgba(248, 249, 250, 0.95); - border: 1px solid #dee2e6; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0,0,0,0.15); - backdrop-filter: blur(8px); - min-width: 300px; - min-height: 200px; - `; - - // Style header - const header = this.element.querySelector('.control-header'); - if (header) { - header.style.cssText = ` - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 12px; - background: rgba(0,0,0,0.05); - border-bottom: 1px solid #dee2e6; - cursor: move; - user-select: none; - `; - } - - // Style close button - const closeBtn = this.element.querySelector('.control-close'); - if (closeBtn) { - closeBtn.style.cssText = ` - background: none; - border: none; - font-size: 16px; - cursor: pointer; - padding: 0; - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - `; - } - - // Add resize handle - this.addResizeHandle(); - this.buildContent(); - } - - return this.isExpanded; - }, false, 'expand'); - } - - /** - * Collapse back to icon-only state at original position - */ - collapse() { - return this.safeOperation(() => { - this.isExpanded = false; - this.isHeaderOnly = false; - const panel = this.element?.querySelector('.control-panel-expanded'); - const toggleBtn = this.element?.querySelector('.control-toggle'); - - if (panel && toggleBtn) { - panel.style.display = 'none'; - toggleBtn.style.display = 'block'; - - // Restore original position - if (this.originalPosition) { - // Clear any drag positioning - this.element.style.left = this.originalPosition.left || ''; - this.element.style.right = this.originalPosition.right || ''; - this.element.style.top = this.originalPosition.top || ''; - this.element.style.bottom = this.originalPosition.bottom || ''; - this.element.style.transform = this.originalPosition.transform || ''; - } - - // Remove resize handle - this.removeResizeHandle(); - } - - return !this.isExpanded; - }, false, 'collapse'); - } - - /** - * Toggle header-only visibility (content show/hide) - */ - toggleHeaderOnly() { - return this.safeOperation(() => { - if (!this.isExpanded) { - // If collapsed, expand first - this.expand(); - return; - } - - const content = this.element?.querySelector('.control-content'); - if (content) { - this.isHeaderOnly = !this.isHeaderOnly; - content.style.display = this.isHeaderOnly ? 'none' : 'block'; - } - - return this.isHeaderOnly; - }, false, 'toggleHeaderOnly'); - } - - /** - * Start drag operation - */ - startDrag(event) { - if (!this.isExpanded) return; // Only drag when expanded - - this.isDragging = true; - const rect = this.element.getBoundingClientRect(); - - // Calculate offset from mouse to element origin - this.dragOffset = { - x: event.clientX - rect.left, - y: event.clientY - rect.top - }; - - // Clear any positioning styles that interfere with dragging - this.element.style.right = ''; - this.element.style.bottom = ''; - this.element.style.transform = ''; - - // Add global mouse move and up handlers - const handleMouseMove = (e) => this.handleDrag(e); - const handleMouseUp = () => this.stopDrag(); - - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - - // Store handlers for cleanup (but don't use the tracked version to avoid conflicts) - this._dragHandlers = { move: handleMouseMove, up: handleMouseUp }; - - event.preventDefault(); - } - - /** - * Handle drag movement - */ - handleDrag(event) { - if (!this.isDragging || !this.element) return; - - // Calculate new position based on mouse position and offset - const newX = event.clientX - this.dragOffset.x; - const newY = event.clientY - this.dragOffset.y; - - // Update element position - this.element.style.left = `${newX}px`; - this.element.style.top = `${newY}px`; - - this.position.x = newX; - this.position.y = newY; - - event.preventDefault(); - } - - /** - * Stop drag operation - */ - stopDrag() { - if (!this.isDragging) return; - - this.isDragging = false; - - // Clean up event handlers - if (this._dragHandlers) { - document.removeEventListener('mousemove', this._dragHandlers.move); - document.removeEventListener('mouseup', this._dragHandlers.up); - delete this._dragHandlers; - } - } - - /** - * Add resize handle to expanded control - */ - addResizeHandle() { - // Remove existing resize handle if any - this.removeResizeHandle(); - - const resizeHandle = document.createElement('div'); - resizeHandle.className = 'control-resize-handle'; - resizeHandle.innerHTML = '↙'; // Bottom-left resize indicator - resizeHandle.style.cssText = ` - position: absolute; - bottom: 0; - left: 0; - width: 20px; - height: 20px; - cursor: nw-resize; - font-size: 16px; - display: flex; - align-items: center; - justify-content: center; - background: rgba(0,0,0,0.1); - border-radius: 0 8px 0 0; - user-select: none; - `; - - // Add to the expanded panel - const panel = this.element?.querySelector('.control-panel-expanded'); - if (panel) { - panel.appendChild(resizeHandle); - - // Set up resize handlers - this.addEventListener(resizeHandle, 'mousedown', (e) => this.startResize(e)); - } - } - - /** - * Remove resize handle - */ - removeResizeHandle() { - const handle = this.element?.querySelector('.control-resize-handle'); - if (handle && handle.parentNode) { - handle.parentNode.removeChild(handle); - } - } - - /** - * Start resize operation - */ - startResize(event) { - event.stopPropagation(); // Prevent drag from starting - if (!this.isExpanded) return; - - this.isResizing = true; - const rect = this.element.getBoundingClientRect(); - - // Store initial size and mouse position - this.resizeStart = { - width: rect.width, - height: rect.height, - mouseX: event.clientX, - mouseY: event.clientY - }; - - // Add global mouse move and up handlers - const handleMouseMove = (e) => this.handleResize(e); - const handleMouseUp = () => this.stopResize(); - - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - - // Store handlers for cleanup - this._resizeHandlers = { move: handleMouseMove, up: handleMouseUp }; - - event.preventDefault(); - } - - /** - * Handle resize movement (bottom-left corner resize) - */ - handleResize(event) { - if (!this.isResizing || !this.element) return; - - const panel = this.element.querySelector('.control-panel-expanded'); - if (!panel) return; - - // Calculate size change based on mouse movement - const deltaX = this.resizeStart.mouseX - event.clientX; // Inverted for left edge - const deltaY = event.clientY - this.resizeStart.mouseY; - - // Calculate new dimensions (minimum size constraints) - const newWidth = Math.max(200, this.resizeStart.width + deltaX); - const newHeight = Math.max(150, this.resizeStart.height + deltaY); - - // Apply new size to the panel - panel.style.width = `${newWidth}px`; - panel.style.height = `${newHeight}px`; - - // Update stored size - this.size.width = newWidth; - this.size.height = newHeight; - - event.preventDefault(); - } - - /** - * Stop resize operation - */ - stopResize() { - if (!this.isResizing) return; - - this.isResizing = false; - - // Clean up event handlers - if (this._resizeHandlers) { - document.removeEventListener('mousemove', this._resizeHandlers.move); - document.removeEventListener('mouseup', this._resizeHandlers.up); - delete this._resizeHandlers; - } - } - - /** - * Position the control based on compass position (used by show method) - */ - positionControl() { - if (!this.element) return; - - // Use the compass positioning from setupStyles - this.storeOriginalPosition(); - } - - /** - * Build the control content (to be overridden by subclasses) - */ - buildContent() { - // Default implementation - subclasses should override this - const content = this.element?.querySelector('.control-content'); - if (content) { - content.innerHTML = this.config.defaultContent; - } - } - - /** - * Show the control - */ - show() { - return this.safeOperation(() => { - if (!this.element) { - this.createElement(); - } - - document.body.appendChild(this.element); - this.positionControl(); - this.buildContent(); - - return this.element; - }, null, 'show'); - } - - /** - * Hide the control - */ - hide() { - return this.safeOperation(() => { - if (this.element && this.element.parentNode) { - this.element.parentNode.removeChild(this.element); - } - }, null, 'hide'); - } - - /** - * Destroy the control and clean up resources - */ - destroy() { - return this.safeOperation(() => { - // Clean up event listeners - for (const [element, event, handler] of this.eventHandlers.values()) { - element.removeEventListener(event, handler); - } - this.eventHandlers.clear(); - - // Remove element from DOM - if (this.element && this.element.parentNode) { - this.element.parentNode.removeChild(this.element); - } - - this.element = null; - }, null, 'destroy'); - } -} - -// Export for module systems or attach to global for direct usage -if (typeof module !== 'undefined' && module.exports) { - module.exports = ControlBase; -} else { - window.ControlBase = ControlBase; -} \ No newline at end of file diff --git a/markitect/static/js/controls/debug-control.js b/markitect/static/js/controls/debug-control.js deleted file mode 100644 index 1135235b..00000000 --- a/markitect/static/js/controls/debug-control.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Debug Control - Displays debug information and system messages - * Implements the Robustness Principle with Fail Fast mode support - */ - -class DebugControl extends ControlBase { - constructor() { - super(); - this.config = { - icon: 'πŸͺ²', - title: 'Debug', - className: 'debug-control', - defaultContent: 'Click to view debug information', - ariaLabel: 'Debug Control', - position: 'w' - }; - - // Store messages for debug display - this.messages = []; - } - - buildContent() { - const content = this.element?.querySelector('.control-content'); - if (content) { - const messages = window.MarkitectDebugSystem ? - window.MarkitectDebugSystem.getMessages() : []; - - content.innerHTML = ` -
-

Debug Messages

-
- ${messages.length > 0 ? - messages.slice(-10).map(msg => - `
- [${msg.category}] ${msg.component}: ${msg.message} -
${msg.displayTime}
-
` - ).join('') : - '

No debug messages yet

' - } -
- -
- `; - } - } -} - -window.DebugControl = DebugControl; \ No newline at end of file diff --git a/markitect/static/js/controls/edit-control.js b/markitect/static/js/controls/edit-control.js deleted file mode 100644 index c42c9109..00000000 --- a/markitect/static/js/controls/edit-control.js +++ /dev/null @@ -1,568 +0,0 @@ -/** - * EditControl - Document Editing Tools and Actions Control - * - * Provides a comprehensive set of document editing tools including text formatting, - * document actions (print, save, export), navigation helpers, and editing modes. - * Designed to enhance the writing and editing experience within the TestDrive-JSUI - * environment. - * - * Features: - * - Document actions (print, save, export to various formats) - * - Text formatting tools (bold, italic, headers) - * - Navigation helpers (scroll to top/bottom, go to line) - * - Word processing features (find/replace, word count) - * - Accessibility tools (font size, contrast adjustment) - * - Markdown formatting shortcuts - * - * Dependencies: - * - ControlBase (base control functionality) - */ - -/** - * EditControl - Comprehensive document editing control - * - * This control provides writers and editors with essential tools for document - * creation and modification. It includes both basic text operations and - * advanced features for content management and formatting. - */ -class EditControl extends ControlBase { - constructor() { - super(); - - // Configure for editing functionality - this.config = { - icon: '✏️', - title: 'Edit', - className: 'edit-control', - defaultContent: 'Document editing tools loading...', - ariaLabel: 'Document Edit Control', - position: 'e' // East positioning - }; - - // Edit control state - this.editingMode = 'view'; // 'view', 'edit', 'preview' - this.fontSize = 16; - this.lastSaveTime = null; - this.unsavedChanges = false; - this.shortcuts = new Map(); - - this.initializeShortcuts(); - } - - /** - * Initialize keyboard shortcuts for editing - */ - initializeShortcuts() { - this.shortcuts.set('Ctrl+S', () => this.saveDocument()); - this.shortcuts.set('Ctrl+P', () => this.printDocument()); - this.shortcuts.set('Ctrl+F', () => this.showFindDialog()); - this.shortcuts.set('Ctrl+B', () => this.toggleBold()); - this.shortcuts.set('Ctrl+I', () => this.toggleItalic()); - this.shortcuts.set('Escape', () => this.exitEditMode()); - } - - /** - * Generate the main editing tools HTML - */ - generateEditToolsHTML() { - return this.safeOperation(() => { - return ` -
-

Edit Tools

- - -
-
Document Actions
- - - - - - - - -
- - - - - -
-
Text Tools
- - - -
- - - -
- - -
- - -
-
Markdown Tools
- -
- - - - - - - -
-
- - -
-
-
Mode: ${this.editingMode}
-
Font: ${this.fontSize}px
- ${this.lastSaveTime ? `
Saved: ${new Date(this.lastSaveTime).toLocaleTimeString()}
` : ''} - ${this.unsavedChanges ? '
⚠️ Unsaved changes
' : ''} -
-
-
- `; - - }, '

Error generating edit tools

', 'generateEditToolsHTML'); - } - - /** - * Print the document - */ - printDocument() { - return this.safeOperation(() => { - window.print(); - - // Show feedback - this.showActionFeedback('πŸ–¨οΈ Print dialog opened', '#28a745'); - }, null, 'printDocument'); - } - - /** - * Save document (placeholder - would integrate with actual save system) - */ - saveDocument() { - return this.safeOperation(() => { - // In a real implementation, this would save to a backend - this.lastSaveTime = Date.now(); - this.unsavedChanges = false; - - // Update display - this.buildContent(); - - // Show feedback - this.showActionFeedback('πŸ’Ύ Document saved', '#007bff'); - }, null, 'saveDocument'); - } - - /** - * Export document to various formats - */ - exportDocument() { - return this.safeOperation(() => { - const contentArea = document.querySelector('#markitect-content') || document.body; - const htmlContent = contentArea.innerHTML; - const textContent = contentArea.textContent; - - // Create export menu - const exportMenu = document.createElement('div'); - exportMenu.style.cssText = ` - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: white; - border: 2px solid #007bff; - border-radius: 8px; - padding: 1rem; - z-index: 10000; - box-shadow: 0 4px 20px rgba(0,0,0,0.3); - `; - - exportMenu.innerHTML = ` -

Export Document

- - - - - `; - - // Add export functions - exportMenu.exportAsHTML = () => { - this.downloadFile(htmlContent, 'document.html', 'text/html'); - document.body.removeChild(exportMenu); - }; - - exportMenu.exportAsText = () => { - this.downloadFile(textContent, 'document.txt', 'text/plain'); - document.body.removeChild(exportMenu); - }; - - exportMenu.exportAsMarkdown = () => { - // Simple HTML to Markdown conversion (basic) - let markdown = htmlContent - .replace(/]*>(.*?)<\/h1>/gi, '# $1\n\n') - .replace(/]*>(.*?)<\/h2>/gi, '## $1\n\n') - .replace(/]*>(.*?)<\/h3>/gi, '### $1\n\n') - .replace(/]*>(.*?)<\/p>/gi, '$1\n\n') - .replace(/]*>(.*?)<\/strong>/gi, '**$1**') - .replace(/]*>(.*?)<\/em>/gi, '*$1*') - .replace(/<[^>]*>/g, ''); // Remove remaining HTML tags - - this.downloadFile(markdown, 'document.md', 'text/markdown'); - document.body.removeChild(exportMenu); - }; - - document.body.appendChild(exportMenu); - - }, null, 'exportDocument'); - } - - /** - * Download a file with given content - */ - downloadFile(content, filename, mimeType) { - const blob = new Blob([content], { type: mimeType }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - } - - /** - * Reset all changes and restore document to original state - */ - resetAll() { - return this.safeOperation(() => { - // Show confirmation dialog - const confirmed = window.confirm( - 'Reset all changes?\n\nThis will:\n' + - 'β€’ Restore document to original state\n' + - 'β€’ Clear all unsaved changes\n' + - 'β€’ Reset font size and other settings\n\n' + - 'This action cannot be undone.' - ); - - if (!confirmed) { - this.showActionFeedback('🚫 Reset cancelled', '#6c757d'); - return; - } - - // Reset edit control state - this.fontSize = 16; - this.editingMode = 'view'; - this.unsavedChanges = false; - this.lastSaveTime = null; - - // Reset font size - this.applyFontSize(); - - // Clear any highlights - document.querySelectorAll('.edit-highlight').forEach(el => { - el.outerHTML = el.innerHTML; - }); - - // Try to reset sections if SectionManager is available - if (window.sectionManager && typeof window.sectionManager.resetAllSections === 'function') { - window.sectionManager.resetAllSections(); - } - - // Try to reset document controls if available - if (window.documentControls && typeof window.documentControls.resetAllChanges === 'function') { - window.documentControls.resetAllChanges(); - } - - // Clear any debug messages if debug control is available - if (window.debugControl && typeof window.debugControl.clearMessages === 'function') { - window.debugControl.clearMessages(); - } - - // Reload the page as ultimate fallback - if (window.confirm('Reload page to complete reset?')) { - window.location.reload(); - return; - } - - // Update the control display - this.buildContent(); - - // Show feedback - this.showActionFeedback('πŸ”„ All changes reset', '#ffc107', '#212529'); - - }, null, 'resetAll'); - } - - /** - * Scroll to top of document - */ - scrollToTop() { - window.scrollTo({ top: 0, behavior: 'smooth' }); - this.showActionFeedback('⬆️ Scrolled to top', '#6c757d'); - } - - /** - * Scroll to bottom of document - */ - scrollToBottom() { - window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); - this.showActionFeedback('⬇️ Scrolled to bottom', '#6c757d'); - } - - /** - * Show go to line dialog - */ - showGoToLine() { - const lineNumber = prompt('Go to line number:'); - if (lineNumber && !isNaN(lineNumber)) { - // Simple implementation - scroll to approximate position - const totalHeight = document.body.scrollHeight; - const approximatePosition = (parseInt(lineNumber) / 100) * totalHeight; - window.scrollTo({ top: approximatePosition, behavior: 'smooth' }); - this.showActionFeedback(`🎯 Went to line ${lineNumber}`, '#6c757d'); - } - } - - /** - * Show find and replace dialog - */ - showFindReplace() { - const searchTerm = prompt('Find text:'); - if (searchTerm) { - // Simple highlight implementation - this.highlightText(searchTerm); - this.showActionFeedback(`πŸ” Highlighted "${searchTerm}"`, '#ffc107', '#000'); - } - } - - /** - * Highlight text in the document - */ - highlightText(searchTerm) { - return this.safeOperation(() => { - // Remove previous highlights - document.querySelectorAll('.edit-highlight').forEach(el => { - el.outerHTML = el.innerHTML; - }); - - // Add new highlights - const contentArea = document.querySelector('#markitect-content') || document.body; - const walker = document.createTreeWalker( - contentArea, - NodeFilter.SHOW_TEXT, - null, - false - ); - - const textNodes = []; - let node; - while (node = walker.nextNode()) { - textNodes.push(node); - } - - textNodes.forEach(textNode => { - const parent = textNode.parentNode; - const text = textNode.textContent; - if (text.toLowerCase().includes(searchTerm.toLowerCase())) { - const regex = new RegExp(`(${searchTerm})`, 'gi'); - const highlightedHTML = text.replace(regex, '$1'); - - const wrapper = document.createElement('div'); - wrapper.innerHTML = highlightedHTML; - while (wrapper.firstChild) { - parent.insertBefore(wrapper.firstChild, textNode); - } - parent.removeChild(textNode); - } - }); - }, null, 'highlightText'); - } - - /** - * Increase font size - */ - increaseFontSize() { - this.fontSize = Math.min(this.fontSize + 2, 24); - this.applyFontSize(); - this.buildContent(); - } - - /** - * Decrease font size - */ - decreaseFontSize() { - this.fontSize = Math.max(this.fontSize - 2, 12); - this.applyFontSize(); - this.buildContent(); - } - - /** - * Apply font size to document - */ - applyFontSize() { - const contentArea = document.querySelector('#markitect-content') || document.body; - contentArea.style.fontSize = `${this.fontSize}px`; - } - - /** - * Copy page link to clipboard - */ - copyLink() { - return this.safeOperation(() => { - const url = window.location.href; - if (navigator.clipboard) { - navigator.clipboard.writeText(url).then(() => { - this.showActionFeedback('πŸ“‹ Link copied to clipboard', '#fd7e14'); - }); - } else { - // Fallback for older browsers - prompt('Copy this link:', url); - this.showActionFeedback('πŸ“‹ Link displayed for copying', '#fd7e14'); - } - }, null, 'copyLink'); - } - - /** - * Insert markdown formatting - */ - insertMarkdown(prefix, suffix, placeholder) { - // This would integrate with an actual text editor - // For now, just show what would be inserted - const text = `${prefix}${placeholder}${suffix}`; - if (navigator.clipboard) { - navigator.clipboard.writeText(text); - this.showActionFeedback(`πŸ“‹ Copied: ${text}`, '#495057'); - } else { - prompt('Markdown to copy:', text); - } - } - - /** - * Show action feedback message - */ - showActionFeedback(message, backgroundColor, color = 'white') { - const feedback = document.createElement('div'); - feedback.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - background: ${backgroundColor}; - color: ${color}; - padding: 0.5rem 1rem; - border-radius: 4px; - z-index: 9999; - font-size: 0.8rem; - box-shadow: 0 2px 10px rgba(0,0,0,0.2); - `; - feedback.textContent = message; - document.body.appendChild(feedback); - - setTimeout(() => { - if (feedback.parentNode) { - document.body.removeChild(feedback); - } - }, 3000); - } - - /** - * Build the control content - * Override of base class method to provide edit-specific functionality - */ - buildContent() { - return this.safeOperation(() => { - const content = this.element?.querySelector('.control-content'); - if (content) { - content.innerHTML = this.generateEditToolsHTML(); - - // Store reference to this control for onclick handlers - this.element.editControl = this; - } - }, null, 'buildContent'); - } - - /** - * Exit edit mode - */ - exitEditMode() { - this.editingMode = 'view'; - this.buildContent(); - } -} - -// Export for module systems or attach to global for direct usage -if (typeof module !== 'undefined' && module.exports) { - module.exports = EditControl; -} else { - window.EditControl = EditControl; -} \ No newline at end of file diff --git a/markitect/static/js/controls/status-control.js b/markitect/static/js/controls/status-control.js deleted file mode 100644 index f3ca19d2..00000000 --- a/markitect/static/js/controls/status-control.js +++ /dev/null @@ -1,368 +0,0 @@ -/** - * StatusControl - Document Statistics and Change Tracking Control - * - * Provides real-time document statistics including word count, character count, - * reading time estimation, and change tracking. Monitors document modifications - * and provides insights into document structure and content metrics. - * - * Features: - * - Real-time word and character counting - * - Reading time estimation based on content - * - Document structure analysis (headings, paragraphs, lists) - * - Change tracking with before/after comparisons - * - Content complexity metrics - * - Export functionality for statistics - * - * Dependencies: - * - ControlBase (base control functionality) - */ - -/** - * StatusControl - Document statistics and monitoring control - * - * This control continuously monitors the document for changes and provides - * detailed statistics about content, structure, and reading metrics. - * Useful for writers, editors, and content creators. - */ -class StatusControl extends ControlBase { - constructor() { - super(); - - // Configure for status functionality - this.config = { - icon: 'πŸ“Š', - title: 'Status', - className: 'status-control', - defaultContent: 'Loading document statistics...', - ariaLabel: 'Document Status Control', - position: 'e' // East positioning - }; - - // Status tracking state - this.stats = { - characters: 0, - charactersNoSpaces: 0, - words: 0, - sentences: 0, - paragraphs: 0, - headings: 0, - lists: 0, - images: 0, - links: 0, - readingTimeMinutes: 0 - }; - - this.previousStats = { ...this.stats }; - this.lastUpdateTime = null; - this.updateInterval = null; - this.wordsPerMinute = 200; // Average reading speed - } - - /** - * Extract and count document content statistics - */ - analyzeDocument() { - return this.safeOperation(() => { - const contentArea = document.querySelector('#markitect-content') || document.body; - const textContent = contentArea.textContent || ''; - - // Basic text statistics - this.stats.characters = textContent.length; - this.stats.charactersNoSpaces = textContent.replace(/\s/g, '').length; - - // Word counting (more accurate) - const words = textContent.trim().split(/\s+/).filter(word => word.length > 0); - this.stats.words = words.length; - - // Sentence counting (approximate) - const sentences = textContent.split(/[.!?]+/).filter(s => s.trim().length > 0); - this.stats.sentences = sentences.length; - - // Structural elements - this.stats.paragraphs = contentArea.querySelectorAll('p').length; - this.stats.headings = contentArea.querySelectorAll('h1, h2, h3, h4, h5, h6').length; - this.stats.lists = contentArea.querySelectorAll('ul, ol').length; - this.stats.images = contentArea.querySelectorAll('img').length; - this.stats.links = contentArea.querySelectorAll('a').length; - - // Reading time calculation - this.stats.readingTimeMinutes = Math.ceil(this.stats.words / this.wordsPerMinute); - - this.lastUpdateTime = Date.now(); - return this.stats; - - }, this.stats, 'analyzeDocument'); - } - - /** - * Calculate changes since last analysis - */ - calculateChanges() { - return this.safeOperation(() => { - const changes = {}; - for (const [key, currentValue] of Object.entries(this.stats)) { - const previousValue = this.previousStats[key] || 0; - const difference = currentValue - previousValue; - changes[key] = { - current: currentValue, - previous: previousValue, - change: difference, - hasChanged: difference !== 0 - }; - } - return changes; - }, {}, 'calculateChanges'); - } - - /** - * Format statistics for display - */ - formatStatistics() { - return this.safeOperation(() => { - const changes = this.calculateChanges(); - - const formatChange = (changeData) => { - if (!changeData.hasChanged) return ''; - const sign = changeData.change > 0 ? '+' : ''; - const color = changeData.change > 0 ? '#28a745' : '#dc3545'; - return ` (${sign}${changeData.change})`; - }; - - const formatNumber = (num) => num.toLocaleString(); - - return ` -
-

Document Statistics

- -
-
- Words:
- ${formatNumber(this.stats.words)} - ${formatChange(changes.words)} -
- -
- Characters:
- ${formatNumber(this.stats.characters)} - ${formatChange(changes.characters)} -
- -
- Reading Time:
- ${this.stats.readingTimeMinutes} min - ${formatChange(changes.readingTimeMinutes)} -
- -
- Sentences:
- ${formatNumber(this.stats.sentences)} - ${formatChange(changes.sentences)} -
-
- -
-
Document Structure
- -
- Paragraphs: - ${this.stats.paragraphs}${formatChange(changes.paragraphs)} -
- -
- Headings: - ${this.stats.headings}${formatChange(changes.headings)} -
- -
- Lists: - ${this.stats.lists}${formatChange(changes.lists)} -
- -
- Images: - ${this.stats.images}${formatChange(changes.images)} -
- -
- Links: - ${this.stats.links}${formatChange(changes.links)} -
-
- -
- - - -
- - ${this.lastUpdateTime ? ` -
- Updated: ${new Date(this.lastUpdateTime).toLocaleTimeString()} -
- ` : ''} -
- `; - - }, '

Error displaying statistics

', 'formatStatistics'); - } - - /** - * Refresh statistics and update display - */ - refreshStats() { - return this.safeOperation(() => { - // Save current stats as previous - this.previousStats = { ...this.stats }; - - // Analyze document - this.analyzeDocument(); - - // Update display - this.buildContent(); - - // Show success feedback - const refreshBtn = this.element?.querySelector('button'); - if (refreshBtn) { - const originalText = refreshBtn.innerHTML; - refreshBtn.innerHTML = 'βœ… Updated'; - - setTimeout(() => { - refreshBtn.innerHTML = originalText; - }, 1000); - } - - }, null, 'refreshStats'); - } - - /** - * Export statistics to various formats - */ - exportStats() { - return this.safeOperation(() => { - const exportData = { - timestamp: new Date().toISOString(), - document: { - title: document.title || 'Untitled Document', - url: window.location.href - }, - statistics: this.stats, - metadata: { - wordsPerMinute: this.wordsPerMinute, - analysisDate: new Date(this.lastUpdateTime).toISOString() - } - }; - - // Create downloadable JSON - const dataStr = JSON.stringify(exportData, null, 2); - const dataBlob = new Blob([dataStr], { type: 'application/json' }); - const url = URL.createObjectURL(dataBlob); - - // Create temporary download link - const link = document.createElement('a'); - link.href = url; - link.download = `document-stats-${new Date().toISOString().split('T')[0]}.json`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - - // Clean up - URL.revokeObjectURL(url); - - // Show feedback - const exportBtn = this.element?.querySelector('button:last-child'); - if (exportBtn) { - const originalText = exportBtn.innerHTML; - exportBtn.innerHTML = 'βœ… Exported'; - exportBtn.style.background = '#28a745'; - - setTimeout(() => { - exportBtn.innerHTML = originalText; - exportBtn.style.background = '#28a745'; - }, 2000); - } - - }, null, 'exportStats'); - } - - /** - * Get reading difficulty score (Flesch Reading Ease approximation) - */ - calculateReadabilityScore() { - return this.safeOperation(() => { - if (this.stats.sentences === 0 || this.stats.words === 0) { - return { score: 0, level: 'Unknown' }; - } - - const avgWordsPerSentence = this.stats.words / this.stats.sentences; - const avgSyllablesPerWord = 1.5; // Simplified approximation - - // Flesch Reading Ease formula (simplified) - const score = 206.835 - (1.015 * avgWordsPerSentence) - (84.6 * avgSyllablesPerWord); - - let level; - if (score >= 90) level = 'Very Easy'; - else if (score >= 80) level = 'Easy'; - else if (score >= 70) level = 'Fairly Easy'; - else if (score >= 60) level = 'Standard'; - else if (score >= 50) level = 'Fairly Difficult'; - else if (score >= 30) level = 'Difficult'; - else level = 'Very Difficult'; - - return { score: Math.round(score), level }; - }, { score: 0, level: 'Unknown' }, 'calculateReadabilityScore'); - } - - /** - * Build the control content - * Override of base class method to provide status-specific functionality - */ - buildContent() { - return this.safeOperation(() => { - // Analyze document first - this.analyzeDocument(); - - // Generate and set content - const content = this.element?.querySelector('.control-content'); - if (content) { - content.innerHTML = this.formatStatistics(); - - // Store reference to this control for onclick handlers - this.element.statusControl = this; - } - - // Set up auto-refresh for dynamic content - if (this.updateInterval) { - clearInterval(this.updateInterval); - } - - this.updateInterval = setInterval(() => { - this.refreshStats(); - }, 10000); // Update every 10 seconds - - }, null, 'buildContent'); - } - - /** - * Clean up resources when control is destroyed - */ - destroy() { - if (this.updateInterval) { - clearInterval(this.updateInterval); - this.updateInterval = null; - } - super.destroy(); - } -} - -// Export for module systems or attach to global for direct usage -if (typeof module !== 'undefined' && module.exports) { - module.exports = StatusControl; -} else { - window.StatusControl = StatusControl; -} \ No newline at end of file diff --git a/markitect/static/js/tests/test_edit.html b/markitect/static/js/tests/test_edit.html index 8076d97e..813b65bf 100644 --- a/markitect/static/js/tests/test_edit.html +++ b/markitect/static/js/tests/test_edit.html @@ -131,8 +131,8 @@ - - + + diff --git a/markitect/templates/document.html b/markitect/templates/document.html index fe2718db..67de7f8a 100644 --- a/markitect/templates/document.html +++ b/markitect/templates/document.html @@ -126,11 +126,11 @@ - - - - - + + + + + diff --git a/markitect/templates/edit-mode-fixed.html b/markitect/templates/edit-mode-fixed.html index 6fc88414..114aa9ca 100644 --- a/markitect/templates/edit-mode-fixed.html +++ b/markitect/templates/edit-mode-fixed.html @@ -28,11 +28,11 @@ - - - - - + + + + + diff --git a/testdrive-jsui/README.md b/testdrive-jsui/README.md index 66723522..fe261c26 100644 --- a/testdrive-jsui/README.md +++ b/testdrive-jsui/README.md @@ -128,7 +128,7 @@ The plugin supports various configuration options: ## Extending the Plugin ### Adding New Controls -1. Create new control in `static/js/controls/` +1. Create new control in `../capabilities/testdrive-jsui/js/controls/` 2. Extend `ControlBase` class 3. Register in `main.js` initialization 4. Add compass position (nw, ne, e, se, s, sw, w, nw) diff --git a/testdrive-jsui/static/js/controls/contents-control.js b/testdrive-jsui/static/js/controls/contents-control.js deleted file mode 100644 index 680dbc33..00000000 --- a/testdrive-jsui/static/js/controls/contents-control.js +++ /dev/null @@ -1,287 +0,0 @@ -/** - * ContentsControl - Table of Contents Display Control - * - * Provides an interactive table of contents for document navigation. - * Extracts headings from the document and displays them in a hierarchical - * structure with clickable links for quick navigation. - * - * Features: - * - Automatic heading extraction from document - * - Hierarchical display with proper indentation - * - Clickable navigation links with smooth scrolling - * - Real-time updates when document structure changes - * - Search functionality within the table of contents - * - * Dependencies: - * - ControlBase (base control functionality) - */ - -/** - * ContentsControl - Interactive table of contents control - * - * Built on the base class architecture for consistency with other panels. - * Only implements content-specific functionality while inheriting all - * common panel behavior from ControlBase. - */ -class ContentsControl extends ControlBase { - constructor() { - super(); - - // Configure for contents functionality - this.config = { - icon: 'πŸ“‹', - title: 'Contents', - className: 'contents-control', - defaultContent: 'Loading table of contents...', - ariaLabel: 'Table of Contents Control', - position: 'w' // West positioning - }; - - // Contents-specific state - this.headings = []; - this.lastScanTime = null; - this.updateInterval = null; - this.searchQuery = ''; - } - - /** - * Generate contents control content (called by base class buildContent) - */ - generateContent() { - // Extract headings first - this.extractHeadings(); - - return this.safeOperation(() => { - if (this.headings.length === 0) { - return ` -
-

No headings found in document

- -
- `; - } - - const searchHTML = ` -
- -
- `; - - const filteredHeadings = this.filterHeadings(this.headings, this.searchQuery); - const contentsHTML = filteredHeadings.map(heading => { - const indentLevel = Math.max(0, heading.level - 1); - const indentPx = indentLevel * 15; - - return ` - - `; - }).join(''); - - const statusHTML = ` -
- Found ${filteredHeadings.length} heading${filteredHeadings.length !== 1 ? 's' : ''} -
- `; - - const refreshButtonHTML = ` -
- -
- `; - - return ` - ${searchHTML} - ${statusHTML} - ${contentsHTML} - ${refreshButtonHTML} - `; - - }, 'Error generating contents', 'generateContent'); - } - - /** - * Extract all headings from the document - * Creates a hierarchical structure of the document's heading elements - */ - extractHeadings() { - return this.safeOperation(() => { - const headingSelectors = 'h1, h2, h3, h4, h5, h6'; - const headingElements = document.querySelectorAll(headingSelectors); - const extractedHeadings = []; - - headingElements.forEach((heading, index) => { - const level = parseInt(heading.tagName.charAt(1)); - const text = heading.textContent.trim(); - - // Generate or use existing ID for anchor links - let id = heading.id; - if (!id) { - id = text.toLowerCase() - .replace(/[^\w\s-]/g, '') - .replace(/\s+/g, '-') - .substring(0, 50); - - // Ensure uniqueness - let counter = 1; - let uniqueId = id; - while (document.getElementById(uniqueId)) { - uniqueId = `${id}-${counter}`; - counter++; - } - - heading.id = uniqueId; - id = uniqueId; - } - - extractedHeadings.push({ - id, - text, - level, - element: heading, - index - }); - }); - - this.headings = extractedHeadings; - this.lastScanTime = Date.now(); - return extractedHeadings; - - }, [], 'extractHeadings'); - } - - /** - * Filter headings based on search query - */ - filterHeadings(headings, query) { - if (!query || query.trim() === '') { - return headings; - } - - const normalizedQuery = query.toLowerCase().trim(); - return headings.filter(heading => - heading.text.toLowerCase().includes(normalizedQuery) - ); - } - - /** - * Navigate to a specific heading with smooth scrolling - */ - navigateToHeading(headingId) { - return this.safeOperation(() => { - const targetElement = document.getElementById(headingId); - if (targetElement) { - targetElement.scrollIntoView({ - behavior: 'smooth', - block: 'start' - }); - - // Highlight the target temporarily - const originalStyle = targetElement.style.backgroundColor; - targetElement.style.backgroundColor = '#fff3cd'; - targetElement.style.transition = 'background-color 0.3s ease'; - - setTimeout(() => { - targetElement.style.backgroundColor = originalStyle; - setTimeout(() => { - targetElement.style.transition = ''; - }, 300); - }, 1500); - - return true; - } - return false; - }, false, 'navigateToHeading'); - } - - /** - * Handle search input - */ - handleSearch(query) { - this.searchQuery = query; - this.buildContent(); // Rebuild content with new filter - } - - /** - * Refresh the contents by re-scanning the document - */ - refreshContents() { - return this.safeOperation(() => { - this.extractHeadings(); - this.buildContent(); // Rebuild content with updated headings - - // Show success feedback - const refreshBtn = this.element?.querySelector('button'); - if (refreshBtn && refreshBtn.textContent.includes('Refresh')) { - const originalText = refreshBtn.innerHTML; - refreshBtn.innerHTML = 'βœ… Updated'; - refreshBtn.style.background = '#28a745'; - - setTimeout(() => { - refreshBtn.innerHTML = originalText; - refreshBtn.style.background = '#28a745'; - }, 1000); - } - }, null, 'refreshContents'); - } - - /** - * Override buildContent to add control reference and auto-refresh - */ - buildContent() { - super.buildContent(); - - // Store reference to this control for onclick handlers - if (this.element) { - this.element.contentsControl = this; - } - - // Set up auto-refresh for dynamic content - if (this.updateInterval) { - clearInterval(this.updateInterval); - } - - this.updateInterval = setInterval(() => { - const currentHeadingCount = document.querySelectorAll('h1, h2, h3, h4, h5, h6').length; - if (currentHeadingCount !== this.headings.length) { - this.refreshContents(); - } - }, 5000); // Check every 5 seconds - } - - /** - * Clean up resources when control is destroyed - */ - destroy() { - if (this.updateInterval) { - clearInterval(this.updateInterval); - this.updateInterval = null; - } - super.destroy(); - } -} - -// Export for module systems or attach to global for direct usage -if (typeof module !== 'undefined' && module.exports) { - module.exports = ContentsControl; -} else { - window.ContentsControl = ContentsControl; -} \ No newline at end of file diff --git a/testdrive-jsui/static/js/controls/control-base.js b/testdrive-jsui/static/js/controls/control-base.js deleted file mode 100644 index ecb826e0..00000000 --- a/testdrive-jsui/static/js/controls/control-base.js +++ /dev/null @@ -1,847 +0,0 @@ -/** - * Base Control Class for TestDrive-JSUI Controls - * - * Provides common functionality for positioning, drag, resize, expand/collapse operations. - * This is the foundation class that all UI controls inherit from to ensure consistent - * behavior across the TestDrive-JSUI component system. - * - * Key Features: - * - Drag and drop positioning with compass-based anchoring - * - Resize handles with hover-based visibility - * - Expand/collapse state management - * - Safe operation wrappers with error handling - * - Development mode with strict error checking - * - Accessibility support with proper ARIA labels - * - * Dependencies: - * - None (standalone base class) - * - * Usage: - * Controls inherit from this base by using Object.create(Control) and - * implementing their specific buildContent() methods. - */ - -// Development mode detection for enhanced error reporting -const MARKITECT_STRICT_MODE = ( - window.location.hostname === 'localhost' || - window.location.hostname === '127.0.0.1' || - window.location.search.includes('strict=true') || - window.markitectStrictMode === true -); - -/** - * ControlBase - Foundation class for all TestDrive-JSUI controls - * - * Provides the base functionality that all controls inherit: - * - DOM element management - * - Positioning and drag behavior - * - Resize handle management - * - State persistence - * - Error handling with strict mode support - */ -class ControlBase { - constructor() { - // Default configuration that controls can override - this.config = { - icon: 'πŸ”§', - title: 'Control', - className: 'base-control', - defaultContent: 'Control content', - ariaLabel: 'Base Control', - position: 'w', // Compass position: west (middle-left) - footer: null // Custom footer text - }; - - // Internal state - this.element = null; - this.isExpanded = false; - this.isHeaderOnly = false; // New state for header-only visibility - this.isDragging = false; - this.isResizing = false; - this.position = { x: 0, y: 0 }; - this.size = { - width: 300, - height: Math.floor(window.innerHeight / 3) - }; - this.originalPosition = null; // Store original position for collapse - - // Event handlers storage - this.eventHandlers = new Map(); - } - - /** - * Safe operation wrapper with error handling - * Provides consistent error handling across all control operations - */ - safeOperation(operation, fallback = null, context = 'Unknown') { - try { - return operation(); - } catch (error) { - console.error(`Control operation failed in ${context}:`, error); - - if (MARKITECT_STRICT_MODE) { - throw error; // Re-throw in strict mode for debugging - } - - return fallback; - } - } - - /** - * Create and initialize the control element - * This method sets up the basic DOM structure that all controls use - */ - createElement() { - return this.safeOperation(() => { - if (this.element) { - this.destroy(); // Clean up existing element - } - - const control = document.createElement('div'); - control.className = `control-panel ${this.config.className}`; - control.setAttribute('role', 'dialog'); - control.setAttribute('aria-label', this.config.ariaLabel); - - control.innerHTML = ` - - - `; - - this.element = control; - this.setupStyles(); - this.setupEventListeners(); - return control; - - }, null, 'createElement'); - } - - /** - * Set up base styles for the control - */ - setupStyles() { - if (!this.element) return; - - // Position the element - this.element.style.position = 'fixed'; - this.element.style.zIndex = '1000'; - - // Store original position for collapse - this.storeOriginalPosition(); - - // Style the icon-only toggle button - const toggleBtn = this.element.querySelector('.control-toggle'); - if (toggleBtn) { - toggleBtn.style.cssText = ` - width: 40px; - height: 40px; - border: none; - background: rgba(248, 249, 250, 0.95); - border: 1px solid #dee2e6; - border-radius: 8px; - cursor: pointer; - font-size: 16px; - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 2px 8px rgba(0,0,0,0.1); - transition: all 0.2s ease; - `; - } - } - - /** - * Set up event listeners for control interaction - * Handles dragging, resizing, and toggle functionality - */ - setupEventListeners() { - if (!this.element) return; - - // Icon toggle to expand - const toggleBtn = this.element.querySelector('.control-toggle'); - if (toggleBtn) { - this.addEventListener(toggleBtn, 'click', () => this.expand()); - } - - // Close button to collapse back to icon - const closeBtn = this.element.querySelector('.control-close'); - if (closeBtn) { - this.addEventListener(closeBtn, 'click', () => this.collapse()); - } - - // Header title click to toggle content visibility - const title = this.element.querySelector('.control-title'); - if (title) { - this.addEventListener(title, 'click', () => this.toggleHeaderOnly()); - } - - // Drag functionality on header when expanded - const header = this.element.querySelector('.control-header'); - if (header) { - this.addEventListener(header, 'mousedown', (e) => { - if (this.isExpanded && e.target !== title && e.target !== closeBtn) { - this.startDrag(e); - } - }); - } - } - - /** - * Add event listener with automatic cleanup tracking - */ - addEventListener(element, event, handler) { - const key = `${element.className}_${event}`; - - // Remove existing handler if it exists - if (this.eventHandlers.has(key)) { - const [oldElement, oldEvent, oldHandler] = this.eventHandlers.get(key); - oldElement.removeEventListener(oldEvent, oldHandler); - } - - // Add new handler - element.addEventListener(event, handler); - this.eventHandlers.set(key, [element, event, handler]); - } - - /** - * Store original position for collapse restoration - */ - storeOriginalPosition() { - if (!this.element) return; - - const positionStyles = this.getCompassPosition(); - this.originalPosition = { - top: positionStyles.top, - left: positionStyles.left, - right: positionStyles.right, - bottom: positionStyles.bottom, - transform: positionStyles.transform - }; - - // Apply original position - Object.assign(this.element.style, positionStyles); - } - - /** - * Get compass-based positioning styles - */ - getCompassPosition() { - const positions = { - 'n': { top: '20px', left: '50%', transform: 'translateX(-50%)' }, - 'ne': { top: '20px', right: '20px' }, - 'e': { right: '20px', top: '50%', transform: 'translateY(-50%)' }, - 'se': { bottom: '20px', right: '20px' }, - 's': { bottom: '20px', left: '50%', transform: 'translateX(-50%)' }, - 'sw': { bottom: '20px', left: '20px' }, - 'w': { left: '20px', top: '50%', transform: 'translateY(-50%)' }, - 'nw': { top: '20px', left: '20px' } - }; - - return positions[this.config.position] || positions['w']; - } - - /** - * Expand the control from icon-only state - */ - expand() { - return this.safeOperation(() => { - this.isExpanded = true; - const panel = this.element?.querySelector('.control-panel-expanded'); - const toggleBtn = this.element?.querySelector('.control-toggle'); - - if (panel && toggleBtn) { - panel.style.display = 'block'; - toggleBtn.style.display = 'none'; - - // Calculate default height as 1/3 of window height - const defaultHeight = Math.floor(window.innerHeight / 3); - - // Style expanded panel - panel.style.cssText = ` - position: relative; - display: flex; - flex-direction: column; - background: rgba(248, 249, 250, 0.95); - border: 1px solid #dee2e6; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0,0,0,0.15); - backdrop-filter: blur(8px); - min-width: 300px; - min-height: 200px; - max-height: calc(100vh - 40px); - width: auto; - height: ${defaultHeight}px; - overflow: hidden; - `; - - // Style header - const header = this.element.querySelector('.control-header'); - if (header) { - header.style.cssText = ` - display: flex; - align-items: center; - justify-content: space-between; - padding: 4px 12px; - background: rgba(0,0,0,0.05); - border-bottom: 1px solid #dee2e6; - cursor: move; - user-select: none; - flex-shrink: 0; - min-height: 24px; - border-radius: 7px 7px 0 0; - margin: -1px -1px 0 -1px; - `; - } - - // Style content area container - const contentArea = this.element.querySelector('.control-content'); - if (contentArea) { - contentArea.style.cssText = ` - flex: 1; - overflow: hidden; - display: flex; - flex-direction: column; - min-height: 0; - `; - } - - // Style close button - const closeBtn = this.element.querySelector('.control-close'); - if (closeBtn) { - closeBtn.style.cssText = ` - background: none; - border: none; - font-size: 16px; - cursor: pointer; - padding: 0; - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - `; - } - - // Add resize handle - this.addResizeHandle(); - this.buildContent(); - } - - return this.isExpanded; - }, false, 'expand'); - } - - /** - * Collapse back to icon-only state at original position - */ - collapse() { - return this.safeOperation(() => { - this.isExpanded = false; - this.isHeaderOnly = false; - const panel = this.element?.querySelector('.control-panel-expanded'); - const toggleBtn = this.element?.querySelector('.control-toggle'); - - if (panel && toggleBtn) { - panel.style.display = 'none'; - toggleBtn.style.display = 'block'; - - // Restore original position - if (this.originalPosition) { - // Clear any drag positioning - this.element.style.left = this.originalPosition.left || ''; - this.element.style.right = this.originalPosition.right || ''; - this.element.style.top = this.originalPosition.top || ''; - this.element.style.bottom = this.originalPosition.bottom || ''; - this.element.style.transform = this.originalPosition.transform || ''; - } - - // Reset panel size to defaults - panel.style.width = ''; - panel.style.height = ''; - panel.style.minWidth = '300px'; - panel.style.minHeight = '200px'; - - // Reset internal size tracking - this.size.width = 300; - this.size.height = Math.floor(window.innerHeight / 3); - this.storedWidth = null; - - // Remove resize handle - this.removeResizeHandle(); - } - - return !this.isExpanded; - }, false, 'collapse'); - } - - /** - * Toggle header-only visibility (content show/hide) - */ - toggleHeaderOnly() { - return this.safeOperation(() => { - if (!this.isExpanded) { - // If collapsed, expand first - this.expand(); - return; - } - - const content = this.element?.querySelector('.control-content'); - const panel = this.element?.querySelector('.control-panel-expanded'); - - if (content && panel) { - this.isHeaderOnly = !this.isHeaderOnly; - const resizeHandle = this.element?.querySelector('.control-resize-handle'); - - if (this.isHeaderOnly) { - // Store current width before collapsing - const currentWidth = panel.offsetWidth; - this.storedWidth = currentWidth; - - // Hide content and shrink panel height only - content.style.display = 'none'; - panel.style.minHeight = 'auto'; - panel.style.height = 'auto'; - - // Keep the same width and position - panel.style.width = `${currentWidth}px`; - panel.style.minWidth = `${currentWidth}px`; - - // Hide resize handle in header-only mode - if (resizeHandle) { - resizeHandle.style.display = 'none'; - } - } else { - // Show content and restore full panel size - content.style.display = 'block'; - panel.style.minHeight = '200px'; - - // Restore stored width or use default - const widthToRestore = this.storedWidth || 300; - panel.style.minWidth = `${widthToRestore}px`; - - // Restore height if it was auto - if (!panel.style.height || panel.style.height === 'auto') { - panel.style.height = '200px'; - } - if (!panel.style.width || panel.style.width === `${widthToRestore}px`) { - panel.style.width = `${widthToRestore}px`; - } - - // Show resize handle when fully expanded - if (resizeHandle) { - resizeHandle.style.display = 'flex'; - } - } - } - - return this.isHeaderOnly; - }, false, 'toggleHeaderOnly'); - } - - /** - * Start drag operation - */ - startDrag(event) { - if (!this.isExpanded) return; // Only drag when expanded - - this.isDragging = true; - const rect = this.element.getBoundingClientRect(); - - // Calculate offset from mouse to element origin - this.dragOffset = { - x: event.clientX - rect.left, - y: event.clientY - rect.top - }; - - // Store current computed position before clearing styles - const computedStyle = window.getComputedStyle(this.element); - const currentLeft = rect.left; - const currentTop = rect.top; - - // Clear any positioning styles that interfere with dragging - this.element.style.right = ''; - this.element.style.bottom = ''; - this.element.style.transform = ''; - - // Set the element to its current visual position using left/top - this.element.style.left = `${currentLeft}px`; - this.element.style.top = `${currentTop}px`; - - // Update internal position tracking - this.position.x = currentLeft; - this.position.y = currentTop; - - // Add global mouse move and up handlers - const handleMouseMove = (e) => this.handleDrag(e); - const handleMouseUp = () => this.stopDrag(); - - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - - // Store handlers for cleanup (but don't use the tracked version to avoid conflicts) - this._dragHandlers = { move: handleMouseMove, up: handleMouseUp }; - - event.preventDefault(); - } - - /** - * Handle drag movement - */ - handleDrag(event) { - if (!this.isDragging || !this.element) return; - - // Calculate new position based on mouse position and offset - const newX = event.clientX - this.dragOffset.x; - const newY = event.clientY - this.dragOffset.y; - - // Update element position - this.element.style.left = `${newX}px`; - this.element.style.top = `${newY}px`; - - this.position.x = newX; - this.position.y = newY; - - event.preventDefault(); - } - - /** - * Stop drag operation - */ - stopDrag() { - if (!this.isDragging) return; - - this.isDragging = false; - - // Clean up event handlers - if (this._dragHandlers) { - document.removeEventListener('mousemove', this._dragHandlers.move); - document.removeEventListener('mouseup', this._dragHandlers.up); - delete this._dragHandlers; - } - } - - /** - * Add resize handle to expanded control - */ - addResizeHandle() { - // Remove existing resize handle if any - this.removeResizeHandle(); - - const resizeHandle = document.createElement('div'); - resizeHandle.className = 'control-resize-handle'; - resizeHandle.innerHTML = '●'; // Dot resize indicator - resizeHandle.style.cssText = ` - position: absolute; - bottom: 0px; - right: 2px; - width: 12px; - height: 12px; - cursor: se-resize; - font-size: 10px; - line-height: 1; - user-select: none; - color: #999; - background: transparent; - z-index: 10; - `; - - // Add to the expanded panel - const panel = this.element?.querySelector('.control-panel-expanded'); - if (panel) { - panel.appendChild(resizeHandle); - - // Set up resize handlers - this.addEventListener(resizeHandle, 'mousedown', (e) => this.startResize(e)); - this.addEventListener(resizeHandle, 'dblclick', (e) => this.autoResizeToContent(e)); - } - } - - /** - * Remove resize handle - */ - removeResizeHandle() { - const handle = this.element?.querySelector('.control-resize-handle'); - if (handle && handle.parentNode) { - handle.parentNode.removeChild(handle); - } - } - - /** - * Start resize operation - */ - startResize(event) { - event.stopPropagation(); // Prevent drag from starting - if (!this.isExpanded) return; - - this.isResizing = true; - const rect = this.element.getBoundingClientRect(); - - // Store initial size and mouse position - this.resizeStart = { - width: rect.width, - height: rect.height, - mouseX: event.clientX, - mouseY: event.clientY - }; - - // Add global mouse move and up handlers - const handleMouseMove = (e) => this.handleResize(e); - const handleMouseUp = () => this.stopResize(); - - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - - // Store handlers for cleanup - this._resizeHandlers = { move: handleMouseMove, up: handleMouseUp }; - - event.preventDefault(); - } - - /** - * Handle resize movement (bottom-right corner resize) - */ - handleResize(event) { - if (!this.isResizing || !this.element) return; - - const panel = this.element.querySelector('.control-panel-expanded'); - if (!panel) return; - - // Calculate size change based on mouse movement (bottom-right corner) - const deltaX = event.clientX - this.resizeStart.mouseX; // Right direction - const deltaY = event.clientY - this.resizeStart.mouseY; // Down direction - - // Get minimum size (collapsed header size or default minimum) - const headerHeight = this.element.querySelector('.control-header')?.offsetHeight || 40; - const minWidth = 200; - const minHeight = headerHeight + 20; // Header plus small padding - - // Calculate new dimensions with minimum constraints - const newWidth = Math.max(minWidth, this.resizeStart.width + deltaX); - const newHeight = Math.max(minHeight, this.resizeStart.height + deltaY); - - // Apply new size to the panel - panel.style.width = `${newWidth}px`; - panel.style.height = `${newHeight}px`; - - // Update stored size - this.size.width = newWidth; - this.size.height = newHeight; - - event.preventDefault(); - } - - /** - * Stop resize operation - */ - stopResize() { - if (!this.isResizing) return; - - this.isResizing = false; - - // Clean up event handlers - if (this._resizeHandlers) { - document.removeEventListener('mousemove', this._resizeHandlers.move); - document.removeEventListener('mouseup', this._resizeHandlers.up); - delete this._resizeHandlers; - } - } - - /** - * Auto-resize panel to fit content size with viewport repositioning - */ - autoResizeToContent(event) { - return this.safeOperation(() => { - event.preventDefault(); - event.stopPropagation(); - - if (!this.isExpanded) return; - - const panel = this.element?.querySelector('.control-panel-expanded'); - const contentBody = this.element?.querySelector('.control-content-body'); - - if (!panel || !contentBody) return; - - // Get current panel position - const rect = panel.getBoundingClientRect(); - const currentLeft = rect.left; - const currentTop = rect.top; - - // Measure content size by temporarily allowing natural sizing - const originalOverflow = contentBody.style.overflow; - const originalMaxHeight = panel.style.maxHeight; - const originalHeight = panel.style.height; - const originalWidth = panel.style.width; - - // Temporarily remove constraints to measure natural size - contentBody.style.overflow = 'visible'; - panel.style.maxHeight = 'none'; - panel.style.height = 'auto'; - panel.style.width = 'auto'; - - // Force reflow and measure - panel.offsetHeight; // Force reflow - const contentRect = contentBody.getBoundingClientRect(); - const headerHeight = this.element.querySelector('.control-header')?.offsetHeight || 24; - - // Calculate ideal size with padding and margins - const idealWidth = Math.max(300, Math.min(window.innerWidth - 40, contentRect.width + 40)); - const idealHeight = Math.max(200, Math.min(window.innerHeight - 40, contentRect.height + headerHeight + 40)); - - // Restore original constraints - contentBody.style.overflow = originalOverflow; - panel.style.maxHeight = originalMaxHeight; - - // Calculate new position to keep panel in viewport - let newLeft = currentLeft; - let newTop = currentTop; - - // Adjust position if panel would go outside viewport - if (currentLeft + idealWidth > window.innerWidth) { - newLeft = window.innerWidth - idealWidth - 20; - } - if (newLeft < 20) { - newLeft = 20; - } - - if (currentTop + idealHeight > window.innerHeight) { - newTop = window.innerHeight - idealHeight - 20; - } - if (newTop < 20) { - newTop = 20; - } - - // Apply new size and position - panel.style.width = `${idealWidth}px`; - panel.style.height = `${idealHeight}px`; - - // Update position if it changed - if (newLeft !== currentLeft || newTop !== currentTop) { - this.element.style.left = `${newLeft}px`; - this.element.style.top = `${newTop}px`; - this.position.x = newLeft; - this.position.y = newTop; - } - - // Update internal size tracking - this.size.width = idealWidth; - this.size.height = idealHeight; - - }, null, 'autoResizeToContent'); - } - - /** - * Position the control based on compass position (used by show method) - */ - positionControl() { - if (!this.element) return; - - // Use the compass positioning from setupStyles - this.storeOriginalPosition(); - } - - /** - * Build the control content (to be overridden by subclasses) - */ - /** - * Build content with consistent styling - calls subclass generateContent() - */ - buildContent() { - const content = this.element?.querySelector('.control-content'); - if (content) { - // Get content from subclass - const innerContent = this.generateContent ? this.generateContent() : this.config.defaultContent; - - // Apply consistent container styling - content.innerHTML = ` -
-
- ${innerContent} -
-
- `; - } - } - - /** - * Generate content - subclasses should override this method - * @returns {string} HTML content for the panel body - */ - generateContent() { - return this.config.defaultContent || `

Panel content goes here...

`; - } - - /** - * Show the control - */ - show() { - return this.safeOperation(() => { - if (!this.element) { - this.createElement(); - } - - document.body.appendChild(this.element); - this.positionControl(); - this.buildContent(); - - return this.element; - }, null, 'show'); - } - - /** - * Hide the control - */ - hide() { - return this.safeOperation(() => { - if (this.element && this.element.parentNode) { - this.element.parentNode.removeChild(this.element); - } - }, null, 'hide'); - } - - /** - * Destroy the control and clean up resources - */ - destroy() { - return this.safeOperation(() => { - // Clean up event listeners - for (const [element, event, handler] of this.eventHandlers.values()) { - element.removeEventListener(event, handler); - } - this.eventHandlers.clear(); - - // Remove element from DOM - if (this.element && this.element.parentNode) { - this.element.parentNode.removeChild(this.element); - } - - this.element = null; - }, null, 'destroy'); - } -} - -// Export for module systems or attach to global for direct usage -if (typeof module !== 'undefined' && module.exports) { - module.exports = ControlBase; -} else { - window.ControlBase = ControlBase; -} \ No newline at end of file diff --git a/testdrive-jsui/static/js/controls/debug-control.js b/testdrive-jsui/static/js/controls/debug-control.js deleted file mode 100644 index 431659ee..00000000 --- a/testdrive-jsui/static/js/controls/debug-control.js +++ /dev/null @@ -1,483 +0,0 @@ -/** - * DebugControl - System Debug Information and Message Display Control - * - * Provides comprehensive debugging capabilities including system message display, - * error tracking, performance monitoring, and development tools. Essential for - * troubleshooting and development workflows within the TestDrive-JSUI environment. - * - * Features: - * - Real-time debug message display with categorization - * - Error tracking with stack trace information - * - Performance metrics and timing measurements - * - System information display (browser, viewport, etc.) - * - Message filtering and search capabilities - * - Export functionality for debug logs - * - Integration with MarkitectDebugSystem - * - * Dependencies: - * - ControlBase (base control functionality) - * - MarkitectDebugSystem (optional, for enhanced debugging) - */ - -/** - * DebugControl - Development and debugging information control - * - * This control serves as a central hub for all debugging activities, - * providing developers with essential information for troubleshooting - * and performance optimization. - */ -class DebugControl extends ControlBase { - constructor() { - super(); - - // Configure for debug functionality - this.config = { - icon: 'πŸ›', - title: 'Debug', - className: 'debug-control', - defaultContent: 'Debug information loading...', - ariaLabel: 'Debug Information Control', - position: 'w' // West positioning - }; - - // Debug control state - this.messages = []; - this.maxMessages = 100; - this.messageFilter = 'all'; // 'all', 'error', 'warn', 'info', 'debug' - this.autoScroll = true; - this.isRecording = true; - this.startTime = Date.now(); - this.performanceMarks = new Map(); - - this.initializeDebugCapture(); - } - - /** - * Initialize debug message capture - */ - initializeDebugCapture() { - return this.safeOperation(() => { - // Capture console messages - this.originalConsole = { - log: console.log, - error: console.error, - warn: console.warn, - info: console.info, - debug: console.debug - }; - - // Override console methods to capture messages - console.log = (...args) => { - this.originalConsole.log(...args); - this.addDebugMessage('LOG', args.join(' '), 'info'); - }; - - console.error = (...args) => { - this.originalConsole.error(...args); - this.addDebugMessage('ERROR', args.join(' '), 'error'); - }; - - console.warn = (...args) => { - this.originalConsole.warn(...args); - this.addDebugMessage('WARN', args.join(' '), 'warn'); - }; - - console.info = (...args) => { - this.originalConsole.info(...args); - this.addDebugMessage('INFO', args.join(' '), 'info'); - }; - - console.debug = (...args) => { - this.originalConsole.debug(...args); - this.addDebugMessage('DEBUG', args.join(' '), 'debug'); - }; - - // Capture global errors - window.addEventListener('error', (event) => { - this.addDebugMessage('ERROR', `${event.message} at ${event.filename}:${event.lineno}`, 'error'); - }); - - // Capture unhandled promise rejections - window.addEventListener('unhandledrejection', (event) => { - this.addDebugMessage('PROMISE_REJECT', `Unhandled promise rejection: ${event.reason}`, 'error'); - }); - - }, null, 'initializeDebugCapture'); - } - - /** - * Add a debug message to the log - */ - addDebugMessage(category, message, level = 'info') { - return this.safeOperation(() => { - if (!this.isRecording) return; - - const debugMessage = { - id: Date.now() + Math.random(), - timestamp: Date.now(), - category, - message, - level, - displayTime: new Date().toLocaleTimeString(), - relativeTime: Date.now() - this.startTime - }; - - this.messages.push(debugMessage); - - // Limit message history - if (this.messages.length > this.maxMessages) { - this.messages.shift(); - } - - // Update display if visible - if (this.element && this.isExpanded) { - this.updateMessageDisplay(); - } - - }, null, 'addDebugMessage'); - } - - /** - * Get messages filtered by current filter setting - */ - getFilteredMessages() { - if (this.messageFilter === 'all') { - return this.messages; - } - return this.messages.filter(msg => msg.level === this.messageFilter); - } - - /** - * Generate system information HTML - */ - generateSystemInfoHTML() { - return this.safeOperation(() => { - const systemInfo = { - userAgent: navigator.userAgent, - viewport: `${window.innerWidth}x${window.innerHeight}`, - screen: `${screen.width}x${screen.height}`, - colorDepth: screen.colorDepth, - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - language: navigator.language, - cookieEnabled: navigator.cookieEnabled, - onlineStatus: navigator.onLine ? 'Online' : 'Offline', - protocol: window.location.protocol, - memory: performance.memory ? - `Used: ${Math.round(performance.memory.usedJSHeapSize / 1024 / 1024)}MB` : - 'Not available' - }; - - // Get Markitect version from config or default - const markitectVersion = window.markitectConfig?.version || 'Unknown'; - - return ` -
-
-
Markitect: ${markitectVersion}
-
Viewport: ${systemInfo.viewport}
-
Screen: ${systemInfo.screen}
-
Memory: ${systemInfo.memory}
-
Language: ${systemInfo.language}
-
Status: ${systemInfo.onlineStatus}
-
Protocol: ${systemInfo.protocol}
-
-
- `; - - }, '', 'generateSystemInfoHTML'); - } - - /** - * Generate performance metrics HTML - */ - generatePerformanceHTML() { - return this.safeOperation(() => { - const timing = performance.timing; - const navigation = performance.getEntriesByType('navigation')[0]; - - const metrics = { - pageLoad: timing.loadEventEnd - timing.navigationStart, - domReady: timing.domContentLoadedEventEnd - timing.navigationStart, - firstByte: timing.responseStart - timing.navigationStart, - uptime: Date.now() - this.startTime, - messagesCount: this.messages.length - }; - - return ` -
- Performance Metrics:
-
-
Page Load: ${metrics.pageLoad}ms
-
DOM Ready: ${metrics.domReady}ms
-
First Byte: ${metrics.firstByte}ms
-
Session Time: ${Math.round(metrics.uptime / 1000)}s
-
Debug Messages: ${metrics.messagesCount}
-
-
- `; - - }, '', 'generatePerformanceHTML'); - } - - /** - * Generate debug messages HTML - */ - generateMessagesHTML() { - return this.safeOperation(() => { - const filteredMessages = this.getFilteredMessages(); - - if (filteredMessages.length === 0) { - return ` -
- No ${this.messageFilter === 'all' ? '' : this.messageFilter + ' '}messages yet -
- `; - } - - const messagesHTML = filteredMessages.slice(-20).map(msg => { - const levelColors = { - error: '#dc3545', - warn: '#ffc107', - info: '#17a2b8', - debug: '#6c757d' - }; - - const backgroundColor = levelColors[msg.level] || '#6c757d'; - const textColor = msg.level === 'warn' ? '#000' : '#fff'; - - return ` -
-
- - ${msg.category} - - - ${msg.displayTime} - -
-
- ${msg.message} -
-
- `; - }).join(''); - - return ` -
- ${messagesHTML} -
- `; - - }, '

Error displaying messages

', 'generateMessagesHTML'); - } - - /** - * Generate control buttons HTML - */ - generateControlButtonsHTML() { - return ` -
- - - - - - - -
- `; - } - - /** - * Generate filter controls HTML - */ - generateFilterControlsHTML() { - const filters = ['all', 'error', 'warn', 'info', 'debug']; - - const filterButtons = filters.map(filter => { - const isActive = this.messageFilter === filter; - return ` - - `; - }).join(''); - - return ` -
-
Filter:
- ${filterButtons} -
- `; - } - - /** - * Update the message display - */ - updateMessageDisplay() { - return this.safeOperation(() => { - const messagesContainer = this.element?.querySelector('.messages-container'); - if (messagesContainer) { - const parent = messagesContainer.parentElement; - parent.innerHTML = this.generateMessagesHTML(); - - // Auto-scroll to bottom if enabled - if (this.autoScroll) { - const newContainer = parent.querySelector('.messages-container'); - if (newContainer) { - newContainer.scrollTop = newContainer.scrollHeight; - } - } - } - }, null, 'updateMessageDisplay'); - } - - /** - * Clear all debug messages - */ - clearMessages() { - this.messages = []; - if (window.MarkitectDebugSystem) { - window.MarkitectDebugSystem.clearMessages(); - } - this.buildContent(); - } - - /** - * Export debug messages to file - */ - exportMessages() { - return this.safeOperation(() => { - const exportData = { - timestamp: new Date().toISOString(), - session: { - startTime: new Date(this.startTime).toISOString(), - duration: Date.now() - this.startTime, - messageCount: this.messages.length - }, - system: { - userAgent: navigator.userAgent, - viewport: `${window.innerWidth}x${window.innerHeight}`, - url: window.location.href - }, - messages: this.messages - }; - - const dataStr = JSON.stringify(exportData, null, 2); - const dataBlob = new Blob([dataStr], { type: 'application/json' }); - const url = URL.createObjectURL(dataBlob); - - const link = document.createElement('a'); - link.href = url; - link.download = `debug-log-${new Date().toISOString().split('T')[0]}.json`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - - URL.revokeObjectURL(url); - this.addDebugMessage('EXPORT', 'Debug log exported successfully', 'info'); - - }, null, 'exportMessages'); - } - - /** - * Toggle message recording - */ - toggleRecording() { - this.isRecording = !this.isRecording; - this.buildContent(); - this.addDebugMessage('CONTROL', `Recording ${this.isRecording ? 'started' : 'paused'}`, 'info'); - } - - /** - * Add a test message - */ - addTestMessage() { - const testMessages = [ - { category: 'TEST', message: 'This is a test info message', level: 'info' }, - { category: 'TEST', message: 'This is a test warning message', level: 'warn' }, - { category: 'TEST', message: 'This is a test error message', level: 'error' }, - { category: 'TEST', message: 'This is a test debug message', level: 'debug' } - ]; - - const randomMessage = testMessages[Math.floor(Math.random() * testMessages.length)]; - this.addDebugMessage(randomMessage.category, randomMessage.message, randomMessage.level); - } - - /** - * Set message filter - */ - setMessageFilter(filter) { - this.messageFilter = filter; - this.buildContent(); - } - - /** - * Generate debug control content (called by base class buildContent) - */ - generateContent() { - return this.safeOperation(() => { - return ` - ${this.generateSystemInfoHTML()} - ${this.generatePerformanceHTML()} - ${this.generateFilterControlsHTML()} - ${this.generateMessagesHTML()} - ${this.generateControlButtonsHTML()} - -
- Recording: ${this.isRecording ? '🟒 Active' : 'πŸ”΄ Paused'} | - Filter: ${this.messageFilter.toUpperCase()} | - Messages: ${this.getFilteredMessages().length}/${this.messages.length} -
- `; - }, 'Error generating debug content', 'generateContent'); - } - - /** - * Override buildContent to add control reference - */ - buildContent() { - super.buildContent(); - - // Store reference to this control for onclick handlers - if (this.element) { - this.element.debugControl = this; - } - } - - /** - * Clean up resources when control is destroyed - */ - destroy() { - // Restore original console methods - if (this.originalConsole) { - console.log = this.originalConsole.log; - console.error = this.originalConsole.error; - console.warn = this.originalConsole.warn; - console.info = this.originalConsole.info; - console.debug = this.originalConsole.debug; - } - - super.destroy(); - } -} - -// Export for module systems or attach to global for direct usage -if (typeof module !== 'undefined' && module.exports) { - module.exports = DebugControl; -} else { - window.DebugControl = DebugControl; -} \ No newline at end of file diff --git a/testdrive-jsui/static/js/controls/edit-control.js b/testdrive-jsui/static/js/controls/edit-control.js deleted file mode 100644 index 1f36891f..00000000 --- a/testdrive-jsui/static/js/controls/edit-control.js +++ /dev/null @@ -1,573 +0,0 @@ -/** - * EditControl - Document Editing Tools and Actions Control - * - * Provides a comprehensive set of document editing tools including text formatting, - * document actions (print, save, export), navigation helpers, and editing modes. - * Designed to enhance the writing and editing experience within the TestDrive-JSUI - * environment. - * - * Features: - * - Document actions (print, save, export to various formats) - * - Text formatting tools (bold, italic, headers) - * - Navigation helpers (scroll to top/bottom, go to line) - * - Word processing features (find/replace, word count) - * - Accessibility tools (font size, contrast adjustment) - * - Markdown formatting shortcuts - * - * Dependencies: - * - ControlBase (base control functionality) - */ - -/** - * EditControl - Comprehensive document editing control - * - * This control provides writers and editors with essential tools for document - * creation and modification. It includes both basic text operations and - * advanced features for content management and formatting. - */ -class EditControl extends ControlBase { - constructor() { - super(); - - // Configure for editing functionality - this.config = { - icon: '✏️', - title: 'Edit', - className: 'edit-control', - defaultContent: 'Document editing tools loading...', - ariaLabel: 'Document Edit Control', - position: 'e' // East positioning - }; - - // Edit control state - this.editingMode = 'view'; // 'view', 'edit', 'preview' - this.fontSize = 16; - this.lastSaveTime = null; - this.unsavedChanges = false; - this.shortcuts = new Map(); - - this.initializeShortcuts(); - } - - /** - * Initialize keyboard shortcuts for editing - */ - initializeShortcuts() { - this.shortcuts.set('Ctrl+S', () => this.saveDocument()); - this.shortcuts.set('Ctrl+P', () => this.printDocument()); - this.shortcuts.set('Ctrl+F', () => this.showFindDialog()); - this.shortcuts.set('Ctrl+B', () => this.toggleBold()); - this.shortcuts.set('Ctrl+I', () => this.toggleItalic()); - this.shortcuts.set('Escape', () => this.exitEditMode()); - } - - /** - * Generate the main editing tools HTML - */ - generateEditToolsHTML() { - return this.safeOperation(() => { - return ` - -
-
Document Actions
- - - - - - - - -
- - - - - -
-
Text Tools
- - - -
- - - -
- - -
- - -
-
Markdown Tools
- -
- - - - - - - -
-
- - -
-
-
Mode: ${this.editingMode}
-
Font: ${this.fontSize}px
- ${this.lastSaveTime ? `
Saved: ${new Date(this.lastSaveTime).toLocaleTimeString()}
` : ''} - ${this.unsavedChanges ? '
⚠️ Unsaved changes
' : ''} -
-
- `; - - }, '

Error generating edit tools

', 'generateEditToolsHTML'); - } - - /** - * Print the document - */ - printDocument() { - return this.safeOperation(() => { - window.print(); - - // Show feedback - this.showActionFeedback('πŸ–¨οΈ Print dialog opened', '#28a745'); - }, null, 'printDocument'); - } - - /** - * Save document (placeholder - would integrate with actual save system) - */ - saveDocument() { - return this.safeOperation(() => { - // In a real implementation, this would save to a backend - this.lastSaveTime = Date.now(); - this.unsavedChanges = false; - - // Update display - this.buildContent(); - - // Show feedback - this.showActionFeedback('πŸ’Ύ Document saved', '#007bff'); - }, null, 'saveDocument'); - } - - /** - * Export document to various formats - */ - exportDocument() { - return this.safeOperation(() => { - const contentArea = document.querySelector('#markitect-content') || document.body; - const htmlContent = contentArea.innerHTML; - const textContent = contentArea.textContent; - - // Create export menu - const exportMenu = document.createElement('div'); - exportMenu.style.cssText = ` - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: white; - border: 2px solid #007bff; - border-radius: 8px; - padding: 1rem; - z-index: 10000; - box-shadow: 0 4px 20px rgba(0,0,0,0.3); - `; - - exportMenu.innerHTML = ` -
Export Document
- - - - - `; - - // Add export functions - exportMenu.exportAsHTML = () => { - this.downloadFile(htmlContent, 'document.html', 'text/html'); - document.body.removeChild(exportMenu); - }; - - exportMenu.exportAsText = () => { - this.downloadFile(textContent, 'document.txt', 'text/plain'); - document.body.removeChild(exportMenu); - }; - - exportMenu.exportAsMarkdown = () => { - // Simple HTML to Markdown conversion (basic) - let markdown = htmlContent - .replace(/]*>(.*?)<\/h1>/gi, '# $1\n\n') - .replace(/]*>(.*?)<\/h2>/gi, '## $1\n\n') - .replace(/]*>(.*?)<\/h3>/gi, '### $1\n\n') - .replace(/]*>(.*?)<\/p>/gi, '$1\n\n') - .replace(/]*>(.*?)<\/strong>/gi, '**$1**') - .replace(/]*>(.*?)<\/em>/gi, '*$1*') - .replace(/<[^>]*>/g, ''); // Remove remaining HTML tags - - this.downloadFile(markdown, 'document.md', 'text/markdown'); - document.body.removeChild(exportMenu); - }; - - document.body.appendChild(exportMenu); - - }, null, 'exportDocument'); - } - - /** - * Download a file with given content - */ - downloadFile(content, filename, mimeType) { - const blob = new Blob([content], { type: mimeType }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - } - - /** - * Reset all changes and restore document to original state - */ - resetAll() { - return this.safeOperation(() => { - // Show confirmation dialog - const confirmed = window.confirm( - 'Reset all changes?\n\nThis will:\n' + - 'β€’ Restore document to original state\n' + - 'β€’ Clear all unsaved changes\n' + - 'β€’ Reset font size and other settings\n\n' + - 'This action cannot be undone.' - ); - - if (!confirmed) { - this.showActionFeedback('🚫 Reset cancelled', '#6c757d'); - return; - } - - // Reset edit control state - this.fontSize = 16; - this.editingMode = 'view'; - this.unsavedChanges = false; - this.lastSaveTime = null; - - // Reset font size - this.applyFontSize(); - - // Clear any highlights - document.querySelectorAll('.edit-highlight').forEach(el => { - el.outerHTML = el.innerHTML; - }); - - // Try to reset sections if SectionManager is available - if (window.sectionManager && typeof window.sectionManager.resetAllSections === 'function') { - window.sectionManager.resetAllSections(); - } - - // Try to reset document controls if available - if (window.documentControls && typeof window.documentControls.resetAllChanges === 'function') { - window.documentControls.resetAllChanges(); - } - - // Clear any debug messages if debug control is available - if (window.debugControl && typeof window.debugControl.clearMessages === 'function') { - window.debugControl.clearMessages(); - } - - // Reload the page as ultimate fallback - if (window.confirm('Reload page to complete reset?')) { - window.location.reload(); - return; - } - - // Update the control display - this.buildContent(); - - // Show feedback - this.showActionFeedback('πŸ”„ All changes reset', '#ffc107', '#212529'); - - }, null, 'resetAll'); - } - - /** - * Scroll to top of document - */ - scrollToTop() { - window.scrollTo({ top: 0, behavior: 'smooth' }); - this.showActionFeedback('⬆️ Scrolled to top', '#6c757d'); - } - - /** - * Scroll to bottom of document - */ - scrollToBottom() { - window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); - this.showActionFeedback('⬇️ Scrolled to bottom', '#6c757d'); - } - - /** - * Show go to line dialog - */ - showGoToLine() { - const lineNumber = prompt('Go to line number:'); - if (lineNumber && !isNaN(lineNumber)) { - // Simple implementation - scroll to approximate position - const totalHeight = document.body.scrollHeight; - const approximatePosition = (parseInt(lineNumber) / 100) * totalHeight; - window.scrollTo({ top: approximatePosition, behavior: 'smooth' }); - this.showActionFeedback(`🎯 Went to line ${lineNumber}`, '#6c757d'); - } - } - - /** - * Show find and replace dialog - */ - showFindReplace() { - const searchTerm = prompt('Find text:'); - if (searchTerm) { - // Simple highlight implementation - this.highlightText(searchTerm); - this.showActionFeedback(`πŸ” Highlighted "${searchTerm}"`, '#ffc107', '#000'); - } - } - - /** - * Highlight text in the document - */ - highlightText(searchTerm) { - return this.safeOperation(() => { - // Remove previous highlights - document.querySelectorAll('.edit-highlight').forEach(el => { - el.outerHTML = el.innerHTML; - }); - - // Add new highlights - const contentArea = document.querySelector('#markitect-content') || document.body; - const walker = document.createTreeWalker( - contentArea, - NodeFilter.SHOW_TEXT, - null, - false - ); - - const textNodes = []; - let node; - while (node = walker.nextNode()) { - textNodes.push(node); - } - - textNodes.forEach(textNode => { - const parent = textNode.parentNode; - const text = textNode.textContent; - if (text.toLowerCase().includes(searchTerm.toLowerCase())) { - const regex = new RegExp(`(${searchTerm})`, 'gi'); - const highlightedHTML = text.replace(regex, '$1'); - - const wrapper = document.createElement('div'); - wrapper.innerHTML = highlightedHTML; - while (wrapper.firstChild) { - parent.insertBefore(wrapper.firstChild, textNode); - } - parent.removeChild(textNode); - } - }); - }, null, 'highlightText'); - } - - /** - * Increase font size - */ - increaseFontSize() { - this.fontSize = Math.min(this.fontSize + 2, 24); - this.applyFontSize(); - this.buildContent(); - } - - /** - * Decrease font size - */ - decreaseFontSize() { - this.fontSize = Math.max(this.fontSize - 2, 12); - this.applyFontSize(); - this.buildContent(); - } - - /** - * Apply font size to document - */ - applyFontSize() { - const contentArea = document.querySelector('#markitect-content') || document.body; - contentArea.style.fontSize = `${this.fontSize}px`; - } - - /** - * Copy page link to clipboard - */ - copyLink() { - return this.safeOperation(() => { - const url = window.location.href; - if (navigator.clipboard) { - navigator.clipboard.writeText(url).then(() => { - this.showActionFeedback('πŸ“‹ Link copied to clipboard', '#fd7e14'); - }); - } else { - // Fallback for older browsers - prompt('Copy this link:', url); - this.showActionFeedback('πŸ“‹ Link displayed for copying', '#fd7e14'); - } - }, null, 'copyLink'); - } - - /** - * Insert markdown formatting - */ - insertMarkdown(prefix, suffix, placeholder) { - // This would integrate with an actual text editor - // For now, just show what would be inserted - const text = `${prefix}${placeholder}${suffix}`; - if (navigator.clipboard) { - navigator.clipboard.writeText(text); - this.showActionFeedback(`πŸ“‹ Copied: ${text}`, '#495057'); - } else { - prompt('Markdown to copy:', text); - } - } - - /** - * Show action feedback message - */ - showActionFeedback(message, backgroundColor, color = 'white') { - const feedback = document.createElement('div'); - feedback.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - background: ${backgroundColor}; - color: ${color}; - padding: 0.5rem 1rem; - border-radius: 4px; - z-index: 9999; - font-size: 0.8rem; - box-shadow: 0 2px 10px rgba(0,0,0,0.2); - `; - feedback.textContent = message; - document.body.appendChild(feedback); - - setTimeout(() => { - if (feedback.parentNode) { - document.body.removeChild(feedback); - } - }, 3000); - } - - /** - * Build the control content - * Override of base class method to provide edit-specific functionality - */ - /** - * Generate edit control content (called by base class buildContent) - */ - generateContent() { - return this.safeOperation(() => { - return this.generateEditToolsHTML(); - }, 'Error generating edit content', 'generateContent'); - } - - /** - * Override buildContent to add control reference - */ - buildContent() { - super.buildContent(); - - // Store reference to this control for onclick handlers - if (this.element) { - this.element.editControl = this; - } - } - - /** - * Exit edit mode - */ - exitEditMode() { - this.editingMode = 'view'; - this.buildContent(); - } -} - -// Export for module systems or attach to global for direct usage -if (typeof module !== 'undefined' && module.exports) { - module.exports = EditControl; -} else { - window.EditControl = EditControl; -} \ No newline at end of file diff --git a/testdrive-jsui/static/js/controls/status-control.js b/testdrive-jsui/static/js/controls/status-control.js deleted file mode 100644 index 4f1ae60c..00000000 --- a/testdrive-jsui/static/js/controls/status-control.js +++ /dev/null @@ -1,371 +0,0 @@ -/** - * StatusControl - Document Statistics and Change Tracking Control - * - * Provides real-time document statistics including word count, character count, - * reading time estimation, and change tracking. Monitors document modifications - * and provides insights into document structure and content metrics. - * - * Features: - * - Real-time word and character counting - * - Reading time estimation based on content - * - Document structure analysis (headings, paragraphs, lists) - * - Change tracking with before/after comparisons - * - Content complexity metrics - * - Export functionality for statistics - * - * Dependencies: - * - ControlBase (base control functionality) - */ - -/** - * StatusControl - Document statistics and monitoring control - * - * This control continuously monitors the document for changes and provides - * detailed statistics about content, structure, and reading metrics. - * Useful for writers, editors, and content creators. - */ -class StatusControl extends ControlBase { - constructor() { - super(); - - // Configure for status functionality - this.config = { - icon: 'πŸ“Š', - title: 'Status', - className: 'status-control', - defaultContent: 'Loading document statistics...', - ariaLabel: 'Document Status Control', - position: 'e' // East positioning - }; - - // Status tracking state - this.stats = { - characters: 0, - charactersNoSpaces: 0, - words: 0, - sentences: 0, - paragraphs: 0, - headings: 0, - lists: 0, - images: 0, - links: 0, - readingTimeMinutes: 0 - }; - - this.previousStats = { ...this.stats }; - this.lastUpdateTime = null; - this.updateInterval = null; - this.wordsPerMinute = 200; // Average reading speed - } - - /** - * Extract and count document content statistics - */ - analyzeDocument() { - return this.safeOperation(() => { - const contentArea = document.querySelector('#markitect-content') || document.body; - const textContent = contentArea.textContent || ''; - - // Basic text statistics - this.stats.characters = textContent.length; - this.stats.charactersNoSpaces = textContent.replace(/\s/g, '').length; - - // Word counting (more accurate) - const words = textContent.trim().split(/\s+/).filter(word => word.length > 0); - this.stats.words = words.length; - - // Sentence counting (approximate) - const sentences = textContent.split(/[.!?]+/).filter(s => s.trim().length > 0); - this.stats.sentences = sentences.length; - - // Structural elements - this.stats.paragraphs = contentArea.querySelectorAll('p').length; - this.stats.headings = contentArea.querySelectorAll('h1, h2, h3, h4, h5, h6').length; - this.stats.lists = contentArea.querySelectorAll('ul, ol').length; - this.stats.images = contentArea.querySelectorAll('img').length; - this.stats.links = contentArea.querySelectorAll('a').length; - - // Reading time calculation - this.stats.readingTimeMinutes = Math.ceil(this.stats.words / this.wordsPerMinute); - - this.lastUpdateTime = Date.now(); - return this.stats; - - }, this.stats, 'analyzeDocument'); - } - - /** - * Calculate changes since last analysis - */ - calculateChanges() { - return this.safeOperation(() => { - const changes = {}; - for (const [key, currentValue] of Object.entries(this.stats)) { - const previousValue = this.previousStats[key] || 0; - const difference = currentValue - previousValue; - changes[key] = { - current: currentValue, - previous: previousValue, - change: difference, - hasChanged: difference !== 0 - }; - } - return changes; - }, {}, 'calculateChanges'); - } - - /** - * Format statistics for display - */ - formatStatistics() { - return this.safeOperation(() => { - const changes = this.calculateChanges(); - - const formatChange = (changeData) => { - if (!changeData.hasChanged) return ''; - const sign = changeData.change > 0 ? '+' : ''; - const color = changeData.change > 0 ? '#28a745' : '#dc3545'; - return ` (${sign}${changeData.change})`; - }; - - const formatNumber = (num) => num.toLocaleString(); - - return ` -
-
- Words:
- ${formatNumber(this.stats.words)} - ${formatChange(changes.words)} -
- -
- Characters:
- ${formatNumber(this.stats.characters)} - ${formatChange(changes.characters)} -
- -
- Reading Time:
- ${this.stats.readingTimeMinutes} min - ${formatChange(changes.readingTimeMinutes)} -
- -
- Sentences:
- ${formatNumber(this.stats.sentences)} - ${formatChange(changes.sentences)} -
-
- -
-
Document Structure
- -
- Paragraphs: - ${this.stats.paragraphs}${formatChange(changes.paragraphs)} -
- -
- Headings: - ${this.stats.headings}${formatChange(changes.headings)} -
- -
- Lists: - ${this.stats.lists}${formatChange(changes.lists)} -
- -
- Images: - ${this.stats.images}${formatChange(changes.images)} -
- -
- Links: - ${this.stats.links}${formatChange(changes.links)} -
-
- -
- - - -
- - ${this.lastUpdateTime ? ` -
- Updated: ${new Date(this.lastUpdateTime).toLocaleTimeString()} -
- ` : ''} - `; - - }, '

Error displaying statistics

', 'formatStatistics'); - } - - /** - * Refresh statistics and update display - */ - refreshStats() { - return this.safeOperation(() => { - // Save current stats as previous - this.previousStats = { ...this.stats }; - - // Analyze document - this.analyzeDocument(); - - // Update display - this.buildContent(); - - // Show success feedback - const refreshBtn = this.element?.querySelector('button'); - if (refreshBtn) { - const originalText = refreshBtn.innerHTML; - refreshBtn.innerHTML = 'βœ… Updated'; - - setTimeout(() => { - refreshBtn.innerHTML = originalText; - }, 1000); - } - - }, null, 'refreshStats'); - } - - /** - * Export statistics to various formats - */ - exportStats() { - return this.safeOperation(() => { - const exportData = { - timestamp: new Date().toISOString(), - document: { - title: document.title || 'Untitled Document', - url: window.location.href - }, - statistics: this.stats, - metadata: { - wordsPerMinute: this.wordsPerMinute, - analysisDate: new Date(this.lastUpdateTime).toISOString() - } - }; - - // Create downloadable JSON - const dataStr = JSON.stringify(exportData, null, 2); - const dataBlob = new Blob([dataStr], { type: 'application/json' }); - const url = URL.createObjectURL(dataBlob); - - // Create temporary download link - const link = document.createElement('a'); - link.href = url; - link.download = `document-stats-${new Date().toISOString().split('T')[0]}.json`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - - // Clean up - URL.revokeObjectURL(url); - - // Show feedback - const exportBtn = this.element?.querySelector('button:last-child'); - if (exportBtn) { - const originalText = exportBtn.innerHTML; - exportBtn.innerHTML = 'βœ… Exported'; - exportBtn.style.background = '#28a745'; - - setTimeout(() => { - exportBtn.innerHTML = originalText; - exportBtn.style.background = '#28a745'; - }, 2000); - } - - }, null, 'exportStats'); - } - - /** - * Get reading difficulty score (Flesch Reading Ease approximation) - */ - calculateReadabilityScore() { - return this.safeOperation(() => { - if (this.stats.sentences === 0 || this.stats.words === 0) { - return { score: 0, level: 'Unknown' }; - } - - const avgWordsPerSentence = this.stats.words / this.stats.sentences; - const avgSyllablesPerWord = 1.5; // Simplified approximation - - // Flesch Reading Ease formula (simplified) - const score = 206.835 - (1.015 * avgWordsPerSentence) - (84.6 * avgSyllablesPerWord); - - let level; - if (score >= 90) level = 'Very Easy'; - else if (score >= 80) level = 'Easy'; - else if (score >= 70) level = 'Fairly Easy'; - else if (score >= 60) level = 'Standard'; - else if (score >= 50) level = 'Fairly Difficult'; - else if (score >= 30) level = 'Difficult'; - else level = 'Very Difficult'; - - return { score: Math.round(score), level }; - }, { score: 0, level: 'Unknown' }, 'calculateReadabilityScore'); - } - - /** - * Build the control content - * Override of base class method to provide status-specific functionality - */ - /** - * Generate status control content (called by base class buildContent) - */ - generateContent() { - // Analyze document first - this.analyzeDocument(); - - return this.safeOperation(() => { - return this.formatStatistics(); - }, 'Error generating status content', 'generateContent'); - } - - /** - * Override buildContent to add control reference and auto-refresh - */ - buildContent() { - super.buildContent(); - - // Store reference to this control for onclick handlers - if (this.element) { - this.element.statusControl = this; - } - - // Set up auto-refresh for dynamic content - if (this.updateInterval) { - clearInterval(this.updateInterval); - } - - this.updateInterval = setInterval(() => { - this.refreshStats(); - }, 10000); // Update every 10 seconds - } - - /** - * Clean up resources when control is destroyed - */ - destroy() { - if (this.updateInterval) { - clearInterval(this.updateInterval); - this.updateInterval = null; - } - super.destroy(); - } -} - -// Export for module systems or attach to global for direct usage -if (typeof module !== 'undefined' && module.exports) { - module.exports = StatusControl; -} else { - window.StatusControl = StatusControl; -} \ No newline at end of file diff --git a/testdrive-jsui/static/js/tests/test_edit.html b/testdrive-jsui/static/js/tests/test_edit.html index 8076d97e..813b65bf 100644 --- a/testdrive-jsui/static/js/tests/test_edit.html +++ b/testdrive-jsui/static/js/tests/test_edit.html @@ -131,8 +131,8 @@ - - + + diff --git a/testdrive-jsui/test.html b/testdrive-jsui/test.html index 69da30ef..073f1de3 100644 --- a/testdrive-jsui/test.html +++ b/testdrive-jsui/test.html @@ -110,11 +110,11 @@ - - - - - + + + + +