1(function () { 2 'use strict'; 3 4 var items = Array.prototype.slice.call(document.querySelectorAll('.kck-nav__item[data-menu]')); 5 var panels = Array.prototype.slice.call(document.querySelectorAll('.kck-mega')); 6 var closeTimer = null; 7 /* True while a menu is open because of a touch tap. Touch browsers can fire 8 an emulated mouseleave right after the tap, which would close the menu 9 200ms later, so the hover close is skipped until the next outside tap. */ 10 var touchOpen = false; 11 12 /* The top-level trigger is a <button> for Resources and an <a> for Shop 13 Cabinets, Gallery and Company, which open their menu on hover and go to 14 their hub page on click. Everything below looks the trigger up this way. */ 15 function trigger(it) { 16 return it.querySelector('a, button'); 17 } 18 19 function closeMenus() { 20 touchOpen = false; 21 items.forEach(function (it) { 22 it.classList.remove('is-open'); 23 var b = trigger(it); 24 if (b) b.setAttribute('aria-expanded', 'false'); 25 }); 26 panels.forEach(function (p) { 27 p.classList.remove('is-open'); 28 }); 29 document.body.classList.remove('kck-menuopen'); 30 } 31 32 function openMenu(key) { 33 closeMenus(); 34 var it = items.find(function (x) { 35 return x.dataset.menu === key; 36 }); 37 var p = panels.find(function (x) { 38 return x.dataset.panel === key; 39 }); 40 if (!it || !p) return; 41 it.classList.add('is-open'); 42 p.classList.add('is-open'); 43 var b = trigger(it); 44 if (b) b.setAttribute('aria-expanded', 'true'); 45 document.body.classList.add('kck-menuopen'); 46 } 47 48 items.forEach(function (it) { 49 it.addEventListener('mouseenter', function () { 50 clearTimeout(closeTimer); 51 openMenu(it.dataset.menu); 52 }); 53 it.addEventListener('mouseleave', function () { 54 if (touchOpen) return; 55 closeTimer = setTimeout(closeMenus, 200); 56 }); 57 var link = it.querySelector('a'); 58 if (link) { 59 /* Touch at desktop widths (a sideways iPad): the first tap opens the 60 menu, the second goes to the page. Whether the menu was open is read 61 on pointerdown, before the browser's emulated mouseenter opens it. */ 62 var touchTap = false; 63 var wasOpen = false; 64 link.addEventListener('pointerdown', function (e) { 65 touchTap = e.pointerType === 'touch' || e.pointerType === 'pen'; 66 wasOpen = it.classList.contains('is-open'); 67 }); 68 link.addEventListener('click', function (e) { 69 if (touchTap && !wasOpen) { 70 e.preventDefault(); 71 clearTimeout(closeTimer); 72 openMenu(it.dataset.menu); 73 touchOpen = true; 74 } 75 touchTap = false; 76 }); 77 /* Keyboard: Enter follows the link. Arrow Down opens the menu and moves 78 focus to its first link. */ 79 link.addEventListener('keydown', function (e) { 80 if (e.key !== 'ArrowDown') return; 81 e.preventDefault(); 82 openMenu(it.dataset.menu); 83 var p = document.getElementById(link.getAttribute('aria-controls')); 84 var first = p && p.querySelector('a[href]'); 85 if (first) first.focus(); 86 }); 87 return; 88 } 89 var btn = it.querySelector('button'); 90 if (!btn) return; 91 btn.addEventListener('click', function (e) { 92 93 var keyboard = e.detail === 0; 94 var isOpen = it.classList.contains('is-open'); 95 if (keyboard) { 96 isOpen ? closeMenus() : openMenu(it.dataset.menu); 97 } else if (!isOpen) { 98 openMenu(it.dataset.menu); 99 } 100 }); 101 }); 102 103 /* A touch-opened menu closes on the next tap outside the top bar items and 104 the open panel. */ 105 document.addEventListener('pointerdown', function (e) { 106 if (!touchOpen || !e.target.closest) return; 107 if (e.target.closest('.kck-nav__item[data-menu], .kck-mega')) return; 108 closeMenus(); 109 }, true); 110 111 panels.forEach(function (p) { 112 p.addEventListener('mouseenter', function () { 113 clearTimeout(closeTimer); 114 }); 115 p.addEventListener('mouseleave', function () { 116 if (touchOpen) return; 117 closeTimer = setTimeout(closeMenus, 200); 118 }); 119 }); 120 121 (function () { 122 var rail = document.querySelector('.kck-rl'); 123 if (!rail) return; 124 var tabs = Array.prototype.slice.call(rail.querySelectorAll('.kck-rl__item')); 125 var panes = Array.prototype.slice.call(document.querySelectorAll('.kck-rp')); 126 127 function preview(key) {
128 tabs.forEach(function (t) { 129 if (t.dataset.pane === key) t.setAttribute('aria-current', 'true'); 130 else t.removeAttribute('aria-current'); 131 }); 132 panes.forEach(function (p) { 133 var on = p.dataset.pane === key; 134 p.classList.toggle('is-on', on); 135 p.hidden = !on; 136 }); 137 } 138 139 tabs.forEach(function (t) { 140 t.addEventListener('mouseenter', function () { 141 preview(t.dataset.pane); 142 }); 143 t.addEventListener('focus', function () { 144 preview(t.dataset.pane); 145 }); 146 }); 147 148 var panel = document.getElementById('p-resources'); 149 if (panel) { 150 panel.addEventListener('mouseleave', function () { 151 if (tabs.length) preview(tabs[0].dataset.pane); 152 }); 153 } 154 })(); 155 156 document.querySelectorAll('.kck-acc__tog').forEach(function (btn) { 157 btn.addEventListener('click', function () { 158 var open = btn.getAttribute('aria-expanded') === 'true'; 159 btn.setAttribute('aria-expanded', String(!open)); 160 var panel = document.getElementById(btn.getAttribute('aria-controls')); 161 if (panel) panel.hidden = open; 162 }); 163 }); 164 165 var searchCanonical = document.querySelector('[data-kck-search-canonical]'); 166 var desktopSearchSlot = document.querySelector('[data-kck-search-slot="desktop"]'); 167 var mobileSearchSlot = document.querySelector('[data-kck-search-slot="mobile"]'); 168 var searchMQ = window.matchMedia('(max-width:1023px)'); 169 170 function placeSearch() { 171 if (!searchCanonical) return; 172 var target = searchMQ.matches ? mobileSearchSlot : desktopSearchSlot; 173 if (target && searchCanonical.parentNode !== target) target.appendChild(searchCanonical); 174 } 175 176 placeSearch(); 177 if (searchMQ.addEventListener) searchMQ.addEventListener('change', placeSearch); 178 else if (searchMQ.addListener) searchMQ.addListener(placeSearch); 179 180 /* IS THE AUTOCOMPLETE ACTUALLY OPEN? 181 182 Everything below used to run on every outside mousedown, whether or not 183 there was anything to close. On a page like checkout, where the customer 184 never touches the search box, that meant a full close routine on EVERY 185 click into EVERY field. 186 187 The close routine calls jQuery's $input.blur(), and jQuery's .blur() with 188 no argument is .trigger('blur') - it fires the handlers whether or not the 189 element has focus. That reached Algolia's typeahead _onBlur, which does 190 $input.removeAttr('aria-activedescendant'), and jQuery 1.7's removeAttr 191 sets an attribute to "" before removing it. Firefox then tries to resolve 192 an element with an empty id and logs 193 194 Empty string passed to getElementById(). 195 196 once per click, forever, on every page with the search box. Harmless, but 197 it is our loop and it should not be running at all. 198 199 "Open" is answered from the DOM rather than from a flag, because the 200 dropdown can be closed by Algolia itself, by a click, or by Escape, and a 201 flag would drift out of step with at least one of those. */ 202 function algoliaAutocompleteIsOpen() { 203 var menus = document.querySelectorAll( 204 '#algolia-autocomplete-container .aa-dropdown-menu, .aa-dropdown-menu, .aa-Panel' 205 ); 206 for (var i = 0; i < menus.length; i++) { 207 var m = menus[i]; 208 /* offsetParent is null for anything display:none or detached, which is 209 what a closed dropdown is. aria-hidden covers the ones this file has 210 already closed but left in the layout. */ 211 if (m.offsetParent !== null && m.getAttribute('aria-hidden') !== 'true') return true; 212 } 213 214 /* Still open in spirit if focus is inside the search widget - the customer 215 has clicked away and we do want that blur to land. */ 216 var active = document.activeElement; 217 if ( 218 active && 219 active.closest && 220 active.closest('.kck-search-canonical, .algolia-autocomplete, #algolia-autocomplete-container') 221 ) { 222 return true; 223 } 224 225 return false; 226 } 227 228 function closeAlgoliaAutocomplete() { 229 var input = searchCanonical 230 ? searchCanonical.querySelector('#search, input[type="search"], input[type="text"]') 231 : null; 232 /* Read focus ONCE, before anything below changes it. The native blur on 233 the next line moves activeElement to <body>, so a later check against 234 document.activeElement would always be false and the jQuery branch would 235 never blur the input it was supposed to. */ 236 var activeBefore = document.activeElement; 237 238 /* Only blur what is actually focused. */ 239 if (input && input === activeBefore && typeof input.blur === 'function') input.blur(); 240 241 var config = window.algoliaConfig; 242 var bundle = window.algoliaBundle; 243 if (config && config.autocomplete && config.autocomplete.selector && bundle && bundle.$) { 244 var $ = bundle.$; 245 $(config.autocomplete.selector).each(function () { 246 var $input = $(this); 247 var data = $input.data('aaAutocomplete'); 248 /* jQuery's .blur() with no argument is .trigger('blur'), which runs 249 every bound handler regardless of focus. That is the call that 250 reached Algolia's _onBlur on every click. */
251 if (this === activeBefore) $input.blur(); 252 if (data && data.dropdown) { 253 if (typeof data.dropdown.close === 'function') data.dropdown.close(); 254 if (data.dropdown.$menu) data.dropdown.$menu.hide().attr('aria-hidden', 'true'); 255 } 256 }); 257 $('#algolia-autocomplete-container .aa-dropdown-menu, .aa-dropdown-menu, .aa-Panel') 258 .hide() 259 .attr('aria-hidden', 'true'); 260 return; 261 } 262 263 Array.prototype.forEach.call( 264 document.querySelectorAll('#algolia-autocomplete-container .aa-dropdown-menu, .aa-dropdown-menu, .aa-Panel'), 265 function (menu) { 266 menu.style.display = 'none'; 267 menu.setAttribute('aria-hidden', 'true'); 268 }, 269 ); 270 } 271 272 function algoliaAutocompleteOutsideHandler(e) { 273 var target = e.target; 274 if (!target) return; 275 276 if ( 277 target.closest && 278 target.closest('.kck-search-canonical, .algolia-autocomplete, .aa-dropdown-menu, .aa-Panel, #algolia-autocomplete-container') 279 ) { 280 return; 281 } 282 283 var config = window.algoliaConfig; 284 var bundle = window.algoliaBundle; 285 if (config && config.autocomplete && config.autocomplete.selector && bundle && bundle.$) { 286 if (bundle.$(target).closest(config.autocomplete.selector).length) return; 287 } 288 289 /* Nothing open, nothing to close. This one line is what stops the handler 290 doing a full teardown on every click on every page. */ 291 if (!algoliaAutocompleteIsOpen()) return; 292 293 closeAlgoliaAutocomplete(); 294 } 295 296 if (window.__kckAlgoliaAutocompleteOutsideHandler) { 297 document.removeEventListener('touchstart', window.__kckAlgoliaAutocompleteOutsideHandler, true); 298 document.removeEventListener('mousedown', window.__kckAlgoliaAutocompleteOutsideHandler, true); 299 } 300 window.__kckAlgoliaAutocompleteOutsideHandler = algoliaAutocompleteOutsideHandler; 301 document.addEventListener('touchstart', window.__kckAlgoliaAutocompleteOutsideHandler, true); 302 document.addEventListener('mousedown', window.__kckAlgoliaAutocompleteOutsideHandler, true); 303 304 function focusQuietly(el) { 305 if (!el) return; 306 el.setAttribute('data-quiet-focus', ''); 307 el.focus(); 308 function release() { 309 el.removeAttribute('data-quiet-focus'); 310 el.removeEventListener('blur', release); 311 el.removeEventListener('keydown', release); 312 } 313 el.addEventListener('blur', release); 314 el.addEventListener('keydown', release); 315 } 316 317 var drawer = document.getElementById('kck-drawer'); 318 var scrim = document.getElementById('kck-scrim'); 319 var burger = document.getElementById('kck-burger'); 320 var drawerClose = document.getElementById('kck-drawer-close'); 321 var levels = drawer 322 ? Array.prototype.slice.call(drawer.querySelectorAll('.kck-drawer__level')) 323 : []; 324 325 var openers = Array.prototype.slice.call(document.querySelectorAll('#kck-burger')); 326 var lastFocus = null; 327 328 function showLevel(name) { 329 levels.forEach(function (l) { 330 var isRoot = l.dataset.level === 'root'; 331 var active = l.dataset.level === name; 332 l.classList.toggle('is-active', active && !isRoot); 333 if (isRoot) l.classList.toggle('is-behind', name !== 'root'); 334 l.setAttribute('aria-hidden', String(!active)); 335 }); 336 var lv = levels.find(function (l) { 337 return l.dataset.level === name; 338 }); 339 if (!lv) return; 340 lv.scrollTop = 0; 341 } 342 343 function openDrawer() { 344 if (!drawer) return; 345 lastFocus = document.activeElement; 346 drawer.classList.add('is-open'); 347 scrim.classList.add('is-open'); 348 document.body.classList.add('kck-drawer-open'); 349 openers.forEach(function (o) { 350 o.setAttribute('aria-expanded', 'true'); 351 }); 352 drawer.removeAttribute('inert'); 353 showLevel('root'); 354 355 focusQuietly(drawer); 356 } 357 358 function closeDrawer() { 359 if (!drawer) return; 360 drawer.classList.remove('is-open'); 361 scrim.classList.remove('is-open'); 362 document.body.classList.remove('kck-drawer-open'); 363 openers.forEach(function (o) { 364 o.setAttribute('aria-expanded', 'false'); 365 }); 366 drawer.setAttribute('inert', ''); 367 focusQuietly(lastFocus); 368 } 369
370 openers.forEach(function (o) { 371 o.addEventListener('click', openDrawer); 372 }); 373 if (drawerClose) drawerClose.addEventListener('click', closeDrawer); 374 if (scrim) scrim.addEventListener('click', closeDrawer); 375 376 if (drawer) { 377 drawer.setAttribute('inert', ''); 378 drawer.addEventListener('click', function (e) { 379 var go = e.target.closest('[data-goto]'); 380 if (go) { 381 e.preventDefault(); 382 showLevel(go.dataset.goto); 383 return; 384 } 385 var back = e.target.closest('[data-back]'); 386 if (back) { 387 e.preventDefault(); 388 showLevel(back.dataset.back); 389 } 390 }); 391 392 drawer.addEventListener('keydown', function (e) { 393 if (e.key !== 'Tab') return; 394 var f = drawer.querySelectorAll( 395 'a[href],button:not([disabled]),input:not([disabled]),[tabindex]:not([tabindex="-1"])', 396 ); 397 var vis = Array.prototype.filter.call(f, function (el) { 398 return el.offsetParent !== null; 399 }); 400 if (!vis.length) return; 401 var first = vis[0], 402 last = vis[vis.length - 1]; 403 if (e.shiftKey && document.activeElement === first) { 404 e.preventDefault(); 405 last.focus(); 406 } else if (!e.shiftKey && document.activeElement === last) { 407 e.preventDefault(); 408 first.focus(); 409 } 410 }); 411 } 412 413 document.addEventListener('keydown', function (e) { 414 if (e.key !== 'Escape') return; 415 if (drawer && drawer.classList.contains('is-open')) { 416 closeDrawer(); 417 return; 418 } 419 var open = items.find(function (i) { 420 return i.classList.contains('is-open'); 421 }); 422 if (open) { 423 var b = trigger(open); 424 closeMenus(); 425 if (b) b.focus(); 426 } 427 }); 428 429 var bar = document.querySelector('.kck-bar'); 430 if (bar) { 431 var pinAt = 0, 432 ticking = false; 433 434 var chrome = document.querySelector('.kckhf'); 435 436 function measure() { 437 438 var wasPinned = bar.classList.contains('is-pinned'); 439 if (wasPinned) bar.classList.remove('is-pinned'); 440 if (chrome) chrome.style.minHeight = ''; 441 pinAt = bar.getBoundingClientRect().top + window.pageYOffset; 442 pinnedHeight = null; /* the viewport changed, re-measure on next pin */ 443 if (chrome) chrome.style.minHeight = chrome.getBoundingClientRect().height + 'px'; 444 if (wasPinned) bar.classList.add('is-pinned'); 445 } 446 447 /* THIS RUNS ON EVERY ANIMATION FRAME WHILE THE PAGE IS SCROLLING, so what 448 it does per call matters more than what it does. 449 450 As written it did three things per frame that it did not need to do: 451 452 1. WROTE THREE CUSTOM PROPERTIES ON documentElement. A custom property 453 on :root cascades to the whole document, so writing one invalidates 454 style for every element on the page. Three writes per frame, on a 455 checkout page with a large DOM, is a full style recalculation per 456 frame - and the values were almost always the same ones that were 457 already there. This is the expensive one. 458 459 2. CALLED getBoundingClientRect() ON THE PINNED BAR. That forces layout 460 mid-frame. The bar's height only changes when the viewport does, so 461 it is measured once per pin and reused. 462 463 3. BUILT A NEW MediaQueryList EVERY FRAME via window.matchMedia(). The 464 query is constant; the object is now created once. 465 466 The guard is the point: compute, compare, and return without touching 467 the DOM when nothing moved. Scrolling now writes only at the moment the 468 header actually pins or unpins. */ 469 var compactMQ = window.matchMedia ? window.matchMedia('(max-width: 1023px)') : null; 470 var lastCompactHeight = null; 471 var lastCategoryOffset = null; 472 var pinnedHeight = null; 473 474 function syncStickyOffsets() { 475 var compactHeight = compactMQ && compactMQ.matches ? 57 : 65; 476 477 var categoryOffset = 0; 478 if (bar.classList.contains('is-pinned')) { 479 if (pinnedHeight === null) pinnedHeight = Math.ceil(bar.getBoundingClientRect().height); 480 categoryOffset = pinnedHeight; 481 } else { 482 pinnedHeight = null; 483 } 484 485 if (compactHeight === lastCompactHeight && categoryOffset === lastCategoryOffset) return; 486 lastCompactHeight = compactHeight; 487 lastCategoryOffset = categoryOffset; 488 489 document.documentElement.style.setProperty('--kck-category-sticky-top', categoryOffset + 'px'); 490 document.documentElement.style.setProperty('--header-height', compactHeight + 'px'); 491 document.documentElement.style.setProperty('--kck-sticky-header-offset', categoryOffset + 'px'); 492 } 493
494 function update() { 495 ticking = false; 496 var shouldPin = window.pageYOffset > pinAt; 497 var wasPinned = bar.classList.contains('is-pinned'); 498 if (shouldPin !== wasPinned) { 499 bar.classList.toggle('is-pinned', shouldPin); 500 if (typeof closeMenus === 'function') closeMenus(); 501 } 502 syncStickyOffsets(); 503 } 504 505 function onScroll() { 506 if (ticking) return; 507 ticking = true; 508 requestAnimationFrame(update); 509 } 510 511 measure(); 512 update(); 513 window.addEventListener('scroll', onScroll, { passive: true }); 514 window.addEventListener('resize', function () { 515 measure(); 516 update(); 517 }); 518 519 window.addEventListener('load', function () { 520 measure(); 521 update(); 522 }); 523 } 524 525 /* ------------------------------------------------------------------ 526 PROMO BAR COUNTDOWN 527 Ticks the Labor Day sale bar in header.phtml and hides the whole 528 .kck-promo block once the deadline passes. The deadline lives in 529 the markup (data-countdown-deadline), not here. 530 ------------------------------------------------------------------ */ 531 function initPromoCountdown() { 532 var el = document.querySelector('[data-countdown-deadline]'); 533 if (!el) return; 534 535 var deadline = new Date(el.getAttribute('data-countdown-deadline')).getTime(); 536 if (isNaN(deadline)) return; 537 538 var promo = el.closest ? el.closest('.kck-promo') : null; 539 var out = { 540 days: el.querySelector('[data-countdown="days"]'), 541 hours: el.querySelector('[data-countdown="hours"]'), 542 minutes: el.querySelector('[data-countdown="minutes"]'), 543 seconds: el.querySelector('[data-countdown="seconds"]') 544 }; 545 546 var timer = null; 547 548 function pad(n) { 549 return String(n).length < 2 ? '0' + n : String(n); 550 } 551 552 function set(node, val) { 553 if (node && node.textContent !== val) node.textContent = val; 554 } 555 556 function tick() { 557 var diff = deadline - Date.now(); 558 559 if (diff <= 0) { 560 if (promo) promo.hidden = true; 561 if (timer) clearInterval(timer); 562 return; 563 } 564 565 set(out.days, pad(Math.floor(diff / 86400000))); 566 set(out.hours, pad(Math.floor((diff % 86400000) / 3600000))); 567 set(out.minutes, pad(Math.floor((diff % 3600000) / 60000))); 568 set(out.seconds, pad(Math.floor((diff % 60000) / 1000))); 569 } 570 571 tick(); 572 timer = setInterval(tick, 1000); 573 574 // A throttled background tab can drift a full second or more. Resync the 575 // moment it comes back rather than waiting for the next interval. 576 document.addEventListener('visibilitychange', function () { 577 if (!document.hidden) tick(); 578 }); 579 } 580 581 function initUtilityDropdowns() { 582 var root = document.querySelector('.kck-util'); 583 if (!root) return; 584 var openDropdown = null; 585 586 function getDropdown(target) { 587 return target && target.closest ? target.closest('.kckheader-utility__dropdown') : null; 588 } 589 590 function closeDropdown(dropdown) { 591 if (!dropdown) return; 592 dropdown.classList.remove('is-open'); 593 var trigger = dropdown.querySelector('.kckheader-utility__action'); 594 if (trigger) trigger.setAttribute('aria-expanded', 'false'); 595 if (openDropdown === dropdown) openDropdown = null; 596 } 597 598 function closeAllDropdowns() { 599 Array.prototype.forEach.call(root.querySelectorAll('.kckheader-utility__dropdown.is-open'), closeDropdown); 600 openDropdown = null; 601 } 602 603 function openDropdownMenu(dropdown) { 604 if (!dropdown) return; 605 if (openDropdown && openDropdown !== dropdown) closeDropdown(openDropdown); 606 dropdown.classList.add('is-open'); 607 var trigger = dropdown.querySelector('.kckheader-utility__action'); 608 if (trigger) trigger.setAttribute('aria-expanded', 'true'); 609 openDropdown = dropdown; 610 } 611 612 document.addEventListener('click', function (e) { 613 var trigger = e.target.closest ? e.target.closest('.kckheader-utility__action') : null; 614 var dropdown = getDropdown(trigger); 615 if (dropdown && dropdown.querySelector('.kckheader-utility__menu')) { 616 e.preventDefault(); 617 if (dropdown.classList.contains('is-open')) closeDropdown(dropdown); 618 else openDropdownMenu(dropdown); 619 return; 620 } 621 if (openDropdown && (!e.target.closest || !e.target.closest('.kckheader-utility__dropdown'))) closeAllDropdowns(); 622 }); 623 624 root.addEventListener('mouseover', function (e) { 625 var dropdown = getDropdown(e.target); 626 if (!dropdown || !dropdown.querySelector('.kckheader-utility__menu')) return; 627 if (e.relatedTarget && dropdown.contains(e.relatedTarget)) return; 628 openDropdownMenu(dropdown); 629 }); 630 631 root.addEventListener('mouseout', function (e) { 632 var dropdown = getDropdown(e.target); 633 if (!dropdown || !dropdown.querySelector('.kckheader-utility__menu')) return; 634 if (e.relatedTarget && dropdown.contains(e.relatedTarget)) return; 635 window.setTimeout(function () { 636 if (!dropdown.matches(':hover')) closeDropdown(dropdown); 637 }, 180); 638 }); 639 640 document.addEventListener('keydown', function (e) { 641 if (e.key === 'Escape' && openDropdown) closeAllDropdowns(); 642 }); 643 } 644 645 function setCartCount(n) { 646 n = parseInt(n, 10); 647 document.querySelectorAll('[data-cart]').forEach(function (el) { 648 var badge = el.querySelector('.kck-util__badge'); 649 if (!n || n < 1) { 650 if (badge) badge.remove(); 651 return; 652 } 653 if (!badge) { 654 badge = document.createElement('span'); 655 badge.className = 'kck-util__badge'; 656 el.appendChild(badge); 657 } 658 badge.textContent = n > 99 ? '99+' : String(n); 659 el.setAttribute('aria-label', 'Cart, ' + n + ' item' + (n === 1 ? '' : 's')); 660 }); 661 } 662 function initCartBadgeSync() { 663 var cartHost = document.querySelector('.kck-util__cart-live'); 664 if (!cartHost) return; 665 var lastQty = null; 666 667 function parseQty(text) { 668 if (!text) return null; 669 var paren = String(text).match(/\((\d+)\)/); 670 if (paren) return parseInt(paren[1], 10); 671 var bare = String(text).trim().match(/^(\d+)$/); 672 return bare ? parseInt(bare[1], 10) : null; 673 } 674 675 function readQty() { 676 var mini = document.getElementById('mini-cart'); 677 if (!mini) return null; 678 var counter = mini.querySelector('.counter-number'); 679 var qty = counter ? parseQty(counter.textContent) : null; 680 if (qty !== null) return qty; 681 var badge = mini.querySelector('.kckheader-utility__cart-badge'); 682 qty = badge ? parseQty(badge.textContent) : null; 683 if (qty !== null) return qty; 684 var label = mini.querySelector('.mini-cart-heading .label'); 685 return label ? parseQty(label.textContent) : null; 686 } 687 688 function sync() { 689 var qty = readQty(); 690 if (qty === null) return; 691 var text = String(qty); 692 Array.prototype.forEach.call(document.querySelectorAll('.kckheader-utility__cart-badge, .kckheader-utility__cart-qty, #mini-cart .counter-number'), function (el) { 693 if (el.textContent.trim() !== text) el.textContent = text; 694 }); 695 if (qty !== lastQty) { 696 lastQty = qty; 697 setCartCount(qty); 698 } 699 var mini = document.getElementById('mini-cart'); 700 if (mini) {
701 mini.classList.remove('is-empty'); 702 mini.classList.add('is-not-empty'); 703 } 704 } 705 706 sync(); 707 if ('MutationObserver' in window) new MutationObserver(sync).observe(cartHost, { childList: true, subtree: true, characterData: true }); 708 document.addEventListener('ajaxComplete', sync); 709 document.addEventListener('cart:updated', sync); 710 } 711 712 713 function initScrollTop() { 714 if (!document.body || document.getElementById('kck-totop')) return; 715 716 if (!document.getElementById('i-top')) { 717 var svgNs = 'http://www.w3.org/2000/svg'; 718 var sprite = document.createElementNS(svgNs, 'svg'); 719 sprite.setAttribute('aria-hidden', 'true'); 720 sprite.setAttribute('width', '0'); 721 sprite.setAttribute('height', '0'); 722 sprite.style.position = 'absolute'; 723 sprite.style.width = '0'; 724 sprite.style.height = '0'; 725 sprite.style.overflow = 'hidden'; 726 727 var defs = document.createElementNS(svgNs, 'defs'); 728 var symbol = document.createElementNS(svgNs, 'symbol'); 729 symbol.setAttribute('id', 'i-top'); 730 symbol.setAttribute('viewBox', '0 0 24 24'); 731 732 var path1 = document.createElementNS(svgNs, 'path'); 733 path1.setAttribute('d', 'M12 19V6'); 734 var path2 = document.createElementNS(svgNs, 'path'); 735 path2.setAttribute('d', 'M6 11.5L12 5.5l6 6'); 736 737 symbol.appendChild(path1); 738 symbol.appendChild(path2); 739 defs.appendChild(symbol); 740 sprite.appendChild(defs); 741 document.body.appendChild(sprite); 742 } 743 744 var toTop = document.createElement('a'); 745 toTop.className = 'kck-totop'; 746 toTop.id = 'kck-totop'; 747 toTop.href = '#main'; 748 toTop.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" focusable="false"><use href="#i-top" xlink:href="#i-top"></use></svg><span>Back to top</span>'; 749 document.body.appendChild(toTop); 750 751 var threshold = 800; 752 var scrollTicking = false; 753 var motionQuery = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null; 754 755 function updateScrollTop() { 756 var y = window.pageYOffset || document.documentElement.scrollTop || 0; 757 toTop.classList.toggle('is-visible', y > threshold); 758 scrollTicking = false; 759 } 760 761 function onScrollTopScroll() { 762 if (scrollTicking) return; 763 scrollTicking = true; 764 window.requestAnimationFrame(updateScrollTop); 765 } 766 767 toTop.addEventListener('click', function (e) { 768 e.preventDefault(); 769 var behavior = motionQuery && motionQuery.matches ? 'auto' : 'smooth'; 770 try { 771 window.scrollTo({ top: 0, behavior: behavior }); 772 } catch (err) { 773 window.scrollTo(0, 0); 774 } 775 776 if (e.detail === 0) { 777 var top = document.querySelector('.kck-logo, .kck-bar__logo, .logo'); 778 if (top && typeof top.focus === 'function') top.focus(); 779 } 780 }); 781 782 window.addEventListener('scroll', onScrollTopScroll, { passive: true }); 783 updateScrollTop(); 784 } 785 786 if (document.body) initScrollTop(); 787 else document.addEventListener('DOMContentLoaded', initScrollTop); 788 789 /* A VERSION MARKER, so you can tell which build a given tab is running. 790 Check it in any console with: KCK_HEADER 791 Bump the date when this file changes. */ 792 window.KCK_HEADER = { version: '2026.09.04' }; 793 794 window.kckSetCartCount = setCartCount; 795 initUtilityDropdowns(); 796 initCartBadgeSync(); 797 initPromoCountdown(); 798 if (typeof window.KCK_CART_COUNT === 'number') setCartCount(window.KCK_CART_COUNT); 799 800})();
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.