| | |
| | | this.checkout = document.querySelector('aside#cart'); |
| | | this.provider = this.checkout?.querySelector('form')?.dataset.provider || ''; |
| | | |
| | | this.isOpen = this.config.isOpen !== '1' || false; |
| | | this.isOpen = this.config.isOpen === '1' || false; |
| | | this.isOpen = true; |
| | | |
| | | this.isLoggedIn = this.config.is_logged_in || false; |
| | | this.userEmail = this.config.user_email || ''; |
| | | this.savedCards = []; |
| | |
| | | |
| | | this.cache = new window.jvbCache('cart', { TTL: 8.64e+7 }); |
| | | this.a11y = window.jvbA11y; |
| | | this.T = window.jvbTemplates; |
| | | |
| | | this.initCart(); |
| | | |
| | |
| | | this.initElements(); |
| | | this.init(); // provider-specific |
| | | this.initListeners(); |
| | | this.defineTemplates(); |
| | | |
| | | if (this.isLoggedIn) { |
| | | this.loadSavedCards(); |
| | | } |
| | | } |
| | | |
| | | if (!this.isOpen) |
| | | { |
| | | this.closeShop(); |
| | | } |
| | | |
| | | this.popup = window.jvbPopup.registerPopup({ |
| | | popup: this.checkout, |
| | | toggle: this.toggle, |
| | | toggle: this.ui.toggle.btn, |
| | | name: 'Cart', |
| | | onOpen: this.maybeAddEmptyState.bind(this), |
| | | onClose: this.toggleButton.bind(this), |
| | | }); |
| | | } |
| | | |
| | | closeShop() { |
| | | document.querySelectorAll('[data-add-to-cart]').forEach(item => { |
| | | item.querySelectorAll('button, input').forEach(i => { |
| | | i.disabled = true; |
| | | i.title = 'Sorry, we are currently closed.'; |
| | | }); |
| | | |
| | | }); |
| | | } |
| | | |
| | | |
| | | /***************************************************************** |
| | | * ABSTRACT — subclasses must implement |
| | | *****************************************************************/ |
| | |
| | | *****************************************************************/ |
| | | |
| | | async initCart() { |
| | | this.cartItems = await this.cache.get('cart') ?? new Map(); |
| | | if (this.cartItems.size > 0) { |
| | | let stored = await this.cache.get('cart') ?? new Map(); |
| | | if (stored.size > 0) { |
| | | this.cartItems = stored; |
| | | this.updateToggle(); |
| | | this.notifyRestoredCart(); |
| | | } |
| | | } |
| | | |
| | | saveCart() { |
| | | this.updateTotal(); |
| | | this.updateToggle(); |
| | | this.cache.set('cart', this.cartItems); |
| | | } |
| | | |
| | | clearCart() { |
| | | for (let [id, item] of this.cartItems.entries()) { |
| | | this.removeFromCart(id); |
| | | } |
| | | this.cartItems.clear(); |
| | | window.removeChildren(this.table); |
| | | Array.from(this.ui.cart.table.children).forEach(child => { |
| | | if (child.tagName === 'TBODY') { |
| | | child.remove(); |
| | | } |
| | | }); |
| | | this.saveCart(); |
| | | } |
| | | |
| | |
| | | /***************************************************************** |
| | | * ELEMENT SETUP |
| | | *****************************************************************/ |
| | | |
| | | initElements() { |
| | | this.toggle = document.querySelector('.toggle-cart'); |
| | | if (!this.isOpen) { |
| | | this.toggle.disabled = true; |
| | | this.toggle.title = 'Currently closed for online ordering'; |
| | | defineTemplates() { |
| | | // const checkout = this; |
| | | this.T.define('mainCartItem', { |
| | | refs: { |
| | | header: '.header th' |
| | | }, |
| | | setup({el, refs, manyRefs, data}) { |
| | | refs.header.textContent = data.label; |
| | | el.dataset.label = data.label; |
| | | el.dataset.id = data.parentId; |
| | | } |
| | | this.checkoutPanel = this.checkout.querySelector('button[data-tab="checkout"]'); |
| | | this.itemsList = this.checkout.querySelector('.cart-items'); |
| | | this.table = this.checkout.querySelector('.cart-items tbody'); |
| | | this.total = this.checkout.querySelector('.cart-total'); |
| | | this.totalTax = this.total.querySelector('.tax span'); |
| | | this.grandTotal = this.total.querySelector('.total span'); |
| | | }); |
| | | |
| | | this.T.define('childCartItem', { |
| | | refs: { |
| | | quantity: '.quantity', |
| | | label: 'label', |
| | | price: 'span.price', |
| | | button: '[data-remove-from-cart]' |
| | | }, |
| | | manyRefs: {}, |
| | | setup({el, refs, manyRefs, data}) { |
| | | [ |
| | | refs.quantity.dataset.id, |
| | | refs.quantity.dataset.price, |
| | | refs.quantity.dataset.catalogId, |
| | | refs.label.textContent, |
| | | refs.price.textContent, |
| | | refs.button.dataset.id |
| | | ] = [ |
| | | data.id, |
| | | data.price, |
| | | data.catalogId || '', |
| | | data.name, |
| | | window.formatPrice(data.price), |
| | | data.id |
| | | ]; |
| | | } |
| | | }); |
| | | this.T.define('emptyCart'); |
| | | } |
| | | initElements() { |
| | | this.restoreElement = false; |
| | | this.selectors = { |
| | | toggle: { |
| | | btn: '.toggle-cart', |
| | | count: '.toggle-cart .count', |
| | | icon: '.toggle-cart .icon', |
| | | }, |
| | | panels: { |
| | | checkout: { |
| | | toggle: 'button[data-tab="checkout"]', |
| | | form: '#cart form' |
| | | } |
| | | }, |
| | | cart: { |
| | | wrap: '#cart .cart-items', |
| | | empty: '#cart .cart-items .empty', |
| | | table: '#cart .cart-items table', |
| | | body: '#cart .cart-items tbody', |
| | | total: '#cart .cart-total', |
| | | foot: { |
| | | foot: '#cart tfoot', |
| | | total: '#cart tfoot .total', |
| | | tax: '#cart tfoot .tax', |
| | | }, |
| | | grandTotal: '#cart .total span', |
| | | tax: '#cart .cart-total .tax span', |
| | | } |
| | | }; |
| | | this.ui = window.uiFromSelectors(this.selectors); |
| | | this.ui.cart.items = new Map(); |
| | | |
| | | if (!this.isOpen) { |
| | | this.ui.toggle.btn.disabled = true; |
| | | this.ui.toggle.btn.title = 'Currently closed for online ordering'; |
| | | } |
| | | |
| | | this.checkoutForm = this.checkout.querySelector('form'); |
| | | |
| | | this.tabs = new window.jvbTabs(this.checkoutForm, { |
| | | this.tabs = window.jvbTabs.registerTab(this.checkoutForm, { |
| | | updateURL: false |
| | | }); |
| | | } |
| | |
| | | this.checkoutForm.addEventListener('submit', (e) => this.handleFormSubmit(e)); |
| | | document.addEventListener('click', this.clickHandler); |
| | | document.addEventListener('change', this.changeHandler); |
| | | // document.addEventListener('keydown', this.keyHandler); |
| | | } |
| | | |
| | | /***************************************************************** |
| | |
| | | if (window.targetCheck(e, 'button') && window.targetCheck(e, 'div.quantity')) { |
| | | let quantity = window.targetCheck(e, 'div.quantity'); |
| | | this.handleNumberClick(e, quantity); |
| | | } else if (window.targetCheck(e, '[data-add-to-cart]')) { |
| | | } else if (window.targetCheck(e, '[data-add-to-cart]') && window.targetCheck(e, 'button')) { |
| | | let add = window.targetCheck(e, '[data-add-to-cart]'); |
| | | this.handleAddToCart(add); |
| | | } else if (window.targetCheck(e, '[data-remove-from-cart]')) { |
| | |
| | | this.handleRemoveFromCart(remove); |
| | | } else if (window.targetCheck(e, '[data-clear-cart]')) { |
| | | this.clearCart(); |
| | | document.querySelector('.restored')?.remove(); |
| | | } else if (window.targetCheck(e, '[data-dismiss]')) { |
| | | window.targetCheck(e, '[data-dismiss]').closest('.restored')?.remove(); |
| | | } |
| | |
| | | let input = window.targetCheck(e, '.quantity-input'); |
| | | if (input) { |
| | | let item = e.target.closest('.quantity'); |
| | | let value = input.value; |
| | | |
| | | if (window.targetCheck(e, '.cart-items')) { |
| | | let main = document.querySelector(`.menu-section [data-id="${item.dataset.id}"] input`); |
| | | if (main) main.value = input.value; |
| | | } |
| | | let value = e.target.value; |
| | | |
| | | if (value > 0) { |
| | | this.handleAddToCart(item); |
| | |
| | | |
| | | handleEscape(e) { |
| | | if (e.key === 'Escape') { |
| | | if (this.restoreElement) { |
| | | this.restoreElement.classList.remove('show'); |
| | | this.restoreElement = false; |
| | | } |
| | | this.stepMultiplier = 1; |
| | | } else if (e.ctrlKey && e.shiftKey) { |
| | | this.stepMultiplier = Math.max(parseInt(this.stepMultiplier) * 100, 1000); |
| | |
| | | handleAddToCart(item) { |
| | | let id = item.dataset.id; |
| | | let price = parseFloat(item.dataset.price); |
| | | let quantity = parseInt(item.querySelector('.quantity-input')?.value) ?? 1; |
| | | let quantity = parseInt(item.querySelector('.quantity input')?.value) ?? 1; |
| | | let total = parseFloat(price * quantity); |
| | | |
| | | this.createItemElement(item); |
| | | |
| | | this.cartItems.set(id, { |
| | | let parent = this.determineParent(item); |
| | | |
| | | let stored = {}; |
| | | if (this.cartItems.has(id)) { |
| | | stored = this.cartItems.get(id); |
| | | } |
| | | let newValue = { |
| | | post_id: id, |
| | | name: item.dataset.name, |
| | | name: stored.name ?? item.dataset.name, |
| | | label: stored.label ?? parent.dataset.label ??'', |
| | | parentId: stored.parentId ?? parent.dataset.id ??'', |
| | | price: price, |
| | | quantity: quantity, |
| | | total: total, |
| | | catalog_id: item.dataset.catalogId || '', |
| | | }); |
| | | catalog_id: stored.catalogId ?? item.dataset.catalogId ?? '', |
| | | }; |
| | | this.cartItems.set(id, newValue); |
| | | this.saveCart(); |
| | | } |
| | | |
| | | handleRemoveFromCart(item) { |
| | | console.log(item); |
| | | if (confirm('This will remove this item from the cart. Continue?')) { |
| | | if (!item.querySelector('[data-id]')) { |
| | | item = item.closest('.item')?.querySelector('.quantity.field'); |
| | | let id = item.dataset.id??item.id??false; |
| | | if (!id) { |
| | | console.error('No ID found..'); |
| | | } |
| | | let id = item.dataset.id; |
| | | this.cartItems.delete(id); |
| | | this.table.querySelector(`[data-id="${id}"]`)?.closest('tr').remove(); |
| | | |
| | | let input = document.querySelector(`[data-id="${id}"] input`); |
| | | if (input) input.value = 0; |
| | | this.removeFromCart(id); |
| | | |
| | | this.maybeAddEmptyState(); |
| | | this.saveCart(); |
| | | } |
| | | } |
| | | removeFromCart(id) { |
| | | let parent = this.findParentItem(id); |
| | | if (parent) { |
| | | let el = parent.items.get(id); |
| | | el.el.remove(); |
| | | parent.items.delete(id); |
| | | if (el.reference){ |
| | | el.reference.value = 0; |
| | | } |
| | | if (parent.items.size === 0) { |
| | | console.log('Removing parent item'); |
| | | parent.el.remove(); |
| | | this.ui.cart.items.delete(parent.id); |
| | | } |
| | | } |
| | | |
| | | this.cartItems.delete(id); |
| | | } |
| | | |
| | | handleNumberClick(e, container) { |
| | | e.preventDefault(); |
| | |
| | | else if (e.target.closest('.decrease')) change -= 1; |
| | | |
| | | if (change !== 0) { |
| | | let step = parseInt(container.dataset.step); |
| | | let input = container.querySelector('input'); |
| | | let step = parseInt(input.step); |
| | | let value = (input.value === '') ? 0 : parseInt(input.value); |
| | | if (value === 0 && change < 0) return; |
| | | |
| | | input.value = value + (step * change * this.stepMultiplier); |
| | | let increaseBy = step * change * this.stepMultiplier; |
| | | |
| | | let newValue = value + increaseBy; |
| | | input.value = Math.max(newValue, 0); |
| | | input.dispatchEvent(new Event('change', { bubbles: true })); |
| | | this.handleNumberLimits(container); |
| | | } |
| | |
| | | let decrease = container.querySelector('.decrease'); |
| | | let value = parseInt(input.value); |
| | | |
| | | if (value <= min) { |
| | | if (min !== undefined && value <= min) { |
| | | console.log('Resetting to minimum value'); |
| | | input.value = min; |
| | | decrease.disabled = true; |
| | | } else if (value >= max) { |
| | | } else if (max !== undefined && value >= max) { |
| | | console.log('Resetting to maximum value'); |
| | | input.value = max; |
| | | increase.disabled = true; |
| | | } else { |
| | |
| | | *****************************************************************/ |
| | | |
| | | createItemElement(item) { |
| | | let element = this.itemsList.querySelector(`[data-id="${item.dataset.id}"]`); |
| | | let add = false; |
| | | let price = item.dataset.price; |
| | | let quantity = item.querySelector('[name="quantity"]')?.value ?? 1; |
| | | let element = this.maybeCreateCartItem(item); |
| | | |
| | | if (!element) { |
| | | add = true; |
| | | element = window.getTemplate('cartItem'); |
| | | let field = element.querySelector('.quantity'); |
| | | [ |
| | | field.dataset.id, |
| | | element.querySelector('label').textContent, |
| | | element.querySelector('.price').textContent, |
| | | field.dataset.price, |
| | | field.dataset.catalogId, |
| | | ] = [ |
| | | item.dataset.id, |
| | | item.dataset.name, |
| | | window.formatPrice(price), |
| | | price, |
| | | item.dataset.catalogId || '', |
| | | ]; |
| | | let quantity = item.tagName === undefined ? |
| | | item.quantity : |
| | | item.querySelector('input').value ?? 1; |
| | | let price = item.tagName === undefined ? |
| | | item.price : |
| | | item.dataset.price; |
| | | |
| | | element.quantity.value = quantity; |
| | | element.total.textContent = window.formatPrice(quantity * price); |
| | | if (element.reference) { |
| | | element.reference.value = quantity; |
| | | } |
| | | } |
| | | maybeCreateCartItem(item) { |
| | | let normalized = {}; |
| | | if (item.tagName === undefined) { |
| | | //We're getting it from the cached cart |
| | | normalized = { |
| | | id: String(item.post_id), |
| | | name: item.name, |
| | | price: item.price, |
| | | catalogId: item.catalog_id || '', |
| | | quantity: item.quantity, |
| | | label: item.label, |
| | | parentId: String(item.parentId) |
| | | }; |
| | | } else if (item.closest('#cart')) { |
| | | // quantity field clicked from inside the cart itself — item already exists |
| | | let id = item.dataset.id; |
| | | let parent = this.findParentItem(id); |
| | | if (parent) { |
| | | return parent.items.get(id); |
| | | } |
| | | // fallback: shouldn't normally happen, but avoid crashing |
| | | console.warn('Cart quantity changed but no existing row found for id:', id); |
| | | } else { |
| | | element = element.closest('tr'); |
| | | //We're getting it direct from the quantity field |
| | | normalized = { |
| | | id: item.dataset.id, |
| | | name: item.dataset.name, |
| | | price: item.dataset.price, |
| | | catalogId: item.dataset.catalogId || '', |
| | | } |
| | | let parent = this.determineParent(item); |
| | | normalized.parentId = parent.dataset.id??parent.id; |
| | | normalized.label = parent.dataset.label??parent.label; |
| | | } |
| | | |
| | | [ |
| | | element.querySelector('[name="quantity"]').value, |
| | | element.querySelector('.total').textContent, |
| | | ] = [ |
| | | quantity, |
| | | window.formatPrice(quantity * price), |
| | | ]; |
| | | if (!this.ui.cart.items.has(normalized.parentId)) { |
| | | let parentEl = this.T.create('mainCartItem', normalized); |
| | | this.ui.cart.items.set(normalized.parentId, { |
| | | el: parentEl, |
| | | id: normalized.parentId, |
| | | label: normalized.label, |
| | | items: new Map() |
| | | }); |
| | | this.ui.cart.table.insertBefore(parentEl, this.ui.cart.foot.foot); |
| | | } |
| | | |
| | | if (add) { |
| | | element.classList.add('adding'); |
| | | this.table.append(element); |
| | | setTimeout(() => element.classList.remove('adding'), 500); |
| | | let parent = this.ui.cart.items.get(normalized.parentId); |
| | | if (!parent.items.has(normalized.id)) { |
| | | let el = this.T.create('childCartItem', normalized); |
| | | parent.items.set(normalized.id, { |
| | | el: el, |
| | | quantity: el.querySelector('[name="quantity"]'), |
| | | total: el.querySelector('.total'), |
| | | reference: document.querySelector(`.menu-items [data-id="${normalized.id}"] input`), |
| | | }); |
| | | parent.el.append(el); |
| | | } |
| | | |
| | | return parent.items.get(normalized.id); |
| | | } |
| | | determineParent(item) { |
| | | if (item.tagName !== undefined) { |
| | | return this.parentFromHTML(item); |
| | | } |
| | | } |
| | | parentFromHTML(item) { |
| | | let parent = item.closest('.menu-item'); |
| | | if (parent) return parent; |
| | | let id = item.dataset.id; |
| | | return this.findParentItem(id).el??false; |
| | | } |
| | | findParentItem(searchId) { |
| | | let parentItem = false; |
| | | for (let [id, data] of this.ui.cart.items.entries()) { |
| | | if (data.items.has(searchId)) { |
| | | parentItem = this.ui.cart.items.get(id); |
| | | } |
| | | } |
| | | return parentItem; |
| | | } |
| | | |
| | | |
| | | /***************************************************************** |
| | | * CART STATE |
| | | *****************************************************************/ |
| | | |
| | | toggleButton(open = false) |
| | | { |
| | | this.ui.toggle.icon.hidden = open; |
| | | } |
| | | maybeAddEmptyState() { |
| | | let empty = this.itemsList.querySelector('.empty'); |
| | | if (empty) empty.remove(); |
| | | this.toggleButton(true); |
| | | this.findEmpty(); |
| | | |
| | | if (this.cartItems.size === 0) { |
| | | this.checkoutPanel.disabled = true; |
| | | this.checkoutPanel.title = 'Add some things to your cart first!'; |
| | | let emptyEl = window.getTemplate('emptyCart'); |
| | | this.itemsList.append(emptyEl); |
| | | this.table.closest('table').hidden = true; |
| | | this.total.hidden = true; |
| | | this.ui.panels.checkout.toggle.disabled = true; |
| | | this.ui.panels.checkout.toggle.title = 'Add some things to your cart first!'; |
| | | this.ui.cart.empty.hidden = false; |
| | | this.ui.cart.table.hidden = true; |
| | | this.ui.cart.total.hidden = true; |
| | | this.a11y.announce('Nothing in Cart'); |
| | | } else { |
| | | this.checkoutPanel.disabled = false; |
| | | this.table.closest('table').hidden = false; |
| | | this.total.hidden = false; |
| | | this.checkoutPanel.title = 'Checkout'; |
| | | this.ui.cart.empty.hidden = true; |
| | | this.ui.panels.checkout.toggle.disabled = false; |
| | | this.ui.cart.table.hidden = false; |
| | | this.ui.cart.total.hidden = false; |
| | | this.ui.panels.checkout.toggle.title = 'Checkout'; |
| | | } |
| | | } |
| | | findEmpty() { |
| | | if (!this.ui.cart.empty) { |
| | | this.ui.cart.empty = this.T.create('emptyCart'); |
| | | this.ui.cart.wrap.append(this.ui.cart.empty); |
| | | } |
| | | } |
| | | |
| | | notifyRestoredCart() { |
| | | let restored = window.getTemplate('restoredCart'); |
| | | this.checkout.querySelector('.tab-content[data-tab=cartItems]').insertBefore(restored, this.itemsList); |
| | | document.body.append(restored); |
| | | restored.classList.toggle('show'); |
| | | this.restoreElement = restored; |
| | | |
| | | this.cartItems.forEach(item => { |
| | | let element = window.getTemplate('cartItem'); |
| | | let field = element.querySelector('.quantity'); |
| | | [ |
| | | field.dataset.id, |
| | | element.querySelector('label').textContent, |
| | | element.querySelector('.price').textContent, |
| | | field.dataset.price, |
| | | field.dataset.catalogId, |
| | | element.querySelector('[name="quantity"]').value, |
| | | element.querySelector('.total').textContent, |
| | | ] = [ |
| | | item.post_id, |
| | | item.name, |
| | | window.formatPrice(item.price), |
| | | item.price, |
| | | item.catalog_id || '', |
| | | item.quantity, |
| | | window.formatPrice(item.quantity * item.price), |
| | | ]; |
| | | this.table.append(element); |
| | | this.createItemElement(item); |
| | | }); |
| | | this.updateTotal(); |
| | | } |
| | | |
| | | updateToggle() { |
| | | if (this.cartItems.size > 0 && this.ui.toggle.btn.classList.contains('empty')) { |
| | | this.ui.toggle.btn.classList.remove('empty'); |
| | | } |
| | | if (this.cartItems.size === 0 && !this.ui.toggle.btn.classList.contains('empty')) { |
| | | this.ui.toggle.btn.classList.add('empty'); |
| | | } |
| | | let total = 0; |
| | | for (let [id, item] of this.cartItems.entries()) { |
| | | total += item.quantity; |
| | | } |
| | | |
| | | this.ui.toggle.count.textContent = this.cartItems.size === 0 ? '' : total; |
| | | } |
| | | |
| | | /***************************************************************** |
| | | * TOTALS |
| | | *****************************************************************/ |
| | |
| | | let total = 0; |
| | | this.cartItems.forEach(item => total += item.total); |
| | | let tax = total * 0.05; |
| | | window.eraseText(this.totalTax); |
| | | window.eraseText(this.grandTotal); |
| | | window.typeText(this.totalTax, window.formatPrice(tax)); |
| | | window.typeText(this.grandTotal, window.formatPrice(total + tax)); |
| | | this.totalTax.classList.remove('typeText'); |
| | | window.eraseText(this.ui.cart.tax); |
| | | window.eraseText(this.ui.cart.foot.tax); |
| | | window.eraseText(this.ui.cart.foot.total); |
| | | window.typeText(this.ui.cart.foot.tax, window.formatPrice(tax)); |
| | | window.typeText(this.ui.cart.grandTotal, window.formatPrice(total + tax)); |
| | | window.typeText(this.ui.cart.foot.total, window.formatPrice(total + tax)); |
| | | this.ui.cart.tax.classList.remove('typeText'); |
| | | } |
| | | |
| | | /***************************************************************** |