initial commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user