/** * Base Control Class for Markitect UI Controls * Provides common functionality for positioning, drag, resize, expand/collapse * Supports Fail Fast strict mode for development */ // Development mode detection (must match main.js) const MARKITECT_STRICT_MODE = ( window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || window.location.search.includes('strict=true') || window.markitectStrictMode === true ); const Control = { // Default configuration config: { icon: '🔧', title: 'Control', className: 'base-control', defaultContent: 'Control content', ariaLabel: 'Base Control', position: 'w', // Default compass position: west (middle-left) footer: null // If null, will use default Markitect copyright }, // Utility functions for safe operations safeOperation: function(operation, fallback = null, context = 'Unknown') { try { return operation(); } catch (error) { console.warn(`Control operation failed in ${context}:`, error); // Fail Fast in development mode if (MARKITECT_STRICT_MODE) { console.error(`🚨 STRICT MODE: Control operation failed in ${context}`); throw error; // Re-throw for immediate debugging } if (window.MarkitectDebugSystem) { window.MarkitectDebugSystem.addMessage( `Safe operation failed: ${error.message}`, 'WARNING', 'Control', { context, eventType: 'ERROR' } ); } return typeof fallback === 'function' ? fallback() : fallback; } }, safeQuerySelector: function(selector, parent = document) { try { if (!parent || !parent.querySelector) { return null; } return parent.querySelector(selector); } catch (error) { console.warn(`Invalid selector: ${selector}`, error); return null; } }, safeQuerySelectorAll: function(selector, parent = document) { try { if (!parent || !parent.querySelectorAll) { return []; } return Array.from(parent.querySelectorAll(selector)); } catch (error) { console.warn(`Invalid selector: ${selector}`, error); return []; } }, // Version and default footer getMarkitectVersion: function() { return this.safeOperation(() => { // Try to get version from various sources if (window.markitectVersion) { return window.markitectVersion; } // Check for generator meta tag in document head const generatorMeta = this.safeQuerySelector('meta[name="generator"]'); if (generatorMeta) { const content = generatorMeta.getAttribute('content'); if (content && content.includes('Markitect')) { // Extract version from generator content // Expected formats: "Markitect 1.0.0" or "Markitect/1.0.0" or "Markitect v1.0.0" const versionMatch = content.match(/Markitect[\s\/v]*([\d\.]+[\w\-\.]*)/i); if (versionMatch && versionMatch[1]) { return versionMatch[1]; } } } // Fallback version with generation timestamp const now = new Date(); const timestamp = now.toISOString().slice(0, 19).replace('T', ' '); return `Generated ${timestamp}`; }, () => 'Unknown Version', 'getMarkitectVersion'); }, getDefaultFooter: function() { return `© Markitect ${this.getMarkitectVersion()}`; }, getFooter: function() { if (this.config.footer !== null) { return this.config.footer; } return this.getDefaultFooter(); }, // Compass positioning system (top-aligned for proper expansion) compassPositions: { 'n': { top: '20px', left: '50%', transform: 'translateX(-50%)' }, 'nne': { top: '40px', right: '120px' }, 'ne': { top: '20px', right: '20px' }, 'ene': { top: '80px', right: '20px' }, 'e': { top: '50vh', right: '20px', transform: 'translateY(-20px)' }, 'ese': { top: 'calc(50vh + 80px)', right: '20px', transform: 'translateY(-20px)' }, 'se': { bottom: '20px', right: '20px' }, 'sse': { bottom: '40px', right: '120px' }, 's': { bottom: '20px', left: '50%', transform: 'translateX(-50%)' }, 'ssw': { bottom: '40px', left: '120px' }, 'sw': { bottom: '20px', left: '20px' }, 'wsw': { bottom: '80px', left: '20px' }, 'w': { top: '50vh', left: '20px', transform: 'translateY(-20px)' }, 'wnw': { top: 'calc(50vh - 80px)', left: '20px', transform: 'translateY(-20px)' }, 'nw': { top: '20px', left: '20px' }, 'nnw': { top: '40px', left: '120px' } }, // State management isExpanded: false, isDragging: false, isResizing: false, element: null, createControl: function() { return this.safeOperation(() => { console.log(`Creating ${this.config.title} control...`); // Validate configuration if (!this.config || !this.config.title) { throw new Error('Invalid control configuration'); } // Ensure document.body exists if (!document.body) { throw new Error('Document body not available'); } // Create main control element this.element = document.createElement('div'); this.element.className = `control-panel ${this.config.className || ''}`; this.element.setAttribute('role', 'dialog'); this.element.setAttribute('aria-label', this.config.ariaLabel || this.config.title); // Position the control using compass system const position = this.compassPositions[this.config.position] || this.compassPositions['w']; Object.assign(this.element.style, { position: 'fixed', zIndex: '1000', ...position }); // Build the control structure this.buildControlStructure(); // Add to document document.body.appendChild(this.element); console.log(`${this.config.title} control created and positioned at ${this.config.position}`); return this.element; }, () => { console.error(`Failed to create ${this.config?.title || 'Unknown'} control`); return null; }, 'createControl'); }, buildControlStructure: function() { this.safeOperation(() => { if (!this.element) { throw new Error('Control element not available'); } // Sanitize configuration values const safeIcon = (this.config.icon || '🔧').replace(/[<>"'&]/g, ''); const safeTitle = (this.config.title || 'Control').replace(/[<>"'&]/g, ''); const safeContent = (this.config.defaultContent || 'Control content').replace(/[<>]/g, ''); this.element.innerHTML = `
${safeIcon} ${safeTitle}
${safeContent}
`; // Set up event listeners with error protection this.safeOperation(() => this.setupEventListeners(), null, 'setupEventListeners'); this.safeOperation(() => this.addResizeHandle(), null, 'addResizeHandle'); this.safeOperation(() => this.addDragFunctionality(), null, 'addDragFunctionality'); }, () => { console.error('Failed to build control structure'); if (this.element) { this.element.innerHTML = '
Control failed to load
'; } }, 'buildControlStructure'); }, setupEventListeners: function() { const header = this.safeQuerySelector('.control-header', this.element); const closeBtn = this.safeQuerySelector('.control-close', this.element); if (!header || !closeBtn) { console.warn('Control header or close button not found'); return; } // Toggle expand/collapse on header click header.addEventListener('click', (e) => { this.safeOperation(() => { e.stopPropagation(); this.toggle(); }, null, 'headerClick'); }); // Close button closeBtn.addEventListener('click', (e) => { this.safeOperation(() => { e.stopPropagation(); this.collapse(); }, null, 'closeClick'); }); // Show/hide close button and resize handle on hover with bounds checking this.element.addEventListener('mouseenter', () => { this.safeOperation(() => { if (this.isExpanded && closeBtn) { closeBtn.style.display = 'flex'; const resizeHandle = this.safeQuerySelector('.resize-handle', this.element); if (resizeHandle) { resizeHandle.style.display = 'block'; } } }, null, 'mouseEnter'); }); this.element.addEventListener('mouseleave', () => { this.safeOperation(() => { if (closeBtn) { closeBtn.style.display = 'none'; } const resizeHandle = this.safeQuerySelector('.resize-handle', this.element); if (resizeHandle) { resizeHandle.style.display = 'none'; } }, null, 'mouseLeave'); }); }, addResizeHandle: function() { const resizeHandle = document.createElement('div'); resizeHandle.className = 'resize-handle'; resizeHandle.innerHTML = ''; // Small circle via CSS resizeHandle.style.cssText = ` position: absolute; bottom: 2px; right: 2px; width: 8px; height: 8px; cursor: nw-resize; display: none; background: #6c757d; border-radius: 50%; `; this.element.appendChild(resizeHandle); // Resize functionality let startX, startY, startWidth, startHeight; resizeHandle.addEventListener('mousedown', (e) => { e.preventDefault(); e.stopPropagation(); this.isResizing = true; const content = this.element.querySelector('.control-content'); const rect = content.getBoundingClientRect(); startX = e.clientX; startY = e.clientY; startWidth = rect.width; startHeight = rect.height; document.addEventListener('mousemove', handleResize); document.addEventListener('mouseup', stopResize); }); const handleResize = (e) => { if (!this.isResizing) return; const content = this.element.querySelector('.control-content'); const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; const newWidth = Math.max(200, startWidth + deltaX); const newHeight = Math.max(100, startHeight + deltaY); content.style.width = `${newWidth}px`; content.style.height = `${newHeight}px`; }; const stopResize = () => { this.isResizing = false; document.removeEventListener('mousemove', handleResize); document.removeEventListener('mouseup', stopResize); }; }, addDragFunctionality: function() { const header = this.safeQuerySelector('.control-header', this.element); if (!header) { console.warn('Header not found for drag functionality'); return; } let startX, startY, startLeft, startTop, dragTimeout; header.addEventListener('mousedown', (e) => { this.safeOperation(() => { if (e.target.closest('.control-close')) return; // Clear any existing drag timeout if (dragTimeout) { clearTimeout(dragTimeout); } this.isDragging = true; const rect = this.element.getBoundingClientRect(); startX = e.clientX; startY = e.clientY; startLeft = rect.left; startTop = rect.top; document.addEventListener('mousemove', handleDrag); document.addEventListener('mouseup', stopDrag); // Safety timeout to prevent infinite dragging dragTimeout = setTimeout(() => { if (this.isDragging) { console.warn('Drag operation timed out'); stopDrag(); } }, 30000); // 30 second timeout }, null, 'dragStart'); }); const handleDrag = (e) => { this.safeOperation(() => { if (!this.isDragging || !this.element) return; const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; // Constrain to viewport bounds const viewportWidth = window.innerWidth || document.documentElement.clientWidth; const viewportHeight = window.innerHeight || document.documentElement.clientHeight; const newLeft = Math.max(0, Math.min(viewportWidth - 100, startLeft + deltaX)); const newTop = Math.max(0, Math.min(viewportHeight - 50, startTop + deltaY)); this.element.style.left = `${newLeft}px`; this.element.style.top = `${newTop}px`; this.element.style.right = 'auto'; this.element.style.bottom = 'auto'; this.element.style.transform = 'none'; }, null, 'dragMove'); }; const stopDrag = () => { this.safeOperation(() => { this.isDragging = false; if (dragTimeout) { clearTimeout(dragTimeout); dragTimeout = null; } document.removeEventListener('mousemove', handleDrag); document.removeEventListener('mouseup', stopDrag); }, null, 'dragStop'); }; }, expand: function() { this.safeOperation(() => { if (this.isExpanded) return; const content = this.safeQuerySelector('.control-content', this.element); const closeBtn = this.safeQuerySelector('.control-close', this.element); if (!content || !closeBtn) { console.warn('Control content or close button not found for expansion'); return; } content.style.display = 'block'; closeBtn.style.display = 'flex'; this.isExpanded = true; // Style footer this.styleFooter(); console.log(`${this.config.title || 'Unknown'} control expanded`); }, null, 'expand'); }, collapse: function() { this.safeOperation(() => { if (!this.isExpanded) return; const content = this.safeQuerySelector('.control-content', this.element); const closeBtn = this.safeQuerySelector('.control-close', this.element); const resizeHandle = this.safeQuerySelector('.resize-handle', this.element); if (content) { content.style.display = 'none'; content.style.width = ''; content.style.height = ''; } if (closeBtn) { closeBtn.style.display = 'none'; } if (resizeHandle) { resizeHandle.style.display = 'none'; } this.isExpanded = false; console.log(`${this.config.title || 'Unknown'} control collapsed`); }, null, 'collapse'); }, toggle: function() { this.safeOperation(() => { if (this.isExpanded) { this.collapse(); } else { if (this.buildContent) { this.buildContent(); } else { this.expand(); } } }, null, 'toggle'); }, styleFooter: function() { this.safeOperation(() => { const footer = this.safeQuerySelector('.control-footer', this.element); if (!footer) return; const footerText = this.getFooter(); if (footerText && footerText.trim()) { // Sanitize footer text const safeText = footerText.replace(/[<>"'&]/g, ''); footer.textContent = safeText; footer.style.cssText = ` display: block; padding: 0.5rem; font-size: 0.7rem; color: #6c757d; text-align: center; font-style: italic; background: #f8f9fa; border-top: 1px solid #e9ecef; border-radius: 0 0 6px 6px; `; } else { footer.style.display = 'none'; } }, null, 'styleFooter'); }, // Virtual method - should be overridden by specific controls buildContent: function() { this.safeOperation(() => { console.log(`${this.config.title || 'Unknown'} control - buildContent should be overridden`); this.expand(); }, () => { console.error('Failed to build content, expanding basic control'); this.expand(); }, 'buildContent'); } }; // Export for use in other modules window.Control = Control;