PageSourceSearch

https://karlsberg.de/wp-content/plugins/kb-shopify/assets/js/kb-cart.js?ver=1784034812

js karlsberg.de collected 2026-09-26 00:30:42 UTC 7,788 bytes, 238 lines download raw bytes

1/**
2 * kb-cart.js
3 *
4 * Owns the global cart store as `Alpine.store('kbCart')` so any
5 * element on the page (drawer, header button, ATC handlers, etc.)
6 * can bind to the same reactive state.
7 *
8 * Wire-up:
9 *   - We register the store via `alpine:init` AND, if Alpine has
10 *     already booted by the time this script runs (e.g. another
11 *     plugin or a builder loads its own Alpine first), immediately
12 *     on script load. Calling `Alpine.store('kbCart', …)` is safe
13 *     either way — but we MUST register before any of Alpine's
14 *     directive evaluations runs against the auto-injected cart
15 *     drawer markup, otherwise the drawer's bindings throw "store
16 *     undefined" errors that don't auto-recover.
17 *   - Hydration (`refresh()`) is fire-and-forget on init so the count
18 *     badge accurately reflects an existing cart cookie before the
19 *     user interacts.
20 *
21 * Endpoint configuration is injected via wp_localize_script() as
22 * `window.kbShopify` (see Assets::register_assets()).
23 */
24(function () {
25	'use strict';
26
27	var registered = false;
28
29	function cfg() {
30		return window.kbShopify || {};
31	}
32
33	function emptyCart() {
34		return {
35			id: '',
36			checkout_url: '',
37			total_quantity: 0,
38			currency_code: '',
39			cost: { subtotal: null, total: null, tax: null, duty: null },
40			lines: []
41		};
42	}
43
44	async function request(path, options) {
45		options = options || {};
46		var c = cfg();
47		var url = (c.restUrl || '/wp-json/kb-shopify/v1/').replace(/\/$/, '/') + path.replace(/^\//, '');
48		var headers = Object.assign(
49			{ 'Content-Type': 'application/json', 'Accept': 'application/json' },
50			options.headers || {}
51		);
52		if (c.nonce) headers['X-WP-Nonce'] = c.nonce;
53
54		var response = await fetch(url, {
55			method: options.method || 'GET',
56			headers: headers,
57			credentials: 'same-origin',
58			body: options.body ? JSON.stringify(options.body) : undefined
59		});
60
61		var json = null;
62		try { json = await response.json(); } catch (e) { /* tolerate empty body */ }
63
64		if (!response.ok) {
65			var err = new Error((json && json.message) || ('HTTP ' + response.status));
66			err.status = response.status;
67			err.code = json && json.code;
68			err.data = json && json.data;
69			throw err;
70		}
71
72		return json;
73	}
74
75	function buildStore() {
76		return {
77			// ── State ──────────────────────────────────────────────
78			id: '',
79			checkoutUrl: '',
80			totalQuantity: 0,
81			currencyCode: '',
82			cost: { subtotal: null, total: null, tax: null, duty: null },
83			lines: [],
84			isOpen: false,
85			isBusy: false,
86			errorMessage: '',
87			hydrated: false,
88
89			// ── Lifecycle ──────────────────────────────────────────
90			init: function () {
91				var self = this;
92
93				window.addEventListener('kb-shopify:add-to-cart', function (e) {
94					var d = e.detail || {};
95					if (!d.variantId) return;
96					self.add(d.variantId, d.quantity || 1, { open: true });
97				});
98
99				this.refresh();
100			},
101
102			// ── Mutations ──────────────────────────────────────────
103			refresh: async function () {
104				try {
105					var cart = await request('cart');
106					this.applyCart(cart);
107				} catch (e) {
108					// Silent: failed hydration just leaves the cart empty.
109					console.warn('[kb-shopify] cart refresh failed', e);
110				} finally {
111					this.hydrated = true;
112				}
113			},
114
115			add: async function (variantId, quantity, opts) {
116				if (!variantId) return;
117				opts = opts || {};
118				this.beginRequest();
119				try {
120					var cart = await request('cart/lines', {
121						method: 'POST',
122						body: { variant_id: variantId, quantity: Math.max(1, parseInt(quantity, 10) || 1) }
123					});
124					this.applyCart(cart);
125					if (opts.open) this.open();
126				} catch (e) {
127					this.errorMessage = e.message;
128				} finally {
129					this.endRequest();
130				}
131			},
132
133			updateQuantity: async function (lineId, quantity) {
134				if (!lineId) return;
135				quantity = Math.max(0, parseInt(quantity, 10) || 0);
136				this.beginRequest();
137				try {
138					var cart = await request('cart/lines/' + encodeURIComponent(lineId), {
139						method: 'PATCH',
140						body: { quantity: quantity }
141					});
142					this.applyCart(cart);
143				} catch (e) {
144					this.errorMessage = e.message;
145				} finally {
146					this.endRequest();
147				}
148			},
149
150			increment: function (line) {
151				if (!line) return;
152				// Respect Shopify's tracked stock when present. Null means
153				// inventory isn't being tracked for this variant, so allow
154				// the increment and let the server be the authority.
155				var cap = this.lineMaxQty(line);
156				if (cap !== null && line.quantity >= cap) return;
157				this.updateQuantity(line.id, line.quantity + 1);
158			},
159
160			lineMaxQty: function (line) {
161				var m = line && line.merchandise;
162				if (!m) return null;
163				if (typeof m.quantity_available !== 'number' || m.quantity_available <= 0) return null;
164				return m.quantity_available;
165			},
166
167			decrement: function (line) {
168				if (!line) return;
169				// Quantity = 0 server-side maps to "remove line".
170				this.updateQuantity(line.id, line.quantity - 1);
171			},
172
173			remove: async function (lineId) {
174				if (!lineId) return;
175				this.beginRequest();
176				try {
177					var cart = await request('cart/lines/' + encodeURIComponent(lineId), {
178						method: 'DELETE'
179					});
180					this.applyCart(cart);
181				} catch (e) {
182					this.errorMessage = e.message;
183				} finally {
184					this.endRequest();
185				}
186			},
187
188			// ── UI ────────────────────────────────────────────────
189			open:   function () { this.errorMessage = ''; this.isOpen = true; document.documentElement.classList.add('kb-cart-locked'); },
190			close:  function () { this.isOpen = false; document.documentElement.classList.remove('kb-cart-locked'); },
191			toggle: function () { this.isOpen ? this.close() : this.open(); },
192
193			// ── Helpers ────────────────────────────────────────────
194			applyCart: function (cart) {
195				if (!cart) return;
196				this.id             = cart.id || '';
197				this.checkoutUrl    = cart.checkout_url || '';
198				this.totalQuantity  = cart.total_quantity || 0;
199				this.currencyCode   = cart.currency_code || (cfg().currency || 'EUR');
200				this.cost           = cart.cost || emptyCart().cost;
201				this.lines          = Array.isArray(cart.lines) ? cart.lines : [];
202				this.errorMessage   = '';
203			},
204
205			beginRequest: function () { this.isBusy = true; this.errorMessage = ''; },
206			endRequest:   function () { this.isBusy = false; },
207
208			formatMoney: function (money) {
209				if (!money || typeof money.amount !== 'number') return '';
210				try {
211					return new Intl.NumberFormat(
212						(cfg().locale || 'en').replace('_', '-'),
213						{ style: 'currency', currency: money.currency_code || this.currencyCode || 'EUR' }
214					).format(money.amount);
215				} catch (e) {
216					return money.amount.toFixed(2) + ' ' + (money.currency_code || '');
217				}
218			}
219		};
220	}
221
222	function tryRegister() {
223		if (registered) return true;
224		if (!window.Alpine || typeof window.Alpine.store !== 'function') return false;
225		window.Alpine.store('kbCart', buildStore());
226		registered = true;
227		return true;
228	}
229
230	// Cover both orderings:
231	//   1. We loaded before Alpine — alpine:init fires later.
232	//   2. Alpine already booted (e.g. a builder loaded its own copy
233	//      first) — the listener never fires, so register now.
234	if (!tryRegister()) {
235		document.addEventListener('alpine:init', tryRegister);
236		document.addEventListener('alpine:initialized', tryRegister);
237	}
238})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.