/** * 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 ` (${sign}${changeData.change})`; }; const formatNumber = (num) => num.toLocaleString(); return `
Error displaying statistics
', '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; }