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