/** * 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 `

No headings found in document

`; } const searchHTML = `
`; 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 `
H${heading.level} ${heading.text}
`; }).join(''); const statusHTML = `
Found ${filteredHeadings.length} heading${filteredHeadings.length !== 1 ? 's' : ''}
`; const refreshButtonHTML = `
`; 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; }