@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.
- package/dist/cart-panel.cjs.css +6 -3
- package/dist/cart-panel.cjs.js +185 -11
- package/dist/cart-panel.cjs.js.map +1 -1
- package/dist/cart-panel.css +6 -3
- package/dist/cart-panel.esm.css +6 -3
- package/dist/cart-panel.esm.js +182 -11
- package/dist/cart-panel.esm.js.map +1 -1
- package/dist/cart-panel.js +210 -23
- 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 +3 -3
- package/src/cart-panel.js +180 -12
package/dist/cart-panel.js
CHANGED
|
@@ -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
|
|
68
|
-
|
|
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
|
-
|
|
115
|
+
requestAnimationFrame(() => {
|
|
105
116
|
this.setState('ready');
|
|
106
|
-
|
|
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.
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
_.
|
|
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
|
|
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 -
|
|
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
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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;
|