refactor: eliminate duplicate control files and consolidate to capabilities/
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -170,11 +170,11 @@
|
|||||||
<script src="markitect/static/js/core/debug-system.js"></script>
|
<script src="markitect/static/js/core/debug-system.js"></script>
|
||||||
|
|
||||||
<!-- Control system -->
|
<!-- Control system -->
|
||||||
<script src="markitect/static/js/controls/control-base.js"></script>
|
<script src="../js/controls/control-base.js"></script>
|
||||||
<script src="markitect/static/js/controls/status-control.js"></script>
|
<script src="../js/controls/status-control.js"></script>
|
||||||
<script src="markitect/static/js/controls/debug-control.js"></script>
|
<script src="../js/controls/debug-control.js"></script>
|
||||||
<script src="markitect/static/js/controls/contents-control.js"></script>
|
<script src="../js/controls/contents-control.js"></script>
|
||||||
<script src="markitect/static/js/controls/edit-control.js"></script>
|
<script src="../js/controls/edit-control.js"></script>
|
||||||
|
|
||||||
<!-- Main application -->
|
<!-- Main application -->
|
||||||
<script src="markitect/static/js/main.js"></script>
|
<script src="markitect/static/js/main.js"></script>
|
||||||
|
|||||||
@@ -87,10 +87,10 @@ function testFunction() {
|
|||||||
<script src="markitect/static/js/core/debug-system.js"></script>
|
<script src="markitect/static/js/core/debug-system.js"></script>
|
||||||
|
|
||||||
<!-- Load control base -->
|
<!-- Load control base -->
|
||||||
<script src="markitect/static/js/controls/control-base.js"></script>
|
<script src="../js/controls/control-base.js"></script>
|
||||||
|
|
||||||
<!-- Load specific controls -->
|
<!-- Load specific controls -->
|
||||||
<script src="markitect/static/js/controls/status-control.js"></script>
|
<script src="../js/controls/status-control.js"></script>
|
||||||
|
|
||||||
<!-- Load main initialization -->
|
<!-- Load main initialization -->
|
||||||
<script src="markitect/static/js/main.js"></script>
|
<script src="markitect/static/js/main.js"></script>
|
||||||
|
|||||||
@@ -195,8 +195,8 @@
|
|||||||
<script src="markitect/static/js/core/debug-system.js"></script>
|
<script src="markitect/static/js/core/debug-system.js"></script>
|
||||||
|
|
||||||
<!-- Control system -->
|
<!-- Control system -->
|
||||||
<script src="markitect/static/js/controls/control-base.js"></script>
|
<script src="../js/controls/control-base.js"></script>
|
||||||
<script src="markitect/static/js/controls/status-control.js"></script>
|
<script src="../js/controls/status-control.js"></script>
|
||||||
|
|
||||||
<!-- Main application -->
|
<!-- Main application -->
|
||||||
<script src="markitect/static/js/main.js"></script>
|
<script src="markitect/static/js/main.js"></script>
|
||||||
|
|||||||
@@ -43,11 +43,11 @@ class TestDriveJSUIEngine(RenderingEnginePlugin):
|
|||||||
"static/js/core/section-manager.js",
|
"static/js/core/section-manager.js",
|
||||||
"static/js/components/debug-panel.js",
|
"static/js/components/debug-panel.js",
|
||||||
"static/js/components/dom-renderer.js",
|
"static/js/components/dom-renderer.js",
|
||||||
"static/js/controls/control-base.js",
|
"../capabilities/testdrive-jsui/js/controls/control-base.js",
|
||||||
"static/js/controls/contents-control.js",
|
"../capabilities/testdrive-jsui/js/controls/contents-control.js",
|
||||||
"static/js/controls/status-control.js",
|
"../capabilities/testdrive-jsui/js/controls/status-control.js",
|
||||||
"static/js/controls/debug-control.js",
|
"../capabilities/testdrive-jsui/js/controls/debug-control.js",
|
||||||
"static/js/controls/edit-control.js",
|
"../capabilities/testdrive-jsui/js/controls/edit-control.js",
|
||||||
"static/js/config-loader.js",
|
"static/js/config-loader.js",
|
||||||
"static/js/main-updated.js"
|
"static/js/main-updated.js"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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 `
|
|
||||||
<div style="padding: 1rem; text-align: center; color: #666;">
|
|
||||||
<p>No headings found in document</p>
|
|
||||||
<button onclick="this.closest('.contents-control').contentsControl.refreshContents()"
|
|
||||||
style="padding: 0.5rem 1rem; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
🔄 Refresh
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchHTML = `
|
|
||||||
<div style="padding: 0.5rem; border-bottom: 1px solid #eee;">
|
|
||||||
<input type="text"
|
|
||||||
placeholder="Search headings..."
|
|
||||||
style="width: 100%; padding: 0.25rem; border: 1px solid #ddd; border-radius: 3px; font-size: 0.8rem;"
|
|
||||||
onkeyup="this.closest('.contents-control').contentsControl.handleSearch(this.value)">
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const contentsHTML = displayHeadings.map(heading => {
|
|
||||||
const indentLevel = Math.max(0, heading.level - 1);
|
|
||||||
const indentPx = indentLevel * 15;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="contents-item"
|
|
||||||
style="margin-bottom: 0.3rem; padding-left: ${indentPx}px;">
|
|
||||||
<a href="#${heading.id}"
|
|
||||||
onclick="event.preventDefault(); this.closest('.contents-control').contentsControl.navigateToHeading('${heading.id}')"
|
|
||||||
style="display: block; padding: 0.2rem 0; color: #007bff; text-decoration: none; font-size: 0.8rem; line-height: 1.2;"
|
|
||||||
onmouseover="this.style.backgroundColor='#f8f9fa'"
|
|
||||||
onmouseout="this.style.backgroundColor='transparent'">
|
|
||||||
<span class="heading-level" style="color: #666; margin-right: 0.3rem;">H${heading.level}</span>
|
|
||||||
<span class="heading-text">${heading.text}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div style="padding: 0;">
|
|
||||||
${searchHTML}
|
|
||||||
<div style="max-height: 300px; overflow-y: auto; padding: 0.5rem;">
|
|
||||||
<div style="margin-bottom: 0.5rem; font-size: 0.7rem; color: #666; text-align: center;">
|
|
||||||
Found ${displayHeadings.length} heading${displayHeadings.length !== 1 ? 's' : ''}
|
|
||||||
</div>
|
|
||||||
${contentsHTML}
|
|
||||||
</div>
|
|
||||||
<div style="padding: 0.5rem; border-top: 1px solid #eee; text-align: center;">
|
|
||||||
<button onclick="this.closest('.contents-control').contentsControl.refreshContents()"
|
|
||||||
style="padding: 0.3rem 0.6rem; font-size: 0.7rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
🔄 Refresh Contents
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
}, '<p>Error generating contents</p>', '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;
|
|
||||||
}
|
|
||||||
@@ -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 = `
|
|
||||||
<button class="control-toggle" aria-label="${this.config.ariaLabel}">${this.config.icon}</button>
|
|
||||||
<div class="control-panel-expanded" style="display: none;">
|
|
||||||
<div class="control-header">
|
|
||||||
<span class="control-icon">${this.config.icon}</span>
|
|
||||||
<span class="control-title">${this.config.title}</span>
|
|
||||||
<button class="control-close">✕</button>
|
|
||||||
</div>
|
|
||||||
<div class="control-content">
|
|
||||||
${this.config.defaultContent}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -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 = `
|
|
||||||
<div style="padding: 1rem; font-size: 0.8rem;">
|
|
||||||
<h4 style="margin-top: 0;">Debug Messages</h4>
|
|
||||||
<div style="max-height: 200px; overflow-y: auto;">
|
|
||||||
${messages.length > 0 ?
|
|
||||||
messages.slice(-10).map(msg =>
|
|
||||||
`<div style="margin-bottom: 0.5rem; padding: 0.3rem; background: #f8f9fa; border-radius: 3px;">
|
|
||||||
<strong>[${msg.category}]</strong> ${msg.component}: ${msg.message}
|
|
||||||
<div style="font-size: 0.7rem; color: #666;">${msg.displayTime}</div>
|
|
||||||
</div>`
|
|
||||||
).join('') :
|
|
||||||
'<p>No debug messages yet</p>'
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<button onclick="if(window.MarkitectDebugSystem) window.MarkitectDebugSystem.clearMessages(); this.closest('.control-panel').querySelector('.control-content').innerHTML = '<div style="padding: 1rem; font-size: 0.8rem;"><h4 style="margin-top: 0;">Debug Messages</h4><p>Messages cleared</p></div>'"
|
|
||||||
style="margin-top: 0.5rem; padding: 0.3rem 0.6rem; font-size: 0.7rem; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
Clear Messages
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.DebugControl = DebugControl;
|
|
||||||
@@ -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 `
|
|
||||||
<div style="padding: 1rem; font-size: 0.8rem;">
|
|
||||||
<h4 style="margin-top: 0; margin-bottom: 1rem;">Edit Tools</h4>
|
|
||||||
|
|
||||||
<!-- Document Actions -->
|
|
||||||
<div class="action-section" style="margin-bottom: 1rem;">
|
|
||||||
<h5 style="margin: 0 0 0.5rem 0; font-size: 0.9em; color: #666;">Document Actions</h5>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.printDocument()"
|
|
||||||
style="width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
|
|
||||||
🖨️ Print Document
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.saveDocument()"
|
|
||||||
style="width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
|
|
||||||
💾 Save Changes
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.exportDocument()"
|
|
||||||
style="width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #17a2b8; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
|
|
||||||
📄 Export Document
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.resetAll()"
|
|
||||||
style="width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #ffc107; color: #212529; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
|
|
||||||
🔄 Reset All
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Navigation Tools -->
|
|
||||||
<div class="navigation-section" style="margin-bottom: 1rem;">
|
|
||||||
<h5 style="margin: 0 0 0.5rem 0; font-size: 0.9em; color: #666;">Navigation</h5>
|
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem;">
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.scrollToTop()"
|
|
||||||
style="padding: 0.4rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
⬆️ Top
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.scrollToBottom()"
|
|
||||||
style="padding: 0.4rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
⬇️ Bottom
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.showGoToLine()"
|
|
||||||
style="width: 100%; padding: 0.4rem; margin-top: 0.3rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
🎯 Go to Line
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Text Tools -->
|
|
||||||
<div class="text-section" style="margin-bottom: 1rem;">
|
|
||||||
<h5 style="margin: 0 0 0.5rem 0; font-size: 0.9em; color: #666;">Text Tools</h5>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.showFindReplace()"
|
|
||||||
style="width: 100%; padding: 0.4rem; margin-bottom: 0.3rem; background: #ffc107; color: #000; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
🔍 Find & Replace
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem; margin-bottom: 0.3rem;">
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.increaseFontSize()"
|
|
||||||
style="padding: 0.4rem; background: #20c997; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
🔍+ Font
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.decreaseFontSize()"
|
|
||||||
style="padding: 0.4rem; background: #20c997; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
🔍- Font
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.copyLink()"
|
|
||||||
style="width: 100%; padding: 0.4rem; margin-bottom: 0.3rem; background: #fd7e14; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
📋 Copy Page Link
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Markdown Tools -->
|
|
||||||
<div class="markdown-section" style="margin-bottom: 1rem;">
|
|
||||||
<h5 style="margin: 0 0 0.5rem 0; font-size: 0.9em; color: #666;">Markdown Tools</h5>
|
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem;">
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.insertMarkdown('**', '**', 'Bold text')"
|
|
||||||
style="padding: 0.4rem; background: #495057; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
**B**
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.insertMarkdown('*', '*', 'Italic text')"
|
|
||||||
style="padding: 0.4rem; background: #495057; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
*I*
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.insertMarkdown('## ', '', 'Heading')"
|
|
||||||
style="padding: 0.4rem; background: #495057; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
H2
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.insertMarkdown('- ', '', 'List item')"
|
|
||||||
style="padding: 0.4rem; background: #495057; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
•List
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Status Info -->
|
|
||||||
<div class="status-section" style="border-top: 1px solid #eee; padding-top: 0.5rem;">
|
|
||||||
<div style="font-size: 0.7rem; color: #666;">
|
|
||||||
<div>Mode: <span style="color: #007bff;">${this.editingMode}</span></div>
|
|
||||||
<div>Font: <span style="color: #007bff;">${this.fontSize}px</span></div>
|
|
||||||
${this.lastSaveTime ? `<div>Saved: ${new Date(this.lastSaveTime).toLocaleTimeString()}</div>` : ''}
|
|
||||||
${this.unsavedChanges ? '<div style="color: #dc3545;">⚠️ Unsaved changes</div>' : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
}, '<p>Error generating edit tools</p>', '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 = `
|
|
||||||
<h4 style="margin-top: 0;">Export Document</h4>
|
|
||||||
<button onclick="this.parentElement.exportAsHTML()" style="display: block; width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
Export as HTML
|
|
||||||
</button>
|
|
||||||
<button onclick="this.parentElement.exportAsText()" style="display: block; width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #17a2b8; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
Export as Text
|
|
||||||
</button>
|
|
||||||
<button onclick="this.parentElement.exportAsMarkdown()" style="display: block; width: 100%; padding: 0.5rem; margin-bottom: 1rem; background: #6f42c1; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
Export as Markdown
|
|
||||||
</button>
|
|
||||||
<button onclick="document.body.removeChild(this.parentElement)" style="width: 100%; padding: 0.3rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 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[^>]*>(.*?)<\/h1>/gi, '# $1\n\n')
|
|
||||||
.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '## $1\n\n')
|
|
||||||
.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '### $1\n\n')
|
|
||||||
.replace(/<p[^>]*>(.*?)<\/p>/gi, '$1\n\n')
|
|
||||||
.replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**')
|
|
||||||
.replace(/<em[^>]*>(.*?)<\/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, '<span class="edit-highlight" style="background-color: yellow; padding: 0.1rem;">$1</span>');
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -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 `<span style="color: ${color}; font-size: 0.7rem;"> (${sign}${changeData.change})</span>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatNumber = (num) => num.toLocaleString();
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div style="padding: 1rem; font-size: 0.8rem;">
|
|
||||||
<h4 style="margin-top: 0; margin-bottom: 1rem;">Document Statistics</h4>
|
|
||||||
|
|
||||||
<div class="stats-grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 1rem;">
|
|
||||||
<div class="stat-item">
|
|
||||||
<strong>Words:</strong><br>
|
|
||||||
<span style="font-size: 1.1em; color: #007bff;">${formatNumber(this.stats.words)}</span>
|
|
||||||
${formatChange(changes.words)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-item">
|
|
||||||
<strong>Characters:</strong><br>
|
|
||||||
<span style="font-size: 1.1em; color: #007bff;">${formatNumber(this.stats.characters)}</span>
|
|
||||||
${formatChange(changes.characters)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-item">
|
|
||||||
<strong>Reading Time:</strong><br>
|
|
||||||
<span style="font-size: 1.1em; color: #007bff;">${this.stats.readingTimeMinutes} min</span>
|
|
||||||
${formatChange(changes.readingTimeMinutes)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-item">
|
|
||||||
<strong>Sentences:</strong><br>
|
|
||||||
<span style="font-size: 1.1em; color: #007bff;">${formatNumber(this.stats.sentences)}</span>
|
|
||||||
${formatChange(changes.sentences)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="structure-stats" style="border-top: 1px solid #eee; padding-top: 0.5rem; margin-bottom: 1rem;">
|
|
||||||
<h5 style="margin: 0 0 0.5rem 0; font-size: 0.9em;">Document Structure</h5>
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.3rem;">
|
|
||||||
<span>Paragraphs:</span>
|
|
||||||
<span>${this.stats.paragraphs}${formatChange(changes.paragraphs)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.3rem;">
|
|
||||||
<span>Headings:</span>
|
|
||||||
<span>${this.stats.headings}${formatChange(changes.headings)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.3rem;">
|
|
||||||
<span>Lists:</span>
|
|
||||||
<span>${this.stats.lists}${formatChange(changes.lists)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.3rem;">
|
|
||||||
<span>Images:</span>
|
|
||||||
<span>${this.stats.images}${formatChange(changes.images)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.3rem;">
|
|
||||||
<span>Links:</span>
|
|
||||||
<span>${this.stats.links}${formatChange(changes.links)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="actions" style="border-top: 1px solid #eee; padding-top: 0.5rem; text-align: center;">
|
|
||||||
<button onclick="this.closest('.status-control').statusControl.refreshStats()"
|
|
||||||
style="padding: 0.3rem 0.6rem; margin-right: 0.3rem; font-size: 0.7rem; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
🔄 Refresh
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.status-control').statusControl.exportStats()"
|
|
||||||
style="padding: 0.3rem 0.6rem; font-size: 0.7rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
📊 Export
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${this.lastUpdateTime ? `
|
|
||||||
<div style="margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid #eee; font-size: 0.7rem; color: #666; text-align: center;">
|
|
||||||
Updated: ${new Date(this.lastUpdateTime).toLocaleTimeString()}
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
}, '<p>Error displaying statistics</p>', '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;
|
|
||||||
}
|
|
||||||
@@ -131,8 +131,8 @@
|
|||||||
<script src="markitect/static/js/core/debug-system.js"></script>
|
<script src="markitect/static/js/core/debug-system.js"></script>
|
||||||
|
|
||||||
<!-- Control system -->
|
<!-- Control system -->
|
||||||
<script src="markitect/static/js/controls/control-base.js"></script>
|
<script src="../js/controls/control-base.js"></script>
|
||||||
<script src="markitect/static/js/controls/status-control.js"></script>
|
<script src="../js/controls/status-control.js"></script>
|
||||||
|
|
||||||
<!-- Main application -->
|
<!-- Main application -->
|
||||||
<script src="markitect/static/js/main.js"></script>
|
<script src="markitect/static/js/main.js"></script>
|
||||||
|
|||||||
@@ -126,11 +126,11 @@
|
|||||||
<script src="markitect/static/js/core/debug-system.js"></script>
|
<script src="markitect/static/js/core/debug-system.js"></script>
|
||||||
|
|
||||||
<!-- Control system -->
|
<!-- Control system -->
|
||||||
<script src="markitect/static/js/controls/control-base.js"></script>
|
<script src="capabilities/testdrive-jsui/js/controls/control-base.js"></script>
|
||||||
<script src="markitect/static/js/controls/status-control.js"></script>
|
<script src="capabilities/testdrive-jsui/js/controls/status-control.js"></script>
|
||||||
<script src="markitect/static/js/controls/debug-control.js"></script>
|
<script src="capabilities/testdrive-jsui/js/controls/debug-control.js"></script>
|
||||||
<script src="markitect/static/js/controls/contents-control.js"></script>
|
<script src="capabilities/testdrive-jsui/js/controls/contents-control.js"></script>
|
||||||
<script src="markitect/static/js/controls/edit-control.js"></script>
|
<script src="capabilities/testdrive-jsui/js/controls/edit-control.js"></script>
|
||||||
|
|
||||||
<!-- Main application -->
|
<!-- Main application -->
|
||||||
<script src="markitect/static/js/main.js"></script>
|
<script src="markitect/static/js/main.js"></script>
|
||||||
|
|||||||
@@ -28,11 +28,11 @@
|
|||||||
<script src="markitect/static/js/core/section-manager.js"></script>
|
<script src="markitect/static/js/core/section-manager.js"></script>
|
||||||
<script src="markitect/static/js/components/debug-panel.js"></script>
|
<script src="markitect/static/js/components/debug-panel.js"></script>
|
||||||
<script src="markitect/static/js/components/dom-renderer.js"></script>
|
<script src="markitect/static/js/components/dom-renderer.js"></script>
|
||||||
<script src="markitect/static/js/controls/control-base.js"></script>
|
<script src="capabilities/testdrive-jsui/js/controls/control-base.js"></script>
|
||||||
<script src="markitect/static/js/controls/contents-control.js"></script>
|
<script src="capabilities/testdrive-jsui/js/controls/contents-control.js"></script>
|
||||||
<script src="markitect/static/js/controls/status-control.js"></script>
|
<script src="capabilities/testdrive-jsui/js/controls/status-control.js"></script>
|
||||||
<script src="markitect/static/js/controls/debug-control.js"></script>
|
<script src="capabilities/testdrive-jsui/js/controls/debug-control.js"></script>
|
||||||
<script src="markitect/static/js/controls/edit-control.js"></script>
|
<script src="capabilities/testdrive-jsui/js/controls/edit-control.js"></script>
|
||||||
<script src="markitect/static/js/config-loader.js"></script>
|
<script src="markitect/static/js/config-loader.js"></script>
|
||||||
<script src="markitect/static/js/main-updated.js"></script>
|
<script src="markitect/static/js/main-updated.js"></script>
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ The plugin supports various configuration options:
|
|||||||
## Extending the Plugin
|
## Extending the Plugin
|
||||||
|
|
||||||
### Adding New Controls
|
### 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
|
2. Extend `ControlBase` class
|
||||||
3. Register in `main.js` initialization
|
3. Register in `main.js` initialization
|
||||||
4. Add compass position (nw, ne, e, se, s, sw, w, nw)
|
4. Add compass position (nw, ne, e, se, s, sw, w, nw)
|
||||||
|
|||||||
@@ -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 `
|
|
||||||
<div style="text-align: center; color: #666; padding: 2rem 0;">
|
|
||||||
<p>No headings found in document</p>
|
|
||||||
<button onclick="this.closest('.contents-control').contentsControl.refreshContents()"
|
|
||||||
style="padding: 0.5rem 1rem; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; margin-top: 0.5rem;">
|
|
||||||
🔄 Refresh
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchHTML = `
|
|
||||||
<div style="margin-bottom: 0.5rem; border-bottom: 1px solid #eee; padding-bottom: 0.5rem;">
|
|
||||||
<input type="text"
|
|
||||||
placeholder="Search headings..."
|
|
||||||
style="width: 100%; padding: 0.25rem; border: 1px solid #ddd; border-radius: 3px; font-size: 0.8rem; box-sizing: border-box; overflow: visible;"
|
|
||||||
onkeyup="this.closest('.contents-control').contentsControl.handleSearch(this.value)">
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
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 `
|
|
||||||
<div class="contents-item"
|
|
||||||
style="margin-bottom: 0.3rem; padding-left: ${indentPx}px; overflow: visible;">
|
|
||||||
<a href="#${heading.id}"
|
|
||||||
onclick="event.preventDefault(); this.closest('.contents-control').contentsControl.navigateToHeading('${heading.id}')"
|
|
||||||
style="display: block; padding: 0.2rem 0; color: #007bff; text-decoration: none; font-size: 0.8rem; line-height: 1.2; overflow: visible;"
|
|
||||||
onmouseover="this.style.backgroundColor='#f8f9fa'"
|
|
||||||
onmouseout="this.style.backgroundColor='transparent'">
|
|
||||||
<span class="heading-level" style="color: #666; margin-right: 0.3rem;">H${heading.level}</span>
|
|
||||||
<span class="heading-text">${heading.text}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
const statusHTML = `
|
|
||||||
<div style="margin-bottom: 0.5rem; font-size: 0.7rem; color: #666; text-align: center;">
|
|
||||||
Found ${filteredHeadings.length} heading${filteredHeadings.length !== 1 ? 's' : ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const refreshButtonHTML = `
|
|
||||||
<div style="border-top: 1px solid #eee; padding-top: 0.5rem; text-align: center; margin-top: 0.5rem;">
|
|
||||||
<button onclick="this.closest('.contents-control').contentsControl.refreshContents()"
|
|
||||||
style="padding: 0.3rem 0.6rem; font-size: 0.7rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
🔄 Refresh Contents
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -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 = `
|
|
||||||
<button class="control-toggle" aria-label="${this.config.ariaLabel}">${this.config.icon}</button>
|
|
||||||
<div class="control-panel-expanded" style="display: none;">
|
|
||||||
<div class="control-header">
|
|
||||||
<span class="control-icon">${this.config.icon}</span>
|
|
||||||
<span class="control-title">${this.config.title}</span>
|
|
||||||
<button class="control-close">✕</button>
|
|
||||||
</div>
|
|
||||||
<div class="control-content">
|
|
||||||
${this.config.defaultContent}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
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 = `
|
|
||||||
<div class="control-content-container" style="
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
margin: 0 0 10px 1rem;
|
|
||||||
padding: 0.75rem 1rem 1rem 0;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
min-height: 0;
|
|
||||||
border-radius: 0 0 6px 6px;
|
|
||||||
">
|
|
||||||
<div class="control-content-body" style="
|
|
||||||
padding: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
">
|
|
||||||
${innerContent}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate content - subclasses should override this method
|
|
||||||
* @returns {string} HTML content for the panel body
|
|
||||||
*/
|
|
||||||
generateContent() {
|
|
||||||
return this.config.defaultContent || `<p>Panel content goes here...</p>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
@@ -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 `
|
|
||||||
<div class="system-info" style="margin-bottom: 1rem; padding: 0.5rem; background: #f8f9fa; border-radius: 3px; font-size: 0.7rem;">
|
|
||||||
<div style="line-height: 1.3;">
|
|
||||||
<div><strong>Markitect:</strong> ${markitectVersion}</div>
|
|
||||||
<div><strong>Viewport:</strong> ${systemInfo.viewport}</div>
|
|
||||||
<div><strong>Screen:</strong> ${systemInfo.screen}</div>
|
|
||||||
<div><strong>Memory:</strong> ${systemInfo.memory}</div>
|
|
||||||
<div><strong>Language:</strong> ${systemInfo.language}</div>
|
|
||||||
<div><strong>Status:</strong> ${systemInfo.onlineStatus}</div>
|
|
||||||
<div><strong>Protocol:</strong> ${systemInfo.protocol}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
}, '', '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 `
|
|
||||||
<div class="performance-info" style="margin-bottom: 1rem; padding: 0.5rem; background: #e7f3ff; border-radius: 3px; font-size: 0.7rem;">
|
|
||||||
<strong>Performance Metrics:</strong><br>
|
|
||||||
<div style="margin-top: 0.3rem; line-height: 1.3;">
|
|
||||||
<div><strong>Page Load:</strong> ${metrics.pageLoad}ms</div>
|
|
||||||
<div><strong>DOM Ready:</strong> ${metrics.domReady}ms</div>
|
|
||||||
<div><strong>First Byte:</strong> ${metrics.firstByte}ms</div>
|
|
||||||
<div><strong>Session Time:</strong> ${Math.round(metrics.uptime / 1000)}s</div>
|
|
||||||
<div><strong>Debug Messages:</strong> ${metrics.messagesCount}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
}, '', 'generatePerformanceHTML');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate debug messages HTML
|
|
||||||
*/
|
|
||||||
generateMessagesHTML() {
|
|
||||||
return this.safeOperation(() => {
|
|
||||||
const filteredMessages = this.getFilteredMessages();
|
|
||||||
|
|
||||||
if (filteredMessages.length === 0) {
|
|
||||||
return `
|
|
||||||
<div style="text-align: center; padding: 1rem; color: #666; font-style: italic;">
|
|
||||||
No ${this.messageFilter === 'all' ? '' : this.messageFilter + ' '}messages yet
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 `
|
|
||||||
<div class="debug-message" style="margin-bottom: 0.5rem; padding: 0.3rem; background: #f8f9fa; border-left: 3px solid ${backgroundColor}; font-size: 0.7rem; border-radius: 0 3px 3px 0;">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.2rem;">
|
|
||||||
<span style="background: ${backgroundColor}; color: ${textColor}; padding: 0.1rem 0.3rem; border-radius: 2px; font-size: 0.6rem; font-weight: bold;">
|
|
||||||
${msg.category}
|
|
||||||
</span>
|
|
||||||
<span style="color: #666; font-size: 0.6rem;">
|
|
||||||
${msg.displayTime}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div style="word-break: break-word; line-height: 1.2;">
|
|
||||||
${msg.message}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="messages-container" style="max-height: 200px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 3px; padding: 0.5rem; background: white;">
|
|
||||||
${messagesHTML}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
}, '<p>Error displaying messages</p>', 'generateMessagesHTML');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate control buttons HTML
|
|
||||||
*/
|
|
||||||
generateControlButtonsHTML() {
|
|
||||||
return `
|
|
||||||
<div class="debug-controls" style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem; margin: 0.5rem 0;">
|
|
||||||
<button onclick="this.closest('.debug-control').debugControl.clearMessages()"
|
|
||||||
style="padding: 0.3rem; font-size: 0.7rem; background: #dc3545; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
🗑️ Clear
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.debug-control').debugControl.exportMessages()"
|
|
||||||
style="padding: 0.3rem; font-size: 0.7rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
💾 Export
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.debug-control').debugControl.toggleRecording()"
|
|
||||||
style="padding: 0.3rem; font-size: 0.7rem; background: ${this.isRecording ? '#ffc107' : '#6c757d'}; color: ${this.isRecording ? '#000' : '#fff'}; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
${this.isRecording ? '⏸️ Pause' : '▶️ Record'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.debug-control').debugControl.addTestMessage()"
|
|
||||||
style="padding: 0.3rem; font-size: 0.7rem; background: #17a2b8; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
🧪 Test
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate filter controls HTML
|
|
||||||
*/
|
|
||||||
generateFilterControlsHTML() {
|
|
||||||
const filters = ['all', 'error', 'warn', 'info', 'debug'];
|
|
||||||
|
|
||||||
const filterButtons = filters.map(filter => {
|
|
||||||
const isActive = this.messageFilter === filter;
|
|
||||||
return `
|
|
||||||
<button onclick="this.closest('.debug-control').debugControl.setMessageFilter('${filter}')"
|
|
||||||
style="padding: 0.2rem 0.4rem; margin-right: 0.2rem; font-size: 0.6rem; background: ${isActive ? '#007bff' : '#e9ecef'}; color: ${isActive ? 'white' : '#495057'}; border: none; border-radius: 2px; cursor: pointer;">
|
|
||||||
${filter.toUpperCase()}
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div style="margin-bottom: 0.5rem; padding: 0.3rem; background: #f1f3f4; border-radius: 3px;">
|
|
||||||
<div style="font-size: 0.7rem; margin-bottom: 0.3rem; color: #666;">Filter:</div>
|
|
||||||
${filterButtons}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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()}
|
|
||||||
|
|
||||||
<div style="margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid #eee; font-size: 0.7rem; color: #666; text-align: center;">
|
|
||||||
Recording: ${this.isRecording ? '🟢 Active' : '🔴 Paused'} |
|
|
||||||
Filter: ${this.messageFilter.toUpperCase()} |
|
|
||||||
Messages: ${this.getFilteredMessages().length}/${this.messages.length}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}, '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;
|
|
||||||
}
|
|
||||||
@@ -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 -->
|
|
||||||
<div class="action-section" style="margin-bottom: 1rem;">
|
|
||||||
<div style="margin: 0 0 0.5rem 0; font-size: 0.9em; color: #666; font-weight: 600;">Document Actions</div>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.printDocument()"
|
|
||||||
style="width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
|
|
||||||
🖨️ Print Document
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.saveDocument()"
|
|
||||||
style="width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
|
|
||||||
💾 Save Changes
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.exportDocument()"
|
|
||||||
style="width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #17a2b8; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
|
|
||||||
📄 Export Document
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.resetAll()"
|
|
||||||
style="width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #ffc107; color: #212529; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">
|
|
||||||
🔄 Reset All
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Navigation Tools -->
|
|
||||||
<div class="navigation-section" style="margin-bottom: 1rem;">
|
|
||||||
<div style="margin: 0 0 0.5rem 0; font-size: 0.9em; color: #666; font-weight: 600;">Navigation</div>
|
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem;">
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.scrollToTop()"
|
|
||||||
style="padding: 0.4rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
⬆️ Top
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.scrollToBottom()"
|
|
||||||
style="padding: 0.4rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
⬇️ Bottom
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.showGoToLine()"
|
|
||||||
style="width: 100%; padding: 0.4rem; margin-top: 0.3rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
🎯 Go to Line
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Text Tools -->
|
|
||||||
<div class="text-section" style="margin-bottom: 1rem;">
|
|
||||||
<div style="margin: 0 0 0.5rem 0; font-size: 0.9em; color: #666; font-weight: 600;">Text Tools</div>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.showFindReplace()"
|
|
||||||
style="width: 100%; padding: 0.4rem; margin-bottom: 0.3rem; background: #ffc107; color: #000; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
🔍 Find & Replace
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem; margin-bottom: 0.3rem;">
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.increaseFontSize()"
|
|
||||||
style="padding: 0.4rem; background: #20c997; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
🔍+ Font
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.decreaseFontSize()"
|
|
||||||
style="padding: 0.4rem; background: #20c997; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
🔍- Font
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.copyLink()"
|
|
||||||
style="width: 100%; padding: 0.4rem; margin-bottom: 0.3rem; background: #fd7e14; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
📋 Copy Page Link
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Markdown Tools -->
|
|
||||||
<div class="markdown-section" style="margin-bottom: 1rem;">
|
|
||||||
<div style="margin: 0 0 0.5rem 0; font-size: 0.9em; color: #666; font-weight: 600;">Markdown Tools</div>
|
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem;">
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.insertMarkdown('**', '**', 'Bold text')"
|
|
||||||
style="padding: 0.4rem; background: #495057; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
**B**
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.insertMarkdown('*', '*', 'Italic text')"
|
|
||||||
style="padding: 0.4rem; background: #495057; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
*I*
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.insertMarkdown('## ', '', 'Heading')"
|
|
||||||
style="padding: 0.4rem; background: #495057; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
H2
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.edit-control').editControl.insertMarkdown('- ', '', 'List item')"
|
|
||||||
style="padding: 0.4rem; background: #495057; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.7rem;">
|
|
||||||
•List
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Status Info -->
|
|
||||||
<div class="status-section" style="border-top: 1px solid #eee; padding-top: 0.5rem;">
|
|
||||||
<div style="font-size: 0.7rem; color: #666;">
|
|
||||||
<div>Mode: <span style="color: #007bff;">${this.editingMode}</span></div>
|
|
||||||
<div>Font: <span style="color: #007bff;">${this.fontSize}px</span></div>
|
|
||||||
${this.lastSaveTime ? `<div>Saved: ${new Date(this.lastSaveTime).toLocaleTimeString()}</div>` : ''}
|
|
||||||
${this.unsavedChanges ? '<div style="color: #dc3545;">⚠️ Unsaved changes</div>' : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
}, '<p>Error generating edit tools</p>', '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 = `
|
|
||||||
<div style="margin-top: 0; font-weight: 600; font-size: 1.1em; color: #333; margin-bottom: 1rem;">Export Document</div>
|
|
||||||
<button onclick="this.parentElement.exportAsHTML()" style="display: block; width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
Export as HTML
|
|
||||||
</button>
|
|
||||||
<button onclick="this.parentElement.exportAsText()" style="display: block; width: 100%; padding: 0.5rem; margin-bottom: 0.3rem; background: #17a2b8; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
Export as Text
|
|
||||||
</button>
|
|
||||||
<button onclick="this.parentElement.exportAsMarkdown()" style="display: block; width: 100%; padding: 0.5rem; margin-bottom: 1rem; background: #6f42c1; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
Export as Markdown
|
|
||||||
</button>
|
|
||||||
<button onclick="document.body.removeChild(this.parentElement)" style="width: 100%; padding: 0.3rem; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 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[^>]*>(.*?)<\/h1>/gi, '# $1\n\n')
|
|
||||||
.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '## $1\n\n')
|
|
||||||
.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '### $1\n\n')
|
|
||||||
.replace(/<p[^>]*>(.*?)<\/p>/gi, '$1\n\n')
|
|
||||||
.replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**')
|
|
||||||
.replace(/<em[^>]*>(.*?)<\/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, '<span class="edit-highlight" style="background-color: yellow; padding: 0.1rem;">$1</span>');
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -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 `<span style="color: ${color}; font-size: 0.7rem;"> (${sign}${changeData.change})</span>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatNumber = (num) => num.toLocaleString();
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="stats-grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 1rem;">
|
|
||||||
<div class="stat-item">
|
|
||||||
<strong>Words:</strong><br>
|
|
||||||
<span style="font-size: 1.1em; color: #007bff;">${formatNumber(this.stats.words)}</span>
|
|
||||||
${formatChange(changes.words)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-item">
|
|
||||||
<strong>Characters:</strong><br>
|
|
||||||
<span style="font-size: 1.1em; color: #007bff;">${formatNumber(this.stats.characters)}</span>
|
|
||||||
${formatChange(changes.characters)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-item">
|
|
||||||
<strong>Reading Time:</strong><br>
|
|
||||||
<span style="font-size: 1.1em; color: #007bff;">${this.stats.readingTimeMinutes} min</span>
|
|
||||||
${formatChange(changes.readingTimeMinutes)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-item">
|
|
||||||
<strong>Sentences:</strong><br>
|
|
||||||
<span style="font-size: 1.1em; color: #007bff;">${formatNumber(this.stats.sentences)}</span>
|
|
||||||
${formatChange(changes.sentences)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="structure-stats" style="border-top: 1px solid #eee; padding-top: 0.5rem; margin-bottom: 1rem;">
|
|
||||||
<div style="margin: 0 0 0.5rem 0; font-size: 0.9em; font-weight: 600; color: #555;">Document Structure</div>
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.3rem;">
|
|
||||||
<span>Paragraphs:</span>
|
|
||||||
<span>${this.stats.paragraphs}${formatChange(changes.paragraphs)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.3rem;">
|
|
||||||
<span>Headings:</span>
|
|
||||||
<span>${this.stats.headings}${formatChange(changes.headings)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.3rem;">
|
|
||||||
<span>Lists:</span>
|
|
||||||
<span>${this.stats.lists}${formatChange(changes.lists)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.3rem;">
|
|
||||||
<span>Images:</span>
|
|
||||||
<span>${this.stats.images}${formatChange(changes.images)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.3rem;">
|
|
||||||
<span>Links:</span>
|
|
||||||
<span>${this.stats.links}${formatChange(changes.links)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="actions" style="border-top: 1px solid #eee; padding-top: 0.5rem; text-align: center;">
|
|
||||||
<button onclick="this.closest('.status-control').statusControl.refreshStats()"
|
|
||||||
style="padding: 0.3rem 0.6rem; margin-right: 0.3rem; font-size: 0.7rem; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
🔄 Refresh
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="this.closest('.status-control').statusControl.exportStats()"
|
|
||||||
style="padding: 0.3rem 0.6rem; font-size: 0.7rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">
|
|
||||||
📊 Export
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${this.lastUpdateTime ? `
|
|
||||||
<div style="margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid #eee; font-size: 0.7rem; color: #666; text-align: center;">
|
|
||||||
Updated: ${new Date(this.lastUpdateTime).toLocaleTimeString()}
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
`;
|
|
||||||
|
|
||||||
}, '<p>Error displaying statistics</p>', '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;
|
|
||||||
}
|
|
||||||
@@ -131,8 +131,8 @@
|
|||||||
<script src="markitect/static/js/core/debug-system.js"></script>
|
<script src="markitect/static/js/core/debug-system.js"></script>
|
||||||
|
|
||||||
<!-- Control system -->
|
<!-- Control system -->
|
||||||
<script src="markitect/static/js/controls/control-base.js"></script>
|
<script src="../js/controls/control-base.js"></script>
|
||||||
<script src="markitect/static/js/controls/status-control.js"></script>
|
<script src="../js/controls/status-control.js"></script>
|
||||||
|
|
||||||
<!-- Main application -->
|
<!-- Main application -->
|
||||||
<script src="markitect/static/js/main.js"></script>
|
<script src="markitect/static/js/main.js"></script>
|
||||||
|
|||||||
@@ -110,11 +110,11 @@
|
|||||||
<script src="static/js/components/debug-panel.js"></script>
|
<script src="static/js/components/debug-panel.js"></script>
|
||||||
<script src="static/js/components/document-controls.js"></script>
|
<script src="static/js/components/document-controls.js"></script>
|
||||||
<script src="static/js/components/dom-renderer.js"></script>
|
<script src="static/js/components/dom-renderer.js"></script>
|
||||||
<script src="static/js/controls/control-base.js"></script>
|
<script src="../capabilities/testdrive-jsui/js/controls/control-base.js"></script>
|
||||||
<script src="static/js/controls/contents-control.js"></script>
|
<script src="../capabilities/testdrive-jsui/js/controls/contents-control.js"></script>
|
||||||
<script src="static/js/controls/status-control.js"></script>
|
<script src="../capabilities/testdrive-jsui/js/controls/status-control.js"></script>
|
||||||
<script src="static/js/controls/debug-control.js"></script>
|
<script src="../capabilities/testdrive-jsui/js/controls/debug-control.js"></script>
|
||||||
<script src="static/js/controls/edit-control.js"></script>
|
<script src="../capabilities/testdrive-jsui/js/controls/edit-control.js"></script>
|
||||||
<script src="static/js/config-loader.js"></script>
|
<script src="static/js/config-loader.js"></script>
|
||||||
<script src="static/js/main-updated.js"></script>
|
<script src="static/js/main-updated.js"></script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user