All files / coherent.js/website/public toc-active.js

0% Statements 0/182
0% Branches 0/1
0% Functions 0/1
0% Lines 0/182

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
    });
  }
})();