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>
368 lines
14 KiB
JavaScript
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;
|
|
} |