PageSourceSearch

https://lucylueorganics.com/cdn/shop/t/38/assets/global.min.js?v=158901376790325187221775443728

js lucylueorganics.com collected 2026-10-07 03:22:01 UTC 48,295 bytes, 1,334 lines download raw bytes

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.