1function getFocusableElements(container) { 2 return Array.from( 3 container.querySelectorAll( 4 "summary, a[href], button:enabled, [tabindex]:not([tabindex^='-']), [draggable], area, input:not([type=hidden]):enabled, select:enabled, textarea:enabled, object, iframe" 5 ) 6 ); 7} 8 9class SectionId { 10 static #separator = '__'; 11 12 // for a qualified section id (e.g. 'template--22224696705326__main'), return just the section id (e.g. 'template--22224696705326') 13 static parseId(qualifiedSectionId) { 14 return qualifiedSectionId.split(SectionId.#separator)[0]; 15 } 16 17 // for a qualified section id (e.g. 'template--22224696705326__main'), return just the section name (e.g. 'main') 18 static parseSectionName(qualifiedSectionId) { 19 return qualifiedSectionId.split(SectionId.#separator)[1]; 20 } 21 22 // for a section id (e.g. 'template--22224696705326') and a section name (e.g. 'recommended-products'), return a qualified section id (e.g. 'template--22224696705326__recommended-products') 23 static getIdForSection(sectionId, sectionName) { 24 return `${sectionId}${SectionId.#separator}${sectionName}`; 25 } 26} 27 28class HTMLUpdateUtility { 29 /** 30 * Used to swap an HTML node with a new node. 31 * The new node is inserted as a previous sibling to the old node, the old node is hidden, and then the old node is removed. 32 * 33 * The function currently uses a double buffer approach, but this should be replaced by a view transition once it is more widely supported https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API 34 */ 35 static viewTransition(oldNode, newContent, preProcessCallbacks = [], postProcessCallbacks = []) { 36 preProcessCallbacks?.forEach((callback) => callback(newContent)); 37 38 const newNodeWrapper = document.createElement('div'); 39 HTMLUpdateUtility.setInnerHTML(newNodeWrapper, newContent.outerHTML); 40 const newNode = newNodeWrapper.firstChild; 41 42 // dedupe IDs 43 const uniqueKey = Date.now(); 44 oldNode.querySelectorAll('[id], [form]').forEach((element) => { 45 element.id && (element.id = `${element.id}-${uniqueKey}`); 46 element.form && element.setAttribute('form', `${element.form.getAttribute('id')}-${uniqueKey}`); 47 }); 48 49 oldNode.parentNode.insertBefore(newNode, oldNode); 50 oldNode.style.display = 'none'; 51 52 postProcessCallbacks?.forEach((callback) => callback(newNode)); 53 54 setTimeout(() => oldNode.remove(), 500); 55 } 56 57 // Sets inner HTML and reinjects the script tags to allow execution. By default, scripts are disabled when using element.innerHTML. 58 static setInnerHTML(element, html) { 59 element.innerHTML = html; 60 element.querySelectorAll('script').forEach((oldScriptTag) => { 61 const newScriptTag = document.createElement('script'); 62 Array.from(oldScriptTag.attributes).forEach((attribute) => { 63 newScriptTag.setAttribute(attribute.name, attribute.value); 64 }); 65 newScriptTag.appendChild(document.createTextNode(oldScriptTag.innerHTML)); 66 oldScriptTag.parentNode.replaceChild(newScriptTag, oldScriptTag); 67 }); 68 } 69} 70 71document.querySelectorAll('[id^="Details-"] summary').forEach((summary) => { 72 summary.setAttribute('role', 'button'); 73 summary.setAttribute('aria-expanded', summary.parentNode.hasAttribute('open')); 74 75 if (summary.nextElementSibling.getAttribute('id')) { 76 summary.setAttribute('aria-controls', summary.nextElementSibling.id); 77 } 78 79 summary.addEventListener('click', (event) => { 80 event.currentTarget.setAttribute('aria-expanded', !event.currentTarget.closest('details').hasAttribute('open')); 81 }); 82 83 if (summary.closest('header-drawer, menu-drawer')) return; 84 summary.parentElement.addEventListener('keyup', onKeyUpEscape); 85}); 86 87const trapFocusHandlers = {}; 88 89function trapFocus(container, elementToFocus = container) { 90 var elements = getFocusableElements(container); 91 var first = elements[0]; 92 var last = elements[elements.length - 1]; 93 94 removeTrapFocus(); 95 96 trapFocusHandlers.focusin = (event) => { 97 if (event.target !== container && event.target !== last && event.target !== first) return; 98 99 document.addEventListener('keydown', trapFocusHandlers.keydown); 100 }; 101 102 trapFocusHandlers.focusout = function () { 103 document.removeEventListener('keydown', trapFocusHandlers.keydown); 104 }; 105 106 trapFocusHandlers.keydown = function (event) { 107 if (event.code.toUpperCase() !== 'TAB') return; // If not TAB key 108 // On the last focusable element and tab forward, focus the first element. 109 if (event.target === last && !event.shiftKey) { 110 event.preventDefault(); 111 first.focus(); 112 } 113 114 // On the first focusable element and tab backward, focus the last element. 115 if ((event.target === container || event.target === first) && event.shiftKey) { 116 event.preventDefault(); 117 last.focus(); 118 } 119 }; 120 121 document.addEventListener('focusout', trapFocusHandlers.focusout); 122 document.addEventListener('focusin', trapFocusHandlers.focusin); 123 124 elementToFocus.focus(); 125 126 if ( 127 elementToFocus.tagName === 'INPUT' && 128 ['search', 'text', 'email', 'url'].includes(elementToFocus.type) && 129 elementToFocus.value 130 ) { 131 elementToFocus.setSelectionRange(0, elementToFocus.value.length); 132 } 133} 134 135// Here run the querySelector to figure out if the browser supports :focus-visible or not and run code based on it. 136try { 137 document.querySelector(':focus-visible'); 138} catch (e) { 139 focusVisiblePolyfill(); 140} 141 142function focusVisiblePolyfill() { 143 const navKeys = [ 144 'ARROWUP', 145 'ARROWDOWN', 146 'ARROWLEFT', 147 'ARROWRIGHT', 148 'TAB', 149 'ENTER', 150 'SPACE', 151 'ESCAPE', 152 'HOME', 153 'END', 154 'PAGEUP', 155 'PAGEDOWN', 156 ];
157 let currentFocusedElement = null; 158 let mouseClick = null; 159 160 window.addEventListener('keydown', (event) => { 161 if (navKeys.includes(event.code.toUpperCase())) { 162 mouseClick = false; 163 } 164 }); 165 166 window.addEventListener('mousedown', (event) => { 167 mouseClick = true; 168 }); 169 170 window.addEventListener( 171 'focus', 172 () => { 173 if (currentFocusedElement) currentFocusedElement.classList.remove('focused'); 174 175 if (mouseClick) return; 176 177 currentFocusedElement = document.activeElement; 178 currentFocusedElement.classList.add('focused'); 179 }, 180 true 181 ); 182} 183 184function pauseAllMedia() { 185 document.querySelectorAll('.js-youtube').forEach((video) => { 186 video.contentWindow.postMessage('{"event":"command","func":"' + 'pauseVideo' + '","args":""}', '*'); 187 }); 188 document.querySelectorAll('.js-vimeo').forEach((video) => { 189 video.contentWindow.postMessage('{"method":"pause"}', '*'); 190 }); 191 document.querySelectorAll('video').forEach((video) => video.pause()); 192 document.querySelectorAll('product-model').forEach((model) => { 193 if (model.modelViewerUI) model.modelViewerUI.pause(); 194 }); 195} 196 197function removeTrapFocus(elementToFocus = null) { 198 document.removeEventListener('focusin', trapFocusHandlers.focusin); 199 document.removeEventListener('focusout', trapFocusHandlers.focusout); 200 document.removeEventListener('keydown', trapFocusHandlers.keydown); 201 202 if (elementToFocus) elementToFocus.focus(); 203} 204 205function onKeyUpEscape(event) { 206 if (event.code.toUpperCase() !== 'ESCAPE') return; 207 208 const openDetailsElement = event.target.closest('details[open]'); 209 if (!openDetailsElement) return; 210 211 const summaryElement = openDetailsElement.querySelector('summary'); 212 openDetailsElement.removeAttribute('open'); 213 summaryElement.setAttribute('aria-expanded', false); 214 summaryElement.focus(); 215} 216 217class QuantityInput extends HTMLElement { 218 constructor() { 219 super(); 220 this.input = this.querySelector('input'); 221 this.changeEvent = new Event('change', { bubbles: true }); 222 this.input.addEventListener('change', this.onInputChange.bind(this)); 223 this.querySelectorAll('button').forEach((button) => 224 button.addEventListener('click', this.onButtonClick.bind(this)) 225 ); 226 } 227 228 quantityUpdateUnsubscriber = undefined; 229 230 connectedCallback() { 231 this.validateQtyRules(); 232 this.quantityUpdateUnsubscriber = subscribe(PUB_SUB_EVENTS.quantityUpdate, this.validateQtyRules.bind(this)); 233 } 234 235 disconnectedCallback() { 236 if (this.quantityUpdateUnsubscriber) { 237 this.quantityUpdateUnsubscriber(); 238 } 239 } 240 241 onInputChange(event) { 242 this.validateQtyRules(); 243 } 244 245 onButtonClick(event) { 246 event.preventDefault(); 247 const previousValue = this.input.value; 248 249 if (event.target.name === 'plus') { 250 if (parseInt(this.input.dataset.min) > parseInt(this.input.step) && this.input.value == 0) { 251 this.input.value = this.input.dataset.min; 252 } else { 253 this.input.stepUp(); 254 } 255 } else { 256 this.input.stepDown(); 257 } 258 259 if (previousValue !== this.input.value) this.input.dispatchEvent(this.changeEvent); 260 261 if (this.input.dataset.min === previousValue && event.target.name === 'minus') { 262 this.input.value = parseInt(this.input.min); 263 } 264 } 265 266 validateQtyRules() { 267 const value = parseInt(this.input.value); 268 if (this.input.min) { 269 const buttonMinus = this.querySelector(".quantity__button[name='minus']"); 270 buttonMinus.classList.toggle('disabled', parseInt(value) <= parseInt(this.input.min)); 271 } 272 if (this.input.max) { 273 const max = parseInt(this.input.max); 274 const buttonPlus = this.querySelector(".quantity__button[name='plus']"); 275 buttonPlus.classList.toggle('disabled', value >= max); 276 } 277 } 278} 279 280customElements.define('quantity-input', QuantityInput); 281 282function debounce(fn, wait) { 283 let t; 284 return (...args) => { 285 clearTimeout(t); 286 t = setTimeout(() => fn.apply(this, args), wait); 287 }; 288} 289 290 291function throttle(fn, delay) { 292 let lastCall = 0; 293 return function (...args) { 294 const now = new Date().getTime(); 295 if (now - lastCall < delay) { 296 return; 297 }
298 lastCall = now; 299 return fn(...args); 300 }; 301} 302 303function fetchConfig(type = 'json') { 304 return { 305 method: 'POST', 306 headers: { 'Content-Type': 'application/json', Accept: `application/${type}` }, 307 }; 308} 309 310/* 311 * Shopify Common JS 312 * 313 */ 314if (typeof window.Shopify == 'undefined') { 315 window.Shopify = {}; 316} 317 318Shopify.bind = function (fn, scope) { 319 return function () { 320 return fn.apply(scope, arguments); 321 }; 322}; 323 324Shopify.setSelectorByValue = function (selector, value) { 325 for (var i = 0, count = selector.options.length; i < count; i++) { 326 var option = selector.options[i]; 327 if (value == option.value || value == option.innerHTML) { 328 selector.selectedIndex = i; 329 return i; 330 } 331 } 332}; 333 334Shopify.addListener = function (target, eventName, callback) { 335 target.addEventListener 336 ? target.addEventListener(eventName, callback, false) 337 : target.attachEvent('on' + eventName, callback); 338}; 339 340Shopify.postLink = function (path, options) { 341 options = options || {}; 342 var method = options['method'] || 'post'; 343 var params = options['parameters'] || {}; 344 345 var form = document.createElement('form'); 346 form.setAttribute('method', method); 347 form.setAttribute('action', path); 348 349 for (var key in params) { 350 var hiddenField = document.createElement('input'); 351 hiddenField.setAttribute('type', 'hidden'); 352 hiddenField.setAttribute('name', key); 353 hiddenField.setAttribute('value', params[key]); 354 form.appendChild(hiddenField); 355 } 356 document.body.appendChild(form); 357 form.submit(); 358 document.body.removeChild(form); 359}; 360 361Shopify.CountryProvinceSelector = function (country_domid, province_domid, options) { 362 this.countryEl = document.getElementById(country_domid); 363 this.provinceEl = document.getElementById(province_domid); 364 this.provinceContainer = document.getElementById(options['hideElement'] || province_domid); 365 366 Shopify.addListener(this.countryEl, 'change', Shopify.bind(this.countryHandler, this)); 367 368 this.initCountry(); 369 this.initProvince(); 370}; 371 372Shopify.CountryProvinceSelector.prototype = { 373 initCountry: function () { 374 var value = this.countryEl.getAttribute('data-default'); 375 Shopify.setSelectorByValue(this.countryEl, value); 376 this.countryHandler(); 377 }, 378 379 initProvince: function () { 380 var value = this.provinceEl.getAttribute('data-default'); 381 if (value && this.provinceEl.options.length > 0) { 382 Shopify.setSelectorByValue(this.provinceEl, value); 383 } 384 }, 385 386 countryHandler: function (e) { 387 var opt = this.countryEl.options[this.countryEl.selectedIndex]; 388 var raw = opt.getAttribute('data-provinces'); 389 var provinces = JSON.parse(raw); 390 391 this.clearOptions(this.provinceEl); 392 if (provinces && provinces.length == 0) { 393 this.provinceContainer.style.display = 'none'; 394 } else { 395 for (var i = 0; i < provinces.length; i++) { 396 var opt = document.createElement('option'); 397 opt.value = provinces[i][0]; 398 opt.innerHTML = provinces[i][1]; 399 this.provinceEl.appendChild(opt); 400 } 401 402 this.provinceContainer.style.display = ''; 403 } 404 }, 405 406 clearOptions: function (selector) { 407 while (selector.firstChild) { 408 selector.removeChild(selector.firstChild); 409 } 410 }, 411 412 setOptions: function (selector, values) { 413 for (var i = 0, count = values.length; i < values.length; i++) { 414 var opt = document.createElement('option'); 415 opt.value = values[i]; 416 opt.innerHTML = values[i]; 417 selector.appendChild(opt); 418 } 419 }, 420}; 421 422class MenuDrawer extends HTMLElement { 423 constructor() { 424 super(); 425 426 this.mainDetailsToggle = this.querySelector('details'); 427 428 this.addEventListener('keyup', this.onKeyUp.bind(this)); 429 this.addEventListener('focusout', this.onFocusOut.bind(this)); 430 this.bindEvents(); 431 } 432 433 bindEvents() { 434 this.querySelectorAll('summary').forEach((summary) => 435 summary.addEventListener('click', this.onSummaryClick.bind(this)) 436 ); 437 this.querySelectorAll( 438 'button:not(.localization-selector):not(.country-selector__close-button):not(.country-filter__reset-button)' 439 ).forEach((button) => button.addEventListener('click', this.onCloseButtonClick.bind(this))); 440 } 441 442 onKeyUp(event) { 443 if (event.code.toUpperCase() !== 'ESCAPE') return; 444 445 const openDetailsElement = event.target.closest('details[open]'); 446 if (!openDetailsElement) return; 447 448 openDetailsElement === this.mainDetailsToggle 449 ? this.closeMenuDrawer(event, this.mainDetailsToggle.querySelector('summary')) 450 : this.closeSubmenu(openDetailsElement); 451 } 452 453 onSummaryClick(event) { 454 const summaryElement = event.currentTarget; 455 const detailsElement = summaryElement.parentNode; 456 const parentMenuElement = detailsElement.closest('.has-submenu'); 457 const isOpen = detailsElement.hasAttribute('open'); 458 const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); 459 460 function addTrapFocus() { 461 trapFocus(summaryElement.nextElementSibling, detailsElement.querySelector('button')); 462 summaryElement.nextElementSibling.removeEventListener('transitionend', addTrapFocus); 463 } 464 465 if (detailsElement === this.mainDetailsToggle) { 466 if (isOpen) event.preventDefault(); 467 isOpen ? this.closeMenuDrawer(event, summaryElement) : this.openMenuDrawer(summaryElement); 468 469 if (window.matchMedia('(max-width: 990px)')) { 470 document.documentElement.style.setProperty('--viewport-height', `${window.innerHeight}px`); 471 } 472 } else { 473 setTimeout(() => { 474 detailsElement.classList.add('menu-opening'); 475 summaryElement.setAttribute('aria-expanded', true); 476 parentMenuElement && parentMenuElement.classList.add('submenu-open'); 477 !reducedMotion || reducedMotion.matches 478 ? addTrapFocus() 479 : summaryElement.nextElementSibling.addEventListener('transitionend', addTrapFocus); 480 }, 100); 481 } 482 } 483 484 openMenuDrawer(summaryElement) { 485 setTimeout(() => { 486 this.mainDetailsToggle.classList.add('menu-opening'); 487 }); 488 summaryElement.setAttribute('aria-expanded', true); 489 trapFocus(this.mainDetailsToggle, summaryElement); 490 document.body.classList.add(`overflow-hidden-${this.dataset.breakpoint}`); 491 } 492 493 closeMenuDrawer(event, elementToFocus = false) { 494 if (event === undefined) return; 495 496 this.mainDetailsToggle.classList.remove('menu-opening'); 497 this.mainDetailsToggle.querySelectorAll('details').forEach((details) => { 498 details.removeAttribute('open'); 499 details.classList.remove('menu-opening'); 500 }); 501 this.mainDetailsToggle.querySelectorAll('.submenu-open').forEach((submenu) => { 502 submenu.classList.remove('submenu-open'); 503 }); 504 document.body.classList.remove(`overflow-hidden-${this.dataset.breakpoint}`); 505 removeTrapFocus(elementToFocus); 506 this.closeAnimation(this.mainDetailsToggle); 507 508 if (event instanceof KeyboardEvent) elementToFocus?.setAttribute('aria-expanded', false); 509 } 510 511 onFocusOut() { 512 setTimeout(() => { 513 if (this.mainDetailsToggle.hasAttribute('open') && !this.mainDetailsToggle.contains(document.activeElement)) 514 this.closeMenuDrawer(); 515 }); 516 } 517 518 onCloseButtonClick(event) { 519 const detailsElement = event.currentTarget.closest('details'); 520 this.closeSubmenu(detailsElement); 521 } 522 523 closeSubmenu(detailsElement) { 524 const parentMenuElement = detailsElement.closest('.submenu-open'); 525 parentMenuElement && parentMenuElement.classList.remove('submenu-open'); 526 detailsElement.classList.remove('menu-opening'); 527 detailsElement.querySelector('summary').setAttribute('aria-expanded', false);
528 removeTrapFocus(detailsElement.querySelector('summary')); 529 this.closeAnimation(detailsElement); 530 } 531 532 closeAnimation(detailsElement) { 533 let animationStart; 534 535 const handleAnimation = (time) => { 536 if (animationStart === undefined) { 537 animationStart = time; 538 } 539 540 const elapsedTime = time - animationStart; 541 542 if (elapsedTime < 400) { 543 window.requestAnimationFrame(handleAnimation); 544 } else { 545 detailsElement.removeAttribute('open'); 546 if (detailsElement.closest('details[open]')) { 547 trapFocus(detailsElement.closest('details[open]'), detailsElement.querySelector('summary')); 548 } 549 } 550 }; 551 552 window.requestAnimationFrame(handleAnimation); 553 } 554} 555 556customElements.define('menu-drawer', MenuDrawer); 557 558class HeaderDrawer extends MenuDrawer { 559 constructor() { 560 super(); 561 } 562 563 openMenuDrawer(summaryElement) { 564 this.header = this.header || document.querySelector('.section-header'); 565 this.borderOffset = 566 this.borderOffset || this.closest('.header-wrapper').classList.contains('header-wrapper--border-bottom') ? 1 : 0; 567 document.documentElement.style.setProperty( 568 '--header-bottom-position', 569 `${parseInt(this.header.getBoundingClientRect().bottom - this.borderOffset)}px` 570 ); 571 this.header.classList.add('menu-open'); 572 573 setTimeout(() => { 574 this.mainDetailsToggle.classList.add('menu-opening'); 575 }); 576 577 summaryElement.setAttribute('aria-expanded', true); 578 window.addEventListener('resize', this.onResize); 579 trapFocus(this.mainDetailsToggle, summaryElement); 580 document.body.classList.add(`overflow-hidden-${this.dataset.breakpoint}`); 581 } 582 583 closeMenuDrawer(event, elementToFocus) { 584 if (!elementToFocus) return; 585 super.closeMenuDrawer(event, elementToFocus); 586 this.header.classList.remove('menu-open'); 587 window.removeEventListener('resize', this.onResize); 588 } 589 590 onResize = () => { 591 this.header && 592 document.documentElement.style.setProperty( 593 '--header-bottom-position', 594 `${parseInt(this.header.getBoundingClientRect().bottom - this.borderOffset)}px` 595 ); 596 document.documentElement.style.setProperty('--viewport-height', `${window.innerHeight}px`); 597 }; 598} 599 600customElements.define('header-drawer', HeaderDrawer); 601 602class ModalDialog extends HTMLElement { 603 constructor() { 604 super(); 605 this.querySelector('[id^="ModalClose-"]').addEventListener('click', this.hide.bind(this, false)); 606 this.addEventListener('keyup', (event) => { 607 if (event.code.toUpperCase() === 'ESCAPE') this.hide(); 608 }); 609 if (this.classList.contains('media-modal')) { 610 this.addEventListener('pointerup', (event) => { 611 if (event.pointerType === 'mouse' && !event.target.closest('deferred-media, product-model')) this.hide(); 612 }); 613 } else { 614 this.addEventListener('click', (event) => { 615 if (event.target === this) this.hide(); 616 }); 617 } 618 } 619 620 connectedCallback() { 621 if (this.moved) return; 622 this.moved = true; 623 this.dataset.section = this.closest('.shopify-section').id.replace('shopify-section-', ''); 624 document.body.appendChild(this); 625 } 626 627 show(opener) { 628 this.openedBy = opener; 629 const popup = this.querySelector('.template-popup'); 630 document.body.classList.add('overflow-hidden'); 631 this.setAttribute('open', ''); 632 if (popup) popup.loadContent(); 633 trapFocus(this, this.querySelector('[role="dialog"]')); 634 window.pauseAllMedia(); 635 } 636 637 hide() { 638 document.body.classList.remove('overflow-hidden'); 639 document.body.dispatchEvent(new CustomEvent('modalClosed')); 640 this.removeAttribute('open'); 641 removeTrapFocus(this.openedBy); 642 window.pauseAllMedia(); 643 } 644} 645customElements.define('modal-dialog', ModalDialog); 646 647class BulkModal extends HTMLElement { 648 constructor() { 649 super(); 650 } 651 652 connectedCallback() { 653 const handleIntersection = (entries, observer) => { 654 if (!entries[0].isIntersecting) return; 655 observer.unobserve(this); 656 if (this.innerHTML.trim() === '') { 657 const productUrl = this.dataset.url.split('?')[0]; 658 fetch(`${productUrl}?section_id=bulk-quick-order-list`) 659 .then((response) => response.text()) 660 .then((responseText) => { 661 const html = new DOMParser().parseFromString(responseText, 'text/html'); 662 const sourceQty = html.querySelector('.quick-order-list-container').parentNode; 663 this.innerHTML = sourceQty.innerHTML; 664 }) 665 .catch((e) => { 666 console.error(e); 667 }); 668 } 669 }; 670 671 new IntersectionObserver(handleIntersection.bind(this)).observe( 672 document.querySelector(`#QuickBulk-${this.dataset.productId}-${this.dataset.sectionId}`) 673 ); 674 } 675} 676 677customElements.define('bulk-modal', BulkModal); 678 679class ModalOpener extends HTMLElement { 680 constructor() { 681 super(); 682 683 const button = this.querySelector('button'); 684 685 if (!button) return; 686 button.addEventListener('click', () => { 687 const modal = document.querySelector(this.getAttribute('data-modal')); 688 if (modal) modal.show(button); 689 }); 690 } 691} 692customElements.define('modal-opener', ModalOpener); 693 694class DeferredMedia extends HTMLElement { 695 constructor() { 696 super(); 697 const poster = this.querySelector('[id^="Deferred-Poster-"]'); 698 if (!poster) return; 699 poster.addEventListener('click', this.loadContent.bind(this)); 700 } 701 702 loadContent(focus = true) { 703 window.pauseAllMedia(); 704 if (!this.getAttribute('loaded')) { 705 const content = document.createElement('div'); 706 content.appendChild(this.querySelector('template').content.firstElementChild.cloneNode(true)); 707 708 this.setAttribute('loaded', true); 709 const deferredElement = this.appendChild(content.querySelector('video, model-viewer, iframe')); 710 if (focus) deferredElement.focus(); 711 if (deferredElement.nodeName == 'VIDEO' && deferredElement.getAttribute('autoplay')) { 712 // force autoplay for safari 713 deferredElement.play(); 714 } 715 716 // Workaround for safari iframe bug 717 const formerStyle = deferredElement.getAttribute('style'); 718 deferredElement.setAttribute('style', 'display: block;'); 719 window.setTimeout(() => { 720 deferredElement.setAttribute('style', formerStyle); 721 }, 0); 722 } 723 } 724} 725 726customElements.define('deferred-media', DeferredMedia); 727 728class SliderComponent extends HTMLElement { 729 constructor() { 730 super(); 731 this.slider = this.querySelector('[id^="Slider-"]'); 732 this.sliderItems = this.querySelectorAll('[id^="Slide-"]'); 733 this.enableSliderLooping = false;
734 this.currentPageElement = this.querySelector('.slider-counter--current'); 735 this.pageTotalElement = this.querySelector('.slider-counter--total'); 736 this.prevButton = this.querySelector('button[name="previous"]'); 737 this.nextButton = this.querySelector('button[name="next"]'); 738 739 if (!this.slider || !this.nextButton) return; 740 741 this.initPages(); 742 const resizeObserver = new ResizeObserver((entries) => this.initPages()); 743 resizeObserver.observe(this.slider); 744 745 this.slider.addEventListener('scroll', this.update.bind(this)); 746 this.prevButton.addEventListener('click', this.onButtonClick.bind(this)); 747 this.nextButton.addEventListener('click', this.onButtonClick.bind(this)); 748 } 749 750 initPages() { 751 this.sliderItemsToShow = Array.from(this.sliderItems).filter((element) => element.clientWidth > 0); 752 if (this.sliderItemsToShow.length < 2) return; 753 this.sliderItemOffset = this.sliderItemsToShow[1].offsetLeft - this.sliderItemsToShow[0].offsetLeft; 754 this.slidesPerPage = Math.floor( 755 (this.slider.clientWidth - this.sliderItemsToShow[0].offsetLeft) / this.sliderItemOffset 756 ); 757 this.totalPages = this.sliderItemsToShow.length - this.slidesPerPage + 1; 758 this.update(); 759 } 760 761 resetPages() { 762 this.sliderItems = this.querySelectorAll('[id^="Slide-"]'); 763 this.initPages(); 764 } 765 766 update() { 767 if (!this.slider || !this.nextButton) return; 768 769 const previousPage = this.currentPage; 770 this.currentPage = Math.round(this.slider.scrollLeft / this.sliderItemOffset) + 1; 771 772 if (this.currentPageElement && this.pageTotalElement) { 773 this.currentPageElement.textContent = this.currentPage; 774 this.pageTotalElement.textContent = this.totalPages; 775 } 776 777 if (this.currentPage != previousPage) { 778 this.dispatchEvent( 779 new CustomEvent('slideChanged', { 780 detail: { 781 currentPage: this.currentPage, 782 currentElement: this.sliderItemsToShow[this.currentPage - 1], 783 }, 784 }) 785 ); 786 } 787 788 if (this.enableSliderLooping) return; 789 790 if (this.isSlideVisible(this.sliderItemsToShow[0]) && this.slider.scrollLeft === 0) { 791 this.prevButton.setAttribute('disabled', 'disabled'); 792 } else { 793 this.prevButton.removeAttribute('disabled'); 794 } 795 796 if (this.isSlideVisible(this.sliderItemsToShow[this.sliderItemsToShow.length - 1])) { 797 this.nextButton.setAttribute('disabled', 'disabled'); 798 } else { 799 this.nextButton.removeAttribute('disabled'); 800 } 801 } 802 803 isSlideVisible(element, offset = 0) { 804 const lastVisibleSlide = this.slider.clientWidth + this.slider.scrollLeft - offset; 805 return element.offsetLeft + element.clientWidth <= lastVisibleSlide && element.offsetLeft >= this.slider.scrollLeft; 806 } 807 808 onButtonClick(event) { 809 event.preventDefault(); 810 const step = event.currentTarget.dataset.step || 1; 811 this.slideScrollPosition = 812 event.currentTarget.name === 'next' 813 ? this.slider.scrollLeft + step * this.sliderItemOffset 814 : this.slider.scrollLeft - step * this.sliderItemOffset; 815 this.setSlidePosition(this.slideScrollPosition); 816 } 817 818 setSlidePosition(position) { 819 this.slider.scrollTo({ 820 left: position, 821 }); 822 } 823} 824 825customElements.define('slider-component', SliderComponent); 826 827class SlideshowComponent extends SliderComponent { 828 constructor() { 829 super(); 830 this.sliderControlWrapper = this.querySelector('.slider-buttons'); 831 this.enableSliderLooping = true; 832 833 if (!this.sliderControlWrapper) return; 834 835 this.sliderFirstItemNode = this.slider.querySelector('.slideshow__slide'); 836 if (this.sliderItemsToShow.length > 0) this.currentPage = 1; 837 838 this.announcementBarSlider = this.querySelector('.announcement-bar-slider'); 839 // Value below should match --duration-announcement-bar
839CSS value 840 this.announcerBarAnimationDelay = this.announcementBarSlider ? 250 : 0; 841 842 this.sliderControlLinksArray = Array.from(this.sliderControlWrapper.querySelectorAll('.slider-counter__link')); 843 this.sliderControlLinksArray.forEach((link) => link.addEventListener('click', this.linkToSlide.bind(this))); 844 this.slider.addEventListener('scroll', this.setSlideVisibility.bind(this)); 845 this.setSlideVisibility(); 846 847 if (this.announcementBarSlider) { 848 this.announcementBarArrowButtonWasClicked = false; 849 850 this.reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); 851 this.reducedMotion.addEventListener('change', () => { 852 if (this.slider.getAttribute('data-autoplay') === 'true') this.setAutoPlay(); 853 }); 854 855 [this.prevButton, this.nextButton].forEach((button) => { 856 button.addEventListener( 857 'click', 858 () => { 859 this.announcementBarArrowButtonWasClicked = true; 860 }, 861 { once: true } 862 ); 863 }); 864 } 865 866 if (this.slider.getAttribute('data-autoplay') === 'true') this.setAutoPlay(); 867 } 868 869 setAutoPlay() { 870 this.autoplaySpeed = this.slider.dataset.speed * 1000; 871 this.addEventListener('mouseover', this.focusInHandling.bind(this)); 872 this.addEventListener('mouseleave', this.focusOutHandling.bind(this)); 873 this.addEventListener('focusin', this.focusInHandling.bind(this)); 874 this.addEventListener('focusout', this.focusOutHandling.bind(this)); 875 876 if (this.querySelector('.slideshow__autoplay')) { 877 this.sliderAutoplayButton = this.querySelector('.slideshow__autoplay'); 878 this.sliderAutoplayButton.addEventListener('click', this.autoPlayToggle.bind(this)); 879 this.autoplayButtonIsSetToPlay = true; 880 this.play(); 881 } else { 882 this.reducedMotion.matches || this.announcementBarArrowButtonWasClicked ? this.pause() : this.play(); 883 } 884 } 885 886 onButtonClick(event) { 887 super.onButtonClick(event); 888 this.wasClicked = true; 889 890 const isFirstSlide = this.currentPage === 1; 891 const isLastSlide = this.currentPage === this.sliderItemsToShow.length; 892 893 if (!isFirstSlide && !isLastSlide) { 894 this.applyAnimationToAnnouncementBar(event.currentTarget.name); 895 return; 896 } 897 898 if (isFirstSlide && event.currentTarget.name === 'previous') { 899 this.slideScrollPosition = 900 this.slider.scrollLeft + this.sliderFirstItemNode.clientWidth * this.sliderItemsToShow.length; 901 } else if (isLastSlide && event.currentTarget.name === 'next') { 902 this.slideScrollPosition = 0; 903 } 904 905 this.setSlidePosition(this.slideScrollPosition); 906 907 this.applyAnimationToAnnouncementBar(event.currentTarget.name); 908 } 909 910 setSlidePosition(position) { 911 if (this.setPositionTimeout) clearTimeout(this.setPositionTimeout); 912 this.setPositionTimeout = setTimeout(() => { 913 this.slider.scrollTo({ 914 left: position, 915 }); 916 }, this.announcerBarAnimationDelay); 917 } 918 919 update() { 920 super.update(); 921 this.sliderControlButtons = this.querySelectorAll('.slider-counter__link'); 922 this.prevButton.removeAttribute('disabled'); 923 924 if (!this.sliderControlButtons.length) return; 925 926 this.sliderControlButtons.forEach((link) => { 927 link.classList.remove('slider-counter__link--active'); 928 link.removeAttribute('aria-current'); 929 }); 930 this.sliderControlButtons[this.currentPage - 1].classList.add('slider-counter__link--active'); 931 this.sliderControlButtons[this.currentPage - 1].setAttribute('aria-current', true); 932 } 933 934 autoPlayToggle() { 935 this.togglePlayButtonState(this.autoplayButtonIsSetToPlay); 936 this.autoplayButtonIsSetToPlay ? this.pause() : this.play(); 937 this.autoplayButtonIsSetToPlay = !this.autoplayButtonIsSetToPlay; 938 } 939 940 focusOutHandling(event) { 941 if (this.sliderAutoplayButton) { 942 const focusedOnAutoplayButton = 943 event.target === this.sliderAutoplayButton || this.sliderAutoplayButton.contains(event.target); 944 if (!this.autoplayButtonIsSetToPlay || focusedOnAutoplayButton) return; 945 this.play(); 946 } else if (!this.reducedMotion.matches && !this.announcementBarArrowButtonWasClicked) { 947 this.play(); 948 } 949 } 950 951 focusInHandling(event) { 952 if (this.sliderAutoplayButton) { 953 const focusedOnAutoplayButton = 954 event.target === this.sliderAutoplayButton || this.sliderAutoplayButton.contains(event.target); 955 if (focusedOnAutoplayButton && this.autoplayButtonIsSetToPlay) { 956 this.play(); 957 } else if (this.autoplayButtonIsSetToPlay) { 958 this.pause(); 959 } 960 } else if (this.announcementBarSlider.contains(event.target)) { 961 this.pause(); 962 } 963 } 964 965 play() { 966 this.slider.setAttribute('aria-live', 'off'); 967 clearInterval(this.autoplay); 968 this.autoplay = setInterval(this.autoRotateSlides.bind(this), this.autoplaySpeed); 969 } 970 971 pause() { 972 this.slider.setAttribute('aria-live', 'polite'); 973 clearInterval(this.autoplay); 974 } 975 976 togglePlayButtonState(pauseAutoplay) { 977 if (pauseAutoplay) { 978 this.sliderAutoplayButton.classList.add('slideshow__autoplay--paused'); 979 this.sliderAutoplayButton.setAttribute('aria-label', window.accessibilityStrings.playSlideshow); 980 } else { 981 this.sliderAutoplayButton.classList.remove('slideshow__autoplay--paused'); 982 this.sliderAutoplayButton.setAttribute('aria-label', window.accessibilityStrings.pauseSlideshow); 983 } 984 } 985 986 autoRotateSlides() { 987 const slideScrollPosition = 988 this.currentPage === this.sliderItems.length ? 0 : this.slider.scrollLeft + this.sliderItemOffset; 989 990 this.setSlidePosition(slideScrollPosition); 991 this.applyAnimationToAnnouncementBar(); 992 } 993 994 setSlideVisibility(event) { 995 this.sliderItemsToShow.forEach((item, index) => { 996 const linkElements = item.querySelectorAll('a'); 997 if (index === this.currentPage - 1) { 998 if (linkElements.length)
999 linkElements.forEach((button) => { 1000 button.removeAttribute('tabindex'); 1001 }); 1002 item.setAttribute('aria-hidden', 'false'); 1003 item.removeAttribute('tabindex'); 1004 } else { 1005 if (linkElements.length) 1006 linkElements.forEach((button) => { 1007 button.setAttribute('tabindex', '-1'); 1008 }); 1009 item.setAttribute('aria-hidden', 'true'); 1010 item.setAttribute('tabindex', '-1'); 1011 } 1012 }); 1013 this.wasClicked = false; 1014 } 1015 1016 applyAnimationToAnnouncementBar(button = 'next') { 1017 if (!this.announcementBarSlider) return; 1018 1019 const itemsCount = this.sliderItems.length; 1020 const increment = button === 'next' ? 1 : -1; 1021 1022 const currentIndex = this.currentPage - 1; 1023 let nextIndex = (currentIndex + increment) % itemsCount; 1024 nextIndex = nextIndex === -1 ? itemsCount - 1 : nextIndex; 1025 1026 const nextSlide = this.sliderItems[nextIndex]; 1027 const currentSlide = this.sliderItems[currentIndex]; 1028 1029 const animationClassIn = 'announcement-bar-slider--fade-in'; 1030 const animationClassOut = 'announcement-bar-slider--fade-out'; 1031 1032 const isFirstSlide = currentIndex === 0; 1033 const isLastSlide = currentIndex === itemsCount - 1; 1034 1035 const shouldMoveNext = (button === 'next' && !isLastSlide) || (button === 'previous' && isFirstSlide); 1036 const direction = shouldMoveNext ? 'next' : 'previous'; 1037 1038 currentSlide.classList.add(`${animationClassOut}-${direction}`); 1039 nextSlide.classList.add(`${animationClassIn}-${direction}`); 1040 1041 setTimeout(() => { 1042 currentSlide.classList.remove(`${animationClassOut}-${direction}`); 1043 nextSlide.classList.remove(`${animationClassIn}-${direction}`); 1044 }, this.announcerBarAnimationDelay * 2); 1045 } 1046 1047 linkToSlide(event) { 1048 event.preventDefault(); 1049 const slideScrollPosition = 1050 this.slider.scrollLeft + 1051 this.sliderFirstItemNode.clientWidth * 1052 (this.sliderControlLinksArray.indexOf(event.currentTarget) + 1 - this.currentPage); 1053 this.slider.scrollTo({ 1054 left: slideScrollPosition, 1055 }); 1056 } 1057} 1058 1059customElements.define('slideshow-component', SlideshowComponent); 1060 1061class VariantSelects extends HTMLElement { 1062 constructor() { 1063 super(); 1064 } 1065 1066 connectedCallback() { 1067 this.addEventListener('change', (event) => { 1068 const target = this.getInputForEventTarget(event.target); 1069 this.updateSelectionMetadata(event); 1070 1071 publish(PUB_SUB_EVENTS.optionValueSelectionChange, { 1072 data: { 1073 event, 1074 target, 1075 selectedOptionValues: this.selectedOptionValues, 1076 }, 1077 }); 1078 }); 1079 } 1080 1081 updateSelectionMetadata({ target }) { 1082 const { value, tagName } = target; 1083 1084 if (tagName === 'SELECT' && target.selectedOptions.length) { 1085 Array.from(target.options) 1086 .find((option) => option.getAttribute('selected')) 1087 .removeAttribute('selected'); 1088 target.selectedOptions[0].setAttribute('selected', 'selected'); 1089 1090 const swatchValue = target.selectedOptions[0].dataset.optionSwatchValue; 1091 const selectedDropdownSwatchValue = target 1092 .closest('.product-form__input') 1093 .querySelector('[data-selected-value] > .swatch'); 1094 if (!selectedDropdownSwatchValue) return; 1095 if (swatchValue) { 1096 selectedDropdownSwatchValue.style.setProperty('--swatch--background', swatchValue); 1097 1098 selectedDropdownSwatchValue.classList.remove('swatch--unavailable'); 1099 } else { 1100 selectedDropdownSwatchValue.style.setProperty('--swatch--background', 'unset'); 1101 selectedDropdownSwatchValue.classList.add('swatch--unavailable'); 1102 } 1103 1104 selectedDropdownSwatchValue.style.setProperty(
1105 '--swatch-focal-point', 1106 target.selectedOptions[0].dataset.optionSwatchFocalPoint || 'unset' 1107 ); 1108 } else if (tagName === 'INPUT' && target.type === 'radio') { 1109 const selectedSwatchValue = target.closest(`.product-form__input`).querySelector('[data-selected-value]'); 1110 if (selectedSwatchValue) selectedSwatchValue.innerHTML = value; 1111 } 1112 } 1113 1114 getInputForEventTarget(target) { 1115 return target.tagName === 'SELECT' ? target.selectedOptions[0] : target; 1116 } 1117 1118 get selectedOptionValues() { 1119 return Array.from(this.querySelectorAll('select option[selected], fieldset input:checked')).map( 1120 ({ dataset }) => dataset.optionValueId 1121 ); 1122 } 1123} 1124 1125customElements.define('variant-selects', VariantSelects); 1126 1127class ProductRecommendations extends HTMLElement { 1128 constructor() { 1129 super(); 1130 } 1131 1132 connectedCallback() { 1133 const handleIntersection = (entries, observer) => { 1134 if (!entries[0].isIntersecting) return; 1135 observer.unobserve(this); 1136 1137 fetch(this.dataset.url) 1138 .then(response => response.text()) 1139 .then(text => { 1140 const html = document.createElement('div'); 1141 html.innerHTML = text; 1142 const recommendations = html.querySelector('product-recommendations'); 1143 1144 if (recommendations && recommendations.innerHTML.trim().length) { 1145 this.innerHTML = recommendations.innerHTML; 1146 } 1147 1148 if (html.querySelector('.col')) { 1149 this.classList.add('product-recommendations--loaded'); 1150 } 1151 1152 setTimeout(() => { 1153 ProductRecommendations.initializeAddToCartListeners(); 1154 if (typeof fc_ChangeVariable === 'function') { 1155 fc_ChangeVariable(); 1156 } 1157 if (typeof fc_LazyLoad === 'function') { 1158 fc_LazyLoad(); 1159 } 1160 }, 0); 1161 }) 1162 .catch(e => { 1163 console.error(e); 1164 }); 1165 } 1166 1167 new IntersectionObserver(handleIntersection.bind(this), {rootMargin: '0px 0px 400px 0px'}).observe(this); 1168 } 1169 1170 static initializeAddToCartListeners() { 1171 const productAddToCart = document.querySelectorAll('.product-recommendations [data-section-type="product"]'); 1172 productAddToCart.forEach(form => { 1173 new theme.Product(form); 1174 }); 1175 } 1176} 1177 1178customElements.define('product-recommendations', ProductRecommendations); 1179 1180 1181class AccountIcon extends HTMLElement { 1182 constructor() { 1183 super(); 1184 1185 this.icon = this.querySelector('.icon'); 1186 } 1187 1188 connectedCallback() { 1189 document.addEventListener('storefront:signincompleted', this.handleStorefrontSignInCompleted.bind(this)); 1190 } 1191 1192 handleStorefrontSignInCompleted(event) { 1193 if (event?.detail?.avatar) { 1194 this.icon?.replaceWith(event.detail.avatar.cloneNode()); 1195 } 1196 } 1197} 1198 1199customElements.define('account-icon', AccountIcon); 1200 1201class BulkAdd extends HTMLElement { 1202 static ASYNC_REQUEST_DELAY = 250; 1203 1204 constructor() { 1205 super(); 1206 this.queue = []; 1207 this.setRequestStarted(false); 1208 this.ids = []; 1209 } 1210 1211 startQueue(id, quantity) { 1212 this.queue.push({ id, quantity }); 1213 1214 const interval = setInterval(() => { 1215 if (this.queue.length > 0) { 1216 if (!this.requestStarted) { 1217 this.sendRequest(this.queue); 1218 } 1219 } else { 1220 clearInterval(interval); 1221 } 1222 }, BulkAdd.ASYNC_REQUEST_DELAY); 1223 } 1224 1225 sendRequest(queue) { 1226 this.setRequestStarted(true); 1227 const items = {}; 1228 1229 queue.forEach((queueItem) => { 1230 items[parseInt(queueItem.id)] = queueItem.quantity; 1231 }); 1232 this.queue = this.queue.filter((queueElement) => !queue.includes(queueElement)); 1233 1234 this.updateMultipleQty(items); 1235 } 1236 1237 setRequestStarted(requestStarted) { 1238 this._requestStarted = requestStarted; 1239 } 1240 1241 get requestStarted() { 1242 return this._requestStarted; 1243 } 1244 1245 resetQuantityInput(id) { 1246 const input = this.querySelector(`#Quantity-${id}`); 1247 input.value = input.getAttribute('value'); 1248 this.isEnterPressed = false;
1249 } 1250 1251 setValidity(event, index, message) { 1252 event.target.setCustomValidity(message); 1253 event.target.reportValidity(); 1254 this.resetQuantityInput(index); 1255 event.target.select(); 1256 } 1257 1258 validateQuantity(event) { 1259 const inputValue = parseInt(event.target.value); 1260 const index = event.target.dataset.index; 1261 1262 if (inputValue < event.target.dataset.min) { 1263 this.setValidity(event, index, window.quickOrderListStrings.min_error.replace('[min]', event.target.dataset.min)); 1264 } else if (inputValue > parseInt(event.target.max)) { 1265 this.setValidity(event, index, window.quickOrderListStrings.max_error.replace('[max]', event.target.max)); 1266 } else if (inputValue % parseInt(event.target.step) != 0) { 1267 this.setValidity(event, index, window.quickOrderListStrings.step_error.replace('[step]', event.target.step)); 1268 } else { 1269 event.target.setCustomValidity(''); 1270 event.target.reportValidity(); 1271 event.target.setAttribute('value', inputValue); 1272 this.startQueue(index, inputValue); 1273 } 1274 } 1275 1276 getSectionInnerHTML(html, selector) { 1277 return new DOMParser().parseFromString(html, 'text/html').querySelector(selector).innerHTML; 1278 } 1279} 1280 1281if (!customElements.get('bulk-add')) { 1282 customElements.define('bulk-add', BulkAdd); 1283} 1284 1285class CartPerformance { 1286 static #metric_prefix = "cart-performance" 1287 1288 static createStartingMarker(benchmarkName) { 1289 const metricName = `${CartPerformance.#metric_prefix}:${benchmarkName}` 1290 return performance.mark(`${metricName}:start`); 1291 } 1292 1293 static measureFromEvent(benchmarkName, event) { 1294 const metricName = `${CartPerformance.#metric_prefix}:${benchmarkName}` 1295 const startMarker = performance.mark(`${metricName}:start`, { 1296 startTime: event.timeStamp 1297 }); 1298 1299 const endMarker = performance.mark(`${metricName}:end`); 1300 1301 performance.measure( 1302 benchmarkName, 1303 `${metricName}:start`, 1304 `${metricName}:end` 1305 ); 1306 } 1307 1308 static measureFromMarker(benchmarkName, startMarker) { 1309 const metricName = `${CartPerformance.#metric_prefix}:${benchmarkName}` 1310 const endMarker = performance.mark(`${metricName}:end`); 1311 1312 performance.measure( 1313 benchmarkName, 1314 startMarker.name, 1315 `${metricName}:end` 1316 ); 1317 } 1318 1319 static measure(benchmarkName, callback) { 1320 const metricName = `${CartPerformance.#metric_prefix}:${benchmarkName}` 1321 const startMarker = performance.mark(`${metricName}:start`); 1322 1323 callback(); 1324 1325 const endMarker = performance.mark(`${metricName}:end`); 1326 1327 performance.measure( 1328 benchmarkName, 1329 `${metricName}:start`, 1330 `${metricName}:end` 1331 ); 1332 } 1333} 1334
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.