@magic-spells/cart-panel 0.3.0 → 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.
@@ -2,133 +2,495 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- require('@magic-spells/focus-trap');
6
5
  var EventEmitter = require('@magic-spells/event-emitter');
7
- var cartItem = require('@magic-spells/cart-item');
6
+
7
+ // =============================================================================
8
+ // CartItem Component
9
+ // =============================================================================
8
10
 
9
11
  /**
10
- * Custom element that creates an accessible modal cart dialog with focus management
11
- * @extends HTMLElement
12
+ * CartItem class that handles the functionality of a cart item component
12
13
  */
13
- class CartDialog extends HTMLElement {
14
- #handleTransitionEnd;
15
- #currentCart = null;
16
- #eventEmitter;
17
- #isInitialRender = true;
14
+ class CartItem extends HTMLElement {
15
+ // Static template functions shared across all instances
16
+ static #templates = new Map();
17
+ static #processingTemplate = null;
18
+
19
+ // Private fields
20
+ #currentState = 'ready';
21
+ #isDestroying = false;
22
+ #isAppearing = false;
23
+ #handlers = {};
24
+ #itemData = null;
25
+ #cartData = null;
26
+ #lastRenderedHTML = '';
27
+
28
+ /**
29
+ * Set the template function for rendering cart items
30
+ * @param {string} name - Template name ('default' for default template)
31
+ * @param {Function} templateFn - Function that takes (itemData, cartData) and returns HTML string
32
+ */
33
+ static setTemplate(name, templateFn) {
34
+ if (typeof name !== 'string') {
35
+ throw new Error('Template name must be a string');
36
+ }
37
+ if (typeof templateFn !== 'function') {
38
+ throw new Error('Template must be a function');
39
+ }
40
+ CartItem.#templates.set(name, templateFn);
41
+ }
18
42
 
19
43
  /**
20
- * Clean up event listeners when component is removed from DOM
44
+ * Set the processing template function for rendering processing overlay
45
+ * @param {Function} templateFn - Function that returns HTML string for processing state
21
46
  */
47
+ static setProcessingTemplate(templateFn) {
48
+ if (typeof templateFn !== 'function') {
49
+ throw new Error('Processing template must be a function');
50
+ }
51
+ CartItem.#processingTemplate = templateFn;
52
+ }
53
+
54
+ /**
55
+ * Create a cart item with appearing animation
56
+ * @param {Object} itemData - Shopify cart item data
57
+ * @param {Object} cartData - Full Shopify cart object
58
+ * @returns {CartItem} Cart item instance that will animate in
59
+ */
60
+ static createAnimated(itemData, cartData) {
61
+ return new CartItem(itemData, cartData, { animate: true });
62
+ }
63
+
64
+ /**
65
+ * Define which attributes should be observed for changes
66
+ */
67
+ static get observedAttributes() {
68
+ return ['state', 'key'];
69
+ }
70
+
71
+ /**
72
+ * Called when observed attributes change
73
+ */
74
+ attributeChangedCallback(name, oldValue, newValue) {
75
+ if (oldValue === newValue) return;
76
+
77
+ if (name === 'state') {
78
+ this.#currentState = newValue || 'ready';
79
+ }
80
+ }
81
+
82
+ constructor(itemData = null, cartData = null, options = {}) {
83
+ super();
84
+
85
+ // Store item and cart data if provided
86
+ this.#itemData = itemData;
87
+ this.#cartData = cartData;
88
+
89
+ // Set initial state - start with 'appearing' only if explicitly requested
90
+ const shouldAnimate = options.animate || this.hasAttribute('animate-in');
91
+ this.#currentState =
92
+ itemData && shouldAnimate ? 'appearing' : this.getAttribute('state') || 'ready';
93
+
94
+ // Bind event handlers
95
+ this.#handlers = {
96
+ click: this.#handleClick.bind(this),
97
+ change: this.#handleChange.bind(this),
98
+ transitionEnd: this.#handleTransitionEnd.bind(this),
99
+ };
100
+ }
101
+
102
+ connectedCallback() {
103
+ const _ = this;
104
+
105
+ // If we have item data, render it first
106
+ if (_.#itemData) _.#render();
107
+
108
+ // Find child elements and attach listeners
109
+ _.#queryDOM();
110
+ _.#updateLinePriceElements();
111
+ _.#attachListeners();
112
+
113
+ // If we started with 'appearing' state, handle the entry animation
114
+ if (_.#currentState === 'appearing') {
115
+ _.setAttribute('state', 'appearing');
116
+ _.#isAppearing = true;
117
+
118
+ requestAnimationFrame(() => {
119
+ _.style.height = `${_.scrollHeight}px`;
120
+ requestAnimationFrame(() => _.setState('ready'));
121
+ });
122
+ }
123
+ }
124
+
22
125
  disconnectedCallback() {
126
+ // Cleanup event listeners
127
+ this.#detachListeners();
128
+ }
129
+
130
+ /**
131
+ * Query and cache DOM elements
132
+ */
133
+ #queryDOM() {
134
+ this.content = this.querySelector('cart-item-content');
135
+ this.processing = this.querySelector('cart-item-processing');
136
+ }
137
+
138
+ /**
139
+ * Attach event listeners
140
+ */
141
+ #attachListeners() {
142
+ const _ = this;
143
+ _.addEventListener('click', _.#handlers.click);
144
+ _.addEventListener('change', _.#handlers.change);
145
+ _.addEventListener('quantity-input:change', _.#handlers.change);
146
+ _.addEventListener('transitionend', _.#handlers.transitionEnd);
147
+ }
148
+
149
+ /**
150
+ * Detach event listeners
151
+ */
152
+ #detachListeners() {
23
153
  const _ = this;
24
- if (_.contentPanel) {
25
- _.contentPanel.removeEventListener('transitionend', _.#handleTransitionEnd);
154
+ _.removeEventListener('click', _.#handlers.click);
155
+ _.removeEventListener('change', _.#handlers.change);
156
+ _.removeEventListener('quantity-input:change', _.#handlers.change);
157
+ _.removeEventListener('transitionend', _.#handlers.transitionEnd);
158
+ }
159
+
160
+ /**
161
+ * Get the current state
162
+ */
163
+ get state() {
164
+ return this.#currentState;
165
+ }
166
+
167
+ /**
168
+ * Get the cart key for this item
169
+ */
170
+ get cartKey() {
171
+ return this.getAttribute('key');
172
+ }
173
+
174
+ /**
175
+ * Handle click events (for Remove buttons, etc.)
176
+ */
177
+ #handleClick(e) {
178
+ // Check if clicked element is a remove button
179
+ const removeButton = e.target.closest('[data-action-remove-item]');
180
+ if (removeButton) {
181
+ e.preventDefault();
182
+ this.#emitRemoveEvent();
26
183
  }
184
+ }
27
185
 
28
- // Ensure body scroll is restored if component is removed while open
29
- document.body.classList.remove('overflow-hidden');
30
- this.#restoreScroll();
186
+ /**
187
+ * Handle change events (for quantity inputs and quantity-input component)
188
+ */
189
+ #handleChange(e) {
190
+ // Check if event is from quantity-input component
191
+ if (e.type === 'quantity-input:change') {
192
+ this.#emitQuantityChangeEvent(e.detail.value);
193
+ return;
194
+ }
31
195
 
32
- // Detach event listeners
33
- this.#detachListeners();
196
+ // Check if changed element is a quantity input
197
+ const quantityInput = e.target.closest('[data-cart-quantity]');
198
+ if (quantityInput) {
199
+ this.#emitQuantityChangeEvent(quantityInput.value);
200
+ }
34
201
  }
35
202
 
36
203
  /**
37
- * Locks body scrolling
38
- * @private
204
+ * Handle transition end events for destroy animation and appearing animation
39
205
  */
40
- #lockScroll() {
41
- // Apply overflow hidden to body
42
- document.body.classList.add('overflow-hidden');
206
+ #handleTransitionEnd(e) {
207
+ if (e.propertyName === 'height' && this.#isDestroying) {
208
+ // Remove from DOM after height animation completes
209
+ this.remove();
210
+ } else if (e.propertyName === 'height' && this.#isAppearing) {
211
+ // Remove explicit height after appearing animation completes
212
+ this.style.height = '';
213
+ this.#isAppearing = false;
214
+ }
43
215
  }
44
216
 
45
217
  /**
46
- * Restores body scrolling when cart dialog is closed
47
- * @private
218
+ * Emit remove event
48
219
  */
49
- #restoreScroll() {
50
- // Remove overflow hidden from body
51
- document.body.classList.remove('overflow-hidden');
220
+ #emitRemoveEvent() {
221
+ this.dispatchEvent(
222
+ new CustomEvent('cart-item:remove', {
223
+ bubbles: true,
224
+ detail: {
225
+ cartKey: this.cartKey,
226
+ element: this,
227
+ },
228
+ })
229
+ );
52
230
  }
53
231
 
54
232
  /**
55
- * Initializes the cart dialog, sets up focus trap and overlay
233
+ * Emit quantity change event
56
234
  */
57
- constructor() {
58
- super();
235
+ #emitQuantityChangeEvent(quantity) {
236
+ this.dispatchEvent(
237
+ new CustomEvent('cart-item:quantity-change', {
238
+ bubbles: true,
239
+ detail: {
240
+ cartKey: this.cartKey,
241
+ quantity: parseInt(quantity),
242
+ element: this,
243
+ },
244
+ })
245
+ );
246
+ }
247
+
248
+ /**
249
+ * Render cart item from data using the appropriate template
250
+ */
251
+ #render() {
59
252
  const _ = this;
60
- _.id = _.getAttribute('id');
61
- _.setAttribute('role', 'dialog');
62
- _.setAttribute('aria-modal', 'true');
63
- _.setAttribute('aria-hidden', 'true');
253
+ if (!_.#itemData || CartItem.#templates.size === 0) return;
64
254
 
65
- _.triggerEl = null;
255
+ // Set the key attribute from item data
256
+ const key = _.#itemData.key || _.#itemData.id;
257
+ if (key) _.setAttribute('key', key);
66
258
 
67
- // Initialize event emitter
68
- _.#eventEmitter = new EventEmitter();
259
+ // Generate HTML from template and store for future comparisons
260
+ const templateHTML = _.#generateTemplateHTML();
261
+ _.#lastRenderedHTML = templateHTML;
69
262
 
70
- // Create a handler for transition end events
71
- _.#handleTransitionEnd = (e) => {
72
- if (e.propertyName === 'opacity' && _.getAttribute('aria-hidden') === 'true') {
73
- _.contentPanel.classList.add('hidden');
263
+ // Generate processing HTML from template or use default
264
+ const processingHTML = CartItem.#processingTemplate
265
+ ? CartItem.#processingTemplate()
266
+ : '<div class="cart-item-loader"></div>';
74
267
 
75
- // Emit afterHide event - cart dialog has completed its transition
76
- _.#emit('cart-dialog:afterHide', { triggerElement: _.triggerEl });
77
- }
78
- };
268
+ // Create the cart-item structure with template content inside cart-item-content
269
+ _.innerHTML = `
270
+ <cart-item-content>
271
+ ${templateHTML}
272
+ </cart-item-content>
273
+ <cart-item-processing>
274
+ ${processingHTML}
275
+ </cart-item-processing>
276
+ `;
79
277
  }
80
278
 
81
- connectedCallback() {
279
+ /**
280
+ * Update the cart item with new data
281
+ * @param {Object} itemData - Shopify cart item data
282
+ * @param {Object} cartData - Full Shopify cart object
283
+ */
284
+ setData(itemData, cartData = null) {
82
285
  const _ = this;
83
286
 
84
- // Now that we're in the DOM, find the content panel and set up focus trap
85
- _.contentPanel = _.querySelector('cart-panel');
287
+ // Update internal data
288
+ _.#itemData = itemData;
289
+ if (cartData) _.#cartData = cartData;
290
+
291
+ // Generate new HTML with updated data
292
+ const newHTML = _.#generateTemplateHTML();
86
293
 
87
- if (!_.contentPanel) {
88
- console.error('cart-panel element not found inside cart-dialog');
294
+ // Compare with previously rendered HTML
295
+ if (newHTML === _.#lastRenderedHTML) {
296
+ // HTML hasn't changed, just reset processing state
297
+ _.setState('ready');
298
+ _.#updateQuantityInput();
89
299
  return;
90
300
  }
91
301
 
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');
302
+ // HTML is different, proceed with full update
303
+ _.setState('ready');
304
+ _.#render();
305
+ _.#queryDOM();
306
+ _.#updateLinePriceElements();
307
+ }
96
308
 
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));
309
+ /**
310
+ * Generate HTML from the current template with current data
311
+ * @returns {string} Generated HTML string or empty string if no template
312
+ * @private
313
+ */
314
+ #generateTemplateHTML() {
315
+ // If no templates are available, return empty string
316
+ if (!this.#itemData || CartItem.#templates.size === 0) {
317
+ return '';
318
+ }
319
+
320
+ // Determine which template to use
321
+ const templateName = this.#itemData.properties?._cart_template || 'default';
322
+ const templateFn = CartItem.#templates.get(templateName) || CartItem.#templates.get('default');
100
323
 
101
- // Insert focus trap inside the cart-panel
102
- _.contentPanel.appendChild(_.focusTrap);
324
+ if (!templateFn) {
325
+ return '';
103
326
  }
104
327
 
105
- // Ensure we have labelledby and describedby references
106
- if (!_.getAttribute('aria-labelledby')) {
107
- const heading = _.querySelector('h1, h2, h3');
108
- if (heading && !heading.id) {
109
- heading.id = `${_.id}-title`;
110
- }
111
- if (heading?.id) {
112
- _.setAttribute('aria-labelledby', heading.id);
113
- }
328
+ // Generate and return HTML from template
329
+ return templateFn(this.#itemData, this.#cartData);
330
+ }
331
+
332
+ /**
333
+ * Update quantity input component to match server data
334
+ * @private
335
+ */
336
+ #updateQuantityInput() {
337
+ if (!this.#itemData) return;
338
+
339
+ const quantityInput = this.querySelector('quantity-input');
340
+ if (quantityInput) {
341
+ quantityInput.value = this.#itemData.quantity;
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Update elements with data-content-line-price attribute
347
+ * @private
348
+ */
349
+ #updateLinePriceElements() {
350
+ if (!this.#itemData) return;
351
+
352
+ const linePriceElements = this.querySelectorAll('[data-content-line-price]');
353
+ const formattedLinePrice = this.#formatCurrency(this.#itemData.line_price || 0);
354
+
355
+ linePriceElements.forEach((element) => {
356
+ element.textContent = formattedLinePrice;
357
+ });
358
+ }
359
+
360
+ /**
361
+ * Format currency value from cents to dollar string
362
+ * @param {number} cents - Price in cents
363
+ * @returns {string} Formatted currency string (e.g., "$29.99")
364
+ * @private
365
+ */
366
+ #formatCurrency(cents) {
367
+ if (typeof cents !== 'number') return '$0.00';
368
+ return `$${(cents / 100).toFixed(2)}`;
369
+ }
370
+
371
+ /**
372
+ * Get the current item data
373
+ */
374
+ get itemData() {
375
+ return this.#itemData;
376
+ }
377
+
378
+ /**
379
+ * Set the state of the cart item
380
+ * @param {string} state - 'ready', 'processing', 'destroying', or 'appearing'
381
+ */
382
+ setState(state) {
383
+ if (['ready', 'processing', 'destroying', 'appearing'].includes(state)) {
384
+ this.setAttribute('state', state);
114
385
  }
386
+ }
387
+
388
+ /**
389
+ * Gracefully animate this cart item closed, then remove it
390
+ */
391
+ destroyYourself() {
392
+ const _ = this;
393
+
394
+ // bail if already in the middle of a destroy cycle
395
+ if (_.#isDestroying) return;
396
+ _.#isDestroying = true;
397
+
398
+ // snapshot the current rendered height before applying any "destroying" styles
399
+ const initialHeight = _.offsetHeight;
400
+ _.setState('destroying');
401
+
402
+ // lock the measured height on the next animation frame to ensure layout is fully flushed
403
+ requestAnimationFrame(() => {
404
+ _.style.height = `${initialHeight}px`;
405
+
406
+ // read the css custom property for timing, defaulting to 400ms
407
+ const destroyDuration =
408
+ getComputedStyle(_).getPropertyValue('--cart-item-destroying-duration')?.trim() || '400ms';
409
+
410
+ // animate only the height to zero; other properties stay under stylesheet control
411
+ _.style.transition = `height ${destroyDuration} ease`;
412
+ _.style.height = '0px';
413
+
414
+ setTimeout(() => _.remove(), 600);
415
+ });
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Supporting component classes for cart item
421
+ */
422
+ class CartItemContent extends HTMLElement {
423
+ constructor() {
424
+ super();
425
+ }
426
+ }
427
+
428
+ class CartItemProcessing extends HTMLElement {
429
+ constructor() {
430
+ super();
431
+ }
432
+ }
433
+
434
+ // =============================================================================
435
+ // Register Custom Elements
436
+ // =============================================================================
437
+
438
+ if (!customElements.get('cart-item')) {
439
+ customElements.define('cart-item', CartItem);
440
+ }
441
+ if (!customElements.get('cart-item-content')) {
442
+ customElements.define('cart-item-content', CartItemContent);
443
+ }
444
+ if (!customElements.get('cart-item-processing')) {
445
+ customElements.define('cart-item-processing', CartItemProcessing);
446
+ }
447
+
448
+ // Make CartItem available globally for Shopify themes
449
+ if (typeof window !== 'undefined') {
450
+ window.CartItem = CartItem;
451
+ }
452
+
453
+ // =============================================================================
454
+ // CartPanel Component
455
+ // =============================================================================
456
+
457
+ /**
458
+ * Shopping cart panel web component for Shopify.
459
+ * Manages cart data and AJAX requests, delegates modal behavior to dialog-panel.
460
+ * @extends HTMLElement
461
+ */
462
+ class CartPanel extends HTMLElement {
463
+ #currentCart = null;
464
+ #eventEmitter;
465
+ #isInitialRender = true;
466
+
467
+ constructor() {
468
+ super();
469
+ this.#eventEmitter = new EventEmitter();
470
+ }
471
+
472
+ connectedCallback() {
473
+ this.#attachListeners();
115
474
 
116
- // Add modal overlay if it doesn't already exist
117
- if (!_.querySelector('cart-overlay')) {
118
- _.prepend(document.createElement('cart-overlay'));
475
+ // Load cart data immediately unless manual mode is enabled
476
+ if (!this.hasAttribute('manual')) {
477
+ this.refreshCart();
119
478
  }
120
- _.#attachListeners();
121
- _.#bindKeyboard();
479
+ }
122
480
 
123
- // Load cart data immediately after component initialization
124
- _.refreshCart();
481
+ disconnectedCallback() {
482
+ // Clean up handled by garbage collection
125
483
  }
126
484
 
485
+ // =========================================================================
486
+ // Public API - Event Emitter
487
+ // =========================================================================
488
+
127
489
  /**
128
- * Event emitter method - Add an event listener with a cleaner API
129
- * @param {string} eventName - Name of the event to listen for
130
- * @param {Function} callback - Callback function to execute when event is fired
131
- * @returns {CartDialog} Returns this for method chaining
490
+ * Add an event listener
491
+ * @param {string} eventName - Name of the event
492
+ * @param {Function} callback - Callback function
493
+ * @returns {CartPanel} Returns this for method chaining
132
494
  */
133
495
  on(eventName, callback) {
134
496
  this.#eventEmitter.on(eventName, callback);
@@ -136,26 +498,164 @@ class CartDialog extends HTMLElement {
136
498
  }
137
499
 
138
500
  /**
139
- * Event emitter method - Remove an event listener
140
- * @param {string} eventName - Name of the event to stop listening for
141
- * @param {Function} callback - Callback function to remove
142
- * @returns {CartDialog} Returns this for method chaining
501
+ * Remove an event listener
502
+ * @param {string} eventName - Name of the event
503
+ * @param {Function} callback - Callback function
504
+ * @returns {CartPanel} Returns this for method chaining
143
505
  */
144
506
  off(eventName, callback) {
145
507
  this.#eventEmitter.off(eventName, callback);
146
508
  return this;
147
509
  }
148
510
 
511
+ // =========================================================================
512
+ // Public API - Dialog Control
513
+ // =========================================================================
514
+
515
+ /**
516
+ * Show the cart by finding and opening the nearest dialog-panel ancestor
517
+ * @param {HTMLElement} [triggerEl=null] - The element that triggered the open
518
+ * @param {Object} [cartObj=null] - Optional cart object to use instead of fetching
519
+ */
520
+ show(triggerEl = null, cartObj = null) {
521
+ const _ = this;
522
+ const dialogPanel = _.#findDialogPanel();
523
+
524
+ if (dialogPanel) {
525
+ dialogPanel.show(triggerEl);
526
+ _.refreshCart(cartObj);
527
+ _.#emit('cart-panel:show', { triggerElement: triggerEl });
528
+ } else {
529
+ console.warn('cart-panel: No dialog-panel ancestor found. Cart panel is visible but not in a modal.');
530
+ }
531
+ }
532
+
533
+ /**
534
+ * Hide the cart by finding and closing the nearest dialog-panel ancestor
535
+ */
536
+ hide() {
537
+ const dialogPanel = this.#findDialogPanel();
538
+ if (dialogPanel) {
539
+ dialogPanel.hide();
540
+ this.#emit('cart-panel:hide', {});
541
+ }
542
+ }
543
+
544
+ // =========================================================================
545
+ // Public API - Cart Data
546
+ // =========================================================================
547
+
548
+ /**
549
+ * Fetch current cart data from Shopify
550
+ * @returns {Promise<Object>} Cart data object
551
+ */
552
+ getCart() {
553
+ return fetch('/cart.json', {
554
+ credentials: 'same-origin',
555
+ })
556
+ .then((response) => {
557
+ if (!response.ok) {
558
+ throw Error(response.statusText);
559
+ }
560
+ return response.json();
561
+ })
562
+ .catch((error) => {
563
+ console.error('Error fetching cart:', error);
564
+ return { error: true, message: error.message };
565
+ });
566
+ }
567
+
149
568
  /**
150
- * Internal method to emit events via the event emitter
151
- * @param {string} eventName - Name of the event to emit
152
- * @param {*} [data] - Optional data to include with the event
569
+ * Update cart item quantity on Shopify
570
+ * @param {string|number} key - Cart item key/ID
571
+ * @param {number} quantity - New quantity (0 to remove)
572
+ * @returns {Promise<Object>} Updated cart data object
573
+ */
574
+ updateCartItem(key, quantity) {
575
+ return fetch('/cart/change.json', {
576
+ method: 'POST',
577
+ credentials: 'same-origin',
578
+ body: JSON.stringify({ id: key, quantity: quantity }),
579
+ headers: { 'Content-Type': 'application/json' },
580
+ })
581
+ .then((response) => {
582
+ if (!response.ok) {
583
+ throw Error(response.statusText);
584
+ }
585
+ return response.json();
586
+ })
587
+ .catch((error) => {
588
+ console.error('Error updating cart item:', error);
589
+ return { error: true, message: error.message };
590
+ });
591
+ }
592
+
593
+ /**
594
+ * Refresh cart display - fetches from server if no cart object provided
595
+ * @param {Object} [cartObj=null] - Cart data object to render, or null to fetch
596
+ * @returns {Promise<Object>} Cart data object
597
+ */
598
+ async refreshCart(cartObj = null) {
599
+ const _ = this;
600
+
601
+ // Fetch from server if no cart object provided
602
+ cartObj = cartObj || (await _.getCart());
603
+ if (!cartObj || cartObj.error) {
604
+ console.warn('Cart data has error or is null:', cartObj);
605
+ return cartObj;
606
+ }
607
+
608
+ _.#currentCart = cartObj;
609
+ _.#renderCartItems(cartObj);
610
+ _.#renderCartPanel(cartObj);
611
+
612
+ const cartWithCalculatedFields = _.#addCalculatedFields(cartObj);
613
+ _.#emit('cart-panel:refreshed', { cart: cartWithCalculatedFields });
614
+ _.#emit('cart-panel:data-changed', cartWithCalculatedFields);
615
+
616
+ return cartObj;
617
+ }
618
+
619
+ // =========================================================================
620
+ // Public API - Templates
621
+ // =========================================================================
622
+
623
+ /**
624
+ * Set the template function for cart items
625
+ * @param {string} templateName - Name of the template
626
+ * @param {Function} templateFn - Function that takes (itemData, cartData) and returns HTML string
627
+ */
628
+ setCartItemTemplate(templateName, templateFn) {
629
+ CartItem.setTemplate(templateName, templateFn);
630
+ }
631
+
632
+ /**
633
+ * Set the processing template function for cart items
634
+ * @param {Function} templateFn - Function that returns HTML string for processing state
635
+ */
636
+ setCartItemProcessingTemplate(templateFn) {
637
+ CartItem.setProcessingTemplate(templateFn);
638
+ }
639
+
640
+ // =========================================================================
641
+ // Private Methods - Core
642
+ // =========================================================================
643
+
644
+ /**
645
+ * Find the nearest dialog-panel ancestor
646
+ * @private
647
+ */
648
+ #findDialogPanel() {
649
+ return this.closest('dialog-panel');
650
+ }
651
+
652
+ /**
653
+ * Emit an event via EventEmitter and native CustomEvent
153
654
  * @private
154
655
  */
155
656
  #emit(eventName, data = null) {
156
657
  this.#eventEmitter.emit(eventName, data);
157
658
 
158
- // Also emit as native DOM events for better compatibility
159
659
  this.dispatchEvent(
160
660
  new CustomEvent(eventName, {
161
661
  detail: data,
@@ -165,98 +665,57 @@ class CartDialog extends HTMLElement {
165
665
  }
166
666
 
167
667
  /**
168
- * Attach event listeners for cart dialog functionality
668
+ * Attach event listeners
169
669
  * @private
170
670
  */
171
671
  #attachListeners() {
172
- const _ = this;
173
-
174
- // Handle trigger buttons
175
- document.addEventListener('click', (e) => {
176
- const trigger = e.target.closest(`[aria-controls="${_.id}"]`);
177
- if (!trigger) return;
178
-
179
- if (trigger.getAttribute('data-prevent-default') === 'true') {
180
- e.preventDefault();
181
- }
182
-
183
- _.show(trigger);
184
- });
185
-
186
672
  // Handle close buttons
187
- _.addEventListener('click', (e) => {
673
+ this.addEventListener('click', (e) => {
188
674
  if (!e.target.closest('[data-action-hide-cart]')) return;
189
- _.hide();
675
+ this.hide();
190
676
  });
191
677
 
192
678
  // Handle cart item remove events
193
- _.addEventListener('cart-item:remove', (e) => {
194
- _.#handleCartItemRemove(e);
679
+ this.addEventListener('cart-item:remove', (e) => {
680
+ this.#handleCartItemRemove(e);
195
681
  });
196
682
 
197
683
  // Handle cart item quantity change events
198
- _.addEventListener('cart-item:quantity-change', (e) => {
199
- _.#handleCartItemQuantityChange(e);
684
+ this.addEventListener('cart-item:quantity-change', (e) => {
685
+ this.#handleCartItemQuantityChange(e);
200
686
  });
201
-
202
- // Add transition end listener
203
- _.contentPanel.addEventListener('transitionend', _.#handleTransitionEnd);
204
687
  }
205
688
 
206
- /**
207
- * Detach event listeners
208
- * @private
209
- */
210
- #detachListeners() {
211
- const _ = this;
212
- if (_.contentPanel) {
213
- _.contentPanel.removeEventListener('transitionend', _.#handleTransitionEnd);
214
- }
215
- }
216
-
217
- /**
218
- * Binds keyboard events for accessibility
219
- * @private
220
- */
221
- #bindKeyboard() {
222
- this.addEventListener('keydown', (e) => {
223
- if (e.key === 'Escape') {
224
- this.hide();
225
- }
226
- });
227
- }
689
+ // =========================================================================
690
+ // Private Methods - Cart Item Event Handlers
691
+ // =========================================================================
228
692
 
229
693
  /**
230
694
  * Handle cart item removal
231
695
  * @private
232
696
  */
233
697
  #handleCartItemRemove(e) {
698
+ const _ = this;
234
699
  const { cartKey, element } = e.detail;
235
700
 
236
- // Set item to processing state
237
701
  element.setState('processing');
238
702
 
239
- // Remove item by setting quantity to 0
240
- this.updateCartItem(cartKey, 0)
703
+ _.updateCartItem(cartKey, 0)
241
704
  .then((updatedCart) => {
242
705
  if (updatedCart && !updatedCart.error) {
243
- // Success - let smart comparison handle the removal animation
244
- this.#currentCart = updatedCart;
245
- this.#renderCartItems(updatedCart);
246
- this.#renderCartPanel(updatedCart);
247
-
248
- // Emit cart updated and data changed events
249
- const cartWithCalculatedFields = this.#addCalculatedFields(updatedCart);
250
- this.#emit('cart-dialog:updated', { cart: cartWithCalculatedFields });
251
- this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
706
+ _.#currentCart = updatedCart;
707
+ _.#renderCartItems(updatedCart);
708
+ _.#renderCartPanel(updatedCart);
709
+
710
+ const cartWithCalculatedFields = _.#addCalculatedFields(updatedCart);
711
+ _.#emit('cart-panel:updated', { cart: cartWithCalculatedFields });
712
+ _.#emit('cart-panel:data-changed', cartWithCalculatedFields);
252
713
  } else {
253
- // Error - reset to ready state
254
714
  element.setState('ready');
255
715
  console.error('Failed to remove cart item:', cartKey);
256
716
  }
257
717
  })
258
718
  .catch((error) => {
259
- // Error - reset to ready state
260
719
  element.setState('ready');
261
720
  console.error('Error removing cart item:', error);
262
721
  });
@@ -267,49 +726,46 @@ class CartDialog extends HTMLElement {
267
726
  * @private
268
727
  */
269
728
  #handleCartItemQuantityChange(e) {
729
+ const _ = this;
270
730
  const { cartKey, quantity, element } = e.detail;
271
731
 
272
- // Set item to processing state
273
732
  element.setState('processing');
274
733
 
275
- // Update item quantity
276
- this.updateCartItem(cartKey, quantity)
734
+ _.updateCartItem(cartKey, quantity)
277
735
  .then((updatedCart) => {
278
736
  if (updatedCart && !updatedCart.error) {
279
- // Success - update cart data and refresh items
280
- this.#currentCart = updatedCart;
281
- this.#renderCartItems(updatedCart);
282
- this.#renderCartPanel(updatedCart);
283
-
284
- // Emit cart updated and data changed events
285
- const cartWithCalculatedFields = this.#addCalculatedFields(updatedCart);
286
- this.#emit('cart-dialog:updated', { cart: cartWithCalculatedFields });
287
- this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
737
+ _.#currentCart = updatedCart;
738
+ _.#renderCartItems(updatedCart);
739
+ _.#renderCartPanel(updatedCart);
740
+
741
+ const cartWithCalculatedFields = _.#addCalculatedFields(updatedCart);
742
+ _.#emit('cart-panel:updated', { cart: cartWithCalculatedFields });
743
+ _.#emit('cart-panel:data-changed', cartWithCalculatedFields);
288
744
  } else {
289
- // Error - reset to ready state
290
745
  element.setState('ready');
291
746
  console.error('Failed to update cart item quantity:', cartKey, quantity);
292
747
  }
293
748
  })
294
749
  .catch((error) => {
295
- // Error - reset to ready state
296
750
  element.setState('ready');
297
751
  console.error('Error updating cart item quantity:', error);
298
752
  });
299
753
  }
300
754
 
755
+ // =========================================================================
756
+ // Private Methods - Rendering
757
+ // =========================================================================
758
+
301
759
  /**
302
- * Update cart count elements across the site
760
+ * Update cart count elements across the page
303
761
  * @private
304
762
  */
305
763
  #renderCartCount(cartData) {
306
764
  if (!cartData) return;
307
765
 
308
- // Calculate visible item count (excluding _hide_in_cart items)
309
766
  const visibleItems = this.#getVisibleCartItems(cartData);
310
767
  const visibleItemCount = visibleItems.reduce((total, item) => total + item.quantity, 0);
311
768
 
312
- // Update all cart count elements across the site
313
769
  const cartCountElements = document.querySelectorAll('[data-content-cart-count]');
314
770
  cartCountElements.forEach((element) => {
315
771
  element.textContent = visibleItemCount;
@@ -317,170 +773,110 @@ class CartDialog extends HTMLElement {
317
773
  }
318
774
 
319
775
  /**
320
- * Update cart subtotal elements across the site
776
+ * Update cart subtotal elements across the page
321
777
  * @private
322
778
  */
323
779
  #renderCartSubtotal(cartData) {
324
780
  if (!cartData) return;
325
781
 
326
- // Calculate subtotal from all items except those marked to ignore pricing
327
782
  const pricedItems = cartData.items.filter((item) => {
328
783
  const ignorePrice = item.properties?._ignore_price_in_subtotal;
329
784
  return !ignorePrice;
330
785
  });
331
786
  const subtotal = pricedItems.reduce((total, item) => total + (item.line_price || 0), 0);
332
787
 
333
- // Update all cart subtotal elements across the site
334
788
  const cartSubtotalElements = document.querySelectorAll('[data-content-cart-subtotal]');
335
789
  cartSubtotalElements.forEach((element) => {
336
- // Format as currency (assuming cents, convert to dollars)
337
790
  const formatted = (subtotal / 100).toFixed(2);
338
791
  element.textContent = `$${formatted}`;
339
792
  });
340
793
  }
341
794
 
342
795
  /**
343
- * Update cart items display based on cart data
796
+ * Update cart panel sections (has-items/empty)
344
797
  * @private
345
798
  */
346
799
  #renderCartPanel(cart = null) {
347
- const cartData = cart || this.#currentCart;
800
+ const _ = this;
801
+ const cartData = cart || _.#currentCart;
348
802
  if (!cartData) return;
349
803
 
350
- // Get cart sections
351
- const hasItemsSection = this.querySelector('[data-cart-has-items]');
352
- const emptySection = this.querySelector('[data-cart-is-empty]');
353
- const itemsContainer = this.querySelector('[data-content-cart-items]');
804
+ const visibleItems = _.#getVisibleCartItems(cartData);
805
+ const hasVisibleItems = visibleItems.length > 0;
354
806
 
355
- if (!hasItemsSection || !emptySection || !itemsContainer) {
356
- console.warn(
357
- 'Cart sections not found. Expected [data-cart-has-items], [data-cart-is-empty], and [data-content-cart-items]'
358
- );
359
- return;
360
- }
807
+ // Set state attribute for CSS styling (e.g., Tailwind variants)
808
+ _.setAttribute('state', hasVisibleItems ? 'has-items' : 'empty');
361
809
 
362
- // Check visible item count for showing/hiding sections
363
- const visibleItems = this.#getVisibleCartItems(cartData);
364
- const hasVisibleItems = visibleItems.length > 0;
810
+ const hasItemsSection = _.querySelector('[data-cart-has-items]');
811
+ const emptySection = _.querySelector('[data-cart-is-empty]');
365
812
 
366
- // Show/hide sections based on visible item count
367
- if (hasVisibleItems) {
368
- hasItemsSection.style.display = '';
369
- emptySection.style.display = 'none';
370
- } else {
371
- hasItemsSection.style.display = 'none';
372
- emptySection.style.display = '';
813
+ if (hasItemsSection && emptySection) {
814
+ hasItemsSection.style.display = hasVisibleItems ? '' : 'none';
815
+ emptySection.style.display = hasVisibleItems ? 'none' : '';
373
816
  }
374
817
 
375
- // Update cart count and subtotal across the site
376
- this.#renderCartCount(cartData);
377
- this.#renderCartSubtotal(cartData);
818
+ _.#renderCartCount(cartData);
819
+ _.#renderCartSubtotal(cartData);
378
820
  }
379
821
 
380
822
  /**
381
- * Fetch current cart data from server
382
- * @returns {Promise<Object>} Cart data object
823
+ * Render cart items with smart add/update/remove
824
+ * @private
383
825
  */
384
- getCart() {
385
- return fetch('/cart.json', {
386
- crossDomain: true,
387
- credentials: 'same-origin',
388
- })
389
- .then((response) => {
390
- if (!response.ok) {
391
- throw Error(response.statusText);
392
- }
393
- return response.json();
394
- })
395
- .catch((error) => {
396
- console.error('Error fetching cart:', error);
397
- return { error: true, message: error.message };
398
- });
399
- }
826
+ #renderCartItems(cartData) {
827
+ const _ = this;
828
+ const itemsContainer = _.querySelector('[data-content-cart-items]');
400
829
 
401
- /**
402
- * Update cart item quantity on server
403
- * @param {string|number} key - Cart item key/ID
404
- * @param {number} quantity - New quantity (0 to remove)
405
- * @returns {Promise<Object>} Updated cart data object
406
- */
407
- updateCartItem(key, quantity) {
408
- return fetch('/cart/change.json', {
409
- crossDomain: true,
410
- method: 'POST',
411
- credentials: 'same-origin',
412
- body: JSON.stringify({ id: key, quantity: quantity }),
413
- headers: { 'Content-Type': 'application/json' },
414
- })
415
- .then((response) => {
416
- if (!response.ok) {
417
- throw Error(response.statusText);
418
- }
419
- return response.json();
420
- })
421
- .catch((error) => {
422
- console.error('Error updating cart item:', error);
423
- return { error: true, message: error.message };
830
+ if (!itemsContainer || !cartData || !cartData.items) return;
831
+
832
+ const visibleItems = _.#getVisibleCartItems(cartData);
833
+
834
+ // Initial render - load all items without animation
835
+ if (_.#isInitialRender) {
836
+ itemsContainer.innerHTML = '';
837
+ visibleItems.forEach((itemData) => {
838
+ itemsContainer.appendChild(new CartItem(itemData, cartData));
424
839
  });
425
- }
840
+ _.#isInitialRender = false;
841
+ return;
842
+ }
426
843
 
427
- /**
428
- * Refresh cart data from server and update components
429
- * @param {Object} [cartObj=null] - Optional cart object to use instead of fetching
430
- * @returns {Promise<Object>} Cart data object
431
- */
432
- refreshCart(cartObj = null) {
433
- // If cart object is provided, use it directly
434
- if (cartObj && !cartObj.error) {
435
- // console.log('Using provided cart data:', cartObj);
436
- this.#currentCart = cartObj;
437
- this.#renderCartItems(cartObj);
438
- this.#renderCartPanel(cartObj);
844
+ // Get current DOM items
845
+ const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
846
+ const currentKeys = new Set(currentItems.map((item) => item.getAttribute('key')));
439
847
 
440
- // Emit cart refreshed and data changed events
441
- const cartWithCalculatedFields = this.#addCalculatedFields(cartObj);
442
- this.#emit('cart-dialog:refreshed', { cart: cartWithCalculatedFields });
443
- this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
848
+ // Get new cart data keys
849
+ const newKeys = visibleItems.map((item) => item.key || item.id);
850
+ const newKeysSet = new Set(newKeys);
444
851
 
445
- return Promise.resolve(cartObj);
446
- }
852
+ // Step 1: Remove items no longer in cart
853
+ _.#removeItemsFromDOM(itemsContainer, newKeysSet);
447
854
 
448
- // Otherwise fetch from server
449
- return this.getCart().then((cartData) => {
450
- // console.log('Cart data received:', cartData);
451
- if (cartData && !cartData.error) {
452
- this.#currentCart = cartData;
453
- this.#renderCartItems(cartData);
454
- this.#renderCartPanel(cartData);
455
-
456
- // Emit cart refreshed and data changed events
457
- const cartWithCalculatedFields = this.#addCalculatedFields(cartData);
458
- this.#emit('cart-dialog:refreshed', { cart: cartWithCalculatedFields });
459
- this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
460
- } else {
461
- console.warn('Cart data has error or is null:', cartData);
462
- }
463
- return cartData;
464
- });
855
+ // Step 2: Update existing items
856
+ _.#updateItemsInDOM(itemsContainer, cartData);
857
+
858
+ // Step 3: Add new items with animation
859
+ const itemsToAdd = visibleItems.filter(
860
+ (itemData) => !currentKeys.has(itemData.key || itemData.id)
861
+ );
862
+ _.#addItemsToDOM(itemsContainer, itemsToAdd, newKeys, cartData);
465
863
  }
466
864
 
467
865
  /**
468
- * Remove items from DOM that are no longer in cart data
866
+ * Remove items from DOM that are no longer in cart
469
867
  * @private
470
868
  */
471
869
  #removeItemsFromDOM(itemsContainer, newKeysSet) {
472
870
  const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
473
-
474
871
  const itemsToRemove = currentItems.filter((item) => !newKeysSet.has(item.getAttribute('key')));
475
872
 
476
873
  itemsToRemove.forEach((item) => {
477
- console.log('destroy yourself', item);
478
874
  item.destroyYourself();
479
875
  });
480
876
  }
481
877
 
482
878
  /**
483
- * Update existing cart-item elements with fresh cart data
879
+ * Update existing cart-item elements with fresh data
484
880
  * @private
485
881
  */
486
882
  #updateItemsInDOM(itemsContainer, cartData) {
@@ -490,12 +886,7 @@ class CartDialog extends HTMLElement {
490
886
  existingItems.forEach((cartItemEl) => {
491
887
  const key = cartItemEl.getAttribute('key');
492
888
  const updatedItemData = visibleItems.find((item) => (item.key || item.id) === key);
493
-
494
- if (updatedItemData) {
495
- // Update cart-item with fresh data and full cart context
496
- // The cart-item will handle HTML comparison and only re-render if needed
497
- cartItemEl.setData(updatedItemData, cartData);
498
- }
889
+ if (updatedItemData) cartItemEl.setData(updatedItemData, cartData);
499
890
  });
500
891
  }
501
892
 
@@ -503,19 +894,15 @@ class CartDialog extends HTMLElement {
503
894
  * Add new items to DOM with animation delay
504
895
  * @private
505
896
  */
506
- #addItemsToDOM(itemsContainer, itemsToAdd, newKeys) {
507
- // Delay adding new items by 300ms to let cart slide open first
897
+ #addItemsToDOM(itemsContainer, itemsToAdd, newKeys, cartData) {
508
898
  setTimeout(() => {
509
899
  itemsToAdd.forEach((itemData) => {
510
- const cartItem$1 = cartItem.CartItem.createAnimated(itemData);
900
+ const cartItem = CartItem.createAnimated(itemData, cartData);
511
901
  const targetIndex = newKeys.indexOf(itemData.key || itemData.id);
512
902
 
513
- // Find the correct position to insert the new item
514
903
  if (targetIndex === 0) {
515
- // Insert at the beginning
516
- itemsContainer.insertBefore(cartItem$1, itemsContainer.firstChild);
904
+ itemsContainer.insertBefore(cartItem, itemsContainer.firstChild);
517
905
  } else {
518
- // Find the item that should come before this one
519
906
  let insertAfter = null;
520
907
  for (let i = targetIndex - 1; i >= 0; i--) {
521
908
  const prevKey = newKeys[i];
@@ -527,262 +914,59 @@ class CartDialog extends HTMLElement {
527
914
  }
528
915
 
529
916
  if (insertAfter) {
530
- insertAfter.insertAdjacentElement('afterend', cartItem$1);
917
+ insertAfter.insertAdjacentElement('afterend', cartItem);
531
918
  } else {
532
- itemsContainer.appendChild(cartItem$1);
919
+ itemsContainer.appendChild(cartItem);
533
920
  }
534
921
  }
535
922
  });
536
923
  }, 100);
537
924
  }
538
925
 
926
+ // =========================================================================
927
+ // Private Methods - Helpers
928
+ // =========================================================================
929
+
539
930
  /**
540
- * Filter cart items to exclude those with _hide_in_cart property
931
+ * Filter cart items to exclude hidden items
541
932
  * @private
542
933
  */
543
934
  #getVisibleCartItems(cartData) {
544
935
  if (!cartData || !cartData.items) return [];
545
936
  return cartData.items.filter((item) => {
546
- // Check for _hide_in_cart in various possible locations
547
937
  const hidden = item.properties?._hide_in_cart;
548
-
549
938
  return !hidden;
550
939
  });
551
940
  }
552
941
 
553
942
  /**
554
- * Add calculated fields to cart object for events
943
+ * Add calculated fields to cart object
555
944
  * @private
556
945
  */
557
946
  #addCalculatedFields(cartData) {
558
947
  if (!cartData) return cartData;
559
948
 
560
- // For display counts: use visible items (excludes _hide_in_cart)
561
949
  const visibleItems = this.#getVisibleCartItems(cartData);
562
950
  const calculated_count = visibleItems.reduce((total, item) => total + item.quantity, 0);
563
951
 
564
- // For pricing: use all items except those marked to ignore pricing
565
- const pricedItems = cartData.items.filter((item) => {
566
- const ignorePrice = item.properties?._ignore_price_in_subtotal;
567
- return !ignorePrice;
568
- });
569
- const calculated_subtotal = pricedItems.reduce(
570
- (total, item) => total + (item.line_price || 0),
571
- 0
572
- );
573
-
574
- return {
575
- ...cartData,
576
- calculated_count,
577
- calculated_subtotal,
578
- };
579
- }
580
-
581
- /**
582
- * Render cart items from Shopify cart data with smart comparison
583
- * @private
584
- */
585
- #renderCartItems(cartData) {
586
- const itemsContainer = this.querySelector('[data-content-cart-items]');
587
-
588
- if (!itemsContainer || !cartData || !cartData.items) {
589
- console.warn('Cannot render cart items:', {
590
- itemsContainer: !!itemsContainer,
591
- cartData: !!cartData,
592
- items: cartData?.items?.length,
593
- });
594
- return;
595
- }
596
-
597
- // Filter out items with _hide_in_cart property
598
- const visibleItems = this.#getVisibleCartItems(cartData);
599
-
600
- // Handle initial render - load all items without animation
601
- if (this.#isInitialRender) {
602
- // console.log('Initial cart render:', visibleItems.length, 'visible items');
603
-
604
- // Clear existing items
605
- itemsContainer.innerHTML = '';
606
-
607
- // Create cart-item elements without animation
608
- visibleItems.forEach((itemData) => {
609
- const cartItem$1 = new cartItem.CartItem(itemData); // No animation
610
- itemsContainer.appendChild(cartItem$1);
611
- });
612
-
613
- this.#isInitialRender = false;
952
+ const pricedItems = cartData.items.filter((item) => !item.properties?._ignore_price_in_subtotal);
953
+ const calculated_subtotal = pricedItems.reduce((total, item) => total + (item.line_price || 0), 0);
614
954
 
615
- return;
616
- }
617
-
618
- // Get current DOM items and their keys
619
- const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
620
- const currentKeys = new Set(currentItems.map((item) => item.getAttribute('key')));
621
-
622
- // Get new cart data keys in order (only visible items)
623
- const newKeys = visibleItems.map((item) => item.key || item.id);
624
- const newKeysSet = new Set(newKeys);
625
-
626
- // Step 1: Remove items that are no longer in cart data
627
- this.#removeItemsFromDOM(itemsContainer, newKeysSet);
628
-
629
- // Step 2: Update existing items with fresh data (handles templates, bundles, etc.)
630
- this.#updateItemsInDOM(itemsContainer, cartData);
631
-
632
- // Step 3: Add new items that weren't in DOM (with animation delay)
633
- const itemsToAdd = visibleItems.filter(
634
- (itemData) => !currentKeys.has(itemData.key || itemData.id)
635
- );
636
- this.#addItemsToDOM(itemsContainer, itemsToAdd, newKeys);
637
- }
638
-
639
- /**
640
- * Set the template function for cart items
641
- * @param {Function} templateFn - Function that takes item data and returns HTML string
642
- */
643
- setCartItemTemplate(templateName, templateFn) {
644
- cartItem.CartItem.setTemplate(templateName, templateFn);
645
- }
646
-
647
- /**
648
- * Set the processing template function for cart items
649
- * @param {Function} templateFn - Function that returns HTML string for processing state
650
- */
651
- setCartItemProcessingTemplate(templateFn) {
652
- cartItem.CartItem.setProcessingTemplate(templateFn);
653
- }
654
-
655
- /**
656
- * Shows the cart dialog and traps focus within it
657
- * @param {HTMLElement} [triggerEl=null] - The element that triggered the cart dialog
658
- * @fires CartDialog#show - Fired when the cart dialog has been shown
659
- */
660
- show(triggerEl = null, cartObj) {
661
- const _ = this;
662
- _.triggerEl = triggerEl || false;
663
-
664
- // Lock body scrolling
665
- _.#lockScroll();
666
-
667
- // Remove the hidden class first to ensure content is rendered
668
- _.contentPanel.classList.remove('hidden');
669
-
670
- // Give the browser a moment to process before starting animation
671
- requestAnimationFrame(() => {
672
- // Update ARIA states
673
- _.setAttribute('aria-hidden', 'false');
674
-
675
- if (_.triggerEl) {
676
- _.triggerEl.setAttribute('aria-expanded', 'true');
677
- }
678
-
679
- // Focus management
680
- const firstFocusable = _.querySelector(
681
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
682
- );
683
-
684
- if (firstFocusable) {
685
- requestAnimationFrame(() => {
686
- firstFocusable.focus();
687
- });
688
- }
689
-
690
- // Refresh cart data when showing
691
- _.refreshCart(cartObj);
692
-
693
- // Emit show event - cart dialog is now visible
694
- _.#emit('cart-dialog:show', { triggerElement: _.triggerEl });
695
- });
696
- }
697
-
698
- /**
699
- * Hides the cart dialog and restores focus
700
- * @fires CartDialog#hide - Fired when the cart dialog has started hiding (transition begins)
701
- * @fires CartDialog#afterHide - Fired when the cart dialog has completed its hide transition
702
- */
703
- hide() {
704
- const _ = this;
705
-
706
- // Update ARIA states
707
- if (_.triggerEl) {
708
- // remove focus from modal panel first
709
- _.triggerEl.focus();
710
- // mark trigger as no longer expanded
711
- _.triggerEl.setAttribute('aria-expanded', 'false');
712
- } else {
713
- // If no trigger element, blur any focused element inside the panel
714
- const activeElement = document.activeElement;
715
- if (activeElement && _.contains(activeElement)) {
716
- activeElement.blur();
717
- }
718
- }
719
-
720
- requestAnimationFrame(() => {
721
- // Set aria-hidden to start transition
722
- // The transitionend event handler will add display:none when complete
723
- _.setAttribute('aria-hidden', 'true');
724
-
725
- // Emit hide event - cart dialog is now starting to hide
726
- _.#emit('cart-dialog:hide', { triggerElement: _.triggerEl });
727
-
728
- // Restore body scroll
729
- _.#restoreScroll();
730
- });
731
- }
732
- }
733
-
734
- /**
735
- * Custom element that creates a clickable overlay for the cart dialog
736
- * @extends HTMLElement
737
- */
738
- class CartOverlay extends HTMLElement {
739
- constructor() {
740
- super();
741
- this.setAttribute('tabindex', '-1');
742
- this.setAttribute('aria-hidden', 'true');
743
- this.cartDialog = this.closest('cart-dialog');
744
- this.#attachListeners();
745
- }
746
-
747
- #attachListeners() {
748
- this.addEventListener('click', () => {
749
- this.cartDialog.hide();
750
- });
955
+ return { ...cartData, calculated_count, calculated_subtotal };
751
956
  }
752
957
  }
753
958
 
754
- /**
755
- * Custom element that wraps the content of the cart dialog
756
- * @extends HTMLElement
757
- */
758
- class CartPanel extends HTMLElement {
759
- constructor() {
760
- super();
761
- this.setAttribute('role', 'document');
762
- }
763
- }
959
+ // =============================================================================
960
+ // Register Custom Elements
961
+ // =============================================================================
764
962
 
765
- if (!customElements.get('cart-dialog')) {
766
- customElements.define('cart-dialog', CartDialog);
767
- }
768
- if (!customElements.get('cart-overlay')) {
769
- customElements.define('cart-overlay', CartOverlay);
770
- }
771
963
  if (!customElements.get('cart-panel')) {
772
964
  customElements.define('cart-panel', CartPanel);
773
965
  }
774
966
 
775
- // Make CartItem available globally for Shopify themes
776
- if (typeof window !== 'undefined') {
777
- window.CartItem = cartItem.CartItem;
778
- }
779
-
780
- Object.defineProperty(exports, 'CartItem', {
781
- enumerable: true,
782
- get: function () { return cartItem.CartItem; }
783
- });
784
- exports.CartDialog = CartDialog;
785
- exports.CartOverlay = CartOverlay;
967
+ exports.CartItem = CartItem;
968
+ exports.CartItemContent = CartItemContent;
969
+ exports.CartItemProcessing = CartItemProcessing;
786
970
  exports.CartPanel = CartPanel;
787
- exports.default = CartDialog;
971
+ exports.default = CartPanel;
788
972
  //# sourceMappingURL=cart-panel.cjs.js.map