1/** 2 * Main JavaScript functionality 3 * - Smooth scroll for anchor links with header offset 4 */ 5document.addEventListener('DOMContentLoaded', function() { 6 7 /** 8 * Get the current header height dynamically 9 * Accounts for different header states and screen sizes 10 */ 11 function getHeaderOffset() { 12 const header = document.getElementById('site-header'); 13 if (!header) return 0; 14 15 // Get the actual height of the header 16 const headerHeight = header.offsetHeight; 17 18 return headerHeight; 19 } 20 21 /** 22 * Smooth scroll to target element with header offset 23 * @param {Element} target - The target element to scroll to 24 * @param {number} duration - Animation duration in milliseconds 25 */ 26 function smoothScrollTo(target, duration = 800) { 27 const header = document.getElementById('site-header'); 28 const hero = document.querySelector('.hero-section') || document.querySelector('.tst-hero-section'); 29 const heroHeight = hero ? hero.offsetHeight : 100; 30 const stickyThreshold = heroHeight - 10; // From header.js 31 32 const targetPosition = target.getBoundingClientRect().top + window.pageYOffset; 33 const startPosition = window.pageYOffset; 34 35 // Calculate final scroll position with header offset 36 let finalScrollPosition = targetPosition - getHeaderOffset(); 37 38 // If target element is after the hero, ensure we scroll past the sticky threshold 39 // This guarantees the sticky header will appear 40 if (targetPosition > heroHeight && finalScrollPosition <= stickyThreshold) { 41 finalScrollPosition = stickyThreshold + 1; 42 } 43 44 const distance = finalScrollPosition - startPosition; 45 let startTime = null; 46 47 // Easing function for smooth animation (easeInOutCubic) 48 function easeInOutCubic(t) { 49 return t < 0.5 50 ? 4 * t * t * t 51 : 1 - Math.pow(-2 * t + 2, 3) / 2; 52 } 53 54 function animation(currentTime) { 55 if (startTime === null) startTime = currentTime; 56 const timeElapsed = currentTime - startTime; 57 const progress = Math.min(timeElapsed / duration, 1); 58 const ease = easeInOutCubic(progress); 59 60 window.scrollTo(0, startPosition + distance * ease); 61 62 if (timeElapsed < duration) { 63 requestAnimationFrame(animation); 64 } 65 } 66 67 requestAnimationFrame(animation); 68 } 69 70 /** 71 * Handle anchor link clicks 72 */ 73 function initAnchorLinks() { 74 // Select all anchor links (links starting with #) 75 const anchorLinks = document.querySelectorAll('a[href^="#"]'); 76 77 anchorLinks.forEach(link => { 78 link.addEventListener('click', function(e) { 79 const href = this.getAttribute('href'); 80 81 // Skip if it's just "#" or empty 82 if (!href || href === '#') return; 83 84 // Find the target element 85 const targetId = href.substring(1); 86 const targetElement = document.getElementById(targetId); 87 88 // If target exists, prevent default and smooth scroll 89 if (targetElement) { 90 e.preventDefault(); 91 92 // Close mobile menu if open 93 const menuToggle = document.querySelector('.menu-toggle'); 94 const primaryMenu = document.getElementById('primary-menu'); 95 if (menuToggle && primaryMenu && menuToggle.getAttribute('aria-expanded') === 'true') { 96 menuToggle.click(); 97 } 98 99 // Perform smooth scroll 100 smoothScrollTo(targetElement); 101 102 // Update URL hash without jumping 103 if (history.pushState) { 104 history.pushState(null, null, href); 105 } else { 106 // Fallback for older browsers 107 window.location.hash = href; 108 } 109 } 110 }); 111 }); 112 } 113 114 /** 115 * Open accordion item if it's in the URL hash 116 */ 117 function openAccordionFromHash() { 118 const hash = window.location.hash; 119 if (!hash) return; 120 121 const targetId = hash.substring(1); 122 const targetElement = document.getElementById(targetId); 123 124 if (!targetElement) return; 125 126 // Check if the target is an accordion collapse element 127 if (targetElement.classList.contains('accordion-collapse')) { 128 const accordionItem = targetElement.closest('.accordion-item'); 129 130 // Listen for the accordion to finish opening 131 targetElement.addEventListener('shown.bs.collapse', function onShown() { 132 // Remove the event listener after it fires once 133 targetElement.removeEventListener('shown.bs.collapse', onShown); 134 135 // Scroll to the accordion item after it's fully expanded 136 if (accordionItem) { 137 smoothScrollTo(accordionItem); 138 } 139 }); 140 141 // Find the Bootstrap collapse instance or create one 142 const bsCollapse = bootstrap.Collapse.getOrCreateInstance(targetElement); 143 144 // Open the accordion item 145 bsCollapse.show(); 146 } else { 147 // Regular scroll for non-accordion elements 148 setTimeout(() => { 149 smoothScrollTo(targetElement); 150 }, 100); 151 } 152 } 153 154 /** 155 * Handle initial page load with hash 156 */ 157 function handleInitialHash() { 158 const hash = window.location.hash; 159 if (hash) { 160 // Prevent default browser scroll to hash 161 if (history.scrollRestoration) { 162 history.scrollRestoration = 'manual'; 163 } 164 165 const targetId = hash.substring(1); 166 const targetElement = document.getElementById(targetId); 167 168 if (targetElement) { 169 // Scroll to top first to prevent browser's default scroll 170 window.scrollTo(0, 0); 171 172 // Wait a bit for page to fully load, then open accordion 173 setTimeout(() => { 174 openAccordionFromHash(); 175 }, 300); 176 } 177 } 178 } 179 180 /** 181 * Handle hash changes (when clicking links or using browser back/forward) 182 */ 183 window.addEventListener('hashchange', function(e) { 184 e.preventDefault(); 185 openAccordionFromHash(); 186 }); 187 188 // Initialize anchor link functionality 189 initAnchorLinks(); 190 191 // Handle initial hash on page load 192 handleInitialHash(); 193 194 // Re-initialize if new content is added dynamically 195 // (useful for AJAX-loaded content) 196 const observer = new MutationObserver(function(mutations) { 197 mutations.forEach(function(mutation) { 198 if (mutation.addedNodes.length) { 199 initAnchorLinks(); 200 } 201 }); 202 }); 203 204 // Observe the body for changes 205 observer.observe(document.body, { 206 childList: true, 207 subtree: true 208 }); 209});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.