1/** 2 * Mega menu header behavior â UAT prototype. 3 * 4 * The nav is plain nested <details>/<summary> so it works with zero JS 5 * (keyboard operable, screen-reader friendly, all links crawlable). This 6 * module only adds the desktop/mobile conveniences native <details> can't 7 * do on its own: closing sibling dropdowns, an off-canvas mobile drawer, 8 * outside-click / Escape dismissal, and a mobile accordion behavior for the 9 * nested category groups. 10 */ 11(function () { 12 "use strict"; 13 14 var DESKTOP_QUERY = "(min-width: 992px)"; 15 16 function init(root) { 17 var header = root.querySelector(".site-header"); 18 if (!header) return; 19 20 var nav = header.querySelector(".primary-nav"); 21 var toggle = header.querySelector(".nav-toggle"); 22 var closeBtn = header.querySelector(".primary-nav__close"); 23 var scrim = header.querySelector(".nav-scrim"); 24 var topDetails = Array.prototype.slice.call( 25 nav.querySelectorAll(":scope > .primary-nav__list > li > details.nav-dropdown") 26 ); 27 var categoryDetails = Array.prototype.slice.call( 28 nav.querySelectorAll("details.nav-category") 29 ); 30 31 setupTopLevelExclusivity(topDetails); 32 setupDesktopHoverOpen(topDetails); 33 setupMobileCategoryAccordion(categoryDetails); 34 setupMobileDrawer(nav, toggle, closeBtn, scrim, topDetails); 35 setupOutsideAndEscapeDismiss(header, nav, topDetails, toggle); 36 setupMegaTabs(nav, topDetails); 37 markCurrentPage(nav); 38 } 39 40 // The mega panel's category/grid tabs are pure-CSS (:hover via :has()) so 41 // they work with zero JS, but that means a panel only stays open while the 42 // pointer is literally on top of its label â crossing the gap to the 43 // content grid drops hover and snaps back to the first tab mid-move (see 44 // mega-menu.css). Track the last-entered label per panel instead and make 45 // that "sticky" via .is-active, which content-grid hovering doesn't clear. 46 function setupMegaTabs(nav, topDetails) { 47 var bodies = Array.prototype.slice.call(nav.querySelectorAll(".nav-tabs__body")); 48 bodies.forEach(function (body) { 49 var labels = Array.prototype.slice.call(body.querySelectorAll(".nav-tabs__label")); 50 var panels = Array.prototype.slice.call(body.querySelectorAll(".nav-tabs__panel")); 51 if (!labels.length || !panels.length) return; 52 53 function activate(index) { 54 labels.forEach(function (label, i) { 55 label.classList.toggle("is-active", i === index); 56 label.setAttribute("aria-expanded", i === index ? "true" : "false"); 57 }); 58 panels.forEach(function (panel, i) { 59 panel.classList.toggle("is-active", i === index); 60 }); 61 } 62 63 function deactivateAll() { 64 labels.forEach(function (label) { 65 label.classList.remove("is-active"); 66 label.setAttribute("aria-expanded", "false"); 67 }); 68 panels.forEach(function (panel) { 69 panel.classList.remove("is-active"); 70 }); 71 } 72 73 labels.forEach(function (label, index) { 74 // Desktop: real mouse hover / keyboard focus previews the panel. 75 // Gated to desktop widths only â see the click handler below for why. 76 label.addEventListener("mouseenter", function () { 77 if (window.matchMedia(DESKTOP_QUERY).matches) activate(index); 78 }); 79 label.addEventListener("focus", function () { 80 if (window.matchMedia(DESKTOP_QUERY).matches) activate(index); 81 }); 82 83 // Mobile/touch: there's no hover, and each label is a real link, so 84 // a tap without this handler navigates away instantly â the grade 85 // panel below it is never reachable. On mobile a tap NEVER follows 86 // the link: it only opens/closes that category's panel (tapping an 87 // already-open label collapses it again, standard accordion style). 88 // This isn't a loss of navigation â every label's own href is 89 // already duplicated as the first link inside its own panel, so the 90 // exact same destination is one more tap away once it's open. 91 // (A touch tap fires `focus` before `click`, so the desktop handler
92 // above is deliberately skipped below 992px â otherwise the label 93 // would already read as active by the time this runs, and every 94 // tap would navigate straight through with no preview.) 95 label.addEventListener("click", function (evt) { 96 if (window.matchMedia(DESKTOP_QUERY).matches) return; 97 evt.preventDefault(); 98 if (label.classList.contains("is-active")) { 99 deactivateAll(); 100 return; 101 } 102 activate(index); 103 var panel = panels[index]; 104 if (panel) panel.scrollIntoView({ behavior: "smooth", block: "nearest" }); 105 }); 106 }); 107 108 activate(0); 109 body.classList.add("js-tabs"); 110 }); 111 112 // Reset every mega panel whenever its dropdown is (re)opened, so it 113 // doesn't reopen showing whatever was last hovered/tapped. Desktop 114 // starts back on the first tab (it's always previewing something); 115 // mobile starts fully collapsed, since there each category is opened 116 // by an explicit tap (see the click handler above). 117 topDetails.forEach(function (details) { 118 details.addEventListener("toggle", function () { 119 if (!details.open) return; 120 var isDesktop = window.matchMedia(DESKTOP_QUERY).matches; 121 var bodiesInPanel = details.querySelectorAll(".nav-tabs__body.js-tabs"); 122 bodiesInPanel.forEach(function (body) { 123 var labels = body.querySelectorAll(".nav-tabs__label"); 124 var panels = body.querySelectorAll(".nav-tabs__panel"); 125 labels.forEach(function (label, i) { 126 var isOpen = isDesktop && i === 0; 127 label.classList.toggle("is-active", isOpen); 128 label.setAttribute("aria-expanded", isOpen ? "true" : "false"); 129 }); 130 panels.forEach(function (panel, i) { 131 panel.classList.toggle("is-active", isDesktop && i === 0); 132 }); 133 }); 134 }); 135 }); 136 } 137 138 // Only one top-level dropdown open at a time (desktop hover-alternative 139 // and mobile accordion both benefit from this). 140 function setupTopLevelExclusivity(topDetails) { 141 topDetails.forEach(function (details) { 142 details.addEventListener("toggle", function () { 143 if (!details.open) return; 144 topDetails.forEach(function (other) { 145 if (other !== details) other.open = false; 146 }); 147 }); 148 }); 149 } 150 151 // Desktop only: hovering "About Us" / "Pipes & Tubes" / "Our Products" 152 // opens its panel without needing a click. This sets the real [open] 153 // attribute (rather than just showing the panel via CSS on :hover), since 154 // a closed <details>'s content is hidden by the browser's own UA 155 // stylesheet â an author :hover rule can't reliably override that across 156 // browsers, but setting [open] uses the same native mechanism a click 157 // already does, so it's guaranteed to work and automatically fires the 158 // existing toggle listener above (closing any other open sibling) and the 159 // mega-tab reset-to-first-tab listener in setupMegaTabs. A short delay on 160 // mouseleave avoids it snapping shut if the pointer briefly dips outside 161 // the label/panel while moving between them. Mobile never gets these 162 // listeners' effect since they bail out below 992px, leaving its own 163 // tap-to-toggle behavior (native summary activation) untouched. 164 function setupDesktopHoverOpen(topDetails) { 165 topDetails.forEach(function (details) { 166 var closeTimer = null; 167 details.addEventListener("mouseenter", function () { 168 if (!window.matchMedia(DESKTOP_QUERY).matches) return; 169 clearTimeout(closeTimer); 170 details.open = true; 171 }); 172 details.addEventListener("mouseleave", function () { 173 if (!window.matchMedia(DESKTOP_QUERY).matches) return; 174 closeTimer = setTimeout(function () { 175 details.open = false; 176 }, 150); 177 }); 178 }); 179 } 180 181 // On mobile, keep the nested per-material accordions to one-open-at-a-time 182 // per group so the drawer doesn't turn into an endless scroll. Desktop 183 // leaves them alone â a mega menu grid is supposed to show many columns. 184 function setupMobileCategoryAccordion(categoryDetails) {
185 categoryDetails.forEach(function (details) { 186 details.addEventListener("toggle", function () { 187 if (window.matchMedia(DESKTOP_QUERY).matches) return; 188 if (!details.open) return; 189 var siblings = details.parentElement.querySelectorAll(":scope > details.nav-category"); 190 siblings.forEach(function (sib) { 191 if (sib !== details) sib.open = false; 192 }); 193 }); 194 }); 195 } 196 197 function setupMobileDrawer(nav, toggle, closeBtn, scrim, topDetails) { 198 if (!toggle) return; 199 200 function openDrawer() { 201 nav.classList.add("is-open"); 202 toggle.setAttribute("aria-expanded", "true"); 203 if (scrim) scrim.classList.add("is-visible"); 204 document.body.style.overflow = "hidden"; 205 } 206 207 function closeDrawer() { 208 nav.classList.remove("is-open"); 209 toggle.setAttribute("aria-expanded", "false"); 210 if (scrim) scrim.classList.remove("is-visible"); 211 document.body.style.overflow = ""; 212 topDetails.forEach(function (d) { 213 d.open = false; 214 }); 215 } 216 217 toggle.addEventListener("click", function () { 218 var isOpen = nav.classList.contains("is-open"); 219 if (isOpen) closeDrawer(); 220 else openDrawer(); 221 }); 222 223 if (closeBtn) closeBtn.addEventListener("click", closeDrawer); 224 if (scrim) scrim.addEventListener("click", closeDrawer); 225 226 window.addEventListener("resize", debounce(function () { 227 if (window.matchMedia(DESKTOP_QUERY).matches) closeDrawer(); 228 }, 150)); 229 } 230 231 function setupOutsideAndEscapeDismiss(header, nav, topDetails, toggle) { 232 document.addEventListener("click", function (evt) { 233 if (header.contains(evt.target)) return; 234 topDetails.forEach(function (d) { 235 d.open = false; 236 }); 237 }); 238 239 document.addEventListener("keydown", function (evt) { 240 if (evt.key !== "Escape") return; 241 var anyOpen = topDetails.some(function (d) { 242 return d.open; 243 }); 244 if (anyOpen) { 245 topDetails.forEach(function (d) { 246 d.open = false; 247 }); 248 return; 249 } 250 if (nav.classList.contains("is-open") && toggle) { 251 toggle.click(); 252 toggle.focus(); 253 } 254 }); 255 } 256 257 // Highlight the nav item matching the current document, if any. 258 function markCurrentPage(nav) { 259 // Home now links to the bare root (https://.../) rather than index.html, 260 // so both sides need the same "empty path segment = index.html" fallback 261 // â otherwise visiting either "/" or "/index.html" stops matching Home's 262 // href, which would never highlight as the current page again. 263 var here = location.pathname.split("/").pop() || "index.html"; 264 var links = nav.querySelectorAll(".primary-nav__list > li > a.primary-nav__link"); 265 links.forEach(function (link) { 266 var file = link.getAttribute("href").split("/").pop() || "index.html"; 267 if (file === here) { 268 link.closest(".primary-nav__item").classList.add("primary-nav__item--current"); 269 } 270 }); 271 } 272 273 function debounce(fn, wait) { 274 var t; 275 return function () { 276 clearTimeout(t); 277 var args = arguments; 278 t = setTimeout(function () { 279 fn.apply(null, args); 280 }, wait); 281 }; 282 } 283 284 // The sticky "Get in Touch" bar (#sbcta-bar, per-page markup, styled in 285 // mega-menu.css) is position:fixed with no space reserved for it, so once 286 // its own inline per-page script adds "is-visible" it sits on top of 287 // whatever content is at the bottom of the viewport â e.g. another 288 // contact form's submit button or a table's last row. Rather than edit 289 // every page's copy-pasted toggle script, watch the bar's class here 290 // (this file is already shared/loaded on every page) and push the page 291 // content up by the bar's own height while it's showing. 292 function setupStickyCtaSpacing(root) { 293 var bar = root.getElementById("sbcta-bar"); 294 if (!bar) return; 295 296 function sync() { 297 var visible = bar.classList.contains("is-visible"); 298 document.body.style.paddingBottom = visible ? bar.offsetHeight + "px" : ""; 299 } 300 301 sync(); 302 new MutationObserver(sync).observe(bar, { attributes: true, attributeFilter: ["class"] }); 303 window.addEventListener("resize", debounce(sync, 150)); 304 } 305 306 // On the live site the header markup isn't in the page source â it's 307 // fetched into #globalmainheader and injected via innerHTML, which 308 // finishes well after DOMContentLoaded. Try immediately (works when the 309 // header is already in the page, e.g. this test file); otherwise watch 310 // the DOM until it shows up, then initialize once and stop watching. 311 function boot() { 312 setupStickyCtaSpacing(document); 313 314 if (document.querySelector(".site-header")) { 315 init(document); 316 return; 317 } 318 var observer = new MutationObserver(function () { 319 if (document.querySelector(".site-header")) { 320 observer.disconnect(); 321 init(document); 322 } 323 }); 324 observer.observe(document.body, { childList: true, subtree: true }); 325 } 326 327 if (document.readyState === "loading") { 328 document.addEventListener("DOMContentLoaded", boot); 329 } else {
330 boot(); 331 } 332})();
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.