1document.addEventListener("DOMContentLoaded", () => { 2 const mobileMenuToggle = document.querySelector("#mobile-menu-toggle"); 3 const menuToggleIcon = document.querySelector("#menu-toggle-icon"); 4 const mobileMenuItems = document.querySelectorAll(".mobile-menu-item"); 5 const headerBannerMobile = document.querySelector(".header-banner--mobile"); 6 const body = document.body; 7 const header = document.querySelector("header"); 8 const mobileMenuInitialized = 9 mobileMenuToggle && 10 mobileMenuToggle.getAttribute("data-initialized") === "true"; 11 12 window.navScrollHandled = false; 13 14 const isIOS = 15 /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; 16 const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); 17 const isIOSSafari = isIOS && isSafari; 18 19 // Additional offset for iOS Safari 20 const iOSOffset = isIOSSafari ? 45 : 0; 21 22 // WordPress admin bar offset 23 const wpAdminBar = document.querySelector("#wpadminbar"); 24 const wpAdminBarOffset = wpAdminBar && wpAdminBar.offsetHeight > 0 ? 32 : 0; 25 26 const isHomePage = 27 window.location.pathname === "/" || 28 window.location.pathname === "/index.php" || 29 window.location.pathname === "/index.html"; 30 const searchParams = new URLSearchParams(window.location.search); 31 const targetHash = searchParams.get("target_hash"); 32 33 let isHandlingScroll = false; 34 35 if (isHomePage) { 36 document.querySelectorAll(".nav-item").forEach((item) => { 37 item.classList.add("no-hover"); 38 39 setTimeout(() => { 40 item.classList.remove("no-hover"); 41 }, 500); 42 }); 43 44 if (targetHash) { 45 setTimeout(() => { 46 const targetElement = document.getElementById(targetHash); 47 if (targetElement) { 48 smoothScrollToElement(targetElement, "#" + targetHash); 49 } 50 }, 100); 51 } 52 } 53 54 const closeAllSubitems = () => { 55 document 56 .querySelectorAll(".nav-dropdown.open") 57 .forEach((dropdown) => dropdown.classList.remove("open")); 58 document 59 .querySelectorAll(".mobile-menu-item svg.open") 60 .forEach((icon) => icon.classList.remove("open")); 61 }; 62 63 const handleMobileMenuToggle = (event) => { 64 const mobileNavigation = document.querySelector(".mobile-navigation"); 65 if (!mobileNavigation || !menuToggleIcon) return; 66 67 mobileNavigation.classList.toggle("open"); 68 menuToggleIcon.classList.toggle("open"); 69 body.classList.toggle("mobile-menu-is-open"); 70 71 if (mobileNavigation.classList.contains("open")) { 72 if (header) header.style.top = "0"; 73 } else { 74 handleScroll(); 75 } 76 77 event.stopPropagation(); 78 event.preventDefault(); 79 closeAllSubitems(); 80 }; 81 82 const handleMobileMenuItemClick = (event) => { 83 const icon = event.currentTarget.querySelector("svg"); 84 if (!icon) return; 85 86 const dropdown = event.currentTarget.nextElementSibling; 87 88 if (icon.classList.contains("open")) { 89 icon.classList.remove("open"); 90 if (dropdown) { 91 dropdown.classList.remove("open"); 92 } 93 } else { 94 closeAllSubitems(); 95 icon.classList.add("open"); 96 if (dropdown) { 97 dropdown.classList.add("open"); 98 } 99 } 100 101 event.stopPropagation(); 102 }; 103 104 // Smooth scroll to element with fixed offset 105 const smoothScrollToElement = (element, hash, parentNavItem = null) => { 106 if (!element) return; 107 108 if (isHandlingScroll) return; 109 isHandlingScroll = true; 110 111 const isBusinessGraph = element.id === "business-graph"; 112 const isDataItem = 113 element.classList.contains("data-item") || 114 element.closest(".data-item"); 115 const dataItem = isDataItem 116 ? element.classList.contains("data-item") 117 ? element 118 : element.closest(".data-item") 119 : null; 120 const isPrimaryResearch = 121 element.id === "primary-research" || 122 element.closest("#primary-research"); 123 const primaryResearchTitle = isPrimaryResearch 124 ? element.closest("#primary-research") 125 : null; 126 127 if (isBusinessGraph) { 128 window.navScrollHandled = true; 129 } 130 131 document.body.offsetHeight; 132 133 requestAnimationFrame(() => { 134 const elementPosition = 135 element.getBoundingClientRect().top + window.scrollY; 136 137 let offset; 138 if ( 139 (isDataItem && dataItem) || 140 (isPrimaryResearch && primaryResearchTitle) 141 ) { 142 offset = 140; 143 } else { 144 offset = 80 + iOSOffset; 145 } 146 147 const offsetPosition = elementPosition - offset; 148 149 window.scrollTo({ 150 top: offsetPosition, 151 behavior: "smooth", 152 }); 153 154 if (parentNavItem) { 155 parentNavItem.classList.add("no-hover"); 156 157 setTimeout(() => { 158 parentNavItem.classList.remove("no-hover"); 159 }, 1000); 160 } 161 162 setTimeout(() => { 163 const oldHash = window.location.hash; 164 history.replaceState(null, "", hash); 165 166 if (oldHash !== hash) { 167 if (isBusinessGraph) { 168 // Send info to business-graph.js 169 const customEvent = new CustomEvent( 170 "navHandledHashChange", 171 { 172 detail: { 173 hash: hash, 174 scrollHandled: true, 175 }, 176 } 177 ); 178 window.dispatchEvent(customEvent); 179 window.dispatchEvent(new HashChangeEvent("hashchange")); 180 setTimeout(() => { 181 window.navScrollHandled = false;
182 }, 1000); 183 } else { 184 window.dispatchEvent(new HashChangeEvent("hashchange")); 185 } 186 } 187 188 setTimeout(() => { 189 if (isIOSSafari) { 190 const newPosition = 191 element.getBoundingClientRect().top + 192 window.scrollY; 193 if ( 194 Math.abs(newPosition - window.scrollY - offset) > 10 195 ) { 196 window.scrollTo({ 197 top: newPosition - offset, 198 behavior: "auto", 199 }); 200 } 201 } else { 202 const newPosition = 203 element.getBoundingClientRect().top + 204 window.scrollY; 205 if ( 206 Math.abs(newPosition - window.scrollY - offset) > 10 207 ) { 208 window.scrollTo({ 209 top: newPosition - offset, 210 behavior: "auto", 211 }); 212 } 213 } 214 isHandlingScroll = false; 215 }, 300); 216 }, 500); 217 }); 218 }; 219 220 const resetScrollLock = () => { 221 isHandlingScroll = false; 222 }; 223 224 // Handle all link clicks including jumplinks 225 const handleSubitemClick = (event) => { 226 const href = event.currentTarget.getAttribute("href"); 227 228 if (!href) return; 229 230 const businessGraphPattern = /#business-graph(?:-(\d+))?$/; 231 const match = href.match(businessGraphPattern); 232 const isHashLink = href.startsWith("#") || href.includes("/#"); 233 234 if (isHashLink) { 235 event.preventDefault(); 236 const parentNavItem = event.currentTarget.closest(".nav-item"); 237 238 let targetId; 239 if (match) { 240 targetId = "business-graph"; 241 } else if (href.includes("/#")) { 242 targetId = href.split("/#")[1]; 243 } else { 244 targetId = href.substring(1); 245 } 246 247 const targetElement = document.getElementById(targetId); 248 249 if (targetElement && isHomePage) { 250 if (document.querySelector(".mobile-navigation.open")) { 251 handleMobileMenuToggle({ 252 preventDefault: () => {}, 253 stopPropagation: () => {}, 254 }); 255 } 256 257 setTimeout(() => { 258 smoothScrollToElement(targetElement, href, parentNavItem); 259 }, 50); 260 } else if (href.includes("/#")) { 261 if (document.querySelector(".mobile-navigation.open")) { 262 handleMobileMenuToggle({ 263 preventDefault: () => {}, 264 stopPropagation: () => {}, 265 }); 266 } 267 268 window.location.href = href; 269 } else if (targetElement) { 270 if (document.querySelector(".mobile-navigation.open")) { 271 handleMobileMenuToggle({ 272 preventDefault: () => {}, 273 stopPropagation: () => {}, 274 }); 275 } 276 277 setTimeout(() => { 278 smoothScrollToElement(targetElement, href, parentNavItem); 279 }, 50); 280 } 281 } 282 }; 283 284 const handleScroll = () => { 285 if (isHandlingScroll) return; 286 if (!header || !headerBannerMobile) return; 287 288 const bannerOffset = headerBannerMobile.offsetHeight; 289 290 if (isIOSSafari && window.scrollY > 0) { 291 header.classList.add("scrolled"); 292 header.style.position = "fixed"; 293 header.style.top = `${wpAdminBarOffset}px`; 294 return; 295 } 296 297 if (window.scrollY > bannerOffset) { 298 header.classList.add("scrolled"); 299 header.style.top = `${wpAdminBarOffset}px`; 300 } else { 301 header.classList.remove("scrolled"); 302 header.style.top = `${wpAdminBarOffset + bannerOffset - window.scrollY}px`; 303 } 304 }; 305 306 const debounce = (func, wait) => { 307 let timeout; 308 return function executedFunction(...args) { 309 const later = () => { 310 clearTimeout(timeout); 311 func(...args); 312 }; 313 clearTimeout(timeout); 314 timeout = setTimeout(later, wait); 315 }; 316 }; 317 318 if (window.location.hash) { 319 setTimeout(() => { 320 const businessGraphPattern = /#business-graph(?:-(\d+))?$/; 321 const match = window.location.hash.match(businessGraphPattern); 322 323 let targetId; 324 if (match) { 325 targetId = "business-graph"; 326 } else { 327 targetId = window.location.hash.substring(1); 328 } 329 330 const targetElement = document.getElementById(targetId); 331 if (targetElement) { 332 smoothScrollToElement(targetElement, window.location.hash); 333 } 334 }, 300); 335 } 336 337 // Mobile menu toggle - only add if not already initialized 338 if (mobileMenuToggle && !mobileMenuInitialized) { 339 mobileMenuToggle.addEventListener("click", handleMobileMenuToggle); 340 mobileMenuToggle.setAttribute("data-initialized", "true"); 341 } 342 343 // Mobile menu subitem toggle - only add if not already initialized
344 mobileMenuItems.forEach((menuItem) => { 345 if (!menuItem.hasAttribute("data-initialized")) { 346 menuItem.addEventListener("click", handleMobileMenuItemClick); 347 menuItem.setAttribute("data-initialized", "true"); 348 } 349 }); 350 351 // Add click event listeners to all nav-subitem elements 352 document.querySelectorAll(".nav-subitem").forEach((subitem) => { 353 subitem.addEventListener("click", handleSubitemClick); 354 }); 355 356 // Show and hide nav based on scroll position 357 window.addEventListener("scroll", handleScroll, { passive: true }); 358 359 // Adjust position on resize with debouncing 360 window.addEventListener("resize", debounce(handleScroll, 250)); 361 362 // Listen for header banner close event 363 document.addEventListener("header_banner_closed", () => { 364 handleScroll(); 365 }); 366 367 // Just in case 368 setTimeout(resetScrollLock, 3000); 369});
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.