@magic-spells/cart-panel 0.3.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +269 -521
- package/dist/cart-panel.cjs.css +216 -193
- package/dist/cart-panel.cjs.js +692 -508
- package/dist/cart-panel.cjs.js.map +1 -1
- package/dist/cart-panel.css +216 -193
- package/dist/cart-panel.esm.css +216 -193
- package/dist/cart-panel.esm.js +686 -500
- package/dist/cart-panel.esm.js.map +1 -1
- package/dist/cart-panel.js +1054 -1716
- package/dist/cart-panel.js.map +1 -1
- package/dist/cart-panel.min.css +1 -1
- package/dist/cart-panel.min.js +1 -1
- package/package.json +12 -8
- package/src/cart-item.js +448 -0
- package/src/cart-panel.css +231 -0
- package/src/cart-panel.js +258 -517
- package/dist/cart-panel.scss +0 -107
- package/src/cart-panel.scss +0 -107
package/dist/cart-panel.js
CHANGED
|
@@ -1,1725 +1,1063 @@
|
|
|
1
1
|
(function (global, factory) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
3
|
+
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
4
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.CartPanel = {}));
|
|
5
5
|
})(this, (function (exports) { 'use strict';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
super();
|
|
25
|
-
this.trapStart = null;
|
|
26
|
-
this.trapEnd = null;
|
|
27
|
-
|
|
28
|
-
// Inject styles only once, when the first FocusTrap instance is created.
|
|
29
|
-
if (!FocusTrap.styleInjected) {
|
|
30
|
-
this.injectStyles();
|
|
31
|
-
FocusTrap.styleInjected = true;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Injects necessary styles for the focus trap into the document's head.
|
|
37
|
-
* This ensures that focus-trap-start and focus-trap-end elements are hidden.
|
|
38
|
-
*/
|
|
39
|
-
injectStyles() {
|
|
40
|
-
const style = document.createElement('style');
|
|
41
|
-
style.textContent = `
|
|
42
|
-
focus-trap-start,
|
|
43
|
-
focus-trap-end {
|
|
44
|
-
position: absolute;
|
|
45
|
-
width: 1px;
|
|
46
|
-
height: 1px;
|
|
47
|
-
margin: -1px;
|
|
48
|
-
padding: 0;
|
|
49
|
-
border: 0;
|
|
50
|
-
clip: rect(0, 0, 0, 0);
|
|
51
|
-
overflow: hidden;
|
|
52
|
-
white-space: nowrap;
|
|
7
|
+
class EventEmitter {
|
|
8
|
+
#events;
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
this.#events = new Map();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Binds a listener to an event.
|
|
16
|
+
* @param {string} event - The event to bind the listener to.
|
|
17
|
+
* @param {Function} listener - The listener function to bind.
|
|
18
|
+
* @returns {EventEmitter} The current instance for chaining.
|
|
19
|
+
* @throws {TypeError} If the listener is not a function.
|
|
20
|
+
*/
|
|
21
|
+
on(event, listener) {
|
|
22
|
+
if (typeof listener !== "function") {
|
|
23
|
+
throw new TypeError("Listener must be a function");
|
|
53
24
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// exit if there aren't any
|
|
83
|
-
if (focusableElements.length === 0) return;
|
|
84
|
-
|
|
85
|
-
// create trap start and end elements
|
|
86
|
-
this.trapStart = document.createElement('focus-trap-start');
|
|
87
|
-
this.trapEnd = document.createElement('focus-trap-end');
|
|
88
|
-
|
|
89
|
-
// add to DOM
|
|
90
|
-
this.prepend(this.trapStart);
|
|
91
|
-
this.append(this.trapEnd);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Handles the keydown event. If the Escape key is pressed, the focus trap is exited.
|
|
96
|
-
*
|
|
97
|
-
* @param {KeyboardEvent} e - The keyboard event object.
|
|
98
|
-
*/
|
|
99
|
-
handleKeyDown = (e) => {
|
|
100
|
-
if (e.key === 'Escape') {
|
|
101
|
-
e.preventDefault();
|
|
102
|
-
this.exitTrap();
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Exits the focus trap by hiding the current container and shifting focus
|
|
108
|
-
* back to the trigger element that opened the trap.
|
|
109
|
-
*/
|
|
110
|
-
exitTrap() {
|
|
111
|
-
const container = this.closest('[aria-hidden="false"]');
|
|
112
|
-
if (!container) return;
|
|
113
|
-
|
|
114
|
-
container.setAttribute('aria-hidden', 'true');
|
|
115
|
-
|
|
116
|
-
const trigger = document.querySelector(
|
|
117
|
-
`[aria-expanded="true"][aria-controls="${container.id}"]`
|
|
118
|
-
);
|
|
119
|
-
if (trigger) {
|
|
120
|
-
trigger.setAttribute('aria-expanded', 'false');
|
|
121
|
-
trigger.focus();
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
class FocusTrapStart extends HTMLElement {
|
|
127
|
-
/**
|
|
128
|
-
* Called when the element is connected to the DOM.
|
|
129
|
-
* Sets the tabindex and adds the focus event listener.
|
|
130
|
-
*/
|
|
131
|
-
connectedCallback() {
|
|
132
|
-
this.setAttribute('tabindex', '0');
|
|
133
|
-
this.addEventListener('focus', this.handleFocus);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Called when the element is disconnected from the DOM.
|
|
138
|
-
* Removes the focus event listener.
|
|
139
|
-
*/
|
|
140
|
-
disconnectedCallback() {
|
|
141
|
-
this.removeEventListener('focus', this.handleFocus);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Handles the focus event. If focus moves backwards from the first focusable element,
|
|
146
|
-
* it is cycled to the last focusable element, and vice versa.
|
|
147
|
-
*
|
|
148
|
-
* @param {FocusEvent} e - The focus event object.
|
|
149
|
-
*/
|
|
150
|
-
handleFocus = (e) => {
|
|
151
|
-
const trap = this.closest('focus-trap');
|
|
152
|
-
const focusableElements = getFocusableElements(trap);
|
|
153
|
-
|
|
154
|
-
if (focusableElements.length === 0) return;
|
|
155
|
-
|
|
156
|
-
const firstElement = focusableElements[0];
|
|
157
|
-
const lastElement =
|
|
158
|
-
focusableElements[focusableElements.length - 1];
|
|
159
|
-
|
|
160
|
-
if (e.relatedTarget === firstElement) {
|
|
161
|
-
lastElement.focus();
|
|
162
|
-
} else {
|
|
163
|
-
firstElement.focus();
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
class FocusTrapEnd extends HTMLElement {
|
|
169
|
-
/**
|
|
170
|
-
* Called when the element is connected to the DOM.
|
|
171
|
-
* Sets the tabindex and adds the focus event listener.
|
|
172
|
-
*/
|
|
173
|
-
connectedCallback() {
|
|
174
|
-
this.setAttribute('tabindex', '0');
|
|
175
|
-
this.addEventListener('focus', this.handleFocus);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Called when the element is disconnected from the DOM.
|
|
180
|
-
* Removes the focus event listener.
|
|
181
|
-
*/
|
|
182
|
-
disconnectedCallback() {
|
|
183
|
-
this.removeEventListener('focus', this.handleFocus);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Handles the focus event. When the trap end is focused, focus is shifted back to the trap start.
|
|
188
|
-
*/
|
|
189
|
-
handleFocus = () => {
|
|
190
|
-
const trap = this.closest('focus-trap');
|
|
191
|
-
const trapStart = trap.querySelector('focus-trap-start');
|
|
192
|
-
trapStart.focus();
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (!customElements.get('focus-trap')) {
|
|
197
|
-
customElements.define('focus-trap', FocusTrap);
|
|
198
|
-
}
|
|
199
|
-
if (!customElements.get('focus-trap-start')) {
|
|
200
|
-
customElements.define('focus-trap-start', FocusTrapStart);
|
|
201
|
-
}
|
|
202
|
-
if (!customElements.get('focus-trap-end')) {
|
|
203
|
-
customElements.define('focus-trap-end', FocusTrapEnd);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
class EventEmitter {
|
|
207
|
-
#events;
|
|
208
|
-
|
|
209
|
-
constructor() {
|
|
210
|
-
this.#events = new Map();
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Binds a listener to an event.
|
|
215
|
-
* @param {string} event - The event to bind the listener to.
|
|
216
|
-
* @param {Function} listener - The listener function to bind.
|
|
217
|
-
* @returns {EventEmitter} The current instance for chaining.
|
|
218
|
-
* @throws {TypeError} If the listener is not a function.
|
|
219
|
-
*/
|
|
220
|
-
on(event, listener) {
|
|
221
|
-
if (typeof listener !== "function") {
|
|
222
|
-
throw new TypeError("Listener must be a function");
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const listeners = this.#events.get(event) || [];
|
|
226
|
-
if (!listeners.includes(listener)) {
|
|
227
|
-
listeners.push(listener);
|
|
228
|
-
}
|
|
229
|
-
this.#events.set(event, listeners);
|
|
230
|
-
|
|
231
|
-
return this;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Unbinds a listener from an event.
|
|
236
|
-
* @param {string} event - The event to unbind the listener from.
|
|
237
|
-
* @param {Function} listener - The listener function to unbind.
|
|
238
|
-
* @returns {EventEmitter} The current instance for chaining.
|
|
239
|
-
*/
|
|
240
|
-
off(event, listener) {
|
|
241
|
-
const listeners = this.#events.get(event);
|
|
242
|
-
if (!listeners) return this;
|
|
243
|
-
|
|
244
|
-
const index = listeners.indexOf(listener);
|
|
245
|
-
if (index !== -1) {
|
|
246
|
-
listeners.splice(index, 1);
|
|
247
|
-
if (listeners.length === 0) {
|
|
248
|
-
this.#events.delete(event);
|
|
249
|
-
} else {
|
|
250
|
-
this.#events.set(event, listeners);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return this;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Triggers an event and calls all bound listeners.
|
|
259
|
-
* @param {string} event - The event to trigger.
|
|
260
|
-
* @param {...*} args - Arguments to pass to the listener functions.
|
|
261
|
-
* @returns {boolean} True if the event had listeners, false otherwise.
|
|
262
|
-
*/
|
|
263
|
-
emit(event, ...args) {
|
|
264
|
-
const listeners = this.#events.get(event);
|
|
265
|
-
if (!listeners || listeners.length === 0) return false;
|
|
266
|
-
|
|
267
|
-
for (let i = 0, n = listeners.length; i < n; ++i) {
|
|
268
|
-
try {
|
|
269
|
-
listeners[i].apply(this, args);
|
|
270
|
-
} catch (error) {
|
|
271
|
-
console.error(`Error in listener for event '${event}':`, error);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return true;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Removes all listeners for a specific event or all events.
|
|
280
|
-
* @param {string} [event] - The event to remove listeners from. If not provided, removes all listeners.
|
|
281
|
-
* @returns {EventEmitter} The current instance for chaining.
|
|
282
|
-
*/
|
|
283
|
-
removeAllListeners(event) {
|
|
284
|
-
if (event) {
|
|
285
|
-
this.#events.delete(event);
|
|
286
|
-
} else {
|
|
287
|
-
this.#events.clear();
|
|
288
|
-
}
|
|
289
|
-
return this;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
class QuantityModifier extends HTMLElement {
|
|
294
|
-
// Static flag to track if styles have been injected
|
|
295
|
-
static #stylesInjected = false;
|
|
296
|
-
|
|
297
|
-
constructor() {
|
|
298
|
-
super();
|
|
299
|
-
this.handleDecrement = this.handleDecrement.bind(this);
|
|
300
|
-
this.handleIncrement = this.handleIncrement.bind(this);
|
|
301
|
-
this.handleInputChange = this.handleInputChange.bind(this);
|
|
302
|
-
|
|
303
|
-
// Inject styles once when first component is created
|
|
304
|
-
QuantityModifier.#injectStyles();
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Inject global styles for hiding number input spin buttons
|
|
309
|
-
* Only runs once regardless of how many components exist
|
|
310
|
-
*/
|
|
311
|
-
static #injectStyles() {
|
|
312
|
-
if (QuantityModifier.#stylesInjected) return;
|
|
313
|
-
|
|
314
|
-
// this will hide the arrow buttons in the number input field
|
|
315
|
-
const style = document.createElement('style');
|
|
316
|
-
style.textContent = `
|
|
317
|
-
/* Hide number input spin buttons for quantity-modifier */
|
|
318
|
-
quantity-modifier input::-webkit-outer-spin-button,
|
|
319
|
-
quantity-modifier input::-webkit-inner-spin-button {
|
|
320
|
-
-webkit-appearance: none;
|
|
321
|
-
margin: 0;
|
|
25
|
+
|
|
26
|
+
const listeners = this.#events.get(event) || [];
|
|
27
|
+
if (!listeners.includes(listener)) {
|
|
28
|
+
listeners.push(listener);
|
|
29
|
+
}
|
|
30
|
+
this.#events.set(event, listeners);
|
|
31
|
+
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Unbinds a listener from an event.
|
|
37
|
+
* @param {string} event - The event to unbind the listener from.
|
|
38
|
+
* @param {Function} listener - The listener function to unbind.
|
|
39
|
+
* @returns {EventEmitter} The current instance for chaining.
|
|
40
|
+
*/
|
|
41
|
+
off(event, listener) {
|
|
42
|
+
const listeners = this.#events.get(event);
|
|
43
|
+
if (!listeners) return this;
|
|
44
|
+
|
|
45
|
+
const index = listeners.indexOf(listener);
|
|
46
|
+
if (index !== -1) {
|
|
47
|
+
listeners.splice(index, 1);
|
|
48
|
+
if (listeners.length === 0) {
|
|
49
|
+
this.#events.delete(event);
|
|
50
|
+
} else {
|
|
51
|
+
this.#events.set(event, listeners);
|
|
52
|
+
}
|
|
322
53
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
54
|
+
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Triggers an event and calls all bound listeners.
|
|
60
|
+
* @param {string} event - The event to trigger.
|
|
61
|
+
* @param {...*} args - Arguments to pass to the listener functions.
|
|
62
|
+
* @returns {boolean} True if the event had listeners, false otherwise.
|
|
63
|
+
*/
|
|
64
|
+
emit(event, ...args) {
|
|
65
|
+
const listeners = this.#events.get(event);
|
|
66
|
+
if (!listeners || listeners.length === 0) return false;
|
|
67
|
+
|
|
68
|
+
for (let i = 0, n = listeners.length; i < n; ++i) {
|
|
69
|
+
try {
|
|
70
|
+
listeners[i].apply(this, args);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(`Error in listener for event '${event}':`, error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Removes all listeners for a specific event or all events.
|
|
81
|
+
* @param {string} [event] - The event to remove listeners from. If not provided, removes all listeners.
|
|
82
|
+
* @returns {EventEmitter} The current instance for chaining.
|
|
83
|
+
*/
|
|
84
|
+
removeAllListeners(event) {
|
|
85
|
+
if (event) {
|
|
86
|
+
this.#events.delete(event);
|
|
87
|
+
} else {
|
|
88
|
+
this.#events.clear();
|
|
326
89
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
// Check visible item count for showing/hiding sections
|
|
1299
|
-
const visibleItems = this.#getVisibleCartItems(cartData);
|
|
1300
|
-
const hasVisibleItems = visibleItems.length > 0;
|
|
1301
|
-
|
|
1302
|
-
// Show/hide sections based on visible item count
|
|
1303
|
-
if (hasVisibleItems) {
|
|
1304
|
-
hasItemsSection.style.display = '';
|
|
1305
|
-
emptySection.style.display = 'none';
|
|
1306
|
-
} else {
|
|
1307
|
-
hasItemsSection.style.display = 'none';
|
|
1308
|
-
emptySection.style.display = '';
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
// Update cart count and subtotal across the site
|
|
1312
|
-
this.#renderCartCount(cartData);
|
|
1313
|
-
this.#renderCartSubtotal(cartData);
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
/**
|
|
1317
|
-
* Fetch current cart data from server
|
|
1318
|
-
* @returns {Promise<Object>} Cart data object
|
|
1319
|
-
*/
|
|
1320
|
-
getCart() {
|
|
1321
|
-
return fetch('/cart.json', {
|
|
1322
|
-
crossDomain: true,
|
|
1323
|
-
credentials: 'same-origin',
|
|
1324
|
-
})
|
|
1325
|
-
.then((response) => {
|
|
1326
|
-
if (!response.ok) {
|
|
1327
|
-
throw Error(response.statusText);
|
|
1328
|
-
}
|
|
1329
|
-
return response.json();
|
|
1330
|
-
})
|
|
1331
|
-
.catch((error) => {
|
|
1332
|
-
console.error('Error fetching cart:', error);
|
|
1333
|
-
return { error: true, message: error.message };
|
|
1334
|
-
});
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
/**
|
|
1338
|
-
* Update cart item quantity on server
|
|
1339
|
-
* @param {string|number} key - Cart item key/ID
|
|
1340
|
-
* @param {number} quantity - New quantity (0 to remove)
|
|
1341
|
-
* @returns {Promise<Object>} Updated cart data object
|
|
1342
|
-
*/
|
|
1343
|
-
updateCartItem(key, quantity) {
|
|
1344
|
-
return fetch('/cart/change.json', {
|
|
1345
|
-
crossDomain: true,
|
|
1346
|
-
method: 'POST',
|
|
1347
|
-
credentials: 'same-origin',
|
|
1348
|
-
body: JSON.stringify({ id: key, quantity: quantity }),
|
|
1349
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1350
|
-
})
|
|
1351
|
-
.then((response) => {
|
|
1352
|
-
if (!response.ok) {
|
|
1353
|
-
throw Error(response.statusText);
|
|
1354
|
-
}
|
|
1355
|
-
return response.json();
|
|
1356
|
-
})
|
|
1357
|
-
.catch((error) => {
|
|
1358
|
-
console.error('Error updating cart item:', error);
|
|
1359
|
-
return { error: true, message: error.message };
|
|
1360
|
-
});
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
/**
|
|
1364
|
-
* Refresh cart data from server and update components
|
|
1365
|
-
* @param {Object} [cartObj=null] - Optional cart object to use instead of fetching
|
|
1366
|
-
* @returns {Promise<Object>} Cart data object
|
|
1367
|
-
*/
|
|
1368
|
-
refreshCart(cartObj = null) {
|
|
1369
|
-
// If cart object is provided, use it directly
|
|
1370
|
-
if (cartObj && !cartObj.error) {
|
|
1371
|
-
// console.log('Using provided cart data:', cartObj);
|
|
1372
|
-
this.#currentCart = cartObj;
|
|
1373
|
-
this.#renderCartItems(cartObj);
|
|
1374
|
-
this.#renderCartPanel(cartObj);
|
|
1375
|
-
|
|
1376
|
-
// Emit cart refreshed and data changed events
|
|
1377
|
-
const cartWithCalculatedFields = this.#addCalculatedFields(cartObj);
|
|
1378
|
-
this.#emit('cart-dialog:refreshed', { cart: cartWithCalculatedFields });
|
|
1379
|
-
this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
|
|
1380
|
-
|
|
1381
|
-
return Promise.resolve(cartObj);
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
// Otherwise fetch from server
|
|
1385
|
-
return this.getCart().then((cartData) => {
|
|
1386
|
-
// console.log('Cart data received:', cartData);
|
|
1387
|
-
if (cartData && !cartData.error) {
|
|
1388
|
-
this.#currentCart = cartData;
|
|
1389
|
-
this.#renderCartItems(cartData);
|
|
1390
|
-
this.#renderCartPanel(cartData);
|
|
1391
|
-
|
|
1392
|
-
// Emit cart refreshed and data changed events
|
|
1393
|
-
const cartWithCalculatedFields = this.#addCalculatedFields(cartData);
|
|
1394
|
-
this.#emit('cart-dialog:refreshed', { cart: cartWithCalculatedFields });
|
|
1395
|
-
this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
|
|
1396
|
-
} else {
|
|
1397
|
-
console.warn('Cart data has error or is null:', cartData);
|
|
1398
|
-
}
|
|
1399
|
-
return cartData;
|
|
1400
|
-
});
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
/**
|
|
1404
|
-
* Remove items from DOM that are no longer in cart data
|
|
1405
|
-
* @private
|
|
1406
|
-
*/
|
|
1407
|
-
#removeItemsFromDOM(itemsContainer, newKeysSet) {
|
|
1408
|
-
const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
|
|
1409
|
-
|
|
1410
|
-
const itemsToRemove = currentItems.filter((item) => !newKeysSet.has(item.getAttribute('key')));
|
|
1411
|
-
|
|
1412
|
-
itemsToRemove.forEach((item) => {
|
|
1413
|
-
console.log('destroy yourself', item);
|
|
1414
|
-
item.destroyYourself();
|
|
1415
|
-
});
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
/**
|
|
1419
|
-
* Update existing cart-item elements with fresh cart data
|
|
1420
|
-
* @private
|
|
1421
|
-
*/
|
|
1422
|
-
#updateItemsInDOM(itemsContainer, cartData) {
|
|
1423
|
-
const visibleItems = this.#getVisibleCartItems(cartData);
|
|
1424
|
-
const existingItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
|
|
1425
|
-
|
|
1426
|
-
existingItems.forEach((cartItemEl) => {
|
|
1427
|
-
const key = cartItemEl.getAttribute('key');
|
|
1428
|
-
const updatedItemData = visibleItems.find((item) => (item.key || item.id) === key);
|
|
1429
|
-
|
|
1430
|
-
if (updatedItemData) {
|
|
1431
|
-
// Update cart-item with fresh data and full cart context
|
|
1432
|
-
// The cart-item will handle HTML comparison and only re-render if needed
|
|
1433
|
-
cartItemEl.setData(updatedItemData, cartData);
|
|
1434
|
-
}
|
|
1435
|
-
});
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
/**
|
|
1439
|
-
* Add new items to DOM with animation delay
|
|
1440
|
-
* @private
|
|
1441
|
-
*/
|
|
1442
|
-
#addItemsToDOM(itemsContainer, itemsToAdd, newKeys) {
|
|
1443
|
-
// Delay adding new items by 300ms to let cart slide open first
|
|
1444
|
-
setTimeout(() => {
|
|
1445
|
-
itemsToAdd.forEach((itemData) => {
|
|
1446
|
-
const cartItem = CartItem.createAnimated(itemData);
|
|
1447
|
-
const targetIndex = newKeys.indexOf(itemData.key || itemData.id);
|
|
1448
|
-
|
|
1449
|
-
// Find the correct position to insert the new item
|
|
1450
|
-
if (targetIndex === 0) {
|
|
1451
|
-
// Insert at the beginning
|
|
1452
|
-
itemsContainer.insertBefore(cartItem, itemsContainer.firstChild);
|
|
1453
|
-
} else {
|
|
1454
|
-
// Find the item that should come before this one
|
|
1455
|
-
let insertAfter = null;
|
|
1456
|
-
for (let i = targetIndex - 1; i >= 0; i--) {
|
|
1457
|
-
const prevKey = newKeys[i];
|
|
1458
|
-
const prevItem = itemsContainer.querySelector(`cart-item[key="${prevKey}"]`);
|
|
1459
|
-
if (prevItem) {
|
|
1460
|
-
insertAfter = prevItem;
|
|
1461
|
-
break;
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
if (insertAfter) {
|
|
1466
|
-
insertAfter.insertAdjacentElement('afterend', cartItem);
|
|
1467
|
-
} else {
|
|
1468
|
-
itemsContainer.appendChild(cartItem);
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
});
|
|
1472
|
-
}, 100);
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
/**
|
|
1476
|
-
* Filter cart items to exclude those with _hide_in_cart property
|
|
1477
|
-
* @private
|
|
1478
|
-
*/
|
|
1479
|
-
#getVisibleCartItems(cartData) {
|
|
1480
|
-
if (!cartData || !cartData.items) return [];
|
|
1481
|
-
return cartData.items.filter((item) => {
|
|
1482
|
-
// Check for _hide_in_cart in various possible locations
|
|
1483
|
-
const hidden = item.properties?._hide_in_cart;
|
|
1484
|
-
|
|
1485
|
-
return !hidden;
|
|
1486
|
-
});
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
/**
|
|
1490
|
-
* Add calculated fields to cart object for events
|
|
1491
|
-
* @private
|
|
1492
|
-
*/
|
|
1493
|
-
#addCalculatedFields(cartData) {
|
|
1494
|
-
if (!cartData) return cartData;
|
|
1495
|
-
|
|
1496
|
-
// For display counts: use visible items (excludes _hide_in_cart)
|
|
1497
|
-
const visibleItems = this.#getVisibleCartItems(cartData);
|
|
1498
|
-
const calculated_count = visibleItems.reduce((total, item) => total + item.quantity, 0);
|
|
1499
|
-
|
|
1500
|
-
// For pricing: use all items except those marked to ignore pricing
|
|
1501
|
-
const pricedItems = cartData.items.filter((item) => {
|
|
1502
|
-
const ignorePrice = item.properties?._ignore_price_in_subtotal;
|
|
1503
|
-
return !ignorePrice;
|
|
1504
|
-
});
|
|
1505
|
-
const calculated_subtotal = pricedItems.reduce(
|
|
1506
|
-
(total, item) => total + (item.line_price || 0),
|
|
1507
|
-
0
|
|
1508
|
-
);
|
|
1509
|
-
|
|
1510
|
-
return {
|
|
1511
|
-
...cartData,
|
|
1512
|
-
calculated_count,
|
|
1513
|
-
calculated_subtotal,
|
|
1514
|
-
};
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
/**
|
|
1518
|
-
* Render cart items from Shopify cart data with smart comparison
|
|
1519
|
-
* @private
|
|
1520
|
-
*/
|
|
1521
|
-
#renderCartItems(cartData) {
|
|
1522
|
-
const itemsContainer = this.querySelector('[data-content-cart-items]');
|
|
1523
|
-
|
|
1524
|
-
if (!itemsContainer || !cartData || !cartData.items) {
|
|
1525
|
-
console.warn('Cannot render cart items:', {
|
|
1526
|
-
itemsContainer: !!itemsContainer,
|
|
1527
|
-
cartData: !!cartData,
|
|
1528
|
-
items: cartData?.items?.length,
|
|
1529
|
-
});
|
|
1530
|
-
return;
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
// Filter out items with _hide_in_cart property
|
|
1534
|
-
const visibleItems = this.#getVisibleCartItems(cartData);
|
|
1535
|
-
|
|
1536
|
-
// Handle initial render - load all items without animation
|
|
1537
|
-
if (this.#isInitialRender) {
|
|
1538
|
-
// console.log('Initial cart render:', visibleItems.length, 'visible items');
|
|
1539
|
-
|
|
1540
|
-
// Clear existing items
|
|
1541
|
-
itemsContainer.innerHTML = '';
|
|
1542
|
-
|
|
1543
|
-
// Create cart-item elements without animation
|
|
1544
|
-
visibleItems.forEach((itemData) => {
|
|
1545
|
-
const cartItem = new CartItem(itemData); // No animation
|
|
1546
|
-
itemsContainer.appendChild(cartItem);
|
|
1547
|
-
});
|
|
1548
|
-
|
|
1549
|
-
this.#isInitialRender = false;
|
|
1550
|
-
|
|
1551
|
-
return;
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
// Get current DOM items and their keys
|
|
1555
|
-
const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
|
|
1556
|
-
const currentKeys = new Set(currentItems.map((item) => item.getAttribute('key')));
|
|
1557
|
-
|
|
1558
|
-
// Get new cart data keys in order (only visible items)
|
|
1559
|
-
const newKeys = visibleItems.map((item) => item.key || item.id);
|
|
1560
|
-
const newKeysSet = new Set(newKeys);
|
|
1561
|
-
|
|
1562
|
-
// Step 1: Remove items that are no longer in cart data
|
|
1563
|
-
this.#removeItemsFromDOM(itemsContainer, newKeysSet);
|
|
1564
|
-
|
|
1565
|
-
// Step 2: Update existing items with fresh data (handles templates, bundles, etc.)
|
|
1566
|
-
this.#updateItemsInDOM(itemsContainer, cartData);
|
|
1567
|
-
|
|
1568
|
-
// Step 3: Add new items that weren't in DOM (with animation delay)
|
|
1569
|
-
const itemsToAdd = visibleItems.filter(
|
|
1570
|
-
(itemData) => !currentKeys.has(itemData.key || itemData.id)
|
|
1571
|
-
);
|
|
1572
|
-
this.#addItemsToDOM(itemsContainer, itemsToAdd, newKeys);
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
/**
|
|
1576
|
-
* Set the template function for cart items
|
|
1577
|
-
* @param {Function} templateFn - Function that takes item data and returns HTML string
|
|
1578
|
-
*/
|
|
1579
|
-
setCartItemTemplate(templateName, templateFn) {
|
|
1580
|
-
CartItem.setTemplate(templateName, templateFn);
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
/**
|
|
1584
|
-
* Set the processing template function for cart items
|
|
1585
|
-
* @param {Function} templateFn - Function that returns HTML string for processing state
|
|
1586
|
-
*/
|
|
1587
|
-
setCartItemProcessingTemplate(templateFn) {
|
|
1588
|
-
CartItem.setProcessingTemplate(templateFn);
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
/**
|
|
1592
|
-
* Shows the cart dialog and traps focus within it
|
|
1593
|
-
* @param {HTMLElement} [triggerEl=null] - The element that triggered the cart dialog
|
|
1594
|
-
* @fires CartDialog#show - Fired when the cart dialog has been shown
|
|
1595
|
-
*/
|
|
1596
|
-
show(triggerEl = null, cartObj) {
|
|
1597
|
-
const _ = this;
|
|
1598
|
-
_.triggerEl = triggerEl || false;
|
|
1599
|
-
|
|
1600
|
-
// Lock body scrolling
|
|
1601
|
-
_.#lockScroll();
|
|
1602
|
-
|
|
1603
|
-
// Remove the hidden class first to ensure content is rendered
|
|
1604
|
-
_.contentPanel.classList.remove('hidden');
|
|
1605
|
-
|
|
1606
|
-
// Give the browser a moment to process before starting animation
|
|
1607
|
-
requestAnimationFrame(() => {
|
|
1608
|
-
// Update ARIA states
|
|
1609
|
-
_.setAttribute('aria-hidden', 'false');
|
|
1610
|
-
|
|
1611
|
-
if (_.triggerEl) {
|
|
1612
|
-
_.triggerEl.setAttribute('aria-expanded', 'true');
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
// Focus management
|
|
1616
|
-
const firstFocusable = _.querySelector(
|
|
1617
|
-
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
1618
|
-
);
|
|
1619
|
-
|
|
1620
|
-
if (firstFocusable) {
|
|
1621
|
-
requestAnimationFrame(() => {
|
|
1622
|
-
firstFocusable.focus();
|
|
1623
|
-
});
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
// Refresh cart data when showing
|
|
1627
|
-
_.refreshCart(cartObj);
|
|
1628
|
-
|
|
1629
|
-
// Emit show event - cart dialog is now visible
|
|
1630
|
-
_.#emit('cart-dialog:show', { triggerElement: _.triggerEl });
|
|
1631
|
-
});
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
/**
|
|
1635
|
-
* Hides the cart dialog and restores focus
|
|
1636
|
-
* @fires CartDialog#hide - Fired when the cart dialog has started hiding (transition begins)
|
|
1637
|
-
* @fires CartDialog#afterHide - Fired when the cart dialog has completed its hide transition
|
|
1638
|
-
*/
|
|
1639
|
-
hide() {
|
|
1640
|
-
const _ = this;
|
|
1641
|
-
|
|
1642
|
-
// Update ARIA states
|
|
1643
|
-
if (_.triggerEl) {
|
|
1644
|
-
// remove focus from modal panel first
|
|
1645
|
-
_.triggerEl.focus();
|
|
1646
|
-
// mark trigger as no longer expanded
|
|
1647
|
-
_.triggerEl.setAttribute('aria-expanded', 'false');
|
|
1648
|
-
} else {
|
|
1649
|
-
// If no trigger element, blur any focused element inside the panel
|
|
1650
|
-
const activeElement = document.activeElement;
|
|
1651
|
-
if (activeElement && _.contains(activeElement)) {
|
|
1652
|
-
activeElement.blur();
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
requestAnimationFrame(() => {
|
|
1657
|
-
// Set aria-hidden to start transition
|
|
1658
|
-
// The transitionend event handler will add display:none when complete
|
|
1659
|
-
_.setAttribute('aria-hidden', 'true');
|
|
1660
|
-
|
|
1661
|
-
// Emit hide event - cart dialog is now starting to hide
|
|
1662
|
-
_.#emit('cart-dialog:hide', { triggerElement: _.triggerEl });
|
|
1663
|
-
|
|
1664
|
-
// Restore body scroll
|
|
1665
|
-
_.#restoreScroll();
|
|
1666
|
-
});
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
/**
|
|
1671
|
-
* Custom element that creates a clickable overlay for the cart dialog
|
|
1672
|
-
* @extends HTMLElement
|
|
1673
|
-
*/
|
|
1674
|
-
class CartOverlay extends HTMLElement {
|
|
1675
|
-
constructor() {
|
|
1676
|
-
super();
|
|
1677
|
-
this.setAttribute('tabindex', '-1');
|
|
1678
|
-
this.setAttribute('aria-hidden', 'true');
|
|
1679
|
-
this.cartDialog = this.closest('cart-dialog');
|
|
1680
|
-
this.#attachListeners();
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
#attachListeners() {
|
|
1684
|
-
this.addEventListener('click', () => {
|
|
1685
|
-
this.cartDialog.hide();
|
|
1686
|
-
});
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
/**
|
|
1691
|
-
* Custom element that wraps the content of the cart dialog
|
|
1692
|
-
* @extends HTMLElement
|
|
1693
|
-
*/
|
|
1694
|
-
class CartPanel extends HTMLElement {
|
|
1695
|
-
constructor() {
|
|
1696
|
-
super();
|
|
1697
|
-
this.setAttribute('role', 'document');
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
if (!customElements.get('cart-dialog')) {
|
|
1702
|
-
customElements.define('cart-dialog', CartDialog);
|
|
1703
|
-
}
|
|
1704
|
-
if (!customElements.get('cart-overlay')) {
|
|
1705
|
-
customElements.define('cart-overlay', CartOverlay);
|
|
1706
|
-
}
|
|
1707
|
-
if (!customElements.get('cart-panel')) {
|
|
1708
|
-
customElements.define('cart-panel', CartPanel);
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
// Make CartItem available globally for Shopify themes
|
|
1712
|
-
if (typeof window !== 'undefined') {
|
|
1713
|
-
window.CartItem = CartItem;
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
exports.CartDialog = CartDialog;
|
|
1717
|
-
exports.CartItem = CartItem;
|
|
1718
|
-
exports.CartOverlay = CartOverlay;
|
|
1719
|
-
exports.CartPanel = CartPanel;
|
|
1720
|
-
exports.default = CartDialog;
|
|
1721
|
-
|
|
1722
|
-
Object.defineProperty(exports, '__esModule', { value: true });
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// =============================================================================
|
|
95
|
+
// CartItem Component
|
|
96
|
+
// =============================================================================
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* CartItem class that handles the functionality of a cart item component
|
|
100
|
+
*/
|
|
101
|
+
class CartItem extends HTMLElement {
|
|
102
|
+
// Static template functions shared across all instances
|
|
103
|
+
static #templates = new Map();
|
|
104
|
+
static #processingTemplate = null;
|
|
105
|
+
|
|
106
|
+
// Private fields
|
|
107
|
+
#currentState = 'ready';
|
|
108
|
+
#isDestroying = false;
|
|
109
|
+
#isAppearing = false;
|
|
110
|
+
#handlers = {};
|
|
111
|
+
#itemData = null;
|
|
112
|
+
#cartData = null;
|
|
113
|
+
#lastRenderedHTML = '';
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Set the template function for rendering cart items
|
|
117
|
+
* @param {string} name - Template name ('default' for default template)
|
|
118
|
+
* @param {Function} templateFn - Function that takes (itemData, cartData) and returns HTML string
|
|
119
|
+
*/
|
|
120
|
+
static setTemplate(name, templateFn) {
|
|
121
|
+
if (typeof name !== 'string') {
|
|
122
|
+
throw new Error('Template name must be a string');
|
|
123
|
+
}
|
|
124
|
+
if (typeof templateFn !== 'function') {
|
|
125
|
+
throw new Error('Template must be a function');
|
|
126
|
+
}
|
|
127
|
+
CartItem.#templates.set(name, templateFn);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Set the processing template function for rendering processing overlay
|
|
132
|
+
* @param {Function} templateFn - Function that returns HTML string for processing state
|
|
133
|
+
*/
|
|
134
|
+
static setProcessingTemplate(templateFn) {
|
|
135
|
+
if (typeof templateFn !== 'function') {
|
|
136
|
+
throw new Error('Processing template must be a function');
|
|
137
|
+
}
|
|
138
|
+
CartItem.#processingTemplate = templateFn;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Create a cart item with appearing animation
|
|
143
|
+
* @param {Object} itemData - Shopify cart item data
|
|
144
|
+
* @param {Object} cartData - Full Shopify cart object
|
|
145
|
+
* @returns {CartItem} Cart item instance that will animate in
|
|
146
|
+
*/
|
|
147
|
+
static createAnimated(itemData, cartData) {
|
|
148
|
+
return new CartItem(itemData, cartData, { animate: true });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Define which attributes should be observed for changes
|
|
153
|
+
*/
|
|
154
|
+
static get observedAttributes() {
|
|
155
|
+
return ['state', 'key'];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Called when observed attributes change
|
|
160
|
+
*/
|
|
161
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
162
|
+
if (oldValue === newValue) return;
|
|
163
|
+
|
|
164
|
+
if (name === 'state') {
|
|
165
|
+
this.#currentState = newValue || 'ready';
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
constructor(itemData = null, cartData = null, options = {}) {
|
|
170
|
+
super();
|
|
171
|
+
|
|
172
|
+
// Store item and cart data if provided
|
|
173
|
+
this.#itemData = itemData;
|
|
174
|
+
this.#cartData = cartData;
|
|
175
|
+
|
|
176
|
+
// Set initial state - start with 'appearing' only if explicitly requested
|
|
177
|
+
const shouldAnimate = options.animate || this.hasAttribute('animate-in');
|
|
178
|
+
this.#currentState =
|
|
179
|
+
itemData && shouldAnimate ? 'appearing' : this.getAttribute('state') || 'ready';
|
|
180
|
+
|
|
181
|
+
// Bind event handlers
|
|
182
|
+
this.#handlers = {
|
|
183
|
+
click: this.#handleClick.bind(this),
|
|
184
|
+
change: this.#handleChange.bind(this),
|
|
185
|
+
transitionEnd: this.#handleTransitionEnd.bind(this),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
connectedCallback() {
|
|
190
|
+
const _ = this;
|
|
191
|
+
|
|
192
|
+
// If we have item data, render it first
|
|
193
|
+
if (_.#itemData) _.#render();
|
|
194
|
+
|
|
195
|
+
// Find child elements and attach listeners
|
|
196
|
+
_.#queryDOM();
|
|
197
|
+
_.#updateLinePriceElements();
|
|
198
|
+
_.#attachListeners();
|
|
199
|
+
|
|
200
|
+
// If we started with 'appearing' state, handle the entry animation
|
|
201
|
+
if (_.#currentState === 'appearing') {
|
|
202
|
+
_.setAttribute('state', 'appearing');
|
|
203
|
+
_.#isAppearing = true;
|
|
204
|
+
|
|
205
|
+
requestAnimationFrame(() => {
|
|
206
|
+
_.style.height = `${_.scrollHeight}px`;
|
|
207
|
+
requestAnimationFrame(() => _.setState('ready'));
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
disconnectedCallback() {
|
|
213
|
+
// Cleanup event listeners
|
|
214
|
+
this.#detachListeners();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Query and cache DOM elements
|
|
219
|
+
*/
|
|
220
|
+
#queryDOM() {
|
|
221
|
+
this.content = this.querySelector('cart-item-content');
|
|
222
|
+
this.processing = this.querySelector('cart-item-processing');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Attach event listeners
|
|
227
|
+
*/
|
|
228
|
+
#attachListeners() {
|
|
229
|
+
const _ = this;
|
|
230
|
+
_.addEventListener('click', _.#handlers.click);
|
|
231
|
+
_.addEventListener('change', _.#handlers.change);
|
|
232
|
+
_.addEventListener('quantity-input:change', _.#handlers.change);
|
|
233
|
+
_.addEventListener('transitionend', _.#handlers.transitionEnd);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Detach event listeners
|
|
238
|
+
*/
|
|
239
|
+
#detachListeners() {
|
|
240
|
+
const _ = this;
|
|
241
|
+
_.removeEventListener('click', _.#handlers.click);
|
|
242
|
+
_.removeEventListener('change', _.#handlers.change);
|
|
243
|
+
_.removeEventListener('quantity-input:change', _.#handlers.change);
|
|
244
|
+
_.removeEventListener('transitionend', _.#handlers.transitionEnd);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get the current state
|
|
249
|
+
*/
|
|
250
|
+
get state() {
|
|
251
|
+
return this.#currentState;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get the cart key for this item
|
|
256
|
+
*/
|
|
257
|
+
get cartKey() {
|
|
258
|
+
return this.getAttribute('key');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Handle click events (for Remove buttons, etc.)
|
|
263
|
+
*/
|
|
264
|
+
#handleClick(e) {
|
|
265
|
+
// Check if clicked element is a remove button
|
|
266
|
+
const removeButton = e.target.closest('[data-action-remove-item]');
|
|
267
|
+
if (removeButton) {
|
|
268
|
+
e.preventDefault();
|
|
269
|
+
this.#emitRemoveEvent();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Handle change events (for quantity inputs and quantity-input component)
|
|
275
|
+
*/
|
|
276
|
+
#handleChange(e) {
|
|
277
|
+
// Check if event is from quantity-input component
|
|
278
|
+
if (e.type === 'quantity-input:change') {
|
|
279
|
+
this.#emitQuantityChangeEvent(e.detail.value);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Check if changed element is a quantity input
|
|
284
|
+
const quantityInput = e.target.closest('[data-cart-quantity]');
|
|
285
|
+
if (quantityInput) {
|
|
286
|
+
this.#emitQuantityChangeEvent(quantityInput.value);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Handle transition end events for destroy animation and appearing animation
|
|
292
|
+
*/
|
|
293
|
+
#handleTransitionEnd(e) {
|
|
294
|
+
if (e.propertyName === 'height' && this.#isDestroying) {
|
|
295
|
+
// Remove from DOM after height animation completes
|
|
296
|
+
this.remove();
|
|
297
|
+
} else if (e.propertyName === 'height' && this.#isAppearing) {
|
|
298
|
+
// Remove explicit height after appearing animation completes
|
|
299
|
+
this.style.height = '';
|
|
300
|
+
this.#isAppearing = false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Emit remove event
|
|
306
|
+
*/
|
|
307
|
+
#emitRemoveEvent() {
|
|
308
|
+
this.dispatchEvent(
|
|
309
|
+
new CustomEvent('cart-item:remove', {
|
|
310
|
+
bubbles: true,
|
|
311
|
+
detail: {
|
|
312
|
+
cartKey: this.cartKey,
|
|
313
|
+
element: this,
|
|
314
|
+
},
|
|
315
|
+
})
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Emit quantity change event
|
|
321
|
+
*/
|
|
322
|
+
#emitQuantityChangeEvent(quantity) {
|
|
323
|
+
this.dispatchEvent(
|
|
324
|
+
new CustomEvent('cart-item:quantity-change', {
|
|
325
|
+
bubbles: true,
|
|
326
|
+
detail: {
|
|
327
|
+
cartKey: this.cartKey,
|
|
328
|
+
quantity: parseInt(quantity),
|
|
329
|
+
element: this,
|
|
330
|
+
},
|
|
331
|
+
})
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Render cart item from data using the appropriate template
|
|
337
|
+
*/
|
|
338
|
+
#render() {
|
|
339
|
+
const _ = this;
|
|
340
|
+
if (!_.#itemData || CartItem.#templates.size === 0) return;
|
|
341
|
+
|
|
342
|
+
// Set the key attribute from item data
|
|
343
|
+
const key = _.#itemData.key || _.#itemData.id;
|
|
344
|
+
if (key) _.setAttribute('key', key);
|
|
345
|
+
|
|
346
|
+
// Generate HTML from template and store for future comparisons
|
|
347
|
+
const templateHTML = _.#generateTemplateHTML();
|
|
348
|
+
_.#lastRenderedHTML = templateHTML;
|
|
349
|
+
|
|
350
|
+
// Generate processing HTML from template or use default
|
|
351
|
+
const processingHTML = CartItem.#processingTemplate
|
|
352
|
+
? CartItem.#processingTemplate()
|
|
353
|
+
: '<div class="cart-item-loader"></div>';
|
|
354
|
+
|
|
355
|
+
// Create the cart-item structure with template content inside cart-item-content
|
|
356
|
+
_.innerHTML = `
|
|
357
|
+
<cart-item-content>
|
|
358
|
+
${templateHTML}
|
|
359
|
+
</cart-item-content>
|
|
360
|
+
<cart-item-processing>
|
|
361
|
+
${processingHTML}
|
|
362
|
+
</cart-item-processing>
|
|
363
|
+
`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Update the cart item with new data
|
|
368
|
+
* @param {Object} itemData - Shopify cart item data
|
|
369
|
+
* @param {Object} cartData - Full Shopify cart object
|
|
370
|
+
*/
|
|
371
|
+
setData(itemData, cartData = null) {
|
|
372
|
+
const _ = this;
|
|
373
|
+
|
|
374
|
+
// Update internal data
|
|
375
|
+
_.#itemData = itemData;
|
|
376
|
+
if (cartData) _.#cartData = cartData;
|
|
377
|
+
|
|
378
|
+
// Generate new HTML with updated data
|
|
379
|
+
const newHTML = _.#generateTemplateHTML();
|
|
380
|
+
|
|
381
|
+
// Compare with previously rendered HTML
|
|
382
|
+
if (newHTML === _.#lastRenderedHTML) {
|
|
383
|
+
// HTML hasn't changed, just reset processing state
|
|
384
|
+
_.setState('ready');
|
|
385
|
+
_.#updateQuantityInput();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// HTML is different, proceed with full update
|
|
390
|
+
_.setState('ready');
|
|
391
|
+
_.#render();
|
|
392
|
+
_.#queryDOM();
|
|
393
|
+
_.#updateLinePriceElements();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Generate HTML from the current template with current data
|
|
398
|
+
* @returns {string} Generated HTML string or empty string if no template
|
|
399
|
+
* @private
|
|
400
|
+
*/
|
|
401
|
+
#generateTemplateHTML() {
|
|
402
|
+
// If no templates are available, return empty string
|
|
403
|
+
if (!this.#itemData || CartItem.#templates.size === 0) {
|
|
404
|
+
return '';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Determine which template to use
|
|
408
|
+
const templateName = this.#itemData.properties?._cart_template || 'default';
|
|
409
|
+
const templateFn = CartItem.#templates.get(templateName) || CartItem.#templates.get('default');
|
|
410
|
+
|
|
411
|
+
if (!templateFn) {
|
|
412
|
+
return '';
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Generate and return HTML from template
|
|
416
|
+
return templateFn(this.#itemData, this.#cartData);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Update quantity input component to match server data
|
|
421
|
+
* @private
|
|
422
|
+
*/
|
|
423
|
+
#updateQuantityInput() {
|
|
424
|
+
if (!this.#itemData) return;
|
|
425
|
+
|
|
426
|
+
const quantityInput = this.querySelector('quantity-input');
|
|
427
|
+
if (quantityInput) {
|
|
428
|
+
quantityInput.value = this.#itemData.quantity;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Update elements with data-content-line-price attribute
|
|
434
|
+
* @private
|
|
435
|
+
*/
|
|
436
|
+
#updateLinePriceElements() {
|
|
437
|
+
if (!this.#itemData) return;
|
|
438
|
+
|
|
439
|
+
const linePriceElements = this.querySelectorAll('[data-content-line-price]');
|
|
440
|
+
const formattedLinePrice = this.#formatCurrency(this.#itemData.line_price || 0);
|
|
441
|
+
|
|
442
|
+
linePriceElements.forEach((element) => {
|
|
443
|
+
element.textContent = formattedLinePrice;
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Format currency value from cents to dollar string
|
|
449
|
+
* @param {number} cents - Price in cents
|
|
450
|
+
* @returns {string} Formatted currency string (e.g., "$29.99")
|
|
451
|
+
* @private
|
|
452
|
+
*/
|
|
453
|
+
#formatCurrency(cents) {
|
|
454
|
+
if (typeof cents !== 'number') return '$0.00';
|
|
455
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Get the current item data
|
|
460
|
+
*/
|
|
461
|
+
get itemData() {
|
|
462
|
+
return this.#itemData;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Set the state of the cart item
|
|
467
|
+
* @param {string} state - 'ready', 'processing', 'destroying', or 'appearing'
|
|
468
|
+
*/
|
|
469
|
+
setState(state) {
|
|
470
|
+
if (['ready', 'processing', 'destroying', 'appearing'].includes(state)) {
|
|
471
|
+
this.setAttribute('state', state);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Gracefully animate this cart item closed, then remove it
|
|
477
|
+
*/
|
|
478
|
+
destroyYourself() {
|
|
479
|
+
const _ = this;
|
|
480
|
+
|
|
481
|
+
// bail if already in the middle of a destroy cycle
|
|
482
|
+
if (_.#isDestroying) return;
|
|
483
|
+
_.#isDestroying = true;
|
|
484
|
+
|
|
485
|
+
// snapshot the current rendered height before applying any "destroying" styles
|
|
486
|
+
const initialHeight = _.offsetHeight;
|
|
487
|
+
_.setState('destroying');
|
|
488
|
+
|
|
489
|
+
// lock the measured height on the next animation frame to ensure layout is fully flushed
|
|
490
|
+
requestAnimationFrame(() => {
|
|
491
|
+
_.style.height = `${initialHeight}px`;
|
|
492
|
+
|
|
493
|
+
// read the css custom property for timing, defaulting to 400ms
|
|
494
|
+
const destroyDuration =
|
|
495
|
+
getComputedStyle(_).getPropertyValue('--cart-item-destroying-duration')?.trim() || '400ms';
|
|
496
|
+
|
|
497
|
+
// animate only the height to zero; other properties stay under stylesheet control
|
|
498
|
+
_.style.transition = `height ${destroyDuration} ease`;
|
|
499
|
+
_.style.height = '0px';
|
|
500
|
+
|
|
501
|
+
setTimeout(() => _.remove(), 600);
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Supporting component classes for cart item
|
|
508
|
+
*/
|
|
509
|
+
class CartItemContent extends HTMLElement {
|
|
510
|
+
constructor() {
|
|
511
|
+
super();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
class CartItemProcessing extends HTMLElement {
|
|
516
|
+
constructor() {
|
|
517
|
+
super();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// =============================================================================
|
|
522
|
+
// Register Custom Elements
|
|
523
|
+
// =============================================================================
|
|
524
|
+
|
|
525
|
+
if (!customElements.get('cart-item')) {
|
|
526
|
+
customElements.define('cart-item', CartItem);
|
|
527
|
+
}
|
|
528
|
+
if (!customElements.get('cart-item-content')) {
|
|
529
|
+
customElements.define('cart-item-content', CartItemContent);
|
|
530
|
+
}
|
|
531
|
+
if (!customElements.get('cart-item-processing')) {
|
|
532
|
+
customElements.define('cart-item-processing', CartItemProcessing);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Make CartItem available globally for Shopify themes
|
|
536
|
+
if (typeof window !== 'undefined') {
|
|
537
|
+
window.CartItem = CartItem;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// =============================================================================
|
|
541
|
+
// CartPanel Component
|
|
542
|
+
// =============================================================================
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Shopping cart panel web component for Shopify.
|
|
546
|
+
* Manages cart data and AJAX requests, delegates modal behavior to dialog-panel.
|
|
547
|
+
* @extends HTMLElement
|
|
548
|
+
*/
|
|
549
|
+
class CartPanel extends HTMLElement {
|
|
550
|
+
#currentCart = null;
|
|
551
|
+
#eventEmitter;
|
|
552
|
+
#isInitialRender = true;
|
|
553
|
+
|
|
554
|
+
constructor() {
|
|
555
|
+
super();
|
|
556
|
+
this.#eventEmitter = new EventEmitter();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
connectedCallback() {
|
|
560
|
+
this.#attachListeners();
|
|
561
|
+
|
|
562
|
+
// Load cart data immediately unless manual mode is enabled
|
|
563
|
+
if (!this.hasAttribute('manual')) {
|
|
564
|
+
this.refreshCart();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
disconnectedCallback() {
|
|
569
|
+
// Clean up handled by garbage collection
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// =========================================================================
|
|
573
|
+
// Public API - Event Emitter
|
|
574
|
+
// =========================================================================
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Add an event listener
|
|
578
|
+
* @param {string} eventName - Name of the event
|
|
579
|
+
* @param {Function} callback - Callback function
|
|
580
|
+
* @returns {CartPanel} Returns this for method chaining
|
|
581
|
+
*/
|
|
582
|
+
on(eventName, callback) {
|
|
583
|
+
this.#eventEmitter.on(eventName, callback);
|
|
584
|
+
return this;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Remove an event listener
|
|
589
|
+
* @param {string} eventName - Name of the event
|
|
590
|
+
* @param {Function} callback - Callback function
|
|
591
|
+
* @returns {CartPanel} Returns this for method chaining
|
|
592
|
+
*/
|
|
593
|
+
off(eventName, callback) {
|
|
594
|
+
this.#eventEmitter.off(eventName, callback);
|
|
595
|
+
return this;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// =========================================================================
|
|
599
|
+
// Public API - Dialog Control
|
|
600
|
+
// =========================================================================
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Show the cart by finding and opening the nearest dialog-panel ancestor
|
|
604
|
+
* @param {HTMLElement} [triggerEl=null] - The element that triggered the open
|
|
605
|
+
* @param {Object} [cartObj=null] - Optional cart object to use instead of fetching
|
|
606
|
+
*/
|
|
607
|
+
show(triggerEl = null, cartObj = null) {
|
|
608
|
+
const _ = this;
|
|
609
|
+
const dialogPanel = _.#findDialogPanel();
|
|
610
|
+
|
|
611
|
+
if (dialogPanel) {
|
|
612
|
+
dialogPanel.show(triggerEl);
|
|
613
|
+
_.refreshCart(cartObj);
|
|
614
|
+
_.#emit('cart-panel:show', { triggerElement: triggerEl });
|
|
615
|
+
} else {
|
|
616
|
+
console.warn('cart-panel: No dialog-panel ancestor found. Cart panel is visible but not in a modal.');
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Hide the cart by finding and closing the nearest dialog-panel ancestor
|
|
622
|
+
*/
|
|
623
|
+
hide() {
|
|
624
|
+
const dialogPanel = this.#findDialogPanel();
|
|
625
|
+
if (dialogPanel) {
|
|
626
|
+
dialogPanel.hide();
|
|
627
|
+
this.#emit('cart-panel:hide', {});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// =========================================================================
|
|
632
|
+
// Public API - Cart Data
|
|
633
|
+
// =========================================================================
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Fetch current cart data from Shopify
|
|
637
|
+
* @returns {Promise<Object>} Cart data object
|
|
638
|
+
*/
|
|
639
|
+
getCart() {
|
|
640
|
+
return fetch('/cart.json', {
|
|
641
|
+
credentials: 'same-origin',
|
|
642
|
+
})
|
|
643
|
+
.then((response) => {
|
|
644
|
+
if (!response.ok) {
|
|
645
|
+
throw Error(response.statusText);
|
|
646
|
+
}
|
|
647
|
+
return response.json();
|
|
648
|
+
})
|
|
649
|
+
.catch((error) => {
|
|
650
|
+
console.error('Error fetching cart:', error);
|
|
651
|
+
return { error: true, message: error.message };
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Update cart item quantity on Shopify
|
|
657
|
+
* @param {string|number} key - Cart item key/ID
|
|
658
|
+
* @param {number} quantity - New quantity (0 to remove)
|
|
659
|
+
* @returns {Promise<Object>} Updated cart data object
|
|
660
|
+
*/
|
|
661
|
+
updateCartItem(key, quantity) {
|
|
662
|
+
return fetch('/cart/change.json', {
|
|
663
|
+
method: 'POST',
|
|
664
|
+
credentials: 'same-origin',
|
|
665
|
+
body: JSON.stringify({ id: key, quantity: quantity }),
|
|
666
|
+
headers: { 'Content-Type': 'application/json' },
|
|
667
|
+
})
|
|
668
|
+
.then((response) => {
|
|
669
|
+
if (!response.ok) {
|
|
670
|
+
throw Error(response.statusText);
|
|
671
|
+
}
|
|
672
|
+
return response.json();
|
|
673
|
+
})
|
|
674
|
+
.catch((error) => {
|
|
675
|
+
console.error('Error updating cart item:', error);
|
|
676
|
+
return { error: true, message: error.message };
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Refresh cart display - fetches from server if no cart object provided
|
|
682
|
+
* @param {Object} [cartObj=null] - Cart data object to render, or null to fetch
|
|
683
|
+
* @returns {Promise<Object>} Cart data object
|
|
684
|
+
*/
|
|
685
|
+
async refreshCart(cartObj = null) {
|
|
686
|
+
const _ = this;
|
|
687
|
+
|
|
688
|
+
// Fetch from server if no cart object provided
|
|
689
|
+
cartObj = cartObj || (await _.getCart());
|
|
690
|
+
if (!cartObj || cartObj.error) {
|
|
691
|
+
console.warn('Cart data has error or is null:', cartObj);
|
|
692
|
+
return cartObj;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
_.#currentCart = cartObj;
|
|
696
|
+
_.#renderCartItems(cartObj);
|
|
697
|
+
_.#renderCartPanel(cartObj);
|
|
698
|
+
|
|
699
|
+
const cartWithCalculatedFields = _.#addCalculatedFields(cartObj);
|
|
700
|
+
_.#emit('cart-panel:refreshed', { cart: cartWithCalculatedFields });
|
|
701
|
+
_.#emit('cart-panel:data-changed', cartWithCalculatedFields);
|
|
702
|
+
|
|
703
|
+
return cartObj;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// =========================================================================
|
|
707
|
+
// Public API - Templates
|
|
708
|
+
// =========================================================================
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Set the template function for cart items
|
|
712
|
+
* @param {string} templateName - Name of the template
|
|
713
|
+
* @param {Function} templateFn - Function that takes (itemData, cartData) and returns HTML string
|
|
714
|
+
*/
|
|
715
|
+
setCartItemTemplate(templateName, templateFn) {
|
|
716
|
+
CartItem.setTemplate(templateName, templateFn);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Set the processing template function for cart items
|
|
721
|
+
* @param {Function} templateFn - Function that returns HTML string for processing state
|
|
722
|
+
*/
|
|
723
|
+
setCartItemProcessingTemplate(templateFn) {
|
|
724
|
+
CartItem.setProcessingTemplate(templateFn);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// =========================================================================
|
|
728
|
+
// Private Methods - Core
|
|
729
|
+
// =========================================================================
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Find the nearest dialog-panel ancestor
|
|
733
|
+
* @private
|
|
734
|
+
*/
|
|
735
|
+
#findDialogPanel() {
|
|
736
|
+
return this.closest('dialog-panel');
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Emit an event via EventEmitter and native CustomEvent
|
|
741
|
+
* @private
|
|
742
|
+
*/
|
|
743
|
+
#emit(eventName, data = null) {
|
|
744
|
+
this.#eventEmitter.emit(eventName, data);
|
|
745
|
+
|
|
746
|
+
this.dispatchEvent(
|
|
747
|
+
new CustomEvent(eventName, {
|
|
748
|
+
detail: data,
|
|
749
|
+
bubbles: true,
|
|
750
|
+
})
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Attach event listeners
|
|
756
|
+
* @private
|
|
757
|
+
*/
|
|
758
|
+
#attachListeners() {
|
|
759
|
+
// Handle close buttons
|
|
760
|
+
this.addEventListener('click', (e) => {
|
|
761
|
+
if (!e.target.closest('[data-action-hide-cart]')) return;
|
|
762
|
+
this.hide();
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
// Handle cart item remove events
|
|
766
|
+
this.addEventListener('cart-item:remove', (e) => {
|
|
767
|
+
this.#handleCartItemRemove(e);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// Handle cart item quantity change events
|
|
771
|
+
this.addEventListener('cart-item:quantity-change', (e) => {
|
|
772
|
+
this.#handleCartItemQuantityChange(e);
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// =========================================================================
|
|
777
|
+
// Private Methods - Cart Item Event Handlers
|
|
778
|
+
// =========================================================================
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Handle cart item removal
|
|
782
|
+
* @private
|
|
783
|
+
*/
|
|
784
|
+
#handleCartItemRemove(e) {
|
|
785
|
+
const _ = this;
|
|
786
|
+
const { cartKey, element } = e.detail;
|
|
787
|
+
|
|
788
|
+
element.setState('processing');
|
|
789
|
+
|
|
790
|
+
_.updateCartItem(cartKey, 0)
|
|
791
|
+
.then((updatedCart) => {
|
|
792
|
+
if (updatedCart && !updatedCart.error) {
|
|
793
|
+
_.#currentCart = updatedCart;
|
|
794
|
+
_.#renderCartItems(updatedCart);
|
|
795
|
+
_.#renderCartPanel(updatedCart);
|
|
796
|
+
|
|
797
|
+
const cartWithCalculatedFields = _.#addCalculatedFields(updatedCart);
|
|
798
|
+
_.#emit('cart-panel:updated', { cart: cartWithCalculatedFields });
|
|
799
|
+
_.#emit('cart-panel:data-changed', cartWithCalculatedFields);
|
|
800
|
+
} else {
|
|
801
|
+
element.setState('ready');
|
|
802
|
+
console.error('Failed to remove cart item:', cartKey);
|
|
803
|
+
}
|
|
804
|
+
})
|
|
805
|
+
.catch((error) => {
|
|
806
|
+
element.setState('ready');
|
|
807
|
+
console.error('Error removing cart item:', error);
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Handle cart item quantity change
|
|
813
|
+
* @private
|
|
814
|
+
*/
|
|
815
|
+
#handleCartItemQuantityChange(e) {
|
|
816
|
+
const _ = this;
|
|
817
|
+
const { cartKey, quantity, element } = e.detail;
|
|
818
|
+
|
|
819
|
+
element.setState('processing');
|
|
820
|
+
|
|
821
|
+
_.updateCartItem(cartKey, quantity)
|
|
822
|
+
.then((updatedCart) => {
|
|
823
|
+
if (updatedCart && !updatedCart.error) {
|
|
824
|
+
_.#currentCart = updatedCart;
|
|
825
|
+
_.#renderCartItems(updatedCart);
|
|
826
|
+
_.#renderCartPanel(updatedCart);
|
|
827
|
+
|
|
828
|
+
const cartWithCalculatedFields = _.#addCalculatedFields(updatedCart);
|
|
829
|
+
_.#emit('cart-panel:updated', { cart: cartWithCalculatedFields });
|
|
830
|
+
_.#emit('cart-panel:data-changed', cartWithCalculatedFields);
|
|
831
|
+
} else {
|
|
832
|
+
element.setState('ready');
|
|
833
|
+
console.error('Failed to update cart item quantity:', cartKey, quantity);
|
|
834
|
+
}
|
|
835
|
+
})
|
|
836
|
+
.catch((error) => {
|
|
837
|
+
element.setState('ready');
|
|
838
|
+
console.error('Error updating cart item quantity:', error);
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// =========================================================================
|
|
843
|
+
// Private Methods - Rendering
|
|
844
|
+
// =========================================================================
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Update cart count elements across the page
|
|
848
|
+
* @private
|
|
849
|
+
*/
|
|
850
|
+
#renderCartCount(cartData) {
|
|
851
|
+
if (!cartData) return;
|
|
852
|
+
|
|
853
|
+
const visibleItems = this.#getVisibleCartItems(cartData);
|
|
854
|
+
const visibleItemCount = visibleItems.reduce((total, item) => total + item.quantity, 0);
|
|
855
|
+
|
|
856
|
+
const cartCountElements = document.querySelectorAll('[data-content-cart-count]');
|
|
857
|
+
cartCountElements.forEach((element) => {
|
|
858
|
+
element.textContent = visibleItemCount;
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Update cart subtotal elements across the page
|
|
864
|
+
* @private
|
|
865
|
+
*/
|
|
866
|
+
#renderCartSubtotal(cartData) {
|
|
867
|
+
if (!cartData) return;
|
|
868
|
+
|
|
869
|
+
const pricedItems = cartData.items.filter((item) => {
|
|
870
|
+
const ignorePrice = item.properties?._ignore_price_in_subtotal;
|
|
871
|
+
return !ignorePrice;
|
|
872
|
+
});
|
|
873
|
+
const subtotal = pricedItems.reduce((total, item) => total + (item.line_price || 0), 0);
|
|
874
|
+
|
|
875
|
+
const cartSubtotalElements = document.querySelectorAll('[data-content-cart-subtotal]');
|
|
876
|
+
cartSubtotalElements.forEach((element) => {
|
|
877
|
+
const formatted = (subtotal / 100).toFixed(2);
|
|
878
|
+
element.textContent = `$${formatted}`;
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Update cart panel sections (has-items/empty)
|
|
884
|
+
* @private
|
|
885
|
+
*/
|
|
886
|
+
#renderCartPanel(cart = null) {
|
|
887
|
+
const _ = this;
|
|
888
|
+
const cartData = cart || _.#currentCart;
|
|
889
|
+
if (!cartData) return;
|
|
890
|
+
|
|
891
|
+
const visibleItems = _.#getVisibleCartItems(cartData);
|
|
892
|
+
const hasVisibleItems = visibleItems.length > 0;
|
|
893
|
+
|
|
894
|
+
// Set state attribute for CSS styling (e.g., Tailwind variants)
|
|
895
|
+
_.setAttribute('state', hasVisibleItems ? 'has-items' : 'empty');
|
|
896
|
+
|
|
897
|
+
const hasItemsSection = _.querySelector('[data-cart-has-items]');
|
|
898
|
+
const emptySection = _.querySelector('[data-cart-is-empty]');
|
|
899
|
+
|
|
900
|
+
if (hasItemsSection && emptySection) {
|
|
901
|
+
hasItemsSection.style.display = hasVisibleItems ? '' : 'none';
|
|
902
|
+
emptySection.style.display = hasVisibleItems ? 'none' : '';
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
_.#renderCartCount(cartData);
|
|
906
|
+
_.#renderCartSubtotal(cartData);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Render cart items with smart add/update/remove
|
|
911
|
+
* @private
|
|
912
|
+
*/
|
|
913
|
+
#renderCartItems(cartData) {
|
|
914
|
+
const _ = this;
|
|
915
|
+
const itemsContainer = _.querySelector('[data-content-cart-items]');
|
|
916
|
+
|
|
917
|
+
if (!itemsContainer || !cartData || !cartData.items) return;
|
|
918
|
+
|
|
919
|
+
const visibleItems = _.#getVisibleCartItems(cartData);
|
|
920
|
+
|
|
921
|
+
// Initial render - load all items without animation
|
|
922
|
+
if (_.#isInitialRender) {
|
|
923
|
+
itemsContainer.innerHTML = '';
|
|
924
|
+
visibleItems.forEach((itemData) => {
|
|
925
|
+
itemsContainer.appendChild(new CartItem(itemData, cartData));
|
|
926
|
+
});
|
|
927
|
+
_.#isInitialRender = false;
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Get current DOM items
|
|
932
|
+
const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
|
|
933
|
+
const currentKeys = new Set(currentItems.map((item) => item.getAttribute('key')));
|
|
934
|
+
|
|
935
|
+
// Get new cart data keys
|
|
936
|
+
const newKeys = visibleItems.map((item) => item.key || item.id);
|
|
937
|
+
const newKeysSet = new Set(newKeys);
|
|
938
|
+
|
|
939
|
+
// Step 1: Remove items no longer in cart
|
|
940
|
+
_.#removeItemsFromDOM(itemsContainer, newKeysSet);
|
|
941
|
+
|
|
942
|
+
// Step 2: Update existing items
|
|
943
|
+
_.#updateItemsInDOM(itemsContainer, cartData);
|
|
944
|
+
|
|
945
|
+
// Step 3: Add new items with animation
|
|
946
|
+
const itemsToAdd = visibleItems.filter(
|
|
947
|
+
(itemData) => !currentKeys.has(itemData.key || itemData.id)
|
|
948
|
+
);
|
|
949
|
+
_.#addItemsToDOM(itemsContainer, itemsToAdd, newKeys, cartData);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Remove items from DOM that are no longer in cart
|
|
954
|
+
* @private
|
|
955
|
+
*/
|
|
956
|
+
#removeItemsFromDOM(itemsContainer, newKeysSet) {
|
|
957
|
+
const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
|
|
958
|
+
const itemsToRemove = currentItems.filter((item) => !newKeysSet.has(item.getAttribute('key')));
|
|
959
|
+
|
|
960
|
+
itemsToRemove.forEach((item) => {
|
|
961
|
+
item.destroyYourself();
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Update existing cart-item elements with fresh data
|
|
967
|
+
* @private
|
|
968
|
+
*/
|
|
969
|
+
#updateItemsInDOM(itemsContainer, cartData) {
|
|
970
|
+
const visibleItems = this.#getVisibleCartItems(cartData);
|
|
971
|
+
const existingItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
|
|
972
|
+
|
|
973
|
+
existingItems.forEach((cartItemEl) => {
|
|
974
|
+
const key = cartItemEl.getAttribute('key');
|
|
975
|
+
const updatedItemData = visibleItems.find((item) => (item.key || item.id) === key);
|
|
976
|
+
if (updatedItemData) cartItemEl.setData(updatedItemData, cartData);
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Add new items to DOM with animation delay
|
|
982
|
+
* @private
|
|
983
|
+
*/
|
|
984
|
+
#addItemsToDOM(itemsContainer, itemsToAdd, newKeys, cartData) {
|
|
985
|
+
setTimeout(() => {
|
|
986
|
+
itemsToAdd.forEach((itemData) => {
|
|
987
|
+
const cartItem = CartItem.createAnimated(itemData, cartData);
|
|
988
|
+
const targetIndex = newKeys.indexOf(itemData.key || itemData.id);
|
|
989
|
+
|
|
990
|
+
if (targetIndex === 0) {
|
|
991
|
+
itemsContainer.insertBefore(cartItem, itemsContainer.firstChild);
|
|
992
|
+
} else {
|
|
993
|
+
let insertAfter = null;
|
|
994
|
+
for (let i = targetIndex - 1; i >= 0; i--) {
|
|
995
|
+
const prevKey = newKeys[i];
|
|
996
|
+
const prevItem = itemsContainer.querySelector(`cart-item[key="${prevKey}"]`);
|
|
997
|
+
if (prevItem) {
|
|
998
|
+
insertAfter = prevItem;
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (insertAfter) {
|
|
1004
|
+
insertAfter.insertAdjacentElement('afterend', cartItem);
|
|
1005
|
+
} else {
|
|
1006
|
+
itemsContainer.appendChild(cartItem);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
}, 100);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// =========================================================================
|
|
1014
|
+
// Private Methods - Helpers
|
|
1015
|
+
// =========================================================================
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Filter cart items to exclude hidden items
|
|
1019
|
+
* @private
|
|
1020
|
+
*/
|
|
1021
|
+
#getVisibleCartItems(cartData) {
|
|
1022
|
+
if (!cartData || !cartData.items) return [];
|
|
1023
|
+
return cartData.items.filter((item) => {
|
|
1024
|
+
const hidden = item.properties?._hide_in_cart;
|
|
1025
|
+
return !hidden;
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Add calculated fields to cart object
|
|
1031
|
+
* @private
|
|
1032
|
+
*/
|
|
1033
|
+
#addCalculatedFields(cartData) {
|
|
1034
|
+
if (!cartData) return cartData;
|
|
1035
|
+
|
|
1036
|
+
const visibleItems = this.#getVisibleCartItems(cartData);
|
|
1037
|
+
const calculated_count = visibleItems.reduce((total, item) => total + item.quantity, 0);
|
|
1038
|
+
|
|
1039
|
+
const pricedItems = cartData.items.filter((item) => !item.properties?._ignore_price_in_subtotal);
|
|
1040
|
+
const calculated_subtotal = pricedItems.reduce((total, item) => total + (item.line_price || 0), 0);
|
|
1041
|
+
|
|
1042
|
+
return { ...cartData, calculated_count, calculated_subtotal };
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// =============================================================================
|
|
1047
|
+
// Register Custom Elements
|
|
1048
|
+
// =============================================================================
|
|
1049
|
+
|
|
1050
|
+
if (!customElements.get('cart-panel')) {
|
|
1051
|
+
customElements.define('cart-panel', CartPanel);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
exports.CartItem = CartItem;
|
|
1055
|
+
exports.CartItemContent = CartItemContent;
|
|
1056
|
+
exports.CartItemProcessing = CartItemProcessing;
|
|
1057
|
+
exports.CartPanel = CartPanel;
|
|
1058
|
+
exports.default = CartPanel;
|
|
1059
|
+
|
|
1060
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
1723
1061
|
|
|
1724
1062
|
}));
|
|
1725
1063
|
//# sourceMappingURL=cart-panel.js.map
|