1461 lines
47 KiB
JavaScript
1461 lines
47 KiB
JavaScript
function getFocusableElements(container) {
|
|
return Array.from(
|
|
container.querySelectorAll(
|
|
"summary, a[href], button:enabled, [tabindex]:not([tabindex^='-']), [draggable], area, input:not([type=hidden]):enabled, select:enabled, textarea:enabled, object, iframe"
|
|
)
|
|
);
|
|
}
|
|
|
|
class SectionId {
|
|
static #separator = '__';
|
|
|
|
// for a qualified section id (e.g. 'template--22224696705326__main'), return just the section id (e.g. 'template--22224696705326')
|
|
static parseId(qualifiedSectionId) {
|
|
return qualifiedSectionId.split(SectionId.#separator)[0];
|
|
}
|
|
|
|
// for a qualified section id (e.g. 'template--22224696705326__main'), return just the section name (e.g. 'main')
|
|
static parseSectionName(qualifiedSectionId) {
|
|
return qualifiedSectionId.split(SectionId.#separator)[1];
|
|
}
|
|
|
|
// 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')
|
|
static getIdForSection(sectionId, sectionName) {
|
|
return `${sectionId}${SectionId.#separator}${sectionName}`;
|
|
}
|
|
}
|
|
|
|
class HTMLUpdateUtility {
|
|
/**
|
|
* Used to swap an HTML node with a new node.
|
|
* 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.
|
|
*
|
|
* 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
|
|
*/
|
|
static viewTransition(oldNode, newContent, preProcessCallbacks = [], postProcessCallbacks = []) {
|
|
preProcessCallbacks?.forEach((callback) => callback(newContent));
|
|
|
|
const newNodeWrapper = document.createElement('div');
|
|
HTMLUpdateUtility.setInnerHTML(newNodeWrapper, newContent.outerHTML);
|
|
const newNode = newNodeWrapper.firstChild;
|
|
|
|
// dedupe IDs
|
|
const uniqueKey = Date.now();
|
|
oldNode.querySelectorAll('[id], [form]').forEach((element) => {
|
|
element.id && (element.id = `${element.id}-${uniqueKey}`);
|
|
element.form && element.setAttribute('form', `${element.form.getAttribute('id')}-${uniqueKey}`);
|
|
});
|
|
|
|
oldNode.parentNode.insertBefore(newNode, oldNode);
|
|
oldNode.style.display = 'none';
|
|
|
|
postProcessCallbacks?.forEach((callback) => callback(newNode));
|
|
|
|
setTimeout(() => oldNode.remove(), 500);
|
|
}
|
|
|
|
// Sets inner HTML and reinjects the script tags to allow execution. By default, scripts are disabled when using element.innerHTML.
|
|
static setInnerHTML(element, html) {
|
|
element.innerHTML = html;
|
|
element.querySelectorAll('script').forEach((oldScriptTag) => {
|
|
const newScriptTag = document.createElement('script');
|
|
Array.from(oldScriptTag.attributes).forEach((attribute) => {
|
|
newScriptTag.setAttribute(attribute.name, attribute.value);
|
|
});
|
|
newScriptTag.appendChild(document.createTextNode(oldScriptTag.innerHTML));
|
|
oldScriptTag.parentNode.replaceChild(newScriptTag, oldScriptTag);
|
|
});
|
|
}
|
|
}
|
|
|
|
document.querySelectorAll('[id^="Details-"] summary').forEach((summary) => {
|
|
summary.setAttribute('role', 'button');
|
|
summary.setAttribute('aria-expanded', summary.parentNode.hasAttribute('open'));
|
|
|
|
if (summary.nextElementSibling.getAttribute('id')) {
|
|
summary.setAttribute('aria-controls', summary.nextElementSibling.id);
|
|
}
|
|
|
|
summary.addEventListener('click', (event) => {
|
|
event.currentTarget.setAttribute('aria-expanded', !event.currentTarget.closest('details').hasAttribute('open'));
|
|
});
|
|
|
|
if (summary.closest('header-drawer, menu-drawer')) return;
|
|
summary.parentElement.addEventListener('keyup', onKeyUpEscape);
|
|
});
|
|
|
|
const trapFocusHandlers = {};
|
|
|
|
function trapFocus(container, elementToFocus = container) {
|
|
var elements = getFocusableElements(container);
|
|
var first = elements[0];
|
|
var last = elements[elements.length - 1];
|
|
|
|
removeTrapFocus();
|
|
|
|
trapFocusHandlers.focusin = (event) => {
|
|
if (event.target !== container && event.target !== last && event.target !== first) return;
|
|
|
|
document.addEventListener('keydown', trapFocusHandlers.keydown);
|
|
};
|
|
|
|
trapFocusHandlers.focusout = function () {
|
|
document.removeEventListener('keydown', trapFocusHandlers.keydown);
|
|
};
|
|
|
|
trapFocusHandlers.keydown = function (event) {
|
|
if (event.code.toUpperCase() !== 'TAB') return; // If not TAB key
|
|
// On the last focusable element and tab forward, focus the first element.
|
|
if (event.target === last && !event.shiftKey) {
|
|
event.preventDefault();
|
|
first.focus();
|
|
}
|
|
|
|
// On the first focusable element and tab backward, focus the last element.
|
|
if ((event.target === container || event.target === first) && event.shiftKey) {
|
|
event.preventDefault();
|
|
last.focus();
|
|
}
|
|
};
|
|
|
|
document.addEventListener('focusout', trapFocusHandlers.focusout);
|
|
document.addEventListener('focusin', trapFocusHandlers.focusin);
|
|
|
|
elementToFocus.focus();
|
|
|
|
if (
|
|
elementToFocus.tagName === 'INPUT' &&
|
|
['search', 'text', 'email', 'url'].includes(elementToFocus.type) &&
|
|
elementToFocus.value
|
|
) {
|
|
elementToFocus.setSelectionRange(0, elementToFocus.value.length);
|
|
}
|
|
}
|
|
|
|
// Here run the querySelector to figure out if the browser supports :focus-visible or not and run code based on it.
|
|
try {
|
|
document.querySelector(':focus-visible');
|
|
} catch (e) {
|
|
focusVisiblePolyfill();
|
|
}
|
|
|
|
function focusVisiblePolyfill() {
|
|
const navKeys = [
|
|
'ARROWUP',
|
|
'ARROWDOWN',
|
|
'ARROWLEFT',
|
|
'ARROWRIGHT',
|
|
'TAB',
|
|
'ENTER',
|
|
'SPACE',
|
|
'ESCAPE',
|
|
'HOME',
|
|
'END',
|
|
'PAGEUP',
|
|
'PAGEDOWN',
|
|
];
|
|
let currentFocusedElement = null;
|
|
let mouseClick = null;
|
|
|
|
window.addEventListener('keydown', (event) => {
|
|
if (navKeys.includes(event.code.toUpperCase())) {
|
|
mouseClick = false;
|
|
}
|
|
});
|
|
|
|
window.addEventListener('mousedown', (event) => {
|
|
mouseClick = true;
|
|
});
|
|
|
|
window.addEventListener(
|
|
'focus',
|
|
() => {
|
|
if (currentFocusedElement) currentFocusedElement.classList.remove('focused');
|
|
|
|
if (mouseClick) return;
|
|
|
|
currentFocusedElement = document.activeElement;
|
|
currentFocusedElement.classList.add('focused');
|
|
},
|
|
true
|
|
);
|
|
}
|
|
|
|
function pauseAllMedia() {
|
|
document.querySelectorAll('.js-youtube').forEach((video) => {
|
|
video.contentWindow.postMessage('{"event":"command","func":"' + 'pauseVideo' + '","args":""}', '*');
|
|
});
|
|
document.querySelectorAll('.js-vimeo').forEach((video) => {
|
|
video.contentWindow.postMessage('{"method":"pause"}', '*');
|
|
});
|
|
document.querySelectorAll('video').forEach((video) => video.pause());
|
|
document.querySelectorAll('product-model').forEach((model) => {
|
|
if (model.modelViewerUI) model.modelViewerUI.pause();
|
|
});
|
|
}
|
|
|
|
function removeTrapFocus(elementToFocus = null) {
|
|
document.removeEventListener('focusin', trapFocusHandlers.focusin);
|
|
document.removeEventListener('focusout', trapFocusHandlers.focusout);
|
|
document.removeEventListener('keydown', trapFocusHandlers.keydown);
|
|
|
|
if (elementToFocus) elementToFocus.focus();
|
|
}
|
|
|
|
function onKeyUpEscape(event) {
|
|
if (event.code.toUpperCase() !== 'ESCAPE') return;
|
|
|
|
const openDetailsElement = event.target.closest('details[open]');
|
|
if (!openDetailsElement) return;
|
|
|
|
const summaryElement = openDetailsElement.querySelector('summary');
|
|
openDetailsElement.removeAttribute('open');
|
|
summaryElement.setAttribute('aria-expanded', false);
|
|
summaryElement.focus();
|
|
}
|
|
|
|
class QuantityInput extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.input = this.querySelector('input');
|
|
this.changeEvent = new Event('change', { bubbles: true });
|
|
this.input.addEventListener('change', this.onInputChange.bind(this));
|
|
this.querySelectorAll('button').forEach((button) =>
|
|
button.addEventListener('click', this.onButtonClick.bind(this))
|
|
);
|
|
}
|
|
|
|
quantityUpdateUnsubscriber = undefined;
|
|
|
|
connectedCallback() {
|
|
this.validateQtyRules();
|
|
this.quantityUpdateUnsubscriber = subscribe(PUB_SUB_EVENTS.quantityUpdate, this.validateQtyRules.bind(this));
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
if (this.quantityUpdateUnsubscriber) {
|
|
this.quantityUpdateUnsubscriber();
|
|
}
|
|
}
|
|
|
|
onInputChange(event) {
|
|
this.validateQtyRules();
|
|
}
|
|
|
|
onButtonClick(event) {
|
|
event.preventDefault();
|
|
const previousValue = this.input.value;
|
|
|
|
if (event.target.name === 'plus') {
|
|
if (parseInt(this.input.dataset.min) > parseInt(this.input.step) && this.input.value == 0) {
|
|
this.input.value = this.input.dataset.min;
|
|
} else {
|
|
this.input.stepUp();
|
|
}
|
|
} else {
|
|
this.input.stepDown();
|
|
}
|
|
|
|
if (previousValue !== this.input.value) this.input.dispatchEvent(this.changeEvent);
|
|
|
|
if (this.input.dataset.min === previousValue && event.target.name === 'minus') {
|
|
this.input.value = parseInt(this.input.min);
|
|
}
|
|
}
|
|
|
|
validateQtyRules() {
|
|
const value = parseInt(this.input.value);
|
|
if (this.input.min) {
|
|
const buttonMinus = this.querySelector(".quantity__button[name='minus']");
|
|
buttonMinus.classList.toggle('disabled', parseInt(value) <= parseInt(this.input.min));
|
|
}
|
|
if (this.input.max) {
|
|
const max = parseInt(this.input.max);
|
|
const buttonPlus = this.querySelector(".quantity__button[name='plus']");
|
|
buttonPlus.classList.toggle('disabled', value >= max);
|
|
}
|
|
}
|
|
}
|
|
|
|
customElements.define('quantity-input', QuantityInput);
|
|
|
|
function debounce(fn, wait) {
|
|
let t;
|
|
return (...args) => {
|
|
clearTimeout(t);
|
|
t = setTimeout(() => fn.apply(this, args), wait);
|
|
};
|
|
}
|
|
|
|
|
|
function throttle(fn, delay) {
|
|
let lastCall = 0;
|
|
return function (...args) {
|
|
const now = new Date().getTime();
|
|
if (now - lastCall < delay) {
|
|
return;
|
|
}
|
|
lastCall = now;
|
|
return fn(...args);
|
|
};
|
|
}
|
|
|
|
function fetchConfig(type = 'json') {
|
|
return {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Accept: `application/${type}` },
|
|
};
|
|
}
|
|
|
|
/*
|
|
* Shopify Common JS
|
|
*
|
|
*/
|
|
if (typeof window.Shopify == 'undefined') {
|
|
window.Shopify = {};
|
|
}
|
|
|
|
Shopify.bind = function (fn, scope) {
|
|
return function () {
|
|
return fn.apply(scope, arguments);
|
|
};
|
|
};
|
|
|
|
Shopify.setSelectorByValue = function (selector, value) {
|
|
for (var i = 0, count = selector.options.length; i < count; i++) {
|
|
var option = selector.options[i];
|
|
if (value == option.value || value == option.innerHTML) {
|
|
selector.selectedIndex = i;
|
|
return i;
|
|
}
|
|
}
|
|
};
|
|
|
|
Shopify.addListener = function (target, eventName, callback) {
|
|
target.addEventListener
|
|
? target.addEventListener(eventName, callback, false)
|
|
: target.attachEvent('on' + eventName, callback);
|
|
};
|
|
|
|
Shopify.postLink = function (path, options) {
|
|
options = options || {};
|
|
var method = options['method'] || 'post';
|
|
var params = options['parameters'] || {};
|
|
|
|
var form = document.createElement('form');
|
|
form.setAttribute('method', method);
|
|
form.setAttribute('action', path);
|
|
|
|
for (var key in params) {
|
|
var hiddenField = document.createElement('input');
|
|
hiddenField.setAttribute('type', 'hidden');
|
|
hiddenField.setAttribute('name', key);
|
|
hiddenField.setAttribute('value', params[key]);
|
|
form.appendChild(hiddenField);
|
|
}
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
document.body.removeChild(form);
|
|
};
|
|
|
|
Shopify.CountryProvinceSelector = function (country_domid, province_domid, options) {
|
|
this.countryEl = document.getElementById(country_domid);
|
|
this.provinceEl = document.getElementById(province_domid);
|
|
this.provinceContainer = document.getElementById(options['hideElement'] || province_domid);
|
|
|
|
Shopify.addListener(this.countryEl, 'change', Shopify.bind(this.countryHandler, this));
|
|
|
|
this.initCountry();
|
|
this.initProvince();
|
|
};
|
|
|
|
Shopify.CountryProvinceSelector.prototype = {
|
|
initCountry: function () {
|
|
var value = this.countryEl.getAttribute('data-default');
|
|
Shopify.setSelectorByValue(this.countryEl, value);
|
|
this.countryHandler();
|
|
},
|
|
|
|
initProvince: function () {
|
|
var value = this.provinceEl.getAttribute('data-default');
|
|
if (value && this.provinceEl.options.length > 0) {
|
|
Shopify.setSelectorByValue(this.provinceEl, value);
|
|
}
|
|
},
|
|
|
|
countryHandler: function (e) {
|
|
var opt = this.countryEl.options[this.countryEl.selectedIndex];
|
|
var raw = opt.getAttribute('data-provinces');
|
|
var provinces = JSON.parse(raw);
|
|
|
|
this.clearOptions(this.provinceEl);
|
|
if (provinces && provinces.length == 0) {
|
|
this.provinceContainer.style.display = 'none';
|
|
} else {
|
|
for (var i = 0; i < provinces.length; i++) {
|
|
var opt = document.createElement('option');
|
|
opt.value = provinces[i][0];
|
|
opt.innerHTML = provinces[i][1];
|
|
this.provinceEl.appendChild(opt);
|
|
}
|
|
|
|
this.provinceContainer.style.display = '';
|
|
}
|
|
},
|
|
|
|
clearOptions: function (selector) {
|
|
while (selector.firstChild) {
|
|
selector.removeChild(selector.firstChild);
|
|
}
|
|
},
|
|
|
|
setOptions: function (selector, values) {
|
|
for (var i = 0, count = values.length; i < values.length; i++) {
|
|
var opt = document.createElement('option');
|
|
opt.value = values[i];
|
|
opt.innerHTML = values[i];
|
|
selector.appendChild(opt);
|
|
}
|
|
},
|
|
};
|
|
|
|
class MenuDrawer extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
|
|
this.mainDetailsToggle = this.querySelector('details');
|
|
|
|
this.addEventListener('keyup', this.onKeyUp.bind(this));
|
|
this.addEventListener('focusout', this.onFocusOut.bind(this));
|
|
this.bindEvents();
|
|
}
|
|
|
|
bindEvents() {
|
|
this.querySelectorAll('summary').forEach((summary) =>
|
|
summary.addEventListener('click', this.onSummaryClick.bind(this))
|
|
);
|
|
this.querySelectorAll(
|
|
'button:not(.localization-selector):not(.country-selector__close-button):not(.country-filter__reset-button)'
|
|
).forEach((button) => button.addEventListener('click', this.onCloseButtonClick.bind(this)));
|
|
}
|
|
|
|
onKeyUp(event) {
|
|
if (event.code.toUpperCase() !== 'ESCAPE') return;
|
|
|
|
const openDetailsElement = event.target.closest('details[open]');
|
|
if (!openDetailsElement) return;
|
|
|
|
openDetailsElement === this.mainDetailsToggle
|
|
? this.closeMenuDrawer(event, this.mainDetailsToggle.querySelector('summary'))
|
|
: this.closeSubmenu(openDetailsElement);
|
|
}
|
|
|
|
onSummaryClick(event) {
|
|
const summaryElement = event.currentTarget;
|
|
const detailsElement = summaryElement.parentNode;
|
|
const parentMenuElement = detailsElement.closest('.has-submenu');
|
|
const isOpen = detailsElement.hasAttribute('open');
|
|
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
|
|
function addTrapFocus() {
|
|
trapFocus(summaryElement.nextElementSibling, detailsElement.querySelector('button'));
|
|
summaryElement.nextElementSibling.removeEventListener('transitionend', addTrapFocus);
|
|
}
|
|
|
|
if (detailsElement === this.mainDetailsToggle) {
|
|
if (isOpen) event.preventDefault();
|
|
isOpen ? this.closeMenuDrawer(event, summaryElement) : this.openMenuDrawer(summaryElement);
|
|
|
|
if (window.matchMedia('(max-width: 990px)')) {
|
|
document.documentElement.style.setProperty('--viewport-height', `${window.innerHeight}px`);
|
|
}
|
|
} else {
|
|
setTimeout(() => {
|
|
detailsElement.classList.add('menu-opening');
|
|
summaryElement.setAttribute('aria-expanded', true);
|
|
parentMenuElement && parentMenuElement.classList.add('submenu-open');
|
|
!reducedMotion || reducedMotion.matches
|
|
? addTrapFocus()
|
|
: summaryElement.nextElementSibling.addEventListener('transitionend', addTrapFocus);
|
|
}, 100);
|
|
}
|
|
}
|
|
|
|
openMenuDrawer(summaryElement) {
|
|
setTimeout(() => {
|
|
this.mainDetailsToggle.classList.add('menu-opening');
|
|
});
|
|
summaryElement.setAttribute('aria-expanded', true);
|
|
trapFocus(this.mainDetailsToggle, summaryElement);
|
|
document.body.classList.add(`overflow-hidden-${this.dataset.breakpoint}`);
|
|
}
|
|
|
|
closeMenuDrawer(event, elementToFocus = false) {
|
|
if (event === undefined) return;
|
|
|
|
this.mainDetailsToggle.classList.remove('menu-opening');
|
|
this.mainDetailsToggle.querySelectorAll('details').forEach((details) => {
|
|
details.removeAttribute('open');
|
|
details.classList.remove('menu-opening');
|
|
});
|
|
this.mainDetailsToggle.querySelectorAll('.submenu-open').forEach((submenu) => {
|
|
submenu.classList.remove('submenu-open');
|
|
});
|
|
document.body.classList.remove(`overflow-hidden-${this.dataset.breakpoint}`);
|
|
removeTrapFocus(elementToFocus);
|
|
this.closeAnimation(this.mainDetailsToggle);
|
|
|
|
if (event instanceof KeyboardEvent) elementToFocus?.setAttribute('aria-expanded', false);
|
|
}
|
|
|
|
onFocusOut() {
|
|
setTimeout(() => {
|
|
if (this.mainDetailsToggle.hasAttribute('open') && !this.mainDetailsToggle.contains(document.activeElement))
|
|
this.closeMenuDrawer();
|
|
});
|
|
}
|
|
|
|
onCloseButtonClick(event) {
|
|
const detailsElement = event.currentTarget.closest('details');
|
|
this.closeSubmenu(detailsElement);
|
|
}
|
|
|
|
closeSubmenu(detailsElement) {
|
|
const parentMenuElement = detailsElement.closest('.submenu-open');
|
|
parentMenuElement && parentMenuElement.classList.remove('submenu-open');
|
|
detailsElement.classList.remove('menu-opening');
|
|
detailsElement.querySelector('summary').setAttribute('aria-expanded', false);
|
|
removeTrapFocus(detailsElement.querySelector('summary'));
|
|
this.closeAnimation(detailsElement);
|
|
}
|
|
|
|
closeAnimation(detailsElement) {
|
|
let animationStart;
|
|
|
|
const handleAnimation = (time) => {
|
|
if (animationStart === undefined) {
|
|
animationStart = time;
|
|
}
|
|
|
|
const elapsedTime = time - animationStart;
|
|
|
|
if (elapsedTime < 400) {
|
|
window.requestAnimationFrame(handleAnimation);
|
|
} else {
|
|
detailsElement.removeAttribute('open');
|
|
if (detailsElement.closest('details[open]')) {
|
|
trapFocus(detailsElement.closest('details[open]'), detailsElement.querySelector('summary'));
|
|
}
|
|
}
|
|
};
|
|
|
|
window.requestAnimationFrame(handleAnimation);
|
|
}
|
|
}
|
|
|
|
customElements.define('menu-drawer', MenuDrawer);
|
|
|
|
class HeaderDrawer extends MenuDrawer {
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
openMenuDrawer(summaryElement) {
|
|
this.header = this.header || document.querySelector('.section-header');
|
|
this.borderOffset =
|
|
this.borderOffset || this.closest('.header-wrapper').classList.contains('header-wrapper--border-bottom') ? 1 : 0;
|
|
document.documentElement.style.setProperty(
|
|
'--header-bottom-position',
|
|
`${parseInt(this.header.getBoundingClientRect().bottom - this.borderOffset)}px`
|
|
);
|
|
this.header.classList.add('menu-open');
|
|
|
|
setTimeout(() => {
|
|
this.mainDetailsToggle.classList.add('menu-opening');
|
|
});
|
|
|
|
summaryElement.setAttribute('aria-expanded', true);
|
|
window.addEventListener('resize', this.onResize);
|
|
trapFocus(this.mainDetailsToggle, summaryElement);
|
|
document.body.classList.add(`overflow-hidden-${this.dataset.breakpoint}`);
|
|
}
|
|
|
|
closeMenuDrawer(event, elementToFocus) {
|
|
if (!elementToFocus) return;
|
|
super.closeMenuDrawer(event, elementToFocus);
|
|
this.header.classList.remove('menu-open');
|
|
window.removeEventListener('resize', this.onResize);
|
|
}
|
|
|
|
onResize = () => {
|
|
this.header &&
|
|
document.documentElement.style.setProperty(
|
|
'--header-bottom-position',
|
|
`${parseInt(this.header.getBoundingClientRect().bottom - this.borderOffset)}px`
|
|
);
|
|
document.documentElement.style.setProperty('--viewport-height', `${window.innerHeight}px`);
|
|
};
|
|
}
|
|
|
|
customElements.define('header-drawer', HeaderDrawer);
|
|
|
|
class ModalDialog extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.querySelector('[id^="ModalClose-"]').addEventListener('click', this.hide.bind(this, false));
|
|
this.addEventListener('keyup', (event) => {
|
|
if (event.code.toUpperCase() === 'ESCAPE') this.hide();
|
|
});
|
|
if (this.classList.contains('media-modal')) {
|
|
this.addEventListener('pointerup', (event) => {
|
|
if (event.pointerType === 'mouse' && !event.target.closest('deferred-media, product-model')) this.hide();
|
|
});
|
|
} else {
|
|
this.addEventListener('click', (event) => {
|
|
if (event.target === this) this.hide();
|
|
});
|
|
}
|
|
}
|
|
|
|
connectedCallback() {
|
|
if (this.moved) return;
|
|
this.moved = true;
|
|
this.dataset.section = this.closest('.shopify-section').id.replace('shopify-section-', '');
|
|
document.body.appendChild(this);
|
|
}
|
|
|
|
show(opener) {
|
|
this.openedBy = opener;
|
|
const popup = this.querySelector('.template-popup');
|
|
document.body.classList.add('overflow-hidden');
|
|
this.setAttribute('open', '');
|
|
if (popup) popup.loadContent();
|
|
trapFocus(this, this.querySelector('[role="dialog"]'));
|
|
window.pauseAllMedia();
|
|
}
|
|
|
|
hide() {
|
|
document.body.classList.remove('overflow-hidden');
|
|
document.body.dispatchEvent(new CustomEvent('modalClosed'));
|
|
this.removeAttribute('open');
|
|
removeTrapFocus(this.openedBy);
|
|
window.pauseAllMedia();
|
|
}
|
|
}
|
|
customElements.define('modal-dialog', ModalDialog);
|
|
|
|
class BulkModal extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
connectedCallback() {
|
|
const handleIntersection = (entries, observer) => {
|
|
if (!entries[0].isIntersecting) return;
|
|
observer.unobserve(this);
|
|
if (this.innerHTML.trim() === '') {
|
|
const productUrl = this.dataset.url.split('?')[0];
|
|
fetch(`${productUrl}?section_id=bulk-quick-order-list`)
|
|
.then((response) => response.text())
|
|
.then((responseText) => {
|
|
const html = new DOMParser().parseFromString(responseText, 'text/html');
|
|
const sourceQty = html.querySelector('.quick-order-list-container').parentNode;
|
|
this.innerHTML = sourceQty.innerHTML;
|
|
})
|
|
.catch((e) => {
|
|
console.error(e);
|
|
});
|
|
}
|
|
};
|
|
|
|
new IntersectionObserver(handleIntersection.bind(this)).observe(
|
|
document.querySelector(`#QuickBulk-${this.dataset.productId}-${this.dataset.sectionId}`)
|
|
);
|
|
}
|
|
}
|
|
|
|
customElements.define('bulk-modal', BulkModal);
|
|
|
|
class ModalOpener extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
|
|
const button = this.querySelector('button');
|
|
|
|
if (!button) return;
|
|
button.addEventListener('click', () => {
|
|
const modal = document.querySelector(this.getAttribute('data-modal'));
|
|
if (modal) modal.show(button);
|
|
});
|
|
}
|
|
}
|
|
customElements.define('modal-opener', ModalOpener);
|
|
|
|
class DeferredMedia extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
const poster = this.querySelector('[id^="Deferred-Poster-"]');
|
|
if (!poster) return;
|
|
poster.addEventListener('click', this.loadContent.bind(this));
|
|
}
|
|
|
|
loadContent(focus = true) {
|
|
window.pauseAllMedia();
|
|
if (!this.getAttribute('loaded')) {
|
|
const content = document.createElement('div');
|
|
content.appendChild(this.querySelector('template').content.firstElementChild.cloneNode(true));
|
|
|
|
this.setAttribute('loaded', true);
|
|
const deferredElement = this.appendChild(content.querySelector('video, model-viewer, iframe'));
|
|
if (focus) deferredElement.focus();
|
|
if (deferredElement.nodeName == 'VIDEO' && deferredElement.getAttribute('autoplay')) {
|
|
// force autoplay for safari
|
|
deferredElement.play();
|
|
}
|
|
|
|
// Workaround for safari iframe bug
|
|
const formerStyle = deferredElement.getAttribute('style');
|
|
deferredElement.setAttribute('style', 'display: block;');
|
|
window.setTimeout(() => {
|
|
deferredElement.setAttribute('style', formerStyle);
|
|
}, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
customElements.define('deferred-media', DeferredMedia);
|
|
|
|
class SliderComponent extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.slider = this.querySelector('[id^="Slider-"]');
|
|
this.sliderItems = this.querySelectorAll('[id^="Slide-"]');
|
|
this.enableSliderLooping = false;
|
|
this.currentPageElement = this.querySelector('.slider-counter--current');
|
|
this.pageTotalElement = this.querySelector('.slider-counter--total');
|
|
this.prevButton = this.querySelector('button[name="previous"]');
|
|
this.nextButton = this.querySelector('button[name="next"]');
|
|
|
|
if (!this.slider || !this.nextButton) return;
|
|
|
|
this.initPages();
|
|
const resizeObserver = new ResizeObserver((entries) => this.initPages());
|
|
resizeObserver.observe(this.slider);
|
|
|
|
this.slider.addEventListener('scroll', this.update.bind(this));
|
|
this.prevButton.addEventListener('click', this.onButtonClick.bind(this));
|
|
this.nextButton.addEventListener('click', this.onButtonClick.bind(this));
|
|
}
|
|
|
|
initPages() {
|
|
this.sliderItemsToShow = Array.from(this.sliderItems).filter((element) => element.clientWidth > 0);
|
|
if (this.sliderItemsToShow.length < 2) return;
|
|
this.sliderItemOffset = this.sliderItemsToShow[1].offsetLeft - this.sliderItemsToShow[0].offsetLeft;
|
|
this.slidesPerPage = Math.floor(
|
|
(this.slider.clientWidth - this.sliderItemsToShow[0].offsetLeft) / this.sliderItemOffset
|
|
);
|
|
this.totalPages = this.sliderItemsToShow.length - this.slidesPerPage + 1;
|
|
this.update();
|
|
}
|
|
|
|
resetPages() {
|
|
this.sliderItems = this.querySelectorAll('[id^="Slide-"]');
|
|
this.initPages();
|
|
}
|
|
|
|
update() {
|
|
// Temporarily prevents unneeded updates resulting from variant changes
|
|
// This should be refactored as part of https://github.com/Shopify/dawn/issues/2057
|
|
if (!this.slider || !this.nextButton) return;
|
|
|
|
const previousPage = this.currentPage;
|
|
this.currentPage = Math.round(this.slider.scrollLeft / this.sliderItemOffset) + 1;
|
|
|
|
if (this.currentPageElement && this.pageTotalElement) {
|
|
this.currentPageElement.textContent = this.currentPage;
|
|
this.pageTotalElement.textContent = this.totalPages;
|
|
}
|
|
|
|
if (this.currentPage != previousPage) {
|
|
this.dispatchEvent(
|
|
new CustomEvent('slideChanged', {
|
|
detail: {
|
|
currentPage: this.currentPage,
|
|
currentElement: this.sliderItemsToShow[this.currentPage - 1],
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
if (this.enableSliderLooping) return;
|
|
|
|
if (this.isSlideVisible(this.sliderItemsToShow[0]) && this.slider.scrollLeft === 0) {
|
|
this.prevButton.setAttribute('disabled', 'disabled');
|
|
} else {
|
|
this.prevButton.removeAttribute('disabled');
|
|
}
|
|
|
|
if (this.isSlideVisible(this.sliderItemsToShow[this.sliderItemsToShow.length - 1])) {
|
|
this.nextButton.setAttribute('disabled', 'disabled');
|
|
} else {
|
|
this.nextButton.removeAttribute('disabled');
|
|
}
|
|
}
|
|
|
|
isSlideVisible(element, offset = 0) {
|
|
const lastVisibleSlide = this.slider.clientWidth + this.slider.scrollLeft - offset;
|
|
return element.offsetLeft + element.clientWidth <= lastVisibleSlide && element.offsetLeft >= this.slider.scrollLeft;
|
|
}
|
|
|
|
onButtonClick(event) {
|
|
event.preventDefault();
|
|
const step = event.currentTarget.dataset.step || 1;
|
|
this.slideScrollPosition =
|
|
event.currentTarget.name === 'next'
|
|
? this.slider.scrollLeft + step * this.sliderItemOffset
|
|
: this.slider.scrollLeft - step * this.sliderItemOffset;
|
|
this.setSlidePosition(this.slideScrollPosition);
|
|
}
|
|
|
|
setSlidePosition(position) {
|
|
this.slider.scrollTo({
|
|
left: position,
|
|
});
|
|
}
|
|
}
|
|
|
|
customElements.define('slider-component', SliderComponent);
|
|
|
|
class SlideshowComponent extends SliderComponent {
|
|
constructor() {
|
|
super();
|
|
this.sliderControlWrapper = this.querySelector('.slider-buttons');
|
|
this.enableSliderLooping = true;
|
|
|
|
if (!this.sliderControlWrapper) return;
|
|
|
|
this.sliderFirstItemNode = this.slider.querySelector('.slideshow__slide');
|
|
if (this.sliderItemsToShow.length > 0) this.currentPage = 1;
|
|
|
|
this.announcementBarSlider = this.querySelector('.announcement-bar-slider');
|
|
// Value below should match --duration-announcement-bar CSS value
|
|
this.announcerBarAnimationDelay = this.announcementBarSlider ? 250 : 0;
|
|
|
|
this.sliderControlLinksArray = Array.from(this.sliderControlWrapper.querySelectorAll('.slider-counter__link'));
|
|
this.sliderControlLinksArray.forEach((link) => link.addEventListener('click', this.linkToSlide.bind(this)));
|
|
this.slider.addEventListener('scroll', this.setSlideVisibility.bind(this));
|
|
this.setSlideVisibility();
|
|
|
|
if (this.announcementBarSlider) {
|
|
this.announcementBarArrowButtonWasClicked = false;
|
|
|
|
this.reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
this.reducedMotion.addEventListener('change', () => {
|
|
if (this.slider.getAttribute('data-autoplay') === 'true') this.setAutoPlay();
|
|
});
|
|
|
|
[this.prevButton, this.nextButton].forEach((button) => {
|
|
button.addEventListener(
|
|
'click',
|
|
() => {
|
|
this.announcementBarArrowButtonWasClicked = true;
|
|
},
|
|
{ once: true }
|
|
);
|
|
});
|
|
}
|
|
|
|
if (this.slider.getAttribute('data-autoplay') === 'true') this.setAutoPlay();
|
|
}
|
|
|
|
setAutoPlay() {
|
|
this.autoplaySpeed = this.slider.dataset.speed * 1000;
|
|
this.addEventListener('mouseover', this.focusInHandling.bind(this));
|
|
this.addEventListener('mouseleave', this.focusOutHandling.bind(this));
|
|
this.addEventListener('focusin', this.focusInHandling.bind(this));
|
|
this.addEventListener('focusout', this.focusOutHandling.bind(this));
|
|
|
|
if (this.querySelector('.slideshow__autoplay')) {
|
|
this.sliderAutoplayButton = this.querySelector('.slideshow__autoplay');
|
|
this.sliderAutoplayButton.addEventListener('click', this.autoPlayToggle.bind(this));
|
|
this.autoplayButtonIsSetToPlay = true;
|
|
this.play();
|
|
} else {
|
|
this.reducedMotion.matches || this.announcementBarArrowButtonWasClicked ? this.pause() : this.play();
|
|
}
|
|
}
|
|
|
|
onButtonClick(event) {
|
|
super.onButtonClick(event);
|
|
this.wasClicked = true;
|
|
|
|
const isFirstSlide = this.currentPage === 1;
|
|
const isLastSlide = this.currentPage === this.sliderItemsToShow.length;
|
|
|
|
if (!isFirstSlide && !isLastSlide) {
|
|
this.applyAnimationToAnnouncementBar(event.currentTarget.name);
|
|
return;
|
|
}
|
|
|
|
if (isFirstSlide && event.currentTarget.name === 'previous') {
|
|
this.slideScrollPosition =
|
|
this.slider.scrollLeft + this.sliderFirstItemNode.clientWidth * this.sliderItemsToShow.length;
|
|
} else if (isLastSlide && event.currentTarget.name === 'next') {
|
|
this.slideScrollPosition = 0;
|
|
}
|
|
|
|
this.setSlidePosition(this.slideScrollPosition);
|
|
|
|
this.applyAnimationToAnnouncementBar(event.currentTarget.name);
|
|
}
|
|
|
|
setSlidePosition(position) {
|
|
if (this.setPositionTimeout) clearTimeout(this.setPositionTimeout);
|
|
this.setPositionTimeout = setTimeout(() => {
|
|
this.slider.scrollTo({
|
|
left: position,
|
|
});
|
|
}, this.announcerBarAnimationDelay);
|
|
}
|
|
|
|
update() {
|
|
super.update();
|
|
this.sliderControlButtons = this.querySelectorAll('.slider-counter__link');
|
|
this.prevButton.removeAttribute('disabled');
|
|
|
|
if (!this.sliderControlButtons.length) return;
|
|
|
|
this.sliderControlButtons.forEach((link) => {
|
|
link.classList.remove('slider-counter__link--active');
|
|
link.removeAttribute('aria-current');
|
|
});
|
|
this.sliderControlButtons[this.currentPage - 1].classList.add('slider-counter__link--active');
|
|
this.sliderControlButtons[this.currentPage - 1].setAttribute('aria-current', true);
|
|
}
|
|
|
|
autoPlayToggle() {
|
|
this.togglePlayButtonState(this.autoplayButtonIsSetToPlay);
|
|
this.autoplayButtonIsSetToPlay ? this.pause() : this.play();
|
|
this.autoplayButtonIsSetToPlay = !this.autoplayButtonIsSetToPlay;
|
|
}
|
|
|
|
focusOutHandling(event) {
|
|
if (this.sliderAutoplayButton) {
|
|
const focusedOnAutoplayButton =
|
|
event.target === this.sliderAutoplayButton || this.sliderAutoplayButton.contains(event.target);
|
|
if (!this.autoplayButtonIsSetToPlay || focusedOnAutoplayButton) return;
|
|
this.play();
|
|
} else if (!this.reducedMotion.matches && !this.announcementBarArrowButtonWasClicked) {
|
|
this.play();
|
|
}
|
|
}
|
|
|
|
focusInHandling(event) {
|
|
if (this.sliderAutoplayButton) {
|
|
const focusedOnAutoplayButton =
|
|
event.target === this.sliderAutoplayButton || this.sliderAutoplayButton.contains(event.target);
|
|
if (focusedOnAutoplayButton && this.autoplayButtonIsSetToPlay) {
|
|
this.play();
|
|
} else if (this.autoplayButtonIsSetToPlay) {
|
|
this.pause();
|
|
}
|
|
} else if (this.announcementBarSlider.contains(event.target)) {
|
|
this.pause();
|
|
}
|
|
}
|
|
|
|
play() {
|
|
this.slider.setAttribute('aria-live', 'off');
|
|
clearInterval(this.autoplay);
|
|
this.autoplay = setInterval(this.autoRotateSlides.bind(this), this.autoplaySpeed);
|
|
}
|
|
|
|
pause() {
|
|
this.slider.setAttribute('aria-live', 'polite');
|
|
clearInterval(this.autoplay);
|
|
}
|
|
|
|
togglePlayButtonState(pauseAutoplay) {
|
|
if (pauseAutoplay) {
|
|
this.sliderAutoplayButton.classList.add('slideshow__autoplay--paused');
|
|
this.sliderAutoplayButton.setAttribute('aria-label', window.accessibilityStrings.playSlideshow);
|
|
} else {
|
|
this.sliderAutoplayButton.classList.remove('slideshow__autoplay--paused');
|
|
this.sliderAutoplayButton.setAttribute('aria-label', window.accessibilityStrings.pauseSlideshow);
|
|
}
|
|
}
|
|
|
|
autoRotateSlides() {
|
|
const slideScrollPosition =
|
|
this.currentPage === this.sliderItems.length ? 0 : this.slider.scrollLeft + this.sliderItemOffset;
|
|
|
|
this.setSlidePosition(slideScrollPosition);
|
|
this.applyAnimationToAnnouncementBar();
|
|
}
|
|
|
|
setSlideVisibility(event) {
|
|
this.sliderItemsToShow.forEach((item, index) => {
|
|
const linkElements = item.querySelectorAll('a');
|
|
if (index === this.currentPage - 1) {
|
|
if (linkElements.length)
|
|
linkElements.forEach((button) => {
|
|
button.removeAttribute('tabindex');
|
|
});
|
|
item.setAttribute('aria-hidden', 'false');
|
|
item.removeAttribute('tabindex');
|
|
} else {
|
|
if (linkElements.length)
|
|
linkElements.forEach((button) => {
|
|
button.setAttribute('tabindex', '-1');
|
|
});
|
|
item.setAttribute('aria-hidden', 'true');
|
|
item.setAttribute('tabindex', '-1');
|
|
}
|
|
});
|
|
this.wasClicked = false;
|
|
}
|
|
|
|
applyAnimationToAnnouncementBar(button = 'next') {
|
|
if (!this.announcementBarSlider) return;
|
|
|
|
const itemsCount = this.sliderItems.length;
|
|
const increment = button === 'next' ? 1 : -1;
|
|
|
|
const currentIndex = this.currentPage - 1;
|
|
let nextIndex = (currentIndex + increment) % itemsCount;
|
|
nextIndex = nextIndex === -1 ? itemsCount - 1 : nextIndex;
|
|
|
|
const nextSlide = this.sliderItems[nextIndex];
|
|
const currentSlide = this.sliderItems[currentIndex];
|
|
|
|
const animationClassIn = 'announcement-bar-slider--fade-in';
|
|
const animationClassOut = 'announcement-bar-slider--fade-out';
|
|
|
|
const isFirstSlide = currentIndex === 0;
|
|
const isLastSlide = currentIndex === itemsCount - 1;
|
|
|
|
const shouldMoveNext = (button === 'next' && !isLastSlide) || (button === 'previous' && isFirstSlide);
|
|
const direction = shouldMoveNext ? 'next' : 'previous';
|
|
|
|
currentSlide.classList.add(`${animationClassOut}-${direction}`);
|
|
nextSlide.classList.add(`${animationClassIn}-${direction}`);
|
|
|
|
setTimeout(() => {
|
|
currentSlide.classList.remove(`${animationClassOut}-${direction}`);
|
|
nextSlide.classList.remove(`${animationClassIn}-${direction}`);
|
|
}, this.announcerBarAnimationDelay * 2);
|
|
}
|
|
|
|
linkToSlide(event) {
|
|
event.preventDefault();
|
|
const slideScrollPosition =
|
|
this.slider.scrollLeft +
|
|
this.sliderFirstItemNode.clientWidth *
|
|
(this.sliderControlLinksArray.indexOf(event.currentTarget) + 1 - this.currentPage);
|
|
this.slider.scrollTo({
|
|
left: slideScrollPosition,
|
|
});
|
|
}
|
|
}
|
|
|
|
customElements.define('slideshow-component', SlideshowComponent);
|
|
|
|
class VariantSelects extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.addEventListener('change', (event) => {
|
|
const target = this.getInputForEventTarget(event.target);
|
|
this.updateSelectionMetadata(event);
|
|
|
|
this.dispatchProductSelectEvent();
|
|
|
|
publish(PUB_SUB_EVENTS.optionValueSelectionChange, {
|
|
data: {
|
|
event,
|
|
target,
|
|
selectedOptionValues: this.selectedOptionValues,
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
getAllSelectedOptions() {
|
|
const options = [];
|
|
this.querySelectorAll('fieldset, .product-form__input--dropdown').forEach((group) => {
|
|
const checked = group.querySelector('input:checked') || group.querySelector('select option[selected]');
|
|
if (checked) {
|
|
options.push({ name: checked.dataset.optionName || '', value: checked.value });
|
|
}
|
|
});
|
|
return options;
|
|
}
|
|
|
|
dispatchProductSelectEvent() {
|
|
const { ProductSelectEvent } = window.StandardEvents || {};
|
|
if (!ProductSelectEvent) return;
|
|
|
|
const deferred = ProductSelectEvent.createPromise();
|
|
this.pendingSelectPromise = deferred;
|
|
|
|
this.dispatchEvent(
|
|
new ProductSelectEvent({
|
|
product: {
|
|
id: this.dataset.productId,
|
|
title: this.dataset.productTitle,
|
|
handle: this.dataset.productHandle,
|
|
},
|
|
selectedOptions: this.getAllSelectedOptions(),
|
|
promise: deferred.promise,
|
|
})
|
|
);
|
|
}
|
|
|
|
takePendingSelectPromise() {
|
|
const deferred = this.pendingSelectPromise;
|
|
this.pendingSelectPromise = null;
|
|
return deferred;
|
|
}
|
|
|
|
resolvePendingSelectPromise(variant, sourceVariantSelects = this) {
|
|
const deferred = this.takePendingSelectPromise();
|
|
if (!deferred) return;
|
|
|
|
if (variant) {
|
|
deferred.resolve({
|
|
variant: {
|
|
id: variant.id,
|
|
title: variant.title,
|
|
availableForSale: variant.available,
|
|
price: {
|
|
amount: sourceVariantSelects?.dataset.selectedPriceAmount,
|
|
currencyCode: sourceVariantSelects?.dataset.currencyCode,
|
|
},
|
|
selectedOptions: this.getAllSelectedOptions(),
|
|
},
|
|
});
|
|
} else {
|
|
deferred.resolve({ variant: null });
|
|
}
|
|
}
|
|
|
|
rejectPendingSelectPromise(error) {
|
|
this.takePendingSelectPromise()?.reject(error);
|
|
}
|
|
|
|
updateSelectionMetadata({ target }) {
|
|
const { value, tagName } = target;
|
|
|
|
if (tagName === 'SELECT' && target.selectedOptions.length) {
|
|
Array.from(target.options)
|
|
.find((option) => option.getAttribute('selected'))
|
|
.removeAttribute('selected');
|
|
target.selectedOptions[0].setAttribute('selected', 'selected');
|
|
|
|
const swatchValue = target.selectedOptions[0].dataset.optionSwatchValue;
|
|
const selectedDropdownSwatchValue = target
|
|
.closest('.product-form__input')
|
|
.querySelector('[data-selected-value] > .swatch');
|
|
if (!selectedDropdownSwatchValue) return;
|
|
if (swatchValue) {
|
|
selectedDropdownSwatchValue.style.setProperty('--swatch--background', swatchValue);
|
|
selectedDropdownSwatchValue.classList.remove('swatch--unavailable');
|
|
} else {
|
|
selectedDropdownSwatchValue.style.setProperty('--swatch--background', 'unset');
|
|
selectedDropdownSwatchValue.classList.add('swatch--unavailable');
|
|
}
|
|
|
|
selectedDropdownSwatchValue.style.setProperty(
|
|
'--swatch-focal-point',
|
|
target.selectedOptions[0].dataset.optionSwatchFocalPoint || 'unset'
|
|
);
|
|
} else if (tagName === 'INPUT' && target.type === 'radio') {
|
|
const selectedSwatchValue = target.closest(`.product-form__input`).querySelector('[data-selected-value]');
|
|
if (selectedSwatchValue) selectedSwatchValue.innerHTML = value;
|
|
}
|
|
}
|
|
|
|
getInputForEventTarget(target) {
|
|
return target.tagName === 'SELECT' ? target.selectedOptions[0] : target;
|
|
}
|
|
|
|
get selectedOptionValues() {
|
|
return Array.from(this.querySelectorAll('select option[selected], fieldset input:checked')).map(
|
|
({ dataset }) => dataset.optionValueId
|
|
);
|
|
}
|
|
}
|
|
|
|
customElements.define('variant-selects', VariantSelects);
|
|
|
|
class ProductRecommendations extends HTMLElement {
|
|
observer = undefined;
|
|
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.initializeRecommendations(this.dataset.productId);
|
|
}
|
|
|
|
initializeRecommendations(productId) {
|
|
this.observer?.unobserve(this);
|
|
this.observer = new IntersectionObserver(
|
|
(entries, observer) => {
|
|
if (!entries[0].isIntersecting) return;
|
|
observer.unobserve(this);
|
|
this.loadRecommendations(productId);
|
|
},
|
|
{ rootMargin: '0px 0px 400px 0px' }
|
|
);
|
|
this.observer.observe(this);
|
|
}
|
|
|
|
loadRecommendations(productId) {
|
|
fetch(`${this.dataset.url}&product_id=${productId}§ion_id=${this.dataset.sectionId}`)
|
|
.then((response) => response.text())
|
|
.then((text) => {
|
|
const html = document.createElement('div');
|
|
html.innerHTML = text;
|
|
const recommendations = html.querySelector('product-recommendations');
|
|
|
|
if (recommendations?.innerHTML.trim().length) {
|
|
this.innerHTML = recommendations.innerHTML;
|
|
}
|
|
|
|
if (!this.querySelector('slideshow-component') && this.classList.contains('complementary-products')) {
|
|
this.remove();
|
|
}
|
|
|
|
if (html.querySelector('.grid__item')) {
|
|
this.classList.add('product-recommendations--loaded');
|
|
}
|
|
})
|
|
.catch((e) => {
|
|
console.error(e);
|
|
});
|
|
}
|
|
}
|
|
|
|
customElements.define('product-recommendations', ProductRecommendations);
|
|
|
|
class AccountIcon extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
|
|
this.icon = this.querySelector('.icon');
|
|
}
|
|
|
|
connectedCallback() {
|
|
document.addEventListener('storefront:signincompleted', this.handleStorefrontSignInCompleted.bind(this));
|
|
}
|
|
|
|
handleStorefrontSignInCompleted(event) {
|
|
if (event?.detail?.avatar) {
|
|
this.icon?.replaceWith(event.detail.avatar.cloneNode());
|
|
}
|
|
}
|
|
}
|
|
|
|
customElements.define('account-icon', AccountIcon);
|
|
|
|
class BulkAdd extends HTMLElement {
|
|
static ASYNC_REQUEST_DELAY = 250;
|
|
|
|
constructor() {
|
|
super();
|
|
this.queue = [];
|
|
this.setRequestStarted(false);
|
|
this.ids = [];
|
|
}
|
|
|
|
startQueue(id, quantity) {
|
|
this.queue.push({ id, quantity });
|
|
|
|
const interval = setInterval(() => {
|
|
if (this.queue.length > 0) {
|
|
if (!this.requestStarted) {
|
|
this.sendRequest(this.queue);
|
|
}
|
|
} else {
|
|
clearInterval(interval);
|
|
}
|
|
}, BulkAdd.ASYNC_REQUEST_DELAY);
|
|
}
|
|
|
|
sendRequest(queue) {
|
|
this.setRequestStarted(true);
|
|
const items = {};
|
|
|
|
queue.forEach((queueItem) => {
|
|
items[parseInt(queueItem.id)] = queueItem.quantity;
|
|
});
|
|
this.queue = this.queue.filter((queueElement) => !queue.includes(queueElement));
|
|
|
|
this.updateMultipleQty(items);
|
|
}
|
|
|
|
setRequestStarted(requestStarted) {
|
|
this._requestStarted = requestStarted;
|
|
}
|
|
|
|
get requestStarted() {
|
|
return this._requestStarted;
|
|
}
|
|
|
|
getCartQuantityForLine(id) {
|
|
const input = this.querySelector(`#Quantity-${id}`);
|
|
return parseInt(input?.dataset.cartQuantity || input?.getAttribute('value') || '0', 10) || 0;
|
|
}
|
|
|
|
startCartLinesUpdate(items) {
|
|
const { CartLinesUpdateEvent } = window.StandardEvents || {};
|
|
if (!CartLinesUpdateEvent) return;
|
|
|
|
const linesByAction = Object.entries(items).reduce((groups, [variantId, quantity]) => {
|
|
const nextQuantity = parseInt(quantity, 10);
|
|
const currentQuantity = this.getCartQuantityForLine(variantId);
|
|
|
|
if (Number.isNaN(nextQuantity) || currentQuantity === nextQuantity) return groups;
|
|
|
|
const action = currentQuantity === 0 ? 'add' : nextQuantity === 0 ? 'remove' : 'update';
|
|
let line;
|
|
if (action === 'add') {
|
|
line = { merchandiseId: variantId, quantity: nextQuantity };
|
|
} else {
|
|
const lineKey = this.querySelector(`[data-quantity-variant-id="${variantId}"]`)?.dataset.quantityLineKey;
|
|
// No AJAX line key on the row — likely cached HTML rendered before this
|
|
// attribute landed. Skip rather than emit an event with id: ''.
|
|
if (!lineKey) return groups;
|
|
line = { id: lineKey, quantity: nextQuantity };
|
|
}
|
|
|
|
if (!groups[action]) groups[action] = [];
|
|
groups[action].push(line);
|
|
return groups;
|
|
}, {});
|
|
|
|
const deferreds = Object.entries(linesByAction).map(([action, lines]) => {
|
|
const deferred = CartLinesUpdateEvent.createPromise();
|
|
this.dispatchEvent(
|
|
new CartLinesUpdateEvent({
|
|
action,
|
|
context: 'product',
|
|
lines,
|
|
promise: deferred.promise,
|
|
})
|
|
);
|
|
return deferred;
|
|
});
|
|
|
|
return {
|
|
resolve: (parsedState) => {
|
|
const payload = { cart: CartLinesUpdateEvent.createCartFromAjaxResponse(parsedState) };
|
|
deferreds.forEach((deferred) => deferred.resolve(payload));
|
|
},
|
|
reject: (error) => {
|
|
deferreds.forEach((deferred) => deferred.reject(error));
|
|
},
|
|
};
|
|
}
|
|
|
|
dispatchCartErrorEvent(message, code) {
|
|
const { CartErrorEvent } = window.StandardEvents || {};
|
|
if (!CartErrorEvent) return;
|
|
|
|
this.dispatchEvent(new CartErrorEvent({ error: message, code }));
|
|
}
|
|
|
|
resetQuantityInput(id) {
|
|
const input = this.querySelector(`#Quantity-${id}`);
|
|
input.value = input.getAttribute('value');
|
|
this.isEnterPressed = false;
|
|
}
|
|
|
|
setValidity(event, index, message) {
|
|
event.target.setCustomValidity(message);
|
|
event.target.reportValidity();
|
|
this.resetQuantityInput(index);
|
|
event.target.select();
|
|
}
|
|
|
|
validateQuantity(event) {
|
|
const inputValue = parseInt(event.target.value);
|
|
const index = event.target.dataset.index;
|
|
|
|
if (inputValue < event.target.dataset.min) {
|
|
this.setValidity(event, index, window.quickOrderListStrings.min_error.replace('[min]', event.target.dataset.min));
|
|
} else if (inputValue > parseInt(event.target.max)) {
|
|
this.setValidity(event, index, window.quickOrderListStrings.max_error.replace('[max]', event.target.max));
|
|
} else if (inputValue % parseInt(event.target.step) != 0) {
|
|
this.setValidity(event, index, window.quickOrderListStrings.step_error.replace('[step]', event.target.step));
|
|
} else {
|
|
event.target.setCustomValidity('');
|
|
event.target.reportValidity();
|
|
event.target.setAttribute('value', inputValue);
|
|
this.startQueue(index, inputValue);
|
|
}
|
|
}
|
|
|
|
getSectionInnerHTML(html, selector) {
|
|
return new DOMParser().parseFromString(html, 'text/html').querySelector(selector).innerHTML;
|
|
}
|
|
}
|
|
|
|
if (!customElements.get('bulk-add')) {
|
|
customElements.define('bulk-add', BulkAdd);
|
|
}
|
|
|
|
class CartPerformance {
|
|
static #metric_prefix = "cart-performance"
|
|
|
|
static createStartingMarker(benchmarkName) {
|
|
const metricName = `${CartPerformance.#metric_prefix}:${benchmarkName}`
|
|
return performance.mark(`${metricName}:start`);
|
|
}
|
|
|
|
static measureFromEvent(benchmarkName, event) {
|
|
const metricName = `${CartPerformance.#metric_prefix}:${benchmarkName}`
|
|
const startMarker = performance.mark(`${metricName}:start`, {
|
|
startTime: event.timeStamp
|
|
});
|
|
|
|
const endMarker = performance.mark(`${metricName}:end`);
|
|
|
|
performance.measure(
|
|
metricName,
|
|
`${metricName}:start`,
|
|
`${metricName}:end`
|
|
);
|
|
}
|
|
|
|
static measureFromMarker(benchmarkName, startMarker) {
|
|
const metricName = `${CartPerformance.#metric_prefix}:${benchmarkName}`
|
|
const endMarker = performance.mark(`${metricName}:end`);
|
|
|
|
performance.measure(
|
|
metricName,
|
|
startMarker.name,
|
|
`${metricName}:end`
|
|
);
|
|
}
|
|
|
|
static measure(benchmarkName, callback) {
|
|
const metricName = `${CartPerformance.#metric_prefix}:${benchmarkName}`
|
|
const startMarker = performance.mark(`${metricName}:start`);
|
|
|
|
callback();
|
|
|
|
const endMarker = performance.mark(`${metricName}:end`);
|
|
|
|
performance.measure(
|
|
metricName,
|
|
`${metricName}:start`,
|
|
`${metricName}:end`
|
|
);
|
|
}
|
|
}
|