Files
markitect-main/markitect/static/js/controls/status-control.js
tegwick 5b13c00d3e feat: deploy enhanced ControlBase to MarkiTect md-render --edit
Successfully integrate improved TestDrive-JSUI controls with main MarkiTect system:

## Enhanced Control System
- Updated ControlBase with 5 advanced behaviors from reference implementation
- All controls now support icon-only collapsed state, drag/resize, position restoration
- Seamless integration with md-render --edit command

## Updated Components
- DebugControl: Enhanced with new ControlBase inheritance
- EditControl: Full document editing tools with export/formatting
- StatusControl: Real-time document statistics and metrics
- ContentsControl: Interactive table of contents navigation

## Deployment Integration
- All enhanced controls deployed via asset system
- Compatible with existing edit mode functionality
- Maintains backward compatibility with legacy systems

## Verification
- Successfully renders interactive HTML with md-render --edit
- All control behaviors working in production environment
- Asset deployment system properly handles enhanced controls

The enhanced control system is now live and functional in MarkiTect's editing environment.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 11:35:47 +01:00

368 lines
14 KiB
JavaScript

/**
* 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;
}