1export function initHeaderProductBar() { 2 const bar = document.querySelector('.header-product-bar'); 3 const header = document.querySelector('header'); 4 5 if (!bar || !header) { 6 return; 7 } 8 9 const slider = bar.querySelector('.header-product-bar__slider'); 10 let splideInstance = null; 11 12 const mediaQuery = window.matchMedia('(max-width: 992px)'); 13 14 const cleanupTrackVisibility = () => { 15 const track = slider ? slider.querySelector('.splide__track') : null; 16 if (!track) { 17 return; 18 } 19 track.removeAttribute('hidden'); 20 track.removeAttribute('aria-hidden'); 21 track.style.display = ''; 22 track.style.visibility = ''; 23 track.style.opacity = ''; 24 track.style.height = ''; 25 }; 26 27 let splideRetryCount = 0; 28 const enableMobileSlider = () => { 29 if (!slider || splideInstance) { 30 return; 31 } 32 33 if (!window.Splide) { 34 if (splideRetryCount < 20) { 35 splideRetryCount += 1; 36 window.setTimeout(enableMobileSlider, 100); 37 } 38 return; 39 } 40 41 splideRetryCount = 0; 42 slider.classList.add('splide'); 43 cleanupTrackVisibility(); 44 45 splideInstance = new window.Splide(slider, { 46 type: 'slide', 47 perPage: 2.2, 48 gap: '12px', 49 pagination: false, 50 arrows: false, 51 drag: 'free', 52 focus: 0, 53 padding: { left: '16px', right: '16px' }, 54 breakpoints: { 55 768: { perPage: 1.6 }, 56 576: { perPage: 1.3 } 57 } 58 }); 59 splideInstance.mount(); 60 }; 61 62 const disableMobileSlider = () => { 63 if (splideInstance) { 64 splideInstance.destroy(true); 65 splideInstance = null; 66 } 67 if (slider) { 68 slider.classList.remove('splide'); 69 } 70 cleanupTrackVisibility(); 71 }; 72 73 const syncSlider = (matches) => { 74 if (matches) { 75 enableMobileSlider(); 76 } else { 77 disableMobileSlider(); 78 } 79 }; 80 81 syncSlider(mediaQuery.matches); 82 83 if (mediaQuery.addEventListener) { 84 mediaQuery.addEventListener('change', (event) => syncSlider(event.matches)); 85 } else if (mediaQuery.addListener) { 86 mediaQuery.addListener((event) => syncSlider(event.matches)); 87 } 88 89 const links = bar.querySelectorAll('.header-product-bar__item'); 90 links.forEach((link) => { 91 link.addEventListener('click', () => { 92 links.forEach((item) => { 93 item.classList.remove('is-active'); 94 item.removeAttribute('aria-current'); 95 }); 96 link.classList.add('is-active'); 97 link.setAttribute('aria-current', 'true'); 98 }); 99 }); 100 101 // showThreshold: how close to top the user must scroll to reveal the bar again. 102 const showThreshold = 2; 103 let isHidden = header.classList.contains('header-product-bar-hidden'); 104 let isTicking = false; 105 let isAnchorNavigating = false; 106 let anchorNavigationTimer = null; 107 108 // Measures the full outer height of the bar including CSS margins. 109 const getBarOuterHeight = () => { 110 const style = window.getComputedStyle(bar); 111 const marginTop = parseFloat(style.marginTop) || 0; 112 const marginBottom = parseFloat(style.marginBottom) || 0; 113 return bar.offsetHeight + marginTop + marginBottom; 114 }; 115 116 let barOuterHeight = getBarOuterHeight(); 117 118 // Keep the threshold below the bar so it remains hidden until the user returns to the top. 119 let hideThreshold = barOuterHeight + showThreshold + 16; 120 121 const recalculate = () => { 122 barOuterHeight = getBarOuterHeight(); 123 hideThreshold = barOuterHeight + showThreshold + 16; 124 }; 125 126 const suspendAutoToggleForAnchorNavigation = (ms = 900) => { 127 isAnchorNavigating = true; 128 if (anchorNavigationTimer) { 129 window.clearTimeout(anchorNavigationTimer); 130 } 131 132 anchorNavigationTimer = window.setTimeout(() => { 133 isAnchorNavigating = false; 134 updateScrollState(); 135 }, ms); 136 }; 137 138 const isSamePageAnchorUrl = (href) => { 139 if (!href) { 140 return false; 141 } 142 143 let parsed; 144 try { 145 parsed = new URL(href, window.location.href); 146 } catch (error) { 147 return false; 148 } 149 150 if (!parsed.hash) { 151 return false; 152 } 153 154 return parsed.pathname === window.location.pathname && parsed.search === window.location.search; 155 }; 156 157 const getAnchorTarget = (hash) => { 158 if (!hash) { 159 return null; 160 } 161 162 const rawId = hash.replace(/^#/, ''); 163 if (!rawId) { 164 return null; 165 } 166 167 let decodedId = rawId; 168 try { 169 decodedId = decodeURIComponent(rawId); 170 } catch (error) { 171 decodedId = rawId; 172 } 173 174 return document.getElementById(decodedId); 175 }; 176 177 const updateScrollState = () => { 178 if (isAnchorNavigating) { 179 return; 180 } 181 182 const scrollY = window.scrollY; 183 184 if (!isHidden && scrollY >= hideThreshold) { 185 header.classList.add('header-product-bar-hidden'); 186 isHidden = true; 187 return; 188 } 189 190 if (isHidden && scrollY <= showThreshold) { 191 header.classList.remove('header-product-bar-hidden'); 192 isHidden = false; 193 } 194 }; 195 196 recalculate(); 197 updateScrollState(); 198 199 links.forEach((link) => { 200 link.addEventListener('click', (event) => { 201 const href = link.getAttribute('href') || ''; 202 if (!isSamePageAnchorUrl(href)) { 203 return; 204 } 205 206 const parsedUrl = new URL(href, window.location.href); 207 const target = getAnchorTarget(parsedUrl.hash); 208 if (!target) { 209 return; 210 } 211 212 event.preventDefault(); 213 suspendAutoToggleForAnchorNavigation(); 214 215 // During anchor navigation we collapse the bar immediately, 216 // then scroll smoothly with sticky-header offset.
217 if (!isHidden) { 218 header.classList.add('header-product-bar-hidden'); 219 isHidden = true; 220 } 221 222 window.requestAnimationFrame(() => { 223 const stickyOffset = header.offsetHeight + 8; 224 const targetY = target.getBoundingClientRect().top + window.scrollY - stickyOffset; 225 226 window.scrollTo({ 227 top: Math.max(0, targetY), 228 behavior: 'smooth' 229 }); 230 231 if (window.history && typeof window.history.replaceState === 'function') { 232 window.history.replaceState(null, '', parsedUrl.hash); 233 } 234 }); 235 }); 236 }); 237 238 window.addEventListener('scroll', () => { 239 if (isTicking) { 240 return; 241 } 242 243 isTicking = true; 244 window.requestAnimationFrame(() => { 245 updateScrollState(); 246 isTicking = false; 247 }); 248 }, { passive: true }); 249 250 window.addEventListener('resize', () => { 251 recalculate(); 252 updateScrollState(); 253 }, { passive: true }); 254}
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.