515 lines
20 KiB
JavaScript
515 lines
20 KiB
JavaScript
|
|
/**
|
|||
|
|
* 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 = `
|
|||
|
|
<div class="control-header" style="
|
|||
|
|
display: flex; align-items: center; justify-content: space-between;
|
|||
|
|
padding: 0.5rem; background: #f8f9fa; border-radius: 6px;
|
|||
|
|
cursor: pointer; user-select: none; font-size: 0.9rem;
|
|||
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1); border: 1px solid #dee2e6;
|
|||
|
|
transition: all 0.2s ease; min-width: 120px;">
|
|||
|
|
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|||
|
|
<span style="font-size: 1.2rem;">${safeIcon}</span>
|
|||
|
|
<span class="control-title" style="font-weight: 500; color: #495057;">${safeTitle}</span>
|
|||
|
|
</div>
|
|||
|
|
<button class="control-close" style="
|
|||
|
|
background: none; border: none; font-size: 1.1rem; color: #6c757d;
|
|||
|
|
cursor: pointer; padding: 0; width: 20px; height: 20px;
|
|||
|
|
display: none; align-items: center; justify-content: center;
|
|||
|
|
border-radius: 50%; transition: all 0.2s ease;"
|
|||
|
|
onmouseover="this.style.backgroundColor='#e9ecef'"
|
|||
|
|
onmouseout="this.style.backgroundColor=''"
|
|||
|
|
onclick="event.stopPropagation();">×</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="control-content" style="
|
|||
|
|
display: none; background: white; border: 1px solid #dee2e6;
|
|||
|
|
border-radius: 6px; margin-top: 0.5rem; max-width: 350px;
|
|||
|
|
min-width: 250px; max-height: 400px; overflow-y: auto;
|
|||
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);">
|
|||
|
|
<div style="padding: 1rem;">
|
|||
|
|
${safeContent}
|
|||
|
|
</div>
|
|||
|
|
<div class="control-footer" style="display: none;"></div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
// 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 = '<div style="color: red; padding: 0.5rem;">Control failed to load</div>';
|
|||
|
|
}
|
|||
|
|
}, '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;
|