Press n or j to go to the next uncovered block, b, p or k for the previous block.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | // TOC Active State Management (function() { 'use strict'; let currentActiveId = null; let manuallySelectedId = null; let manualSelectionTimeout = null; let isScrollingToTarget = false; let scrollTargetId = null; const tocLinks = new Map(); // Map of element ID to TOC link // Function to update active TOC item function updateTocActive(targetId, isManualClick = false) { console.log('updateTocActive called:', { targetId, isManualClick, manuallySelectedId, isScrollingToTarget }); // If this is a manual click, set override and scrolling flag if (isManualClick) { console.log('Setting manual override for:', targetId); manuallySelectedId = targetId; isScrollingToTarget = true; scrollTargetId = targetId; // Clear any existing timeout if (manualSelectionTimeout) { clearTimeout(manualSelectionTimeout); } // Clear scrolling flag after scroll animation completes setTimeout(() => { console.log('Scroll animation should be complete'); isScrollingToTarget = false; }, 1000); // Give time for smooth scroll to complete } // If we have a manual selection and this is not a manual click if (manuallySelectedId && !isManualClick) { // If we're scrolling to the target and we've reached it, keep the override but allow future scroll updates if (isScrollingToTarget && targetId === scrollTargetId) { console.log('Reached scroll target, keeping override but allowing future updates'); isScrollingToTarget = false; } // If this is a different target and we're not scrolling to our manual target, clear override else if (!isScrollingToTarget && manuallySelectedId !== targetId) { console.log('User manually scrolled to different section, clearing override'); manuallySelectedId = null; if (manualSelectionTimeout) { clearTimeout(manualSelectionTimeout); manualSelectionTimeout = null; } } // Otherwise ignore the update else if (manuallySelectedId !== targetId) { console.log('Ignoring scroll-based update due to manual override'); return; } } // Remove previous active state if (currentActiveId) { const prevLink = tocLinks.get(currentActiveId); if (prevLink) { prevLink.classList.remove('toc-active'); } } // Add active state to current item const currentLink = tocLinks.get(targetId); if (currentLink) { currentLink.classList.add('toc-active'); currentActiveId = targetId; } } // Make function globally available for onclick handlers window.updateTocActive = updateTocActive; // Initialize when DOM is ready function initTocActiveStates() { // Find all TOC links and map them const tocContainer = document.querySelector('.toc-list'); if (!tocContainer) return; const links = tocContainer.querySelectorAll('a[data-toc-target]'); links.forEach(link => { const targetId = link.getAttribute('data-toc-target'); if (targetId) { tocLinks.set(targetId, link); } }); // Set up intersection observer for automatic highlighting const headings = Array.from(document.querySelectorAll('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]')) .filter(heading => tocLinks.has(heading.id)); if (headings.length === 0) return; const observer = new IntersectionObserver((entries) => { // Find the heading closest to the top of the viewport let closestHeading = null; let closestDistance = Infinity; entries.forEach(entry => { if (entry.isIntersecting && entry.intersectionRatio > 0.05) { const rect = entry.target.getBoundingClientRect(); const distanceFromTop = Math.abs(rect.top); // Prefer headings that are closer to the top and have higher visibility const score = distanceFromTop - (entry.intersectionRatio * 50); if (score < closestDistance) { closestDistance = score; closestHeading = entry.target.id; } } }); // The clearing logic is now handled in updateTocActive function if (closestHeading && closestHeading !== currentActiveId) { updateTocActive(closestHeading, false); } }, { root: null, rootMargin: '-5% 0px -70% 0px', // Less aggressive margins for better detection threshold: [0.05, 0.1, 0.2, 0.3, 0.5, 0.7, 1] // Lower minimum threshold }); // Observe all headings headings.forEach(heading => observer.observe(heading)); // Handle initial state on page load (including anchor from URL) function handleInitialState() { const hash = window.location.hash.substring(1); if (hash && tocLinks.has(hash)) { updateTocActive(hash); } else if (headings.length > 0) { // Default to first heading if no anchor updateTocActive(headings[0].id); } } // Handle browser back/forward navigation window.addEventListener('hashchange', () => { const hash = window.location.hash.substring(1); if (hash && tocLinks.has(hash)) { updateTocActive(hash, true); // Treat hash changes as manual } }); // Initialize after a short delay to ensure layout is complete setTimeout(handleInitialState, 100); } // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initTocActiveStates); } else { initTocActiveStates(); } // Re-initialize if page content changes (for SPA-like behavior) if (window.MutationObserver) { const contentObserver = new MutationObserver((mutations) => { let shouldReinit = false; mutations.forEach(mutation => { if (mutation.type === 'childList' && (mutation.target.classList?.contains('toc-list') || mutation.target.querySelector?.('.toc-list'))) { shouldReinit = true; } }); if (shouldReinit) { setTimeout(initTocActiveStates, 100); } }); contentObserver.observe(document.body, { childList: true, subtree: true }); } })(); |