@magic-spells/cart-panel 0.3.1 → 1.0.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 +7 -7
- package/dist/cart-panel.cjs.css +216 -193
- package/dist/cart-panel.cjs.js +692 -508
- package/dist/cart-panel.cjs.js.map +1 -1
- package/dist/cart-panel.css +216 -193
- package/dist/cart-panel.esm.css +216 -193
- package/dist/cart-panel.esm.js +686 -500
- package/dist/cart-panel.esm.js.map +1 -1
- package/dist/cart-panel.js +1054 -1716
- 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 +12 -8
- package/src/cart-item.js +448 -0
- package/src/cart-panel.css +231 -0
- package/src/cart-panel.js +258 -517
- package/dist/cart-panel.scss +0 -107
- package/src/cart-panel.scss +0 -107
package/src/cart-panel.js
CHANGED
|
@@ -1,159 +1,213 @@
|
|
|
1
|
-
import './cart-panel.
|
|
2
|
-
|
|
3
|
-
import '@magic-spells/focus-trap';
|
|
1
|
+
import './cart-panel.css';
|
|
4
2
|
import EventEmitter from '@magic-spells/event-emitter';
|
|
5
|
-
import { CartItem } from '
|
|
3
|
+
import { CartItem, CartItemContent, CartItemProcessing } from './cart-item.js';
|
|
4
|
+
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// CartPanel Component
|
|
7
|
+
// =============================================================================
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
|
-
*
|
|
10
|
+
* Shopping cart panel web component for Shopify.
|
|
11
|
+
* Manages cart data and AJAX requests, delegates modal behavior to dialog-panel.
|
|
9
12
|
* @extends HTMLElement
|
|
10
13
|
*/
|
|
11
|
-
class
|
|
12
|
-
#handleTransitionEnd;
|
|
14
|
+
class CartPanel extends HTMLElement {
|
|
13
15
|
#currentCart = null;
|
|
14
16
|
#eventEmitter;
|
|
15
17
|
#isInitialRender = true;
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const _ = this;
|
|
22
|
-
if (_.contentPanel) {
|
|
23
|
-
_.contentPanel.removeEventListener('transitionend', _.#handleTransitionEnd);
|
|
24
|
-
}
|
|
19
|
+
constructor() {
|
|
20
|
+
super();
|
|
21
|
+
this.#eventEmitter = new EventEmitter();
|
|
22
|
+
}
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
connectedCallback() {
|
|
25
|
+
this.#attachListeners();
|
|
26
|
+
|
|
27
|
+
// Load cart data immediately unless manual mode is enabled
|
|
28
|
+
if (!this.hasAttribute('manual')) {
|
|
29
|
+
this.refreshCart();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
disconnectedCallback() {
|
|
34
|
+
// Clean up handled by garbage collection
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
// =========================================================================
|
|
38
|
+
// Public API - Event Emitter
|
|
39
|
+
// =========================================================================
|
|
40
|
+
|
|
34
41
|
/**
|
|
35
|
-
*
|
|
36
|
-
* @
|
|
42
|
+
* Add an event listener
|
|
43
|
+
* @param {string} eventName - Name of the event
|
|
44
|
+
* @param {Function} callback - Callback function
|
|
45
|
+
* @returns {CartPanel} Returns this for method chaining
|
|
37
46
|
*/
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
on(eventName, callback) {
|
|
48
|
+
this.#eventEmitter.on(eventName, callback);
|
|
49
|
+
return this;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
/**
|
|
44
|
-
*
|
|
45
|
-
* @
|
|
53
|
+
* Remove an event listener
|
|
54
|
+
* @param {string} eventName - Name of the event
|
|
55
|
+
* @param {Function} callback - Callback function
|
|
56
|
+
* @returns {CartPanel} Returns this for method chaining
|
|
46
57
|
*/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
off(eventName, callback) {
|
|
59
|
+
this.#eventEmitter.off(eventName, callback);
|
|
60
|
+
return this;
|
|
50
61
|
}
|
|
51
62
|
|
|
63
|
+
// =========================================================================
|
|
64
|
+
// Public API - Dialog Control
|
|
65
|
+
// =========================================================================
|
|
66
|
+
|
|
52
67
|
/**
|
|
53
|
-
*
|
|
68
|
+
* Show the cart by finding and opening the nearest dialog-panel ancestor
|
|
69
|
+
* @param {HTMLElement} [triggerEl=null] - The element that triggered the open
|
|
70
|
+
* @param {Object} [cartObj=null] - Optional cart object to use instead of fetching
|
|
54
71
|
*/
|
|
55
|
-
|
|
56
|
-
super();
|
|
72
|
+
show(triggerEl = null, cartObj = null) {
|
|
57
73
|
const _ = this;
|
|
58
|
-
|
|
59
|
-
_.setAttribute('role', 'dialog');
|
|
60
|
-
_.setAttribute('aria-modal', 'true');
|
|
61
|
-
_.setAttribute('aria-hidden', 'true');
|
|
74
|
+
const dialogPanel = _.#findDialogPanel();
|
|
62
75
|
|
|
63
|
-
|
|
76
|
+
if (dialogPanel) {
|
|
77
|
+
dialogPanel.show(triggerEl);
|
|
78
|
+
_.refreshCart(cartObj);
|
|
79
|
+
_.#emit('cart-panel:show', { triggerElement: triggerEl });
|
|
80
|
+
} else {
|
|
81
|
+
console.warn('cart-panel: No dialog-panel ancestor found. Cart panel is visible but not in a modal.');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
64
84
|
|
|
65
|
-
|
|
66
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Hide the cart by finding and closing the nearest dialog-panel ancestor
|
|
87
|
+
*/
|
|
88
|
+
hide() {
|
|
89
|
+
const dialogPanel = this.#findDialogPanel();
|
|
90
|
+
if (dialogPanel) {
|
|
91
|
+
dialogPanel.hide();
|
|
92
|
+
this.#emit('cart-panel:hide', {});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
67
95
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
_.contentPanel.classList.add('hidden');
|
|
96
|
+
// =========================================================================
|
|
97
|
+
// Public API - Cart Data
|
|
98
|
+
// =========================================================================
|
|
72
99
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Fetch current cart data from Shopify
|
|
102
|
+
* @returns {Promise<Object>} Cart data object
|
|
103
|
+
*/
|
|
104
|
+
getCart() {
|
|
105
|
+
return fetch('/cart.json', {
|
|
106
|
+
credentials: 'same-origin',
|
|
107
|
+
})
|
|
108
|
+
.then((response) => {
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
throw Error(response.statusText);
|
|
111
|
+
}
|
|
112
|
+
return response.json();
|
|
113
|
+
})
|
|
114
|
+
.catch((error) => {
|
|
115
|
+
console.error('Error fetching cart:', error);
|
|
116
|
+
return { error: true, message: error.message };
|
|
117
|
+
});
|
|
77
118
|
}
|
|
78
119
|
|
|
79
|
-
|
|
80
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Update cart item quantity on Shopify
|
|
122
|
+
* @param {string|number} key - Cart item key/ID
|
|
123
|
+
* @param {number} quantity - New quantity (0 to remove)
|
|
124
|
+
* @returns {Promise<Object>} Updated cart data object
|
|
125
|
+
*/
|
|
126
|
+
updateCartItem(key, quantity) {
|
|
127
|
+
return fetch('/cart/change.json', {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
credentials: 'same-origin',
|
|
130
|
+
body: JSON.stringify({ id: key, quantity: quantity }),
|
|
131
|
+
headers: { 'Content-Type': 'application/json' },
|
|
132
|
+
})
|
|
133
|
+
.then((response) => {
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
throw Error(response.statusText);
|
|
136
|
+
}
|
|
137
|
+
return response.json();
|
|
138
|
+
})
|
|
139
|
+
.catch((error) => {
|
|
140
|
+
console.error('Error updating cart item:', error);
|
|
141
|
+
return { error: true, message: error.message };
|
|
142
|
+
});
|
|
143
|
+
}
|
|
81
144
|
|
|
82
|
-
|
|
83
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Refresh cart display - fetches from server if no cart object provided
|
|
147
|
+
* @param {Object} [cartObj=null] - Cart data object to render, or null to fetch
|
|
148
|
+
* @returns {Promise<Object>} Cart data object
|
|
149
|
+
*/
|
|
150
|
+
async refreshCart(cartObj = null) {
|
|
151
|
+
const _ = this;
|
|
84
152
|
|
|
85
|
-
if
|
|
86
|
-
|
|
87
|
-
|
|
153
|
+
// Fetch from server if no cart object provided
|
|
154
|
+
cartObj = cartObj || (await _.getCart());
|
|
155
|
+
if (!cartObj || cartObj.error) {
|
|
156
|
+
console.warn('Cart data has error or is null:', cartObj);
|
|
157
|
+
return cartObj;
|
|
88
158
|
}
|
|
89
159
|
|
|
90
|
-
|
|
91
|
-
_
|
|
92
|
-
|
|
93
|
-
_.focusTrap = document.createElement('focus-trap');
|
|
160
|
+
_.#currentCart = cartObj;
|
|
161
|
+
_.#renderCartItems(cartObj);
|
|
162
|
+
_.#renderCartPanel(cartObj);
|
|
94
163
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
164
|
+
const cartWithCalculatedFields = _.#addCalculatedFields(cartObj);
|
|
165
|
+
_.#emit('cart-panel:refreshed', { cart: cartWithCalculatedFields });
|
|
166
|
+
_.#emit('cart-panel:data-changed', cartWithCalculatedFields);
|
|
98
167
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Ensure we have labelledby and describedby references
|
|
104
|
-
if (!_.getAttribute('aria-labelledby')) {
|
|
105
|
-
const heading = _.querySelector('h1, h2, h3');
|
|
106
|
-
if (heading && !heading.id) {
|
|
107
|
-
heading.id = `${_.id}-title`;
|
|
108
|
-
}
|
|
109
|
-
if (heading?.id) {
|
|
110
|
-
_.setAttribute('aria-labelledby', heading.id);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
168
|
+
return cartObj;
|
|
169
|
+
}
|
|
113
170
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
_.#attachListeners();
|
|
119
|
-
_.#bindKeyboard();
|
|
171
|
+
// =========================================================================
|
|
172
|
+
// Public API - Templates
|
|
173
|
+
// =========================================================================
|
|
120
174
|
|
|
121
|
-
|
|
122
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Set the template function for cart items
|
|
177
|
+
* @param {string} templateName - Name of the template
|
|
178
|
+
* @param {Function} templateFn - Function that takes (itemData, cartData) and returns HTML string
|
|
179
|
+
*/
|
|
180
|
+
setCartItemTemplate(templateName, templateFn) {
|
|
181
|
+
CartItem.setTemplate(templateName, templateFn);
|
|
123
182
|
}
|
|
124
183
|
|
|
125
184
|
/**
|
|
126
|
-
*
|
|
127
|
-
* @param {
|
|
128
|
-
* @param {Function} callback - Callback function to execute when event is fired
|
|
129
|
-
* @returns {CartDialog} Returns this for method chaining
|
|
185
|
+
* Set the processing template function for cart items
|
|
186
|
+
* @param {Function} templateFn - Function that returns HTML string for processing state
|
|
130
187
|
*/
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return this;
|
|
188
|
+
setCartItemProcessingTemplate(templateFn) {
|
|
189
|
+
CartItem.setProcessingTemplate(templateFn);
|
|
134
190
|
}
|
|
135
191
|
|
|
192
|
+
// =========================================================================
|
|
193
|
+
// Private Methods - Core
|
|
194
|
+
// =========================================================================
|
|
195
|
+
|
|
136
196
|
/**
|
|
137
|
-
*
|
|
138
|
-
* @
|
|
139
|
-
* @param {Function} callback - Callback function to remove
|
|
140
|
-
* @returns {CartDialog} Returns this for method chaining
|
|
197
|
+
* Find the nearest dialog-panel ancestor
|
|
198
|
+
* @private
|
|
141
199
|
*/
|
|
142
|
-
|
|
143
|
-
this
|
|
144
|
-
return this;
|
|
200
|
+
#findDialogPanel() {
|
|
201
|
+
return this.closest('dialog-panel');
|
|
145
202
|
}
|
|
146
203
|
|
|
147
204
|
/**
|
|
148
|
-
*
|
|
149
|
-
* @param {string} eventName - Name of the event to emit
|
|
150
|
-
* @param {*} [data] - Optional data to include with the event
|
|
205
|
+
* Emit an event via EventEmitter and native CustomEvent
|
|
151
206
|
* @private
|
|
152
207
|
*/
|
|
153
208
|
#emit(eventName, data = null) {
|
|
154
209
|
this.#eventEmitter.emit(eventName, data);
|
|
155
210
|
|
|
156
|
-
// Also emit as native DOM events for better compatibility
|
|
157
211
|
this.dispatchEvent(
|
|
158
212
|
new CustomEvent(eventName, {
|
|
159
213
|
detail: data,
|
|
@@ -163,98 +217,57 @@ class CartDialog extends HTMLElement {
|
|
|
163
217
|
}
|
|
164
218
|
|
|
165
219
|
/**
|
|
166
|
-
* Attach event listeners
|
|
220
|
+
* Attach event listeners
|
|
167
221
|
* @private
|
|
168
222
|
*/
|
|
169
223
|
#attachListeners() {
|
|
170
|
-
const _ = this;
|
|
171
|
-
|
|
172
|
-
// Handle trigger buttons
|
|
173
|
-
document.addEventListener('click', (e) => {
|
|
174
|
-
const trigger = e.target.closest(`[aria-controls="${_.id}"]`);
|
|
175
|
-
if (!trigger) return;
|
|
176
|
-
|
|
177
|
-
if (trigger.getAttribute('data-prevent-default') === 'true') {
|
|
178
|
-
e.preventDefault();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
_.show(trigger);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
224
|
// Handle close buttons
|
|
185
|
-
|
|
225
|
+
this.addEventListener('click', (e) => {
|
|
186
226
|
if (!e.target.closest('[data-action-hide-cart]')) return;
|
|
187
|
-
|
|
227
|
+
this.hide();
|
|
188
228
|
});
|
|
189
229
|
|
|
190
230
|
// Handle cart item remove events
|
|
191
|
-
|
|
192
|
-
|
|
231
|
+
this.addEventListener('cart-item:remove', (e) => {
|
|
232
|
+
this.#handleCartItemRemove(e);
|
|
193
233
|
});
|
|
194
234
|
|
|
195
235
|
// Handle cart item quantity change events
|
|
196
|
-
|
|
197
|
-
|
|
236
|
+
this.addEventListener('cart-item:quantity-change', (e) => {
|
|
237
|
+
this.#handleCartItemQuantityChange(e);
|
|
198
238
|
});
|
|
199
|
-
|
|
200
|
-
// Add transition end listener
|
|
201
|
-
_.contentPanel.addEventListener('transitionend', _.#handleTransitionEnd);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Detach event listeners
|
|
206
|
-
* @private
|
|
207
|
-
*/
|
|
208
|
-
#detachListeners() {
|
|
209
|
-
const _ = this;
|
|
210
|
-
if (_.contentPanel) {
|
|
211
|
-
_.contentPanel.removeEventListener('transitionend', _.#handleTransitionEnd);
|
|
212
|
-
}
|
|
213
239
|
}
|
|
214
240
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
*/
|
|
219
|
-
#bindKeyboard() {
|
|
220
|
-
this.addEventListener('keydown', (e) => {
|
|
221
|
-
if (e.key === 'Escape') {
|
|
222
|
-
this.hide();
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
}
|
|
241
|
+
// =========================================================================
|
|
242
|
+
// Private Methods - Cart Item Event Handlers
|
|
243
|
+
// =========================================================================
|
|
226
244
|
|
|
227
245
|
/**
|
|
228
246
|
* Handle cart item removal
|
|
229
247
|
* @private
|
|
230
248
|
*/
|
|
231
249
|
#handleCartItemRemove(e) {
|
|
250
|
+
const _ = this;
|
|
232
251
|
const { cartKey, element } = e.detail;
|
|
233
252
|
|
|
234
|
-
// Set item to processing state
|
|
235
253
|
element.setState('processing');
|
|
236
254
|
|
|
237
|
-
|
|
238
|
-
this.updateCartItem(cartKey, 0)
|
|
255
|
+
_.updateCartItem(cartKey, 0)
|
|
239
256
|
.then((updatedCart) => {
|
|
240
257
|
if (updatedCart && !updatedCart.error) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
this.#emit('cart-dialog:updated', { cart: cartWithCalculatedFields });
|
|
249
|
-
this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
|
|
258
|
+
_.#currentCart = updatedCart;
|
|
259
|
+
_.#renderCartItems(updatedCart);
|
|
260
|
+
_.#renderCartPanel(updatedCart);
|
|
261
|
+
|
|
262
|
+
const cartWithCalculatedFields = _.#addCalculatedFields(updatedCart);
|
|
263
|
+
_.#emit('cart-panel:updated', { cart: cartWithCalculatedFields });
|
|
264
|
+
_.#emit('cart-panel:data-changed', cartWithCalculatedFields);
|
|
250
265
|
} else {
|
|
251
|
-
// Error - reset to ready state
|
|
252
266
|
element.setState('ready');
|
|
253
267
|
console.error('Failed to remove cart item:', cartKey);
|
|
254
268
|
}
|
|
255
269
|
})
|
|
256
270
|
.catch((error) => {
|
|
257
|
-
// Error - reset to ready state
|
|
258
271
|
element.setState('ready');
|
|
259
272
|
console.error('Error removing cart item:', error);
|
|
260
273
|
});
|
|
@@ -265,49 +278,46 @@ class CartDialog extends HTMLElement {
|
|
|
265
278
|
* @private
|
|
266
279
|
*/
|
|
267
280
|
#handleCartItemQuantityChange(e) {
|
|
281
|
+
const _ = this;
|
|
268
282
|
const { cartKey, quantity, element } = e.detail;
|
|
269
283
|
|
|
270
|
-
// Set item to processing state
|
|
271
284
|
element.setState('processing');
|
|
272
285
|
|
|
273
|
-
|
|
274
|
-
this.updateCartItem(cartKey, quantity)
|
|
286
|
+
_.updateCartItem(cartKey, quantity)
|
|
275
287
|
.then((updatedCart) => {
|
|
276
288
|
if (updatedCart && !updatedCart.error) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
this.#emit('cart-dialog:updated', { cart: cartWithCalculatedFields });
|
|
285
|
-
this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
|
|
289
|
+
_.#currentCart = updatedCart;
|
|
290
|
+
_.#renderCartItems(updatedCart);
|
|
291
|
+
_.#renderCartPanel(updatedCart);
|
|
292
|
+
|
|
293
|
+
const cartWithCalculatedFields = _.#addCalculatedFields(updatedCart);
|
|
294
|
+
_.#emit('cart-panel:updated', { cart: cartWithCalculatedFields });
|
|
295
|
+
_.#emit('cart-panel:data-changed', cartWithCalculatedFields);
|
|
286
296
|
} else {
|
|
287
|
-
// Error - reset to ready state
|
|
288
297
|
element.setState('ready');
|
|
289
298
|
console.error('Failed to update cart item quantity:', cartKey, quantity);
|
|
290
299
|
}
|
|
291
300
|
})
|
|
292
301
|
.catch((error) => {
|
|
293
|
-
// Error - reset to ready state
|
|
294
302
|
element.setState('ready');
|
|
295
303
|
console.error('Error updating cart item quantity:', error);
|
|
296
304
|
});
|
|
297
305
|
}
|
|
298
306
|
|
|
307
|
+
// =========================================================================
|
|
308
|
+
// Private Methods - Rendering
|
|
309
|
+
// =========================================================================
|
|
310
|
+
|
|
299
311
|
/**
|
|
300
|
-
* Update cart count elements across the
|
|
312
|
+
* Update cart count elements across the page
|
|
301
313
|
* @private
|
|
302
314
|
*/
|
|
303
315
|
#renderCartCount(cartData) {
|
|
304
316
|
if (!cartData) return;
|
|
305
317
|
|
|
306
|
-
// Calculate visible item count (excluding _hide_in_cart items)
|
|
307
318
|
const visibleItems = this.#getVisibleCartItems(cartData);
|
|
308
319
|
const visibleItemCount = visibleItems.reduce((total, item) => total + item.quantity, 0);
|
|
309
320
|
|
|
310
|
-
// Update all cart count elements across the site
|
|
311
321
|
const cartCountElements = document.querySelectorAll('[data-content-cart-count]');
|
|
312
322
|
cartCountElements.forEach((element) => {
|
|
313
323
|
element.textContent = visibleItemCount;
|
|
@@ -315,170 +325,110 @@ class CartDialog extends HTMLElement {
|
|
|
315
325
|
}
|
|
316
326
|
|
|
317
327
|
/**
|
|
318
|
-
* Update cart subtotal elements across the
|
|
328
|
+
* Update cart subtotal elements across the page
|
|
319
329
|
* @private
|
|
320
330
|
*/
|
|
321
331
|
#renderCartSubtotal(cartData) {
|
|
322
332
|
if (!cartData) return;
|
|
323
333
|
|
|
324
|
-
// Calculate subtotal from all items except those marked to ignore pricing
|
|
325
334
|
const pricedItems = cartData.items.filter((item) => {
|
|
326
335
|
const ignorePrice = item.properties?._ignore_price_in_subtotal;
|
|
327
336
|
return !ignorePrice;
|
|
328
337
|
});
|
|
329
338
|
const subtotal = pricedItems.reduce((total, item) => total + (item.line_price || 0), 0);
|
|
330
339
|
|
|
331
|
-
// Update all cart subtotal elements across the site
|
|
332
340
|
const cartSubtotalElements = document.querySelectorAll('[data-content-cart-subtotal]');
|
|
333
341
|
cartSubtotalElements.forEach((element) => {
|
|
334
|
-
// Format as currency (assuming cents, convert to dollars)
|
|
335
342
|
const formatted = (subtotal / 100).toFixed(2);
|
|
336
343
|
element.textContent = `$${formatted}`;
|
|
337
344
|
});
|
|
338
345
|
}
|
|
339
346
|
|
|
340
347
|
/**
|
|
341
|
-
* Update cart
|
|
348
|
+
* Update cart panel sections (has-items/empty)
|
|
342
349
|
* @private
|
|
343
350
|
*/
|
|
344
351
|
#renderCartPanel(cart = null) {
|
|
345
|
-
const
|
|
352
|
+
const _ = this;
|
|
353
|
+
const cartData = cart || _.#currentCart;
|
|
346
354
|
if (!cartData) return;
|
|
347
355
|
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
const emptySection = this.querySelector('[data-cart-is-empty]');
|
|
351
|
-
const itemsContainer = this.querySelector('[data-content-cart-items]');
|
|
356
|
+
const visibleItems = _.#getVisibleCartItems(cartData);
|
|
357
|
+
const hasVisibleItems = visibleItems.length > 0;
|
|
352
358
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
'Cart sections not found. Expected [data-cart-has-items], [data-cart-is-empty], and [data-content-cart-items]'
|
|
356
|
-
);
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
+
// Set state attribute for CSS styling (e.g., Tailwind variants)
|
|
360
|
+
_.setAttribute('state', hasVisibleItems ? 'has-items' : 'empty');
|
|
359
361
|
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
const hasVisibleItems = visibleItems.length > 0;
|
|
362
|
+
const hasItemsSection = _.querySelector('[data-cart-has-items]');
|
|
363
|
+
const emptySection = _.querySelector('[data-cart-is-empty]');
|
|
363
364
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
emptySection.style.display = 'none';
|
|
368
|
-
} else {
|
|
369
|
-
hasItemsSection.style.display = 'none';
|
|
370
|
-
emptySection.style.display = '';
|
|
365
|
+
if (hasItemsSection && emptySection) {
|
|
366
|
+
hasItemsSection.style.display = hasVisibleItems ? '' : 'none';
|
|
367
|
+
emptySection.style.display = hasVisibleItems ? 'none' : '';
|
|
371
368
|
}
|
|
372
369
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
this.#renderCartSubtotal(cartData);
|
|
370
|
+
_.#renderCartCount(cartData);
|
|
371
|
+
_.#renderCartSubtotal(cartData);
|
|
376
372
|
}
|
|
377
373
|
|
|
378
374
|
/**
|
|
379
|
-
*
|
|
380
|
-
* @
|
|
375
|
+
* Render cart items with smart add/update/remove
|
|
376
|
+
* @private
|
|
381
377
|
*/
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
credentials: 'same-origin',
|
|
386
|
-
})
|
|
387
|
-
.then((response) => {
|
|
388
|
-
if (!response.ok) {
|
|
389
|
-
throw Error(response.statusText);
|
|
390
|
-
}
|
|
391
|
-
return response.json();
|
|
392
|
-
})
|
|
393
|
-
.catch((error) => {
|
|
394
|
-
console.error('Error fetching cart:', error);
|
|
395
|
-
return { error: true, message: error.message };
|
|
396
|
-
});
|
|
397
|
-
}
|
|
378
|
+
#renderCartItems(cartData) {
|
|
379
|
+
const _ = this;
|
|
380
|
+
const itemsContainer = _.querySelector('[data-content-cart-items]');
|
|
398
381
|
|
|
399
|
-
|
|
400
|
-
* Update cart item quantity on server
|
|
401
|
-
* @param {string|number} key - Cart item key/ID
|
|
402
|
-
* @param {number} quantity - New quantity (0 to remove)
|
|
403
|
-
* @returns {Promise<Object>} Updated cart data object
|
|
404
|
-
*/
|
|
405
|
-
updateCartItem(key, quantity) {
|
|
406
|
-
return fetch('/cart/change.json', {
|
|
407
|
-
crossDomain: true,
|
|
408
|
-
method: 'POST',
|
|
409
|
-
credentials: 'same-origin',
|
|
410
|
-
body: JSON.stringify({ id: key, quantity: quantity }),
|
|
411
|
-
headers: { 'Content-Type': 'application/json' },
|
|
412
|
-
})
|
|
413
|
-
.then((response) => {
|
|
414
|
-
if (!response.ok) {
|
|
415
|
-
throw Error(response.statusText);
|
|
416
|
-
}
|
|
417
|
-
return response.json();
|
|
418
|
-
})
|
|
419
|
-
.catch((error) => {
|
|
420
|
-
console.error('Error updating cart item:', error);
|
|
421
|
-
return { error: true, message: error.message };
|
|
422
|
-
});
|
|
423
|
-
}
|
|
382
|
+
if (!itemsContainer || !cartData || !cartData.items) return;
|
|
424
383
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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);
|
|
384
|
+
const visibleItems = _.#getVisibleCartItems(cartData);
|
|
385
|
+
|
|
386
|
+
// Initial render - load all items without animation
|
|
387
|
+
if (_.#isInitialRender) {
|
|
388
|
+
itemsContainer.innerHTML = '';
|
|
389
|
+
visibleItems.forEach((itemData) => {
|
|
390
|
+
itemsContainer.appendChild(new CartItem(itemData, cartData));
|
|
391
|
+
});
|
|
392
|
+
_.#isInitialRender = false;
|
|
393
|
+
return;
|
|
444
394
|
}
|
|
445
395
|
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
396
|
+
// Get current DOM items
|
|
397
|
+
const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
|
|
398
|
+
const currentKeys = new Set(currentItems.map((item) => item.getAttribute('key')));
|
|
399
|
+
|
|
400
|
+
// Get new cart data keys
|
|
401
|
+
const newKeys = visibleItems.map((item) => item.key || item.id);
|
|
402
|
+
const newKeysSet = new Set(newKeys);
|
|
403
|
+
|
|
404
|
+
// Step 1: Remove items no longer in cart
|
|
405
|
+
_.#removeItemsFromDOM(itemsContainer, newKeysSet);
|
|
406
|
+
|
|
407
|
+
// Step 2: Update existing items
|
|
408
|
+
_.#updateItemsInDOM(itemsContainer, cartData);
|
|
409
|
+
|
|
410
|
+
// Step 3: Add new items with animation
|
|
411
|
+
const itemsToAdd = visibleItems.filter(
|
|
412
|
+
(itemData) => !currentKeys.has(itemData.key || itemData.id)
|
|
413
|
+
);
|
|
414
|
+
_.#addItemsToDOM(itemsContainer, itemsToAdd, newKeys, cartData);
|
|
463
415
|
}
|
|
464
416
|
|
|
465
417
|
/**
|
|
466
|
-
* Remove items from DOM that are no longer in cart
|
|
418
|
+
* Remove items from DOM that are no longer in cart
|
|
467
419
|
* @private
|
|
468
420
|
*/
|
|
469
421
|
#removeItemsFromDOM(itemsContainer, newKeysSet) {
|
|
470
422
|
const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
|
|
471
|
-
|
|
472
423
|
const itemsToRemove = currentItems.filter((item) => !newKeysSet.has(item.getAttribute('key')));
|
|
473
424
|
|
|
474
425
|
itemsToRemove.forEach((item) => {
|
|
475
|
-
console.log('destroy yourself', item);
|
|
476
426
|
item.destroyYourself();
|
|
477
427
|
});
|
|
478
428
|
}
|
|
479
429
|
|
|
480
430
|
/**
|
|
481
|
-
* Update existing cart-item elements with fresh
|
|
431
|
+
* Update existing cart-item elements with fresh data
|
|
482
432
|
* @private
|
|
483
433
|
*/
|
|
484
434
|
#updateItemsInDOM(itemsContainer, cartData) {
|
|
@@ -488,12 +438,7 @@ class CartDialog extends HTMLElement {
|
|
|
488
438
|
existingItems.forEach((cartItemEl) => {
|
|
489
439
|
const key = cartItemEl.getAttribute('key');
|
|
490
440
|
const updatedItemData = visibleItems.find((item) => (item.key || item.id) === key);
|
|
491
|
-
|
|
492
|
-
if (updatedItemData) {
|
|
493
|
-
// Update cart-item with fresh data and full cart context
|
|
494
|
-
// The cart-item will handle HTML comparison and only re-render if needed
|
|
495
|
-
cartItemEl.setData(updatedItemData, cartData);
|
|
496
|
-
}
|
|
441
|
+
if (updatedItemData) cartItemEl.setData(updatedItemData, cartData);
|
|
497
442
|
});
|
|
498
443
|
}
|
|
499
444
|
|
|
@@ -501,19 +446,15 @@ class CartDialog extends HTMLElement {
|
|
|
501
446
|
* Add new items to DOM with animation delay
|
|
502
447
|
* @private
|
|
503
448
|
*/
|
|
504
|
-
#addItemsToDOM(itemsContainer, itemsToAdd, newKeys) {
|
|
505
|
-
// Delay adding new items by 300ms to let cart slide open first
|
|
449
|
+
#addItemsToDOM(itemsContainer, itemsToAdd, newKeys, cartData) {
|
|
506
450
|
setTimeout(() => {
|
|
507
451
|
itemsToAdd.forEach((itemData) => {
|
|
508
|
-
const cartItem = CartItem.createAnimated(itemData);
|
|
452
|
+
const cartItem = CartItem.createAnimated(itemData, cartData);
|
|
509
453
|
const targetIndex = newKeys.indexOf(itemData.key || itemData.id);
|
|
510
454
|
|
|
511
|
-
// Find the correct position to insert the new item
|
|
512
455
|
if (targetIndex === 0) {
|
|
513
|
-
// Insert at the beginning
|
|
514
456
|
itemsContainer.insertBefore(cartItem, itemsContainer.firstChild);
|
|
515
457
|
} else {
|
|
516
|
-
// Find the item that should come before this one
|
|
517
458
|
let insertAfter = null;
|
|
518
459
|
for (let i = targetIndex - 1; i >= 0; i--) {
|
|
519
460
|
const prevKey = newKeys[i];
|
|
@@ -534,246 +475,46 @@ class CartDialog extends HTMLElement {
|
|
|
534
475
|
}, 100);
|
|
535
476
|
}
|
|
536
477
|
|
|
478
|
+
// =========================================================================
|
|
479
|
+
// Private Methods - Helpers
|
|
480
|
+
// =========================================================================
|
|
481
|
+
|
|
537
482
|
/**
|
|
538
|
-
* Filter cart items to exclude
|
|
483
|
+
* Filter cart items to exclude hidden items
|
|
539
484
|
* @private
|
|
540
485
|
*/
|
|
541
486
|
#getVisibleCartItems(cartData) {
|
|
542
487
|
if (!cartData || !cartData.items) return [];
|
|
543
488
|
return cartData.items.filter((item) => {
|
|
544
|
-
// Check for _hide_in_cart in various possible locations
|
|
545
489
|
const hidden = item.properties?._hide_in_cart;
|
|
546
|
-
|
|
547
490
|
return !hidden;
|
|
548
491
|
});
|
|
549
492
|
}
|
|
550
493
|
|
|
551
494
|
/**
|
|
552
|
-
* Add calculated fields to cart object
|
|
495
|
+
* Add calculated fields to cart object
|
|
553
496
|
* @private
|
|
554
497
|
*/
|
|
555
498
|
#addCalculatedFields(cartData) {
|
|
556
499
|
if (!cartData) return cartData;
|
|
557
500
|
|
|
558
|
-
// For display counts: use visible items (excludes _hide_in_cart)
|
|
559
501
|
const visibleItems = this.#getVisibleCartItems(cartData);
|
|
560
502
|
const calculated_count = visibleItems.reduce((total, item) => total + item.quantity, 0);
|
|
561
503
|
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
const ignorePrice = item.properties?._ignore_price_in_subtotal;
|
|
565
|
-
return !ignorePrice;
|
|
566
|
-
});
|
|
567
|
-
const calculated_subtotal = pricedItems.reduce(
|
|
568
|
-
(total, item) => total + (item.line_price || 0),
|
|
569
|
-
0
|
|
570
|
-
);
|
|
571
|
-
|
|
572
|
-
return {
|
|
573
|
-
...cartData,
|
|
574
|
-
calculated_count,
|
|
575
|
-
calculated_subtotal,
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/**
|
|
580
|
-
* Render cart items from Shopify cart data with smart comparison
|
|
581
|
-
* @private
|
|
582
|
-
*/
|
|
583
|
-
#renderCartItems(cartData) {
|
|
584
|
-
const itemsContainer = this.querySelector('[data-content-cart-items]');
|
|
585
|
-
|
|
586
|
-
if (!itemsContainer || !cartData || !cartData.items) {
|
|
587
|
-
console.warn('Cannot render cart items:', {
|
|
588
|
-
itemsContainer: !!itemsContainer,
|
|
589
|
-
cartData: !!cartData,
|
|
590
|
-
items: cartData?.items?.length,
|
|
591
|
-
});
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// Filter out items with _hide_in_cart property
|
|
596
|
-
const visibleItems = this.#getVisibleCartItems(cartData);
|
|
597
|
-
|
|
598
|
-
// Handle initial render - load all items without animation
|
|
599
|
-
if (this.#isInitialRender) {
|
|
600
|
-
// console.log('Initial cart render:', visibleItems.length, 'visible items');
|
|
601
|
-
|
|
602
|
-
// Clear existing items
|
|
603
|
-
itemsContainer.innerHTML = '';
|
|
604
|
-
|
|
605
|
-
// Create cart-item elements without animation
|
|
606
|
-
visibleItems.forEach((itemData) => {
|
|
607
|
-
const cartItem = new CartItem(itemData); // No animation
|
|
608
|
-
itemsContainer.appendChild(cartItem);
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
this.#isInitialRender = false;
|
|
612
|
-
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// Get current DOM items and their keys
|
|
617
|
-
const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
|
|
618
|
-
const currentKeys = new Set(currentItems.map((item) => item.getAttribute('key')));
|
|
619
|
-
|
|
620
|
-
// Get new cart data keys in order (only visible items)
|
|
621
|
-
const newKeys = visibleItems.map((item) => item.key || item.id);
|
|
622
|
-
const newKeysSet = new Set(newKeys);
|
|
623
|
-
|
|
624
|
-
// Step 1: Remove items that are no longer in cart data
|
|
625
|
-
this.#removeItemsFromDOM(itemsContainer, newKeysSet);
|
|
626
|
-
|
|
627
|
-
// Step 2: Update existing items with fresh data (handles templates, bundles, etc.)
|
|
628
|
-
this.#updateItemsInDOM(itemsContainer, cartData);
|
|
629
|
-
|
|
630
|
-
// Step 3: Add new items that weren't in DOM (with animation delay)
|
|
631
|
-
const itemsToAdd = visibleItems.filter(
|
|
632
|
-
(itemData) => !currentKeys.has(itemData.key || itemData.id)
|
|
633
|
-
);
|
|
634
|
-
this.#addItemsToDOM(itemsContainer, itemsToAdd, newKeys);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
/**
|
|
638
|
-
* Set the template function for cart items
|
|
639
|
-
* @param {Function} templateFn - Function that takes item data and returns HTML string
|
|
640
|
-
*/
|
|
641
|
-
setCartItemTemplate(templateName, templateFn) {
|
|
642
|
-
CartItem.setTemplate(templateName, templateFn);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* Set the processing template function for cart items
|
|
647
|
-
* @param {Function} templateFn - Function that returns HTML string for processing state
|
|
648
|
-
*/
|
|
649
|
-
setCartItemProcessingTemplate(templateFn) {
|
|
650
|
-
CartItem.setProcessingTemplate(templateFn);
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
/**
|
|
654
|
-
* Shows the cart dialog and traps focus within it
|
|
655
|
-
* @param {HTMLElement} [triggerEl=null] - The element that triggered the cart dialog
|
|
656
|
-
* @fires CartDialog#show - Fired when the cart dialog has been shown
|
|
657
|
-
*/
|
|
658
|
-
show(triggerEl = null, cartObj) {
|
|
659
|
-
const _ = this;
|
|
660
|
-
_.triggerEl = triggerEl || false;
|
|
661
|
-
|
|
662
|
-
// Lock body scrolling
|
|
663
|
-
_.#lockScroll();
|
|
664
|
-
|
|
665
|
-
// Remove the hidden class first to ensure content is rendered
|
|
666
|
-
_.contentPanel.classList.remove('hidden');
|
|
667
|
-
|
|
668
|
-
// Give the browser a moment to process before starting animation
|
|
669
|
-
requestAnimationFrame(() => {
|
|
670
|
-
// Update ARIA states
|
|
671
|
-
_.setAttribute('aria-hidden', 'false');
|
|
672
|
-
|
|
673
|
-
if (_.triggerEl) {
|
|
674
|
-
_.triggerEl.setAttribute('aria-expanded', 'true');
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// Focus management
|
|
678
|
-
const firstFocusable = _.querySelector(
|
|
679
|
-
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
680
|
-
);
|
|
681
|
-
|
|
682
|
-
if (firstFocusable) {
|
|
683
|
-
requestAnimationFrame(() => {
|
|
684
|
-
firstFocusable.focus();
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// Refresh cart data when showing
|
|
689
|
-
_.refreshCart(cartObj);
|
|
504
|
+
const pricedItems = cartData.items.filter((item) => !item.properties?._ignore_price_in_subtotal);
|
|
505
|
+
const calculated_subtotal = pricedItems.reduce((total, item) => total + (item.line_price || 0), 0);
|
|
690
506
|
|
|
691
|
-
|
|
692
|
-
_.#emit('cart-dialog:show', { triggerElement: _.triggerEl });
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
/**
|
|
697
|
-
* Hides the cart dialog and restores focus
|
|
698
|
-
* @fires CartDialog#hide - Fired when the cart dialog has started hiding (transition begins)
|
|
699
|
-
* @fires CartDialog#afterHide - Fired when the cart dialog has completed its hide transition
|
|
700
|
-
*/
|
|
701
|
-
hide() {
|
|
702
|
-
const _ = this;
|
|
703
|
-
|
|
704
|
-
// Update ARIA states
|
|
705
|
-
if (_.triggerEl) {
|
|
706
|
-
// remove focus from modal panel first
|
|
707
|
-
_.triggerEl.focus();
|
|
708
|
-
// mark trigger as no longer expanded
|
|
709
|
-
_.triggerEl.setAttribute('aria-expanded', 'false');
|
|
710
|
-
} else {
|
|
711
|
-
// If no trigger element, blur any focused element inside the panel
|
|
712
|
-
const activeElement = document.activeElement;
|
|
713
|
-
if (activeElement && _.contains(activeElement)) {
|
|
714
|
-
activeElement.blur();
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
requestAnimationFrame(() => {
|
|
719
|
-
// Set aria-hidden to start transition
|
|
720
|
-
// The transitionend event handler will add display:none when complete
|
|
721
|
-
_.setAttribute('aria-hidden', 'true');
|
|
722
|
-
|
|
723
|
-
// Emit hide event - cart dialog is now starting to hide
|
|
724
|
-
_.#emit('cart-dialog:hide', { triggerElement: _.triggerEl });
|
|
725
|
-
|
|
726
|
-
// Restore body scroll
|
|
727
|
-
_.#restoreScroll();
|
|
728
|
-
});
|
|
507
|
+
return { ...cartData, calculated_count, calculated_subtotal };
|
|
729
508
|
}
|
|
730
509
|
}
|
|
731
510
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
*/
|
|
736
|
-
class CartOverlay extends HTMLElement {
|
|
737
|
-
constructor() {
|
|
738
|
-
super();
|
|
739
|
-
this.setAttribute('tabindex', '-1');
|
|
740
|
-
this.setAttribute('aria-hidden', 'true');
|
|
741
|
-
this.cartDialog = this.closest('cart-dialog');
|
|
742
|
-
this.#attachListeners();
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
#attachListeners() {
|
|
746
|
-
this.addEventListener('click', () => {
|
|
747
|
-
this.cartDialog.hide();
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
}
|
|
511
|
+
// =============================================================================
|
|
512
|
+
// Register Custom Elements
|
|
513
|
+
// =============================================================================
|
|
751
514
|
|
|
752
|
-
/**
|
|
753
|
-
* Custom element that wraps the content of the cart dialog
|
|
754
|
-
* @extends HTMLElement
|
|
755
|
-
*/
|
|
756
|
-
class CartPanel extends HTMLElement {
|
|
757
|
-
constructor() {
|
|
758
|
-
super();
|
|
759
|
-
this.setAttribute('role', 'document');
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
if (!customElements.get('cart-dialog')) {
|
|
764
|
-
customElements.define('cart-dialog', CartDialog);
|
|
765
|
-
}
|
|
766
|
-
if (!customElements.get('cart-overlay')) {
|
|
767
|
-
customElements.define('cart-overlay', CartOverlay);
|
|
768
|
-
}
|
|
769
515
|
if (!customElements.get('cart-panel')) {
|
|
770
516
|
customElements.define('cart-panel', CartPanel);
|
|
771
517
|
}
|
|
772
518
|
|
|
773
|
-
export {
|
|
774
|
-
export default
|
|
775
|
-
|
|
776
|
-
// Make CartItem available globally for Shopify themes
|
|
777
|
-
if (typeof window !== 'undefined') {
|
|
778
|
-
window.CartItem = CartItem;
|
|
779
|
-
}
|
|
519
|
+
export { CartPanel, CartItem, CartItemContent, CartItemProcessing };
|
|
520
|
+
export default CartPanel;
|