import { Controller } from '@hotwired/stimulus' import { csrfToken } from './csrf' // Drives the slide-in cart drawer. One instance, on the drawer root in // checkout/drawer.blade.php. // // - listens on window for `bbk-cart:changed` (from bbk-add-to-cart and from // this drawer's own line forms) and swaps in the server-rendered cart body // - handles the in-drawer quantity / remove forms (fetch + method spoofing) // - re-emits `bbk-cart:updated` {count, total} after every render so the host // (e.g. the header bag icon) can react // // Appearance is entirely CSS-driven: open state is the data-bbk-cart-state // attribute on the root, nothing here touches styles or class lists. export default class extends Controller { static targets = ['panel', 'body'] connect() { this.onChanged = this.onChanged.bind(this) this.onKeydown = this.onKeydown.bind(this) window.addEventListener('bbk-cart:changed', this.onChanged) window.addEventListener('bbk-cart:open', this.open.bind(this)) document.addEventListener('keydown', this.onKeydown) // Prime the host with the count rendered server-side on page load. this.emitUpdated(this.element.querySelector('[data-bbk-cart-count]')) } disconnect() { window.removeEventListener('bbk-cart:changed', this.onChanged) document.removeEventListener('keydown', this.onKeydown) } onChanged(event) { if (event.detail?.html) this.replaceBody(event.detail.html) this.open() } onKeydown(event) { if (event.key === 'Escape' && !this.element.hidden) this.close() } open() { if (!this.element.hidden) return this.element.hidden = false // Next frame, so the panel transitions from its off-canvas start. requestAnimationFrame(() => this.element.setAttribute('data-bbk-cart-state', 'open')) } close() { this.element.removeAttribute('data-bbk-cart-state') const panel = this.panelTarget const done = () => { this.element.hidden = true panel.removeEventListener('transitionend', done) } panel.addEventListener('transitionend', done) } // change on a line quantity input, or submit of a line's remove form submit(event) { event.preventDefault() const form = event.target.closest('form') if (form) this.send(form) } // +/- stepper buttons inside a line step(event) { event.preventDefault() const form = event.target.closest('form') const input = form.querySelector('input[type="number"]') const next = Math.max(0, parseInt(input.value || '0', 10) + Number(event.params.dir)) input.value = String(next) this.send(form) } async send(form) { this.bodyTarget.setAttribute('aria-busy', 'true') try { const response = await fetch(form.action, { method: 'POST', headers: { 'X-CSRF-TOKEN': csrfToken(), 'X-Requested-With': 'XMLHttpRequest', }, body: new FormData(form), }) if (response.ok) this.replaceBody(await response.text()) } finally { this.bodyTarget.removeAttribute('aria-busy') } } replaceBody(html) { this.bodyTarget.innerHTML = html this.emitUpdated(this.bodyTarget.querySelector('[data-bbk-cart-count]')) } emitUpdated(node) { if (!node) return window.dispatchEvent(new CustomEvent('bbk-cart:updated', { detail: { count: parseInt(node.dataset.bbkCartCount || '0', 10), total: parseInt(node.dataset.bbkCartTotal || '0', 10), }, })) } }