@magic-spells/cart-panel 0.1.2 → 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.
@@ -189,8 +189,7 @@ cart-panel {
189
189
  top: 0;
190
190
  right: 0;
191
191
  width: var(--cart-panel-width);
192
- height: 100vh;
193
- opacity: 0;
192
+ height: 100dvh;
194
193
  transform: translateX(100%);
195
194
  pointer-events: none;
196
195
  z-index: var(--cart-panel-z-index);
@@ -198,7 +197,7 @@ cart-panel {
198
197
  box-shadow: var(--cart-panel-shadow);
199
198
  border-radius: var(--cart-panel-border-radius);
200
199
  overflow: hidden;
201
- transition: opacity var(--cart-transition-duration) var(--cart-transition-timing), transform var(--cart-transition-duration) var(--cart-transition-timing);
200
+ transition: transform var(--cart-transition-duration) var(--cart-transition-timing);
202
201
  }
203
202
  cart-panel.hidden {
204
203
  display: none;
@@ -206,10 +205,4 @@ cart-panel.hidden {
206
205
 
207
206
  body.overflow-hidden {
208
207
  overflow: hidden;
209
- position: fixed;
210
- width: 100%;
211
- height: 100%;
212
- left: 0;
213
- right: 0;
214
- margin: 0;
215
208
  }
@@ -12,7 +12,6 @@ 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;
18
17
  #isInitialRender = true;
@@ -35,31 +34,21 @@ class CartDialog extends HTMLElement {
35
34
  }
36
35
 
37
36
  /**
38
- * Saves current scroll position and locks body scrolling
37
+ * Locks body scrolling
39
38
  * @private
40
39
  */
41
40
  #lockScroll() {
42
- const _ = this;
43
- // Save current scroll position
44
- _.#scrollPosition = window.pageYOffset;
45
-
46
- // Apply fixed position to body
41
+ // Apply overflow hidden to body
47
42
  document.body.classList.add('overflow-hidden');
48
- document.body.style.top = `-${_.#scrollPosition}px`;
49
43
  }
50
44
 
51
45
  /**
52
- * Restores scroll position when cart dialog is closed
46
+ * Restores body scrolling when cart dialog is closed
53
47
  * @private
54
48
  */
55
49
  #restoreScroll() {
56
- const _ = this;
57
- // Remove fixed positioning
50
+ // Remove overflow hidden from body
58
51
  document.body.classList.remove('overflow-hidden');
59
- document.body.style.removeProperty('top');
60
-
61
- // Restore scroll position
62
- window.scrollTo(0, _.#scrollPosition);
63
52
  }
64
53
 
65
54
  /**
@@ -100,7 +89,18 @@ class CartDialog extends HTMLElement {
100
89
  return;
101
90
  }
102
91
 
103
- _.focusTrap = document.createElement('focus-trap');
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
+ }
104
104
 
105
105
  // Ensure we have labelledby and describedby references
106
106
  if (!_.getAttribute('aria-labelledby')) {
@@ -113,20 +113,15 @@ class CartDialog extends HTMLElement {
113
113
  }
114
114
  }
115
115
 
116
- // Insert focus trap before the cart-panel
117
- _.contentPanel.parentNode.insertBefore(_.focusTrap, _.contentPanel);
118
- // Move cart-panel inside the focus trap
119
- _.focusTrap.appendChild(_.contentPanel);
120
-
121
- // Setup the trap - this will add focus-trap-start/end elements around the content
122
- _.focusTrap.setupTrap();
123
-
124
116
  // Add modal overlay if it doesn't already exist
125
117
  if (!_.querySelector('cart-overlay')) {
126
118
  _.prepend(document.createElement('cart-overlay'));
127
119
  }
128
120
  _.#attachListeners();
129
121
  _.#bindKeyboard();
122
+
123
+ // Load cart data immediately after component initialization
124
+ _.refreshCart();
130
125
  }
131
126
 
132
127
  /**
@@ -159,6 +154,14 @@ class CartDialog extends HTMLElement {
159
154
  */
160
155
  #emit(eventName, data = null) {
161
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
+ );
162
165
  }
163
166
 
164
167
  /**
@@ -240,11 +243,12 @@ class CartDialog extends HTMLElement {
240
243
  // Success - let smart comparison handle the removal animation
241
244
  this.#currentCart = updatedCart;
242
245
  this.#renderCartItems(updatedCart);
243
- this.#updateCartItems(updatedCart);
246
+ this.#renderCartPanel(updatedCart);
244
247
 
245
248
  // Emit cart updated and data changed events
246
- this.#emit('cart-dialog:updated', { cart: updatedCart });
247
- this.#emit('cart-dialog:data-changed', updatedCart);
249
+ const cartWithCalculatedFields = this.#addCalculatedFields(updatedCart);
250
+ this.#emit('cart-dialog:updated', { cart: cartWithCalculatedFields });
251
+ this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
248
252
  } else {
249
253
  // Error - reset to ready state
250
254
  element.setState('ready');
@@ -275,12 +279,13 @@ class CartDialog extends HTMLElement {
275
279
  // Success - update cart data and refresh items
276
280
  this.#currentCart = updatedCart;
277
281
  this.#renderCartItems(updatedCart);
278
- this.#updateCartItems(updatedCart);
282
+ this.#renderCartPanel(updatedCart);
279
283
  element.setState('ready');
280
284
 
281
285
  // Emit cart updated and data changed events
282
- this.#emit('cart-dialog:updated', { cart: updatedCart });
283
- this.#emit('cart-dialog:data-changed', updatedCart);
286
+ const cartWithCalculatedFields = this.#addCalculatedFields(updatedCart);
287
+ this.#emit('cart-dialog:updated', { cart: cartWithCalculatedFields });
288
+ this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
284
289
  } else {
285
290
  // Error - reset to ready state
286
291
  element.setState('ready');
@@ -294,11 +299,52 @@ class CartDialog extends HTMLElement {
294
299
  });
295
300
  }
296
301
 
302
+ /**
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
+
297
343
  /**
298
344
  * Update cart items display based on cart data
299
345
  * @private
300
346
  */
301
- #updateCartItems(cart = null) {
347
+ #renderCartPanel(cart = null) {
302
348
  const cartData = cart || this.#currentCart;
303
349
  if (!cartData) return;
304
350
 
@@ -314,14 +360,22 @@ class CartDialog extends HTMLElement {
314
360
  return;
315
361
  }
316
362
 
317
- // Show/hide sections based on item count
318
- if (cartData.item_count > 0) {
319
- hasItemsSection.style.display = 'block';
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 = '';
320
370
  emptySection.style.display = 'none';
321
371
  } else {
322
372
  hasItemsSection.style.display = 'none';
323
- emptySection.style.display = 'block';
373
+ emptySection.style.display = '';
324
374
  }
375
+
376
+ // Update cart count and subtotal across the site
377
+ this.#renderCartCount(cartData);
378
+ this.#renderCartSubtotal(cartData);
325
379
  }
326
380
 
327
381
  /**
@@ -373,20 +427,37 @@ class CartDialog extends HTMLElement {
373
427
 
374
428
  /**
375
429
  * Refresh cart data from server and update components
430
+ * @param {Object} [cartObj=null] - Optional cart object to use instead of fetching
376
431
  * @returns {Promise<Object>} Cart data object
377
432
  */
378
- refreshCart() {
379
- console.log('Refreshing cart...');
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
380
450
  return this.getCart().then((cartData) => {
381
- console.log('Cart data received:', cartData);
451
+ // console.log('Cart data received:', cartData);
382
452
  if (cartData && !cartData.error) {
383
453
  this.#currentCart = cartData;
384
454
  this.#renderCartItems(cartData);
385
- this.#updateCartItems(cartData);
455
+ this.#renderCartPanel(cartData);
386
456
 
387
457
  // Emit cart refreshed and data changed events
388
- this.#emit('cart-dialog:refreshed', { cart: cartData });
389
- this.#emit('cart-dialog:data-changed', cartData);
458
+ const cartWithCalculatedFields = this.#addCalculatedFields(cartData);
459
+ this.#emit('cart-dialog:refreshed', { cart: cartWithCalculatedFields });
460
+ this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
390
461
  } else {
391
462
  console.warn('Cart data has error or is null:', cartData);
392
463
  }
@@ -400,14 +471,11 @@ class CartDialog extends HTMLElement {
400
471
  */
401
472
  #removeItemsFromDOM(itemsContainer, newKeysSet) {
402
473
  const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
403
- const itemsToRemove = currentItems.filter((item) => !newKeysSet.has(item.getAttribute('key')));
404
474
 
405
- console.log(
406
- `Removing ${itemsToRemove.length} items:`,
407
- itemsToRemove.map((item) => item.getAttribute('key'))
408
- );
475
+ const itemsToRemove = currentItems.filter((item) => !newKeysSet.has(item.getAttribute('key')));
409
476
 
410
477
  itemsToRemove.forEach((item) => {
478
+ console.log('destroy yourself', item);
411
479
  item.destroyYourself();
412
480
  });
413
481
  }
@@ -417,11 +485,6 @@ class CartDialog extends HTMLElement {
417
485
  * @private
418
486
  */
419
487
  #addItemsToDOM(itemsContainer, itemsToAdd, newKeys) {
420
- console.log(
421
- `Adding ${itemsToAdd.length} items:`,
422
- itemsToAdd.map((item) => item.key || item.id)
423
- );
424
-
425
488
  // Delay adding new items by 300ms to let cart slide open first
426
489
  setTimeout(() => {
427
490
  itemsToAdd.forEach((itemData) => {
@@ -454,6 +517,41 @@ class CartDialog extends HTMLElement {
454
517
  }, 100);
455
518
  }
456
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
+
457
555
  /**
458
556
  * Render cart items from Shopify cart data with smart comparison
459
557
  * @private
@@ -470,53 +568,53 @@ class CartDialog extends HTMLElement {
470
568
  return;
471
569
  }
472
570
 
571
+ // Filter out items with _hide_in_cart property
572
+ const visibleItems = this.#getVisibleCartItems(cartData);
573
+
473
574
  // Handle initial render - load all items without animation
474
575
  if (this.#isInitialRender) {
475
- console.log('Initial cart render:', cartData.items.length, 'items');
576
+ // console.log('Initial cart render:', visibleItems.length, 'visible items');
476
577
 
477
578
  // Clear existing items
478
579
  itemsContainer.innerHTML = '';
479
580
 
480
581
  // Create cart-item elements without animation
481
- cartData.items.forEach((itemData) => {
582
+ visibleItems.forEach((itemData) => {
482
583
  const cartItem$1 = new cartItem.CartItem(itemData); // No animation
584
+ // const cartItem = document.createElement('cart-item');
585
+ // cartItem.setData(itemData);
483
586
  itemsContainer.appendChild(cartItem$1);
484
587
  });
485
588
 
486
589
  this.#isInitialRender = false;
487
- console.log('Initial render complete, container children:', itemsContainer.children.length);
590
+
488
591
  return;
489
592
  }
490
593
 
491
- console.log('Smart rendering cart items:', cartData.items.length, 'items');
492
-
493
594
  // Get current DOM items and their keys
494
595
  const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
495
596
  const currentKeys = new Set(currentItems.map((item) => item.getAttribute('key')));
496
597
 
497
- // Get new cart data keys in order
498
- const newKeys = cartData.items.map((item) => item.key || item.id);
598
+ // Get new cart data keys in order (only visible items)
599
+ const newKeys = visibleItems.map((item) => item.key || item.id);
499
600
  const newKeysSet = new Set(newKeys);
500
601
 
501
602
  // Step 1: Remove items that are no longer in cart data
502
603
  this.#removeItemsFromDOM(itemsContainer, newKeysSet);
503
604
 
504
605
  // Step 2: Add new items that weren't in DOM (with animation delay)
505
- const itemsToAdd = cartData.items.filter(
606
+ const itemsToAdd = visibleItems.filter(
506
607
  (itemData) => !currentKeys.has(itemData.key || itemData.id)
507
608
  );
508
-
509
609
  this.#addItemsToDOM(itemsContainer, itemsToAdd, newKeys);
510
-
511
- console.log('Smart rendering complete, container children:', itemsContainer.children.length);
512
610
  }
513
611
 
514
612
  /**
515
613
  * Set the template function for cart items
516
614
  * @param {Function} templateFn - Function that takes item data and returns HTML string
517
615
  */
518
- setCartItemTemplate(templateFn) {
519
- cartItem.CartItem.setTemplate(templateFn);
616
+ setCartItemTemplate(templateName, templateFn) {
617
+ cartItem.CartItem.setTemplate(templateName, templateFn);
520
618
  }
521
619
 
522
620
  /**
@@ -532,10 +630,13 @@ class CartDialog extends HTMLElement {
532
630
  * @param {HTMLElement} [triggerEl=null] - The element that triggered the cart dialog
533
631
  * @fires CartDialog#show - Fired when the cart dialog has been shown
534
632
  */
535
- show(triggerEl = null) {
633
+ show(triggerEl = null, cartObj) {
536
634
  const _ = this;
537
635
  _.triggerEl = triggerEl || false;
538
636
 
637
+ // Lock body scrolling
638
+ _.#lockScroll();
639
+
539
640
  // Remove the hidden class first to ensure content is rendered
540
641
  _.contentPanel.classList.remove('hidden');
541
642
 
@@ -543,17 +644,16 @@ class CartDialog extends HTMLElement {
543
644
  requestAnimationFrame(() => {
544
645
  // Update ARIA states
545
646
  _.setAttribute('aria-hidden', 'false');
647
+
546
648
  if (_.triggerEl) {
547
649
  _.triggerEl.setAttribute('aria-expanded', 'true');
548
650
  }
549
651
 
550
- // Lock body scrolling and save scroll position
551
- _.#lockScroll();
552
-
553
652
  // Focus management
554
653
  const firstFocusable = _.querySelector(
555
654
  'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
556
655
  );
656
+
557
657
  if (firstFocusable) {
558
658
  requestAnimationFrame(() => {
559
659
  firstFocusable.focus();
@@ -561,7 +661,7 @@ class CartDialog extends HTMLElement {
561
661
  }
562
662
 
563
663
  // Refresh cart data when showing
564
- _.refreshCart();
664
+ _.refreshCart(cartObj);
565
665
 
566
666
  // Emit show event - cart dialog is now visible
567
667
  _.#emit('cart-dialog:show', { triggerElement: _.triggerEl });
@@ -576,23 +676,31 @@ class CartDialog extends HTMLElement {
576
676
  hide() {
577
677
  const _ = this;
578
678
 
579
- // Restore body scroll and scroll position
580
- _.#restoreScroll();
581
-
582
679
  // Update ARIA states
583
680
  if (_.triggerEl) {
584
681
  // remove focus from modal panel first
585
682
  _.triggerEl.focus();
586
683
  // mark trigger as no longer expanded
587
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
+ }
588
691
  }
589
692
 
590
- // Set aria-hidden to start transition
591
- // The transitionend event handler will add display:none when complete
592
- _.setAttribute('aria-hidden', 'true');
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');
593
697
 
594
- // Emit hide event - cart dialog is now starting to hide
595
- _.#emit('cart-dialog:hide', { triggerElement: _.triggerEl });
698
+ // Emit hide event - cart dialog is now starting to hide
699
+ _.#emit('cart-dialog:hide', { triggerElement: _.triggerEl });
700
+
701
+ // Restore body scroll
702
+ _.#restoreScroll();
703
+ });
596
704
  }
597
705
  }
598
706