@magic-spells/cart-panel 0.1.1 → 0.2.0
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 +381 -190
- package/dist/cart-panel.cjs.css +8 -12
- package/dist/cart-panel.cjs.js +338 -56
- package/dist/cart-panel.cjs.js.map +1 -1
- package/dist/cart-panel.css +8 -12
- package/dist/cart-panel.esm.css +8 -12
- package/dist/cart-panel.esm.js +335 -56
- package/dist/cart-panel.esm.js.map +1 -1
- package/dist/cart-panel.js +1637 -1067
- 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/dist/cart-panel.scss +2 -12
- package/package.json +3 -3
- package/src/cart-panel.js +333 -58
- package/src/cart-panel.scss +2 -12
package/dist/cart-panel.cjs.css
CHANGED
|
@@ -4,7 +4,6 @@ cart-item {
|
|
|
4
4
|
--cart-item-appearing-duration: 400ms;
|
|
5
5
|
--cart-item-shadow-color: rgba(0, 0, 0, 0.15);
|
|
6
6
|
--cart-item-shadow-color-strong: rgba(0, 0, 0, 0.5);
|
|
7
|
-
--cart-item-processing-bg: rgba(100, 100, 100, 0.2);
|
|
8
7
|
--cart-item-destroying-bg: rgba(0, 0, 0, 0.1);
|
|
9
8
|
--cart-item-processing-scale: 0.98;
|
|
10
9
|
--cart-item-destroying-scale: 0.85;
|
|
@@ -35,10 +34,14 @@ cart-item::after {
|
|
|
35
34
|
left: 0px;
|
|
36
35
|
transition: background-color var(--cart-item-processing-duration) ease;
|
|
37
36
|
}
|
|
37
|
+
cart-item[state=ready] {
|
|
38
|
+
transition: filter var(--cart-item-processing-duration) ease-out, background-color var(--cart-item-processing-duration) ease-out, box-shadow var(--cart-item-processing-duration) ease-out, height var(--cart-item-appearing-duration) ease-out;
|
|
39
|
+
}
|
|
38
40
|
cart-item[state=ready] cart-item-content {
|
|
39
41
|
transform: scale(1);
|
|
40
42
|
filter: blur(0px);
|
|
41
43
|
opacity: 1;
|
|
44
|
+
transition: transform var(--cart-item-appearing-duration) ease-out, filter var(--cart-item-appearing-duration) ease-out, opacity var(--cart-item-appearing-duration) ease-out;
|
|
42
45
|
}
|
|
43
46
|
cart-item[state=ready] cart-item-processing {
|
|
44
47
|
opacity: 0;
|
|
@@ -55,6 +58,7 @@ cart-item[state=processing] cart-item-content {
|
|
|
55
58
|
filter: blur(var(--cart-item-processing-blur));
|
|
56
59
|
opacity: 0.9;
|
|
57
60
|
pointer-events: none;
|
|
61
|
+
transition: transform var(--cart-item-processing-duration) ease-out, filter var(--cart-item-processing-duration) ease-out, opacity var(--cart-item-processing-duration) ease-out;
|
|
58
62
|
}
|
|
59
63
|
cart-item[state=processing] cart-item-processing {
|
|
60
64
|
opacity: 1;
|
|
@@ -62,7 +66,7 @@ cart-item[state=processing] cart-item-processing {
|
|
|
62
66
|
}
|
|
63
67
|
cart-item[state=destroying] {
|
|
64
68
|
background-color: var(--cart-item-destroying-bg);
|
|
65
|
-
box-shadow: inset 0px
|
|
69
|
+
box-shadow: inset 0px 2px 20px var(--cart-item-shadow-color-strong);
|
|
66
70
|
margin-top: 0px;
|
|
67
71
|
margin-bottom: 0px;
|
|
68
72
|
transition: filter var(--cart-item-destroying-duration) ease, background-color var(--cart-item-destroying-duration) ease, box-shadow var(--cart-item-destroying-duration) ease, margin var(--cart-item-destroying-duration) ease;
|
|
@@ -100,7 +104,6 @@ cart-item[state=appearing] cart-item-processing {
|
|
|
100
104
|
|
|
101
105
|
cart-item-content {
|
|
102
106
|
display: block;
|
|
103
|
-
transition: transform var(--cart-item-processing-duration) ease-out, filter var(--cart-item-processing-duration) ease-out, opacity var(--cart-item-processing-duration) ease-out;
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
cart-item-processing {
|
|
@@ -186,8 +189,7 @@ cart-panel {
|
|
|
186
189
|
top: 0;
|
|
187
190
|
right: 0;
|
|
188
191
|
width: var(--cart-panel-width);
|
|
189
|
-
height:
|
|
190
|
-
opacity: 0;
|
|
192
|
+
height: 100dvh;
|
|
191
193
|
transform: translateX(100%);
|
|
192
194
|
pointer-events: none;
|
|
193
195
|
z-index: var(--cart-panel-z-index);
|
|
@@ -195,7 +197,7 @@ cart-panel {
|
|
|
195
197
|
box-shadow: var(--cart-panel-shadow);
|
|
196
198
|
border-radius: var(--cart-panel-border-radius);
|
|
197
199
|
overflow: hidden;
|
|
198
|
-
transition:
|
|
200
|
+
transition: transform var(--cart-transition-duration) var(--cart-transition-timing);
|
|
199
201
|
}
|
|
200
202
|
cart-panel.hidden {
|
|
201
203
|
display: none;
|
|
@@ -203,10 +205,4 @@ cart-panel.hidden {
|
|
|
203
205
|
|
|
204
206
|
body.overflow-hidden {
|
|
205
207
|
overflow: hidden;
|
|
206
|
-
position: fixed;
|
|
207
|
-
width: 100%;
|
|
208
|
-
height: 100%;
|
|
209
|
-
left: 0;
|
|
210
|
-
right: 0;
|
|
211
|
-
margin: 0;
|
|
212
208
|
}
|
package/dist/cart-panel.cjs.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
-
require('@magic-spells/cart-item');
|
|
5
|
+
var cartItem = require('@magic-spells/cart-item');
|
|
6
6
|
require('@magic-spells/focus-trap');
|
|
7
7
|
var EventEmitter = require('@magic-spells/event-emitter');
|
|
8
8
|
|
|
@@ -12,9 +12,9 @@ var EventEmitter = require('@magic-spells/event-emitter');
|
|
|
12
12
|
*/
|
|
13
13
|
class CartDialog extends HTMLElement {
|
|
14
14
|
#handleTransitionEnd;
|
|
15
|
-
#scrollPosition = 0;
|
|
16
15
|
#currentCart = null;
|
|
17
16
|
#eventEmitter;
|
|
17
|
+
#isInitialRender = true;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Clean up event listeners when component is removed from DOM
|
|
@@ -34,31 +34,21 @@ class CartDialog extends HTMLElement {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
37
|
+
* Locks body scrolling
|
|
38
38
|
* @private
|
|
39
39
|
*/
|
|
40
40
|
#lockScroll() {
|
|
41
|
-
|
|
42
|
-
// Save current scroll position
|
|
43
|
-
_.#scrollPosition = window.pageYOffset;
|
|
44
|
-
|
|
45
|
-
// Apply fixed position to body
|
|
41
|
+
// Apply overflow hidden to body
|
|
46
42
|
document.body.classList.add('overflow-hidden');
|
|
47
|
-
document.body.style.top = `-${_.#scrollPosition}px`;
|
|
48
43
|
}
|
|
49
44
|
|
|
50
45
|
/**
|
|
51
|
-
* Restores
|
|
46
|
+
* Restores body scrolling when cart dialog is closed
|
|
52
47
|
* @private
|
|
53
48
|
*/
|
|
54
49
|
#restoreScroll() {
|
|
55
|
-
|
|
56
|
-
// Remove fixed positioning
|
|
50
|
+
// Remove overflow hidden from body
|
|
57
51
|
document.body.classList.remove('overflow-hidden');
|
|
58
|
-
document.body.style.removeProperty('top');
|
|
59
|
-
|
|
60
|
-
// Restore scroll position
|
|
61
|
-
window.scrollTo(0, _.#scrollPosition);
|
|
62
52
|
}
|
|
63
53
|
|
|
64
54
|
/**
|
|
@@ -99,7 +89,18 @@ class CartDialog extends HTMLElement {
|
|
|
99
89
|
return;
|
|
100
90
|
}
|
|
101
91
|
|
|
102
|
-
|
|
92
|
+
// Check if focus-trap already exists, if not create one
|
|
93
|
+
_.focusTrap = _.contentPanel.querySelector('focus-trap');
|
|
94
|
+
if (!_.focusTrap) {
|
|
95
|
+
_.focusTrap = document.createElement('focus-trap');
|
|
96
|
+
|
|
97
|
+
// Move all existing cart-panel content into the focus trap
|
|
98
|
+
const existingContent = Array.from(_.contentPanel.childNodes);
|
|
99
|
+
existingContent.forEach((child) => _.focusTrap.appendChild(child));
|
|
100
|
+
|
|
101
|
+
// Insert focus trap inside the cart-panel
|
|
102
|
+
_.contentPanel.appendChild(_.focusTrap);
|
|
103
|
+
}
|
|
103
104
|
|
|
104
105
|
// Ensure we have labelledby and describedby references
|
|
105
106
|
if (!_.getAttribute('aria-labelledby')) {
|
|
@@ -112,15 +113,15 @@ class CartDialog extends HTMLElement {
|
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
|
|
116
|
-
_.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// Add modal overlay
|
|
121
|
-
_.prepend(document.createElement('cart-overlay'));
|
|
116
|
+
// Add modal overlay if it doesn't already exist
|
|
117
|
+
if (!_.querySelector('cart-overlay')) {
|
|
118
|
+
_.prepend(document.createElement('cart-overlay'));
|
|
119
|
+
}
|
|
122
120
|
_.#attachListeners();
|
|
123
121
|
_.#bindKeyboard();
|
|
122
|
+
|
|
123
|
+
// Load cart data immediately after component initialization
|
|
124
|
+
_.refreshCart();
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
/**
|
|
@@ -153,6 +154,14 @@ class CartDialog extends HTMLElement {
|
|
|
153
154
|
*/
|
|
154
155
|
#emit(eventName, data = null) {
|
|
155
156
|
this.#eventEmitter.emit(eventName, data);
|
|
157
|
+
|
|
158
|
+
// Also emit as native DOM events for better compatibility
|
|
159
|
+
this.dispatchEvent(
|
|
160
|
+
new CustomEvent(eventName, {
|
|
161
|
+
detail: data,
|
|
162
|
+
bubbles: true,
|
|
163
|
+
})
|
|
164
|
+
);
|
|
156
165
|
}
|
|
157
166
|
|
|
158
167
|
/**
|
|
@@ -176,7 +185,7 @@ class CartDialog extends HTMLElement {
|
|
|
176
185
|
|
|
177
186
|
// Handle close buttons
|
|
178
187
|
_.addEventListener('click', (e) => {
|
|
179
|
-
if (!e.target.closest('[data-action
|
|
188
|
+
if (!e.target.closest('[data-action-hide-cart]')) return;
|
|
180
189
|
_.hide();
|
|
181
190
|
});
|
|
182
191
|
|
|
@@ -231,14 +240,15 @@ class CartDialog extends HTMLElement {
|
|
|
231
240
|
this.updateCartItem(cartKey, 0)
|
|
232
241
|
.then((updatedCart) => {
|
|
233
242
|
if (updatedCart && !updatedCart.error) {
|
|
234
|
-
// Success -
|
|
235
|
-
element.destroyYourself();
|
|
243
|
+
// Success - let smart comparison handle the removal animation
|
|
236
244
|
this.#currentCart = updatedCart;
|
|
237
|
-
this.#
|
|
245
|
+
this.#renderCartItems(updatedCart);
|
|
246
|
+
this.#renderCartPanel(updatedCart);
|
|
238
247
|
|
|
239
248
|
// Emit cart updated and data changed events
|
|
240
|
-
this.#
|
|
241
|
-
this.#emit('cart-dialog:
|
|
249
|
+
const cartWithCalculatedFields = this.#addCalculatedFields(updatedCart);
|
|
250
|
+
this.#emit('cart-dialog:updated', { cart: cartWithCalculatedFields });
|
|
251
|
+
this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
|
|
242
252
|
} else {
|
|
243
253
|
// Error - reset to ready state
|
|
244
254
|
element.setState('ready');
|
|
@@ -266,14 +276,16 @@ class CartDialog extends HTMLElement {
|
|
|
266
276
|
this.updateCartItem(cartKey, quantity)
|
|
267
277
|
.then((updatedCart) => {
|
|
268
278
|
if (updatedCart && !updatedCart.error) {
|
|
269
|
-
// Success - update cart data
|
|
279
|
+
// Success - update cart data and refresh items
|
|
270
280
|
this.#currentCart = updatedCart;
|
|
271
|
-
this.#
|
|
281
|
+
this.#renderCartItems(updatedCart);
|
|
282
|
+
this.#renderCartPanel(updatedCart);
|
|
272
283
|
element.setState('ready');
|
|
273
284
|
|
|
274
285
|
// Emit cart updated and data changed events
|
|
275
|
-
this.#
|
|
276
|
-
this.#emit('cart-dialog:
|
|
286
|
+
const cartWithCalculatedFields = this.#addCalculatedFields(updatedCart);
|
|
287
|
+
this.#emit('cart-dialog:updated', { cart: cartWithCalculatedFields });
|
|
288
|
+
this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
|
|
277
289
|
} else {
|
|
278
290
|
// Error - reset to ready state
|
|
279
291
|
element.setState('ready');
|
|
@@ -288,13 +300,82 @@ class CartDialog extends HTMLElement {
|
|
|
288
300
|
}
|
|
289
301
|
|
|
290
302
|
/**
|
|
291
|
-
* Update cart
|
|
303
|
+
* Update cart count elements across the site
|
|
304
|
+
* @private
|
|
305
|
+
*/
|
|
306
|
+
#renderCartCount(cartData) {
|
|
307
|
+
if (!cartData) return;
|
|
308
|
+
|
|
309
|
+
// Calculate visible item count (excluding _hide_in_cart items)
|
|
310
|
+
const visibleItems = this.#getVisibleCartItems(cartData);
|
|
311
|
+
const visibleItemCount = visibleItems.reduce((total, item) => total + item.quantity, 0);
|
|
312
|
+
|
|
313
|
+
// Update all cart count elements across the site
|
|
314
|
+
const cartCountElements = document.querySelectorAll('[data-content-cart-count]');
|
|
315
|
+
cartCountElements.forEach((element) => {
|
|
316
|
+
element.textContent = visibleItemCount;
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Update cart subtotal elements across the site
|
|
322
|
+
* @private
|
|
323
|
+
*/
|
|
324
|
+
#renderCartSubtotal(cartData) {
|
|
325
|
+
if (!cartData) return;
|
|
326
|
+
|
|
327
|
+
// Calculate subtotal from all items except those marked to ignore pricing
|
|
328
|
+
const pricedItems = cartData.items.filter(item => {
|
|
329
|
+
const ignorePrice = item.properties?._ignore_price_in_subtotal;
|
|
330
|
+
return !ignorePrice;
|
|
331
|
+
});
|
|
332
|
+
const subtotal = pricedItems.reduce((total, item) => total + (item.line_price || 0), 0);
|
|
333
|
+
|
|
334
|
+
// Update all cart subtotal elements across the site
|
|
335
|
+
const cartSubtotalElements = document.querySelectorAll('[data-content-cart-subtotal]');
|
|
336
|
+
cartSubtotalElements.forEach((element) => {
|
|
337
|
+
// Format as currency (assuming cents, convert to dollars)
|
|
338
|
+
const formatted = (subtotal / 100).toFixed(2);
|
|
339
|
+
element.textContent = `$${formatted}`;
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Update cart items display based on cart data
|
|
292
345
|
* @private
|
|
293
346
|
*/
|
|
294
|
-
#
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
347
|
+
#renderCartPanel(cart = null) {
|
|
348
|
+
const cartData = cart || this.#currentCart;
|
|
349
|
+
if (!cartData) return;
|
|
350
|
+
|
|
351
|
+
// Get cart sections
|
|
352
|
+
const hasItemsSection = this.querySelector('[data-cart-has-items]');
|
|
353
|
+
const emptySection = this.querySelector('[data-cart-is-empty]');
|
|
354
|
+
const itemsContainer = this.querySelector('[data-content-cart-items]');
|
|
355
|
+
|
|
356
|
+
if (!hasItemsSection || !emptySection || !itemsContainer) {
|
|
357
|
+
console.warn(
|
|
358
|
+
'Cart sections not found. Expected [data-cart-has-items], [data-cart-is-empty], and [data-content-cart-items]'
|
|
359
|
+
);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Check visible item count for showing/hiding sections
|
|
364
|
+
const visibleItems = this.#getVisibleCartItems(cartData);
|
|
365
|
+
const hasVisibleItems = visibleItems.length > 0;
|
|
366
|
+
|
|
367
|
+
// Show/hide sections based on visible item count
|
|
368
|
+
if (hasVisibleItems) {
|
|
369
|
+
hasItemsSection.style.display = '';
|
|
370
|
+
emptySection.style.display = 'none';
|
|
371
|
+
} else {
|
|
372
|
+
hasItemsSection.style.display = 'none';
|
|
373
|
+
emptySection.style.display = '';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Update cart count and subtotal across the site
|
|
377
|
+
this.#renderCartCount(cartData);
|
|
378
|
+
this.#renderCartSubtotal(cartData);
|
|
298
379
|
}
|
|
299
380
|
|
|
300
381
|
/**
|
|
@@ -346,31 +427,216 @@ class CartDialog extends HTMLElement {
|
|
|
346
427
|
|
|
347
428
|
/**
|
|
348
429
|
* Refresh cart data from server and update components
|
|
430
|
+
* @param {Object} [cartObj=null] - Optional cart object to use instead of fetching
|
|
349
431
|
* @returns {Promise<Object>} Cart data object
|
|
350
432
|
*/
|
|
351
|
-
refreshCart() {
|
|
433
|
+
refreshCart(cartObj = null) {
|
|
434
|
+
// If cart object is provided, use it directly
|
|
435
|
+
if (cartObj && !cartObj.error) {
|
|
436
|
+
// console.log('Using provided cart data:', cartObj);
|
|
437
|
+
this.#currentCart = cartObj;
|
|
438
|
+
this.#renderCartItems(cartObj);
|
|
439
|
+
this.#renderCartPanel(cartObj);
|
|
440
|
+
|
|
441
|
+
// Emit cart refreshed and data changed events
|
|
442
|
+
const cartWithCalculatedFields = this.#addCalculatedFields(cartObj);
|
|
443
|
+
this.#emit('cart-dialog:refreshed', { cart: cartWithCalculatedFields });
|
|
444
|
+
this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
|
|
445
|
+
|
|
446
|
+
return Promise.resolve(cartObj);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Otherwise fetch from server
|
|
352
450
|
return this.getCart().then((cartData) => {
|
|
451
|
+
// console.log('Cart data received:', cartData);
|
|
353
452
|
if (cartData && !cartData.error) {
|
|
354
453
|
this.#currentCart = cartData;
|
|
355
|
-
this.#
|
|
454
|
+
this.#renderCartItems(cartData);
|
|
455
|
+
this.#renderCartPanel(cartData);
|
|
356
456
|
|
|
357
457
|
// Emit cart refreshed and data changed events
|
|
358
|
-
this.#
|
|
359
|
-
this.#emit('cart-dialog:
|
|
458
|
+
const cartWithCalculatedFields = this.#addCalculatedFields(cartData);
|
|
459
|
+
this.#emit('cart-dialog:refreshed', { cart: cartWithCalculatedFields });
|
|
460
|
+
this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
|
|
461
|
+
} else {
|
|
462
|
+
console.warn('Cart data has error or is null:', cartData);
|
|
360
463
|
}
|
|
361
464
|
return cartData;
|
|
362
465
|
});
|
|
363
466
|
}
|
|
364
467
|
|
|
468
|
+
/**
|
|
469
|
+
* Remove items from DOM that are no longer in cart data
|
|
470
|
+
* @private
|
|
471
|
+
*/
|
|
472
|
+
#removeItemsFromDOM(itemsContainer, newKeysSet) {
|
|
473
|
+
const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
|
|
474
|
+
|
|
475
|
+
const itemsToRemove = currentItems.filter((item) => !newKeysSet.has(item.getAttribute('key')));
|
|
476
|
+
|
|
477
|
+
itemsToRemove.forEach((item) => {
|
|
478
|
+
console.log('destroy yourself', item);
|
|
479
|
+
item.destroyYourself();
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Add new items to DOM with animation delay
|
|
485
|
+
* @private
|
|
486
|
+
*/
|
|
487
|
+
#addItemsToDOM(itemsContainer, itemsToAdd, newKeys) {
|
|
488
|
+
// Delay adding new items by 300ms to let cart slide open first
|
|
489
|
+
setTimeout(() => {
|
|
490
|
+
itemsToAdd.forEach((itemData) => {
|
|
491
|
+
const cartItem$1 = cartItem.CartItem.createAnimated(itemData);
|
|
492
|
+
const targetIndex = newKeys.indexOf(itemData.key || itemData.id);
|
|
493
|
+
|
|
494
|
+
// Find the correct position to insert the new item
|
|
495
|
+
if (targetIndex === 0) {
|
|
496
|
+
// Insert at the beginning
|
|
497
|
+
itemsContainer.insertBefore(cartItem$1, itemsContainer.firstChild);
|
|
498
|
+
} else {
|
|
499
|
+
// Find the item that should come before this one
|
|
500
|
+
let insertAfter = null;
|
|
501
|
+
for (let i = targetIndex - 1; i >= 0; i--) {
|
|
502
|
+
const prevKey = newKeys[i];
|
|
503
|
+
const prevItem = itemsContainer.querySelector(`cart-item[key="${prevKey}"]`);
|
|
504
|
+
if (prevItem) {
|
|
505
|
+
insertAfter = prevItem;
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (insertAfter) {
|
|
511
|
+
insertAfter.insertAdjacentElement('afterend', cartItem$1);
|
|
512
|
+
} else {
|
|
513
|
+
itemsContainer.appendChild(cartItem$1);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
}, 100);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Filter cart items to exclude those with _hide_in_cart property
|
|
522
|
+
* @private
|
|
523
|
+
*/
|
|
524
|
+
#getVisibleCartItems(cartData) {
|
|
525
|
+
if (!cartData || !cartData.items) return [];
|
|
526
|
+
return cartData.items.filter((item) => {
|
|
527
|
+
// Check for _hide_in_cart in various possible locations
|
|
528
|
+
const hidden = item.properties?._hide_in_cart;
|
|
529
|
+
|
|
530
|
+
return !hidden;
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Add calculated fields to cart object for events
|
|
536
|
+
* @private
|
|
537
|
+
*/
|
|
538
|
+
#addCalculatedFields(cartData) {
|
|
539
|
+
if (!cartData) return cartData;
|
|
540
|
+
|
|
541
|
+
const visibleItems = this.#getVisibleCartItems(cartData);
|
|
542
|
+
const calculated_count = visibleItems.reduce((total, item) => total + item.quantity, 0);
|
|
543
|
+
const calculated_subtotal = visibleItems.reduce(
|
|
544
|
+
(total, item) => total + (item.line_price || 0),
|
|
545
|
+
0
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
...cartData,
|
|
550
|
+
calculated_count,
|
|
551
|
+
calculated_subtotal,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Render cart items from Shopify cart data with smart comparison
|
|
557
|
+
* @private
|
|
558
|
+
*/
|
|
559
|
+
#renderCartItems(cartData) {
|
|
560
|
+
const itemsContainer = this.querySelector('[data-content-cart-items]');
|
|
561
|
+
|
|
562
|
+
if (!itemsContainer || !cartData || !cartData.items) {
|
|
563
|
+
console.warn('Cannot render cart items:', {
|
|
564
|
+
itemsContainer: !!itemsContainer,
|
|
565
|
+
cartData: !!cartData,
|
|
566
|
+
items: cartData?.items?.length,
|
|
567
|
+
});
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Filter out items with _hide_in_cart property
|
|
572
|
+
const visibleItems = this.#getVisibleCartItems(cartData);
|
|
573
|
+
|
|
574
|
+
// Handle initial render - load all items without animation
|
|
575
|
+
if (this.#isInitialRender) {
|
|
576
|
+
// console.log('Initial cart render:', visibleItems.length, 'visible items');
|
|
577
|
+
|
|
578
|
+
// Clear existing items
|
|
579
|
+
itemsContainer.innerHTML = '';
|
|
580
|
+
|
|
581
|
+
// Create cart-item elements without animation
|
|
582
|
+
visibleItems.forEach((itemData) => {
|
|
583
|
+
const cartItem$1 = new cartItem.CartItem(itemData); // No animation
|
|
584
|
+
// const cartItem = document.createElement('cart-item');
|
|
585
|
+
// cartItem.setData(itemData);
|
|
586
|
+
itemsContainer.appendChild(cartItem$1);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
this.#isInitialRender = false;
|
|
590
|
+
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Get current DOM items and their keys
|
|
595
|
+
const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
|
|
596
|
+
const currentKeys = new Set(currentItems.map((item) => item.getAttribute('key')));
|
|
597
|
+
|
|
598
|
+
// Get new cart data keys in order (only visible items)
|
|
599
|
+
const newKeys = visibleItems.map((item) => item.key || item.id);
|
|
600
|
+
const newKeysSet = new Set(newKeys);
|
|
601
|
+
|
|
602
|
+
// Step 1: Remove items that are no longer in cart data
|
|
603
|
+
this.#removeItemsFromDOM(itemsContainer, newKeysSet);
|
|
604
|
+
|
|
605
|
+
// Step 2: Add new items that weren't in DOM (with animation delay)
|
|
606
|
+
const itemsToAdd = visibleItems.filter(
|
|
607
|
+
(itemData) => !currentKeys.has(itemData.key || itemData.id)
|
|
608
|
+
);
|
|
609
|
+
this.#addItemsToDOM(itemsContainer, itemsToAdd, newKeys);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Set the template function for cart items
|
|
614
|
+
* @param {Function} templateFn - Function that takes item data and returns HTML string
|
|
615
|
+
*/
|
|
616
|
+
setCartItemTemplate(templateName, templateFn) {
|
|
617
|
+
cartItem.CartItem.setTemplate(templateName, templateFn);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Set the processing template function for cart items
|
|
622
|
+
* @param {Function} templateFn - Function that returns HTML string for processing state
|
|
623
|
+
*/
|
|
624
|
+
setCartItemProcessingTemplate(templateFn) {
|
|
625
|
+
cartItem.CartItem.setProcessingTemplate(templateFn);
|
|
626
|
+
}
|
|
627
|
+
|
|
365
628
|
/**
|
|
366
629
|
* Shows the cart dialog and traps focus within it
|
|
367
630
|
* @param {HTMLElement} [triggerEl=null] - The element that triggered the cart dialog
|
|
368
631
|
* @fires CartDialog#show - Fired when the cart dialog has been shown
|
|
369
632
|
*/
|
|
370
|
-
show(triggerEl = null) {
|
|
633
|
+
show(triggerEl = null, cartObj) {
|
|
371
634
|
const _ = this;
|
|
372
635
|
_.triggerEl = triggerEl || false;
|
|
373
636
|
|
|
637
|
+
// Lock body scrolling
|
|
638
|
+
_.#lockScroll();
|
|
639
|
+
|
|
374
640
|
// Remove the hidden class first to ensure content is rendered
|
|
375
641
|
_.contentPanel.classList.remove('hidden');
|
|
376
642
|
|
|
@@ -378,17 +644,16 @@ class CartDialog extends HTMLElement {
|
|
|
378
644
|
requestAnimationFrame(() => {
|
|
379
645
|
// Update ARIA states
|
|
380
646
|
_.setAttribute('aria-hidden', 'false');
|
|
647
|
+
|
|
381
648
|
if (_.triggerEl) {
|
|
382
649
|
_.triggerEl.setAttribute('aria-expanded', 'true');
|
|
383
650
|
}
|
|
384
651
|
|
|
385
|
-
// Lock body scrolling and save scroll position
|
|
386
|
-
_.#lockScroll();
|
|
387
|
-
|
|
388
652
|
// Focus management
|
|
389
653
|
const firstFocusable = _.querySelector(
|
|
390
654
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
391
655
|
);
|
|
656
|
+
|
|
392
657
|
if (firstFocusable) {
|
|
393
658
|
requestAnimationFrame(() => {
|
|
394
659
|
firstFocusable.focus();
|
|
@@ -396,7 +661,7 @@ class CartDialog extends HTMLElement {
|
|
|
396
661
|
}
|
|
397
662
|
|
|
398
663
|
// Refresh cart data when showing
|
|
399
|
-
_.refreshCart();
|
|
664
|
+
_.refreshCart(cartObj);
|
|
400
665
|
|
|
401
666
|
// Emit show event - cart dialog is now visible
|
|
402
667
|
_.#emit('cart-dialog:show', { triggerElement: _.triggerEl });
|
|
@@ -411,23 +676,31 @@ class CartDialog extends HTMLElement {
|
|
|
411
676
|
hide() {
|
|
412
677
|
const _ = this;
|
|
413
678
|
|
|
414
|
-
// Restore body scroll and scroll position
|
|
415
|
-
_.#restoreScroll();
|
|
416
|
-
|
|
417
679
|
// Update ARIA states
|
|
418
680
|
if (_.triggerEl) {
|
|
419
681
|
// remove focus from modal panel first
|
|
420
682
|
_.triggerEl.focus();
|
|
421
683
|
// mark trigger as no longer expanded
|
|
422
684
|
_.triggerEl.setAttribute('aria-expanded', 'false');
|
|
685
|
+
} else {
|
|
686
|
+
// If no trigger element, blur any focused element inside the panel
|
|
687
|
+
const activeElement = document.activeElement;
|
|
688
|
+
if (activeElement && _.contains(activeElement)) {
|
|
689
|
+
activeElement.blur();
|
|
690
|
+
}
|
|
423
691
|
}
|
|
424
692
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
693
|
+
requestAnimationFrame(() => {
|
|
694
|
+
// Set aria-hidden to start transition
|
|
695
|
+
// The transitionend event handler will add display:none when complete
|
|
696
|
+
_.setAttribute('aria-hidden', 'true');
|
|
697
|
+
|
|
698
|
+
// Emit hide event - cart dialog is now starting to hide
|
|
699
|
+
_.#emit('cart-dialog:hide', { triggerElement: _.triggerEl });
|
|
428
700
|
|
|
429
|
-
|
|
430
|
-
|
|
701
|
+
// Restore body scroll
|
|
702
|
+
_.#restoreScroll();
|
|
703
|
+
});
|
|
431
704
|
}
|
|
432
705
|
}
|
|
433
706
|
|
|
@@ -472,6 +745,15 @@ if (!customElements.get('cart-panel')) {
|
|
|
472
745
|
customElements.define('cart-panel', CartPanel);
|
|
473
746
|
}
|
|
474
747
|
|
|
748
|
+
// Make CartItem available globally for Shopify themes
|
|
749
|
+
if (typeof window !== 'undefined') {
|
|
750
|
+
window.CartItem = cartItem.CartItem;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
Object.defineProperty(exports, 'CartItem', {
|
|
754
|
+
enumerable: true,
|
|
755
|
+
get: function () { return cartItem.CartItem; }
|
|
756
|
+
});
|
|
475
757
|
exports.CartDialog = CartDialog;
|
|
476
758
|
exports.CartOverlay = CartOverlay;
|
|
477
759
|
exports.CartPanel = CartPanel;
|