initial commit

This commit is contained in:
Gerhard Scheikl
2026-08-24 21:41:12 +02:00
commit 5c952a25b1
367 changed files with 134961 additions and 0 deletions
+500
View File
@@ -0,0 +1,500 @@
if (!customElements.get('quick-order-list')) {
customElements.define(
'quick-order-list',
class QuickOrderList extends BulkAdd {
cartUpdateUnsubscriber = undefined;
hasPendingQuantityUpdate = false;
constructor() {
super();
this.isListInsideModal = this.closest('bulk-modal');
this.stickyHeaderElement = document.querySelector('sticky-header');
if (this.stickyHeaderElement) {
this.stickyHeader = {
height: this.stickyHeaderElement.offsetHeight,
type: `${this.stickyHeaderElement.getAttribute('data-sticky-type')}`,
};
}
this.totalBar = this.getTotalBar();
if (this.totalBar) {
this.totalBarPosition = window.innerHeight - this.totalBar.offsetHeight;
this.handleResize = this.handleResize.bind(this);
window.addEventListener('resize', this.handleResize);
}
this.querySelector('form').addEventListener('submit', (event) => event.preventDefault());
}
connectedCallback() {
this.cartUpdateUnsubscriber = subscribe(PUB_SUB_EVENTS.cartUpdate, async (event) => {
// skip if cart event was triggered by this section
if (event.source === this.id) return;
this.toggleTableLoading(true);
await this.refresh();
this.toggleTableLoading(false);
});
this.initEventListeners();
}
disconnectedCallback() {
this.cartUpdateUnsubscriber?.();
window.removeEventListener('resize', this.handleResize);
}
handleResize() {
this.totalBarPosition = window.innerHeight - this.totalBar.offsetHeight;
this.stickyHeader.height = this.stickyHeaderElement ? this.stickyHeaderElement.offsetHeight : 0;
}
initEventListeners() {
this.querySelectorAll('.pagination__item').forEach((link) => {
link.addEventListener('click', async (event) => {
event.preventDefault();
event.stopPropagation();
const url = new URL(event.currentTarget.href);
this.toggleTableLoading(true);
await this.refresh(url.searchParams.get('page') || '1');
this.scrollTop();
this.toggleTableLoading(false);
});
});
this.querySelector('.quick-order-list__contents').addEventListener(
'keyup',
this.handleScrollIntoView.bind(this)
);
this.quickOrderListTable.addEventListener('keydown', this.handleSwitchVariantOnEnter.bind(this));
this.initVariantEventListeners();
}
initVariantEventListeners() {
this.allInputsArray = Array.from(this.querySelectorAll('input[type="number"]'));
this.querySelectorAll('quantity-input').forEach((qty) => {
const debouncedOnChange = debounce(this.onChange.bind(this), BulkAdd.ASYNC_REQUEST_DELAY, true);
qty.addEventListener('change', (event) => {
this.hasPendingQuantityUpdate = true;
debouncedOnChange(event);
});
});
this.querySelectorAll('.quick-order-list-remove-button').forEach((button) => {
button.addEventListener('click', (event) => {
event.preventDefault();
this.toggleLoading(true);
this.startQueue(button.dataset.index, 0);
});
});
}
get currentPage() {
return this.querySelector('.pagination-wrapper')?.dataset?.page ?? '1';
}
get cartVariantsForProduct() {
return JSON.parse(this.querySelector('[data-cart-contents]')?.innerHTML || '[]');
}
getCartQuantityForLine(id) {
const cartQuantity = super.getCartQuantityForLine(id);
if (cartQuantity > 0) return cartQuantity;
return this.cartVariantsForProduct.includes(parseInt(id, 10)) ? 1 : 0;
}
onChange(event) {
const inputValue = parseInt(event.target.value);
this.cleanErrorMessageOnType(event);
if (inputValue == 0) {
event.target.setAttribute('value', inputValue);
this.startQueue(event.target.dataset.index, inputValue);
} else {
this.validateQuantity(event);
}
}
cleanErrorMessageOnType(event) {
const handleKeydown = () => {
event.target.setCustomValidity(' ');
event.target.reportValidity();
event.target.removeEventListener('keydown', handleKeydown);
};
event.target.addEventListener('keydown', handleKeydown);
}
validateInput(target) {
const targetValue = parseInt(target.value);
const targetMin = parseInt(target.dataset.min);
const targetStep = parseInt(target.step);
if (target.max) {
return (
targetValue == 0 ||
(targetValue >= targetMin && targetValue <= parseInt(target.max) && targetValue % targetStep == 0)
);
} else {
return targetValue == 0 || (targetValue >= targetMin && targetValue % targetStep == 0);
}
}
get quickOrderListTable() {
return this.querySelector('.quick-order-list__table');
}
getSectionsToRender() {
return [
{
id: this.id,
section: this.dataset.section,
selector: `#${this.id}`,
},
{
id: 'cart-icon-bubble',
section: 'cart-icon-bubble',
selector: '#shopify-section-cart-icon-bubble',
},
{
id: `quick-order-list-live-region-text-${this.dataset.productId}`,
section: 'cart-live-region-text',
selector: '.shopify-section',
},
{
id: 'CartDrawer',
selector: '.drawer__inner',
section: 'cart-drawer',
},
];
}
toggleTableLoading(enable) {
this.quickOrderListTable.classList.toggle('quick-order-list__container--disabled', enable);
this.toggleLoading(enable);
}
async refresh(pageNumber = null) {
const url = this.dataset.url || window.location.pathname;
return fetch(`${url}?section_id=${this.dataset.section}&page=${pageNumber || this.currentPage}`)
.then((response) => response.text())
.then((responseText) => {
const html = new DOMParser().parseFromString(responseText, 'text/html');
const responseQuickOrderList = html.querySelector(`#${this.id}`);
if (!responseQuickOrderList) {
return;
}
this.innerHTML = responseQuickOrderList.innerHTML;
this.initEventListeners();
})
.catch((e) => {
console.error(e);
});
}
renderSections(parsedState) {
const { items, sections } = parsedState;
this.getSectionsToRender().forEach(({ id, selector, section }) => {
const sectionElement = document.getElementById(id);
if (!sectionElement) return;
const newSection = new DOMParser().parseFromString(sections[section], 'text/html').querySelector(selector);
if (section === this.dataset.section) {
if (this.queue.length > 0 || this.hasPendingQuantityUpdate) return;
const focusedElement = document.activeElement;
let focusTarget = focusedElement?.dataset?.target;
if (focusTarget?.includes('remove')) {
focusTarget = focusedElement.closest('quantity-popover')?.querySelector('[data-target*="increment-"]')
?.dataset.target;
}
const total = this.getTotalBar();
if (total) {
total.innerHTML = newSection.querySelector('.quick-order-list__total').innerHTML;
}
const table = this.quickOrderListTable;
const newTable = newSection.querySelector('.quick-order-list__table');
// only update variants if they are from the active page
const shouldUpdateVariants =
this.currentPage === (newSection.querySelector('.pagination-wrapper')?.dataset.page ?? '1');
if (newTable && shouldUpdateVariants) {
table.innerHTML = newTable.innerHTML;
const newFocusTarget = this.querySelector(`[data-target='${focusTarget}']`);
if (newFocusTarget) {
newFocusTarget?.focus({ preventScroll: true });
}
this.initVariantEventListeners();
}
} else if (section === 'cart-drawer') {
sectionElement.closest('cart-drawer')?.classList.toggle('is-empty', items.length === 0);
sectionElement.querySelector(selector).innerHTML = newSection.innerHTML;
} else {
sectionElement.innerHTML = newSection.innerHTML;
}
});
}
getTotalBar() {
return this.querySelector('.quick-order-list__total');
}
scrollTop() {
const { top } = this.getBoundingClientRect();
if (this.isListInsideModal) {
this.scrollIntoView();
} else {
window.scrollTo({ top: top + window.scrollY - (this.stickyHeader?.height || 0), behavior: 'instant' });
}
}
scrollQuickOrderListTable(target) {
const inputTopBorder = target.getBoundingClientRect().top;
const inputBottomBorder = target.getBoundingClientRect().bottom;
if (this.isListInsideModal) {
const totalBarCrossesInput = inputBottomBorder > this.totalBar.getBoundingClientRect().top;
const tableHeadCrossesInput =
inputTopBorder < this.querySelector('.quick-order-list__table thead').getBoundingClientRect().bottom;
if (totalBarCrossesInput || tableHeadCrossesInput) {
this.scrollToCenter(target);
}
} else {
const stickyHeaderBottomBorder = this.stickyHeaderElement?.getBoundingClientRect().bottom;
const totalBarCrossesInput = inputBottomBorder > this.totalBarPosition;
const inputOutsideOfViewPort =
inputBottomBorder < this.querySelector('.variant-item__quantity-wrapper').offsetHeight;
const stickyHeaderCrossesInput =
this.stickyHeaderElement &&
this.stickyHeader.type !== 'on-scroll-up' &&
this.stickyHeader.height > inputTopBorder;
const stickyHeaderScrollupCrossesInput =
this.stickyHeaderElement &&
this.stickyHeader.type === 'on-scroll-up' &&
this.stickyHeader.height > inputTopBorder &&
stickyHeaderBottomBorder > 0;
if (
totalBarCrossesInput ||
inputOutsideOfViewPort ||
stickyHeaderCrossesInput ||
stickyHeaderScrollupCrossesInput
) {
this.scrollToCenter(target);
}
}
}
scrollToCenter(target) {
target.scrollIntoView({
block: 'center',
behavior: 'smooth',
});
}
handleScrollIntoView(event) {
if ((event.key === 'Tab' || event.key === 'Enter') && this.allInputsArray.length !== 1) {
this.scrollQuickOrderListTable(event.target);
}
}
handleSwitchVariantOnEnter(event) {
if (event.key !== 'Enter' || event.target.tagName !== 'INPUT') return;
event.preventDefault();
event.target.blur();
if (!this.validateInput(event.target) || this.allInputsArray.length <= 1) return;
const currentIndex = this.allInputsArray.indexOf(event.target);
const offset = event.shiftKey ? -1 : 1;
const nextIndex = (currentIndex + offset + this.allInputsArray.length) % this.allInputsArray.length;
this.allInputsArray[nextIndex]?.select();
}
updateMultipleQty(items) {
if (this.queue.length == 0) this.hasPendingQuantityUpdate = false;
this.toggleLoading(true);
const url = this.dataset.url || window.location.pathname;
const linesUpdate = this.startCartLinesUpdate(items);
const body = JSON.stringify({
updates: items,
sections: this.getSectionsToRender().map(({ section }) => section),
sections_url: `${url}?page=${this.currentPage}`,
});
this.updateMessage();
this.setErrorMessage();
fetch(`${routes.cart_update_url}`, { ...fetchConfig(), ...{ body } })
.then((response) => response.json())
.then((parsedState) => {
if (parsedState.errors) {
throw Object.assign(new Error(parsedState.errors), { code: 'INVALID' });
}
linesUpdate?.resolve(parsedState);
this.renderSections(parsedState);
publish(PUB_SUB_EVENTS.cartUpdate, {
source: this.id,
cartData: parsedState,
});
})
.catch((e) => {
if (e.code !== 'INVALID') console.error(e);
this.dispatchCartErrorEvent(
e.code === 'INVALID' ? e.message : window.cartStrings.error,
e.code || 'SERVICE_UNAVAILABLE'
);
linesUpdate?.reject(e);
this.setErrorMessage(e.code === 'INVALID' ? e.message : window.cartStrings.error);
})
.finally(() => {
this.queue.length === 0 && this.toggleLoading(false);
this.setRequestStarted(false);
});
}
setErrorMessage(message = null) {
this.errorMessageTemplate =
this.errorMessageTemplate ??
document.getElementById(`QuickOrderListErrorTemplate-${this.dataset.productId}`).cloneNode(true);
const errorElements = document.querySelectorAll('.quick-order-list-error');
errorElements.forEach((errorElement) => {
errorElement.innerHTML = '';
if (!message) return;
const updatedMessageElement = this.errorMessageTemplate.cloneNode(true);
updatedMessageElement.content.querySelector('.quick-order-list-error-message').innerText = message;
errorElement.appendChild(updatedMessageElement.content);
});
}
updateMessage(quantity = null) {
const messages = this.querySelectorAll('.quick-order-list__message-text');
const icons = this.querySelectorAll('.quick-order-list__message-icon');
if (quantity === null || isNaN(quantity)) {
messages.forEach((message) => (message.innerHTML = ''));
icons.forEach((icon) => icon.classList.add('hidden'));
return;
}
const isQuantityNegative = quantity < 0;
const absQuantity = Math.abs(quantity);
const textTemplate = isQuantityNegative
? absQuantity === 1
? window.quickOrderListStrings.itemRemoved
: window.quickOrderListStrings.itemsRemoved
: quantity === 1
? window.quickOrderListStrings.itemAdded
: window.quickOrderListStrings.itemsAdded;
messages.forEach((msg) => (msg.innerHTML = textTemplate.replace('[quantity]', absQuantity)));
if (!isQuantityNegative) {
icons.forEach((i) => i.classList.remove('hidden'));
}
}
updateError(updatedValue, id) {
let message = '';
if (typeof updatedValue === 'undefined') {
message = window.cartStrings.error;
} else {
message = window.cartStrings.quantityError.replace('[quantity]', updatedValue);
}
this.updateLiveRegions(id, message);
}
updateLiveRegions(id, message) {
const variantItemErrorDesktop = document.getElementById(`Quick-order-list-item-error-desktop-${id}`);
if (variantItemErrorDesktop) {
variantItemErrorDesktop.querySelector('.variant-item__error-text').innerHTML = message;
variantItemErrorDesktop.closest('tr').classList.remove('hidden');
}
if (variantItemErrorMobile)
variantItemErrorMobile.querySelector('.variant-item__error-text').innerHTML = message;
this.querySelector('#shopping-cart-variant-item-status').setAttribute('aria-hidden', true);
const cartStatus = document.getElementById('quick-order-list-live-region-text');
cartStatus.setAttribute('aria-hidden', false);
setTimeout(() => {
cartStatus.setAttribute('aria-hidden', true);
}, 1000);
}
toggleLoading(loading, target = this) {
target.querySelector('#shopping-cart-variant-item-status').toggleAttribute('aria-hidden', !loading);
target
.querySelectorAll('.variant-remove-total .loading__spinner')
?.forEach((spinner) => spinner.classList.toggle('hidden', !loading));
}
}
);
}
if (!customElements.get('quick-order-list-remove-all-button')) {
customElements.define(
'quick-order-list-remove-all-button',
class QuickOrderListRemoveAllButton extends HTMLElement {
constructor() {
super();
this.quickOrderList = this.closest('quick-order-list');
this.actions = {
confirm: 'confirm',
remove: 'remove',
cancel: 'cancel',
};
this.addEventListener('click', (event) => {
event.preventDefault();
if (this.dataset.action === this.actions.confirm) {
this.toggleConfirmation(false, true);
} else if (this.dataset.action === this.actions.remove) {
const items = this.quickOrderList.cartVariantsForProduct.reduce(
(acc, variantId) => ({ ...acc, [variantId]: 0 }),
{}
);
this.quickOrderList.updateMultipleQty(items);
this.toggleConfirmation(true, false);
} else if (this.dataset.action === this.actions.cancel) {
this.toggleConfirmation(true, false);
}
});
}
toggleConfirmation(showConfirmation, showInfo) {
this.quickOrderList
.querySelector('.quick-order-list-total__confirmation')
.classList.toggle('hidden', showConfirmation);
this.quickOrderList.querySelector('.quick-order-list-total__info').classList.toggle('hidden', showInfo);
}
}
);
}