@magic-spells/cart-panel 0.1.1 → 0.1.2

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.
@@ -40,6 +40,15 @@
40
40
  CartItem.#processingTemplate = templateFn;
41
41
  }
42
42
 
43
+ /**
44
+ * Create a cart item with appearing animation
45
+ * @param {Object} itemData - Shopify cart item data
46
+ * @returns {CartItem} Cart item instance that will animate in
47
+ */
48
+ static createAnimated(itemData) {
49
+ return new CartItem(itemData, { animate: true });
50
+ }
51
+
43
52
  /**
44
53
  * Define which attributes should be observed for changes
45
54
  */
@@ -58,14 +67,16 @@
58
67
  }
59
68
  }
60
69
 
61
- constructor(itemData = null) {
70
+ constructor(itemData = null, options = {}) {
62
71
  super();
63
72
 
64
73
  // Store item data if provided
65
74
  this.#itemData = itemData;
66
75
 
67
- // Set initial state - start with 'appearing' if we have item data to render
68
- this.#currentState = itemData ? 'appearing' : this.getAttribute('state') || 'ready';
76
+ // Set initial state - start with 'appearing' only if explicitly requested
77
+ const shouldAnimate = options.animate || this.hasAttribute('animate-in');
78
+ this.#currentState =
79
+ itemData && shouldAnimate ? 'appearing' : this.getAttribute('state') || 'ready';
69
80
 
70
81
  // Bind event handlers
71
82
  this.#handlers = {
@@ -101,13 +112,9 @@
101
112
  this.style.height = `${naturalHeight}px`;
102
113
 
103
114
  // Transition to ready state after a brief delay
104
- setTimeout(() => {
115
+ requestAnimationFrame(() => {
105
116
  this.setState('ready');
106
- // Remove explicit height after animation completes
107
- setTimeout(() => {
108
- this.style.height = '';
109
- }, 400); // Match appearing duration
110
- }, 50);
117
+ });
111
118
  });
112
119
  }
113
120
  }
@@ -173,12 +180,15 @@
173
180
  }
174
181
 
175
182
  /**
176
- * Handle transition end events for destroy animation
183
+ * Handle transition end events for destroy animation and appearing animation
177
184
  */
178
185
  #handleTransitionEnd(e) {
179
186
  if (e.propertyName === 'height' && this.#isDestroying) {
180
187
  // Remove from DOM after height animation completes
181
188
  this.remove();
189
+ } else if (e.propertyName === 'height' && this.#currentState === 'ready') {
190
+ // Remove explicit height after appearing animation completes
191
+ this.style.height = '';
182
192
  }
183
193
  }
184
194
 
@@ -525,9 +535,15 @@
525
535
  };
526
536
  }
527
537
 
528
- customElements.define('focus-trap', FocusTrap);
529
- customElements.define('focus-trap-start', FocusTrapStart);
530
- customElements.define('focus-trap-end', FocusTrapEnd);
538
+ if (!customElements.get('focus-trap')) {
539
+ customElements.define('focus-trap', FocusTrap);
540
+ }
541
+ if (!customElements.get('focus-trap-start')) {
542
+ customElements.define('focus-trap-start', FocusTrapStart);
543
+ }
544
+ if (!customElements.get('focus-trap-end')) {
545
+ customElements.define('focus-trap-end', FocusTrapEnd);
546
+ }
531
547
 
532
548
  class EventEmitter {
533
549
  #events;
@@ -625,6 +641,7 @@
625
641
  #scrollPosition = 0;
626
642
  #currentCart = null;
627
643
  #eventEmitter;
644
+ #isInitialRender = true;
628
645
 
629
646
  /**
630
647
  * Clean up event listeners when component is removed from DOM
@@ -722,13 +739,18 @@
722
739
  }
723
740
  }
724
741
 
742
+ // Insert focus trap before the cart-panel
725
743
  _.contentPanel.parentNode.insertBefore(_.focusTrap, _.contentPanel);
744
+ // Move cart-panel inside the focus trap
726
745
  _.focusTrap.appendChild(_.contentPanel);
727
746
 
747
+ // Setup the trap - this will add focus-trap-start/end elements around the content
728
748
  _.focusTrap.setupTrap();
729
749
 
730
- // Add modal overlay
731
- _.prepend(document.createElement('cart-overlay'));
750
+ // Add modal overlay if it doesn't already exist
751
+ if (!_.querySelector('cart-overlay')) {
752
+ _.prepend(document.createElement('cart-overlay'));
753
+ }
732
754
  _.#attachListeners();
733
755
  _.#bindKeyboard();
734
756
  }
@@ -786,7 +808,7 @@
786
808
 
787
809
  // Handle close buttons
788
810
  _.addEventListener('click', (e) => {
789
- if (!e.target.closest('[data-action="hide-cart"]')) return;
811
+ if (!e.target.closest('[data-action-hide-cart]')) return;
790
812
  _.hide();
791
813
  });
792
814
 
@@ -841,9 +863,9 @@
841
863
  this.updateCartItem(cartKey, 0)
842
864
  .then((updatedCart) => {
843
865
  if (updatedCart && !updatedCart.error) {
844
- // Success - remove with animation
845
- element.destroyYourself();
866
+ // Success - let smart comparison handle the removal animation
846
867
  this.#currentCart = updatedCart;
868
+ this.#renderCartItems(updatedCart);
847
869
  this.#updateCartItems(updatedCart);
848
870
 
849
871
  // Emit cart updated and data changed events
@@ -876,8 +898,9 @@
876
898
  this.updateCartItem(cartKey, quantity)
877
899
  .then((updatedCart) => {
878
900
  if (updatedCart && !updatedCart.error) {
879
- // Success - update cart data
901
+ // Success - update cart data and refresh items
880
902
  this.#currentCart = updatedCart;
903
+ this.#renderCartItems(updatedCart);
881
904
  this.#updateCartItems(updatedCart);
882
905
  element.setState('ready');
883
906
 
@@ -898,13 +921,33 @@
898
921
  }
899
922
 
900
923
  /**
901
- * Update cart items
924
+ * Update cart items display based on cart data
902
925
  * @private
903
926
  */
904
927
  #updateCartItems(cart = null) {
905
- // Placeholder for cart item updates
906
- // Could be used to sync cart items with server data
907
- cart || this.#currentCart;
928
+ const cartData = cart || this.#currentCart;
929
+ if (!cartData) return;
930
+
931
+ // Get cart sections
932
+ const hasItemsSection = this.querySelector('[data-cart-has-items]');
933
+ const emptySection = this.querySelector('[data-cart-is-empty]');
934
+ const itemsContainer = this.querySelector('[data-content-cart-items]');
935
+
936
+ if (!hasItemsSection || !emptySection || !itemsContainer) {
937
+ console.warn(
938
+ 'Cart sections not found. Expected [data-cart-has-items], [data-cart-is-empty], and [data-content-cart-items]'
939
+ );
940
+ return;
941
+ }
942
+
943
+ // Show/hide sections based on item count
944
+ if (cartData.item_count > 0) {
945
+ hasItemsSection.style.display = 'block';
946
+ emptySection.style.display = 'none';
947
+ } else {
948
+ hasItemsSection.style.display = 'none';
949
+ emptySection.style.display = 'block';
950
+ }
908
951
  }
909
952
 
910
953
  /**
@@ -959,19 +1002,157 @@
959
1002
  * @returns {Promise<Object>} Cart data object
960
1003
  */
961
1004
  refreshCart() {
1005
+ console.log('Refreshing cart...');
962
1006
  return this.getCart().then((cartData) => {
1007
+ console.log('Cart data received:', cartData);
963
1008
  if (cartData && !cartData.error) {
964
1009
  this.#currentCart = cartData;
1010
+ this.#renderCartItems(cartData);
965
1011
  this.#updateCartItems(cartData);
966
1012
 
967
1013
  // Emit cart refreshed and data changed events
968
1014
  this.#emit('cart-dialog:refreshed', { cart: cartData });
969
1015
  this.#emit('cart-dialog:data-changed', cartData);
1016
+ } else {
1017
+ console.warn('Cart data has error or is null:', cartData);
970
1018
  }
971
1019
  return cartData;
972
1020
  });
973
1021
  }
974
1022
 
1023
+ /**
1024
+ * Remove items from DOM that are no longer in cart data
1025
+ * @private
1026
+ */
1027
+ #removeItemsFromDOM(itemsContainer, newKeysSet) {
1028
+ const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
1029
+ const itemsToRemove = currentItems.filter((item) => !newKeysSet.has(item.getAttribute('key')));
1030
+
1031
+ console.log(
1032
+ `Removing ${itemsToRemove.length} items:`,
1033
+ itemsToRemove.map((item) => item.getAttribute('key'))
1034
+ );
1035
+
1036
+ itemsToRemove.forEach((item) => {
1037
+ item.destroyYourself();
1038
+ });
1039
+ }
1040
+
1041
+ /**
1042
+ * Add new items to DOM with animation delay
1043
+ * @private
1044
+ */
1045
+ #addItemsToDOM(itemsContainer, itemsToAdd, newKeys) {
1046
+ console.log(
1047
+ `Adding ${itemsToAdd.length} items:`,
1048
+ itemsToAdd.map((item) => item.key || item.id)
1049
+ );
1050
+
1051
+ // Delay adding new items by 300ms to let cart slide open first
1052
+ setTimeout(() => {
1053
+ itemsToAdd.forEach((itemData) => {
1054
+ const cartItem = CartItem.createAnimated(itemData);
1055
+ const targetIndex = newKeys.indexOf(itemData.key || itemData.id);
1056
+
1057
+ // Find the correct position to insert the new item
1058
+ if (targetIndex === 0) {
1059
+ // Insert at the beginning
1060
+ itemsContainer.insertBefore(cartItem, itemsContainer.firstChild);
1061
+ } else {
1062
+ // Find the item that should come before this one
1063
+ let insertAfter = null;
1064
+ for (let i = targetIndex - 1; i >= 0; i--) {
1065
+ const prevKey = newKeys[i];
1066
+ const prevItem = itemsContainer.querySelector(`cart-item[key="${prevKey}"]`);
1067
+ if (prevItem) {
1068
+ insertAfter = prevItem;
1069
+ break;
1070
+ }
1071
+ }
1072
+
1073
+ if (insertAfter) {
1074
+ insertAfter.insertAdjacentElement('afterend', cartItem);
1075
+ } else {
1076
+ itemsContainer.appendChild(cartItem);
1077
+ }
1078
+ }
1079
+ });
1080
+ }, 100);
1081
+ }
1082
+
1083
+ /**
1084
+ * Render cart items from Shopify cart data with smart comparison
1085
+ * @private
1086
+ */
1087
+ #renderCartItems(cartData) {
1088
+ const itemsContainer = this.querySelector('[data-content-cart-items]');
1089
+
1090
+ if (!itemsContainer || !cartData || !cartData.items) {
1091
+ console.warn('Cannot render cart items:', {
1092
+ itemsContainer: !!itemsContainer,
1093
+ cartData: !!cartData,
1094
+ items: cartData?.items?.length,
1095
+ });
1096
+ return;
1097
+ }
1098
+
1099
+ // Handle initial render - load all items without animation
1100
+ if (this.#isInitialRender) {
1101
+ console.log('Initial cart render:', cartData.items.length, 'items');
1102
+
1103
+ // Clear existing items
1104
+ itemsContainer.innerHTML = '';
1105
+
1106
+ // Create cart-item elements without animation
1107
+ cartData.items.forEach((itemData) => {
1108
+ const cartItem = new CartItem(itemData); // No animation
1109
+ itemsContainer.appendChild(cartItem);
1110
+ });
1111
+
1112
+ this.#isInitialRender = false;
1113
+ console.log('Initial render complete, container children:', itemsContainer.children.length);
1114
+ return;
1115
+ }
1116
+
1117
+ console.log('Smart rendering cart items:', cartData.items.length, 'items');
1118
+
1119
+ // Get current DOM items and their keys
1120
+ const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
1121
+ const currentKeys = new Set(currentItems.map((item) => item.getAttribute('key')));
1122
+
1123
+ // Get new cart data keys in order
1124
+ const newKeys = cartData.items.map((item) => item.key || item.id);
1125
+ const newKeysSet = new Set(newKeys);
1126
+
1127
+ // Step 1: Remove items that are no longer in cart data
1128
+ this.#removeItemsFromDOM(itemsContainer, newKeysSet);
1129
+
1130
+ // Step 2: Add new items that weren't in DOM (with animation delay)
1131
+ const itemsToAdd = cartData.items.filter(
1132
+ (itemData) => !currentKeys.has(itemData.key || itemData.id)
1133
+ );
1134
+
1135
+ this.#addItemsToDOM(itemsContainer, itemsToAdd, newKeys);
1136
+
1137
+ console.log('Smart rendering complete, container children:', itemsContainer.children.length);
1138
+ }
1139
+
1140
+ /**
1141
+ * Set the template function for cart items
1142
+ * @param {Function} templateFn - Function that takes item data and returns HTML string
1143
+ */
1144
+ setCartItemTemplate(templateFn) {
1145
+ CartItem.setTemplate(templateFn);
1146
+ }
1147
+
1148
+ /**
1149
+ * Set the processing template function for cart items
1150
+ * @param {Function} templateFn - Function that returns HTML string for processing state
1151
+ */
1152
+ setCartItemProcessingTemplate(templateFn) {
1153
+ CartItem.setProcessingTemplate(templateFn);
1154
+ }
1155
+
975
1156
  /**
976
1157
  * Shows the cart dialog and traps focus within it
977
1158
  * @param {HTMLElement} [triggerEl=null] - The element that triggered the cart dialog
@@ -1082,7 +1263,13 @@
1082
1263
  customElements.define('cart-panel', CartPanel);
1083
1264
  }
1084
1265
 
1266
+ // Make CartItem available globally for Shopify themes
1267
+ if (typeof window !== 'undefined') {
1268
+ window.CartItem = CartItem;
1269
+ }
1270
+
1085
1271
  exports.CartDialog = CartDialog;
1272
+ exports.CartItem = CartItem;
1086
1273
  exports.CartOverlay = CartOverlay;
1087
1274
  exports.CartPanel = CartPanel;
1088
1275
  exports.default = CartDialog;