PageSourceSearch

https://www.fluke.com/modules/custom/igcommerce_external_integrati…/js/storefront_api/shopify-cart.js?tlqj4u

js fluke.com collected 2026-09-24 08:50:59 UTC 29,845 bytes, 810 lines download raw bytes

1/**
2 * Shopify Cart Module
3 * Handles all cart-related functionality including UI updates and cart operations.
4 */
5
6window.ShopifyCart = (function () {
7  'use strict';
8
9  let config = null;
10  let elements = null;
11  let cart = null;
12  let sellingPlanGroupNames = {};
13
14  /**
15   * Get the localStorage key for the cart.
16   * Markets locales get a per-locale key to isolate carts.
17   *
18   * @returns {string} - The localStorage key for the cart.
19   */
20  function getCartKey() {
21    if (config && config.shopifyConfig && config.shopifyConfig.lang) {
22      return 'shopifyCart_' + config.shopifyConfig.lang;
23    }
24    return 'shopifyCart';
25  }
26
27  /**
28   * Initialize cart from local storage
29   * 
30   * @returns {Object} - The cart object.
31   * 
32   * Example values:
33   * 
34   * id: "gid://shopify/Cart/83291039293"
35   *   - The Shopify ID of the cart.
36   * 
37   * checkoutUrl: "https://checkout.shopify.com/cart/83291039293"
38   */
39  const initializeCart = () => {
40    const storedCart = localStorage.getItem(getCartKey());
41    if (storedCart) {
42      try {
43        return JSON.parse(storedCart);
44      } catch (e) {
45        console.error('Error parsing stored cart:', e);
46        localStorage.removeItem(getCartKey());
47      }
48    }
49    return null;
50  };
51
52  // Clear a Shopify-expired cart (carts expire server-side after ~30 days of inactivity).
53  // Call whenever getCart() returns null for a stored cart ID so stale localStorage
54  // entries don't block future add-to-cart validation checks.
55  const clearExpiredCart = () => {
56    cart = null;
57    localStorage.removeItem(getCartKey());
58  };
59
60  /**
61   * Format price with currency symbol
62   * 
63   * @param {string} amount - The amount to format.
64   * @param {string} currencyCode - The currency code to format the price in.
65   * @returns {string} - The formatted price.
66   */
67  const formatPrice = (amount, currencyCode) => {
68    if (amount == null || amount === '') return '';
69    const numericAmount = parseFloat(amount);
70    const formatter = new Intl.NumberFormat(config?.shopifyConfig?.lang || 'en-US', {
71      style: 'decimal',
72      minimumFractionDigits: 2
73    });
74    return `${config.currencySymbol}${formatter.format(numericAmount)}`;
75  };
76
77  // Update the SGD tax/total display once, using the pre-tax subtotal from the API.
78  // Injects the "Estimated Taxes" row if absent; updates it if already present.
79  // Computes the tax-inclusive total locally (subtotal + GST) because Shopify's
80  // Storefront API returns totalAmount == subtotalAmount for SGD/markets.
81  const updateSGDCartDisplay = (subtotalAmount) => {
82    const taxRate = parseFloat(config?.shopifyConfig?.shopify_tax_rate) || 0;
83    const numericSubtotal = parseFloat(subtotalAmount);
84    const estimatedTax = numericSubtotal * (taxRate / 100);
85    const formatter = new Intl.NumberFormat(config?.shopifyConfig?.lang || 'en-US', {
86      style: 'decimal',
87      minimumFractionDigits: 2
88    });
89    const taxText = `${config.currencySymbol}${formatter.format(estimatedTax)}`;
90
91    const subtotalLabel = document.querySelector('.cart-subtotal-label');
92    if (subtotalLabel) {
93      if (!document.querySelector('.cart-tax-label')) {
94        subtotalLabel.insertAdjacentHTML('afterend', `<p class="cart-tax-label">${Drupal.t('Estimated Taxes:')}<span id="cart-tax">${taxText}</span></p>`);
95      } else {
96        const taxEl = document.getElementById('cart-tax');
97        if (taxEl) taxEl.textContent = taxText;
98      }
99    }
100
101    const cartTotalDisplay = document.querySelector('#cart-total-display');
102    if (cartTotalDisplay) {
103      cartTotalDisplay.textContent = formatPrice(numericSubtotal + estimatedTax);
104    }
105  };
106
107  /**
108   * Update cart count in UI
109   * 
110   * @param {Object} cart - The cart object.
111   */
112  const updateCartCount = (cart) => {
113    // Try different selectors for cart count
114    let cartCount = document.querySelector('.shopify-buy__cart-toggle__count');
115    
116    if (!cartCount) {
117      cartCount = document.querySelector('[data-element="toggle.count"]');
118      if (!cartCount) {
119        console.debug('Cart count element not found');
120        return;
121      }
122    }
123    
124    const count = cart?.lines?.edges?.reduce((total, edge) => total + edge.node.quantity, 0) || 0;
125    cartCount.textContent = count > 0 ? count : '0';
126  };
127
128  /**
129   * Render cart items in cart container
130   * 
131   * @param {Object} cart - The cart object.
132   */
133  const renderCartItems = (cart) => {
134    // Get cart items container
135    const cartItemsContainer = document.querySelector('#cart-items');
136    if (!cartItemsContainer) {
137      console.debug('Cart items container not found');
138      return;
139    }
140    
141    // Clear existing items
142    cartItemsContainer.innerHTML = '';
143    
144    if (!cart?.lines?.edges?.length) {
145      cartItemsContainer.innerHTML = Drupal.t('Your cart is empty.');
146      
147      // Update cart subtotal if it exists
148      const cartSubtotal = document.querySelector('#cart-subtotal');
149      if (cartSubtotal) {
150        cartSubtotal.textContent = formatPrice(0, 'USD');
151      }
152
153      // Remove active class from cart footer when empty
154      const cartFooter = document.querySelector('#cart-footer');
155      if (cartFooter) {
156        cartFooter.classList.remove('active');
157      }
158      
159      return;
160    }
161    
162    // Add active class to cart footer when there are items
163    const cartFooter = document.querySelector('#cart-footer');
164    if (cartFooter) {
165      cartFooter.classList.add('active');
166    }
167    
168    // Calculate cart total
169    let cartTotal = 0;
170    
171    // Add each item to the cart
172    cart.lines.edges.forEach(edge => {
173      const line = edge.node;
174      const merchandise = line.merchandise;
175      const product = merchandise.product;
176      const quantity = line.quantity;
177      const price = parseFloat(merchandise.price.amount);
178      const lineTotal = price * quantity;
179      cartTotal += lineTotal;
180
181      // Check if this line has a software quantity limit (show quantity but no +/- controls)
182      const isQuantityLimited = (line.attributes || []).some(
183        a => a.key === '_software_limit' && a.value === 'true'
184      );
185
186      const itemElement = document.createElement('div');
187      itemElement.className = 'cart-item';
188      itemElement.dataset.lineId = line.id;
189
190      // Ensure we have proper product information
191      if (!product) {
192        console.warn('Product information missing for cart line:', line.id);
193      }
194
195      // Get product title and image
196      const productTitle = product?.title || 'Product';
197      const imageUrl = product?.featuredImage?.url || '';
198      const imageAlt = product?.featuredImage?.altText || productTitle;
199
200      // Get variant title
201      const variantTitle = merchandise.title !== 'Default Title' ? merchandise.title : '';
202
203      // Get selling plan label if present
204      let sellingPlanLabel = '';
205      const allocation = line.sellingPlanAllocation;
206      if (allocation?.sellingPlan) {
207        const planId = allocation.sellingPlan.id;
208        const rawLabel = sellingPlanGroupNames[planId] || allocation.sellingPlan.name || '';
209        const labelOverrides = config?.shopifyConfig?.sellingPlanLabels || {};
210        sellingPlanLabel = labelOverrides[rawLabel] || rawLabel;
211      }
212
213      // Get variant promo message if present
214      const variantPromo = merchandise.variantPromo?.value || '';
215
216      // Get bundle savings if present (set when bundle substitution was used at add-to-cart)
217      const bundleSavingsRaw = (line.attributes || []).find(a => a.key === '_bundle_savings')?.value;
218      const bundleSavings = bundleSavingsRaw ? parseFloat(bundleSavingsRaw) : 0;
219
220      // Quantity controls: full controls for normal items, static display for limited items
221      const quantityHtml = isQuantityLimited
222        ? `<span class="cart-item-quantity cart-item-quantity--limited">
223            <span class="shopify-quantity-static">${quantity}</span>
224          </span>`
225        : `<span class="cart-item-quantity">
226            <button class="shopify-quantity-button" data-action="decrement">&ndash;</button>
227            <input class="shopify-quantity-input" type="number" min="0" value="${quantity}">
228            <button class="shopify-quantity-button" data-action="increment">+</button>
229          </span>`;
230
231      // Render the cart item
232      itemElement.innerHTML = `
233        <span class="cart-item-image">${imageUrl ? `<img src="${imageUrl}" alt="${imageAlt}" />` : ''}</span>
234        <span class="cart-item-parent">${productTitle}</span>
235        ${sellingPlanLabel ? `<span class="cart-item-selling-plan">${sellingPlanLabel}</span>` : ''}
236        ${variantTitle ? `<span class="cart-item-variant-title">${variantTitle}</span>` : ''}
237        ${variantPromo ? `<span class="cart-item-variant-promo">${variantPromo}</span>` : ''}
238        ${bundleSavings > 0 ? `<span class="cart-item-bundle-savings">${Drupal.t('You saved')} ${formatPrice(bundleSavings, merchandise.price.currencyCode)}</span>` : ''}
239        ${quantityHtml}
240        <span class="cart-item-price" data-raw-price="${price}" data-currency="${merchandise.price.currencyCode}">${formatPrice(lineTotal, merchandise.price.currencyCode)}</span>
241        <button class="cart-item-remove" data-action="remove">${Drupal.t('Remove')}</button>
242      `;
243
244      cartItemsContainer.appendChild(itemElement);
245
246      const removeBtn = itemElement.querySelector('[data-action="remove"]');
247
248      removeBtn.addEventListener('click', () => {
249        window.ShopifyAPIClient.removeCartLine(cart.id, line.id)
250          .then(result => {
251            if (result.data?.cartLinesRemove?.cart) {
252              updateCartState(result.data.cartLinesRemove.cart);
253            }
254          })
255          .catch(error => console.error('Error removing item:', error));
256      });
257
258      // Quantity controls only for non-limited items
259      if (!isQuantityLimited) {
260        const decrementBtn = itemElement.querySelector('[data-action="decrement"]');
261        const incrementBtn = itemElement.querySelector('[data-action="increment"]');
262        const quantityInput = itemElement.querySelector('.shopify-quantity-input');
263
264        decrementBtn.addEventListener('click', () => {
265          const newQuantity = Math.max(0, parseInt(quantityInput.value) - 1);
266          if (newQuantity === 0) {
267            window.ShopifyAPIClient.removeCartLine(cart.id, line.id)
268              .then(result => {
269                if (result.data?.cartLinesRemove?.cart) {
270                  updateCartState(result.data.cartLinesRemove.cart);
271                }
272              })
273              .catch(error => console.error('Error removing item:', error));
274          } else {
275            window.ShopifyAPIClient.updateCartLine(cart.id, line.id, newQuantity)
276              .then(result => {
277                if (result.data?.cartLinesUpdate?.cart) {
278                  updateCartState(result.data.cartLinesUpdate.cart);
279                }
280              })
281              .catch(error => console.error('Error updating quantity:', error));
282          }
283        });
284
285        incrementBtn.addEventListener('click', () => {
286          const newQuantity = parseInt(quantityInput.value) + 1;
287          window.ShopifyAPIClient.updateCartLine(cart.id, line.id, newQuantity)
288            .then(result => {
289              if (result.data?.cartLinesUpdate?.cart) {
290                updateCartState(result.data.cartLinesUpdate.cart);
291              }
292            })
293            .catch(error => console.error('Error updating quantity:', error));
294        });
295
296        quantityInput.addEventListener('change', () => {
297          const newQuantity = parseInt(quantityInput.value);
298          if (isNaN(newQuantity) || newQuantity < 0) {
299            quantityInput.value = quantity;
300            return;
301          }
302
303          if (newQuantity === 0) {
304            window.ShopifyAPIClient.removeCartLine(cart.id, line.id)
305              .then(result => {
306                if (result.data?.cartLinesRemove?.cart) {
307                  updateCartState(result.data.cartLinesRemove.cart);
308                }
309              })
310              .catch(error => console.error('Error removing item:', error));
311          } else {
312            window.ShopifyAPIClient.updateCartLine(cart.id, line.id, newQuantity)
313              .then(result => {
314                if (result.data?.cartLinesUpdate?.cart) {
315                  updateCartState(result.data.cartLinesUpdate.cart);
316                }
317              })
318              .catch(error => console.error('Error updating quantity:', error));
319          }
320        });
321      }
322    });
323    
324    // Update cart subtotal and total
325    const cartSubtotal = document.querySelector('#cart-subtotal');
326    if (cartSubtotal) {
327      const { amount, currencyCode } = cart.cost?.subtotalAmount || { amount: cartTotal, currencyCode: 'USD' };
328      cartSubtotal.textContent = formatPrice(amount, currencyCode);
329
330      if (config.currencyLocale === 'SGD') {
331        updateSGDCartDisplay(amount);
332      } else {
333        const cartTotalDisplay = document.querySelector('#cart-total-display');
334        if (cartTotalDisplay) {
335          const totalData = cart.cost?.totalAmount || { amount, currencyCode };
336          cartTotalDisplay.textContent = formatPrice(totalData.amount, totalData.currencyCode);
337        }
338      }
339    }
340    
341    // Add checkout button click handler
342    const checkoutButton = document.querySelector('#cart-checkout');
343    if (checkoutButton) {
344      checkoutButton.addEventListener('click', () => {
345        if (cart.checkoutUrl) {
346          window.location.href = cart.checkoutUrl;
347        }
348      });
349    }
350  };
351
352  /**
353   * Update cart state and UI
354   * 
355   * @param {Object} updatedCart - The updated cart object.
356   */
357  const updateCartState = (updatedCart) => {
358
359    // Store the current visibility state
360    const wasVisible = isCartVisible;
361    
362    cart = updatedCart;
363    window.cart = cart;
364    localStorage.setItem(getCartKey(), JSON.stringify(cart));
365    
366    updateCartCount(cart);
367    renderCartItems(cart);
368    
369    // Restore visibility state if it was changed during rendering
370    const cartContainer = document.querySelector('#cart-container');
371    if (cartContainer) {
372      if (wasVisible && !cartContainer.classList.contains('active')) {
373        cartContainer.classList.add('active');
374        isCartVisible = true;
375      } else if (!wasVisible && cartContainer.classList.contains('active')) {
376        cartContainer.classList.remove('active');
377        isCartVisible = false;
378      }
379    }
380    
381    return cart;
382  };
383
384  // Cart visibility management
385  let isCartVisible = false;
386
387  /**
388   * Show the cart by adding the active class to the cart container
389   */
390  const showCart = () => {
391    const cartContainer = document.querySelector('#cart-container');
392    if (!cartContainer) {
393      console.warn('Cart container not found');
394      return;
395    }
396    
397    cartContainer.classList.add('active');
398    isCartVisible = true;
399  };
400
401  // Hide cart
402  const hideCart = () => {
403    const cartContainer = document.querySelector('#cart-container');
404    if (!cartContainer) {
405      console.warn('Cart container not found');
406      return;
407    }
408    
409    cartContainer.classList.remove('active');
410    isCartVisible = false;
411  };
412
413  // Toggle cart visibility
414  const toggleCart = () => {
415    if (isCartVisible) {
416      hideCart();
417    } else {
418      showCart();
419    }
420  };
421
422  /**
423   * Fetch product metadata (family + software limit) for cart validation.
424   * Checks caller-provided attributes first, falls back to API lookup.
425   *
426   * @param {string} variantId - The variant being added.
427   * @param {Array|null} attributes - Cart line attributes (may contain _software_service_family / _softw
427are_limit).
428   * @returns {Promise<{family: string|null, softwareLimit: boolean, attributes: Array|null}>}
429   */
430  const resolveProductMeta = async (variantId, attributes) => {
431    let family = null;
432    let softwareLimit = false;
433
434    // Check caller-provided attributes first
435    if (attributes) {
436      const familyAttr = attributes.find(a => a.key === '_software_service_family');
437      if (familyAttr) family = familyAttr.value;
438      const limitAttr = attributes.find(a => a.key === '_software_limit');
439      if (limitAttr) softwareLimit = limitAttr.value === 'true';
440    }
441
442    // If caller didn't provide both, look them up in a single query
443    if (!family || !attributes?.find(a => a.key === '_software_limit')) {
444      try {
445        const meta = await window.ShopifyAPIClient.getProductMeta(variantId);
446        if (!family) family = meta.family;
447        if (!attributes?.find(a => a.key === '_software_limit')) softwareLimit = meta.softwareLimit;
448      } catch (e) {
449        console.warn('Cart validation: Could not look up product metadata', e);
450      }
451    }
452
453    // Ensure attributes array includes resolved values
454    if (!attributes) {
455      attributes = [];
456    } else {
457      attributes = [...attributes];
458    }
459    if (family && !attributes.find(a => a.key === '_software_service_family')) {
460      attributes.push({ key: '_software_service_family', value: family });
461    }
462    if (softwareLimit && !attributes.find(a => a.key === '_software_limit')) {
463      attributes.push({ key: '_software_limit', value: 'true' });
464    }
465
466    return { family, softwareLimit, attributes: attributes.length > 0 ? attributes : null };
467  };
468
469  /**
470   * Check if adding an item would create a software family conflict in the cart.
471   * Only one product per software_service_family is allowed.
472   *
473   * @param {string} variantId - The variant being added.
474   * @param {string|null} family - The resolved family name.
475   * @returns {{blocked: boolean}}
476   */
477  const checkFamilyConflict = (variantId, family) => {
478    // Only enforce one-subscription-at-a-time for Mothra Software products.
479    // Care plans and other product families are allowed to coexist in the cart.
480    if (!family || family !== 'Mothra Software') {
481      return { blocked: false };
482    }
483
484    // No cart yet — no conflict
485    if (!cart || !cart.lines?.edges?.length) {
486      return { blocked: false };
487    }
488
489    // Check existing cart lines for a different variant in the same family
490    const conflict = cart.lines.edges.some(edge => {
491      const lineAttrs = edge.node.attributes || [];
492      const lineFamily = lineAttrs.find(a => a.key === '_software_service_family')?.value;
493      if (lineFamily !== family) return false;
494      // Same family — conflict only if it's a different variant
495      return edge.node.merchandise.id !== variantId;
496    });
497
498    return { blocked: conflict };
499  };
500
501  /**
502   * Check if adding an item would exceed its software quantity limit.
503   * When front.software_limit is true, only one of that SKU is allowed in the cart.
504   *
505   * @param {string} variantId - The variant being added.
506   * @param {boolean} softwareLimit - Whether the product has software_limit enabled.
507   * @returns {{blocked: boolean}}
508   */
509  const checkSoftwareLimit = (variantId, softwareLimit) => {
510    if (!softwareLimit) {
511      return { blocked: false };
512    }
513
514    // No cart yet — not blocked
515    if (!cart || !cart.lines?.edges?.length) {
516      return { blocked: false };
517    }
518
519    // Check if this variant is already in the cart
520    const alreadyInCart = cart.lines.edges.some(edge =>
521      edge.node.merchandise.id === variantId
522    );
523
524    return { blocked: alreadyInCart };
525  };
526
527  /**
528   * Render a family conflict warning message in the cart slideout.
529   */
530  const renderFamilyConflictMessage = () => {
531    const cartItemsContainer = document.querySelector('#cart-items');
532    if (!cartItemsContainer) return;
533
534    // Remove any existing validation message
535    const existing = cartItemsContainer.querySelector('.cart-validation-message');
536    if (existing) existing.remove();
537
538    const msg = document.createElement('div');
539    msg.className = 'cart-validation-message cart-family-conflict';
540    msg.innerHTML = `
541      <p>${Drupal.t('Sorry, you can only purchase one level of subscription at a time. Please remove the current subscription from your cart before adding a different subscription type.')}</p>
542    `;
543    cartItemsContainer.appendChild(msg);
544  };
545
546  /**
547   * Render a software limit warning message in the cart slideout.
548   */
549  const renderSoftwareLimitMessage = () => {
550    const cartItemsContainer = document.querySelector('#cart-items');
551    if (!cartItemsContainer) return;
552
553    // Remove any existing validation message
554    const existing = cartItemsContainer.querySelector('.cart-validation-message');
555    if (existing) existing.remove();
556
557    const msg = document.createElement('div');
558    msg.className = 'cart-validation-message cart-software-limit';
559    msg.innerHTML = `
560      <p>${Drupal.t('This item is already in your cart. Only one of this product can be purchased at a time.')}</p>
561    `;
562    cartItemsContainer.appendChild(msg);
563  };
564
565  // Add to cart function
566  const addToCart = async (variantId, quantity = 1, sellingPlanId = null, attributes = null) => {
567    try {
568      // If cart isn't loaded in memory yet (e.g. another tab added items after page load),
569      // fetch the latest state from Shopify before running validation so cross-tab duplicate
570      // checks (software limit, family conflict) see the current cart contents.
571      if (!cart) {
572        const stored = initializeCart();
573        if (stored?.id) {
574          const refreshResult = await window.ShopifyAPIClient.getCart(stored.id);
575          if (refreshResult.data?.cart) {
576            updateCartState(refreshResult.data.cart);
577          } else {
578            clearExpiredCart();
579          }
580        }
581      }
582
583      // Resolve product metadata (family + software limit) in a single query
584      const meta = await resolveProductMeta(variantId, attributes);
585
586      // Check for software family conflict (different SKU from same family)
587      const familyResult = checkFamilyConflict(variantId, meta.family);
588      if (familyResult.blocked) {
589        renderFamilyConflictMessage();
590        showCart();
591        return;
592      }
593
594      // Check for software quantity limit (same SKU already in cart)
595      const limitResult = checkSoftwareLimit(variantId, meta.softwareLimit);
596      if (limitResult.blocked) {
597        renderSoftwareLimitMessage();
598        showCart();
599        return;
600      }
601
602      // Use enriched attributes (may include family + limit from lookup)
603      const lineAttributes = meta.attributes;
604
605      if (!cart) {
606        const result = await window.ShopifyAPIClient.createCart(variantId, quantity, sellingPlanId, lineAttributes);
607        
608        if (result.data?.cartCreate?.cart) {
609          const newCart = result.data.cartCreate.cart;
610          
611          // Check if we have product information for each line item
612          const hasProductInfo = newCart.lines.edges.every(edge => {
613            const merchandise = edge.node.merchandise;
614            return merchandise && merchandise.product && merchandise.product.title;
615          });
616          
617          if (!hasProductInfo) {
618            console.warn('Cart created but missing product information. Fetching cart again...');
619            // If product information is missing, fetch the cart again to get complete information
620            const refreshResult = await window.ShopifyAPIClient.getCart(newCart.id);
621            if (refreshResult.data?.cart) {
622              updateCartState(refreshResult.data.cart);
623            } else {
624              updateCartState(newCart);
625            }
626          } else {
627            updateCartState(newCart);
628          }
629          
630          // Show cart if it's not already visible
631          if (!isCartVisible) {
632            showCart();
633          }
634        } else if (result.data?.cartCreate?.userErrors?.length) {
635          console.error('Error creating cart:', result.data.cartCreate.userErrors);
636        }
637      } else {
638        // Add to existing cart
639        const result = await window.ShopifyAPIClient.addToCart(cart.id, variantId, quantity, sellingPlanId, lineAttributes);
640        
641        if (result.data?.cartLinesAdd?.cart) {
642          const updatedCart = result.data.cartLinesAdd.cart;
643          
644          // Check if we have product information for each line item
645          const hasProductInfo = updatedCart.lines.edges.every(edge => {
646            const merchandise = edge.node.merchandise;
647            return merchandise && merchandise.product && merchandise.product.title;
648          });
649          
650          if (!hasProductInfo) {
651            console.warn('Item added to cart but missing product information. Fetching cart again...');
652            // If product information is missing, fetch the cart again to get complete information
653            const refreshResult = await window.ShopifyAPIClient.getCart(updatedCart.id);
654            if (refreshResult.data?.cart) {
655              updateCartState(refreshResult.data.cart);
656            } else {
657              updateCartState(updatedCart);
658            }
659          } else {
660            updateCartState(updatedCart);
661          }
662          
663          // Show cart if it's not already visible
664          if (!isCartVisible) {
665            showCart();
666          }
667        } else if (result.data?.cartLinesAdd?.userErrors?.length) {
668          console.error('Error adding to cart:', result.data.cartLinesAdd.userErrors);
669        }
670      }
671    } catch (error) {
672      console.error('Error adding to cart:', error);
673    }
674  };
675
676  // Initialize event listeners
677  const initializeEventListeners = () => {
678    // Initialize cart visibility state
679    const cartContainer = document.querySelector('#cart-container');
680    if (cartContainer) {
681      isCartVisible = cartContainer.classList.contains('active');
682    }
683    
684    // Cart toggle button - ensure it works on all pages
685    const navShopifyCart = document.querySelector('#nav-shopify-cart');
686    if (navShopifyCart) {
687      navShopifyCart.addEventListener('click', (e) => {
688        e.preventDefault();
689        e.stopPropagation(); // Prevent event from bubbling up
690        
691        // Always refresh cart data before showing
692        const initialCart = initializeCart();
693        if (initialCart) {
694          window.ShopifyAPIClient.getCart(initialCart.id)
695            .then(result => {
696              if (result.data?.cart) {
697                updateCartState(result.data.cart);
698              } else {
699                clearExpiredCart();
700              }
701              toggleCart();
702            })
703            .catch(error => {
704              console.error('Error refreshing cart:', error);
705              toggleCart();
706            });
707        } else {
708          // If no cart exists, just show the empty cart UI
709          toggleCart();
710        }
711      });
712    }
713    
714    // Cart close button
715    const cartClose = document.querySelector('#cart-close');
716    if (cartClose) {
717      cartClose.addEventListener('click', (e) => {
718        e.preventDefault();
719        e.stopPropagation();
720        hideCart();
721      });
722    }
723
724    // Continue Shopping button
725    const continueShopping = document.querySelector('#cart-continue-shopping');
726    if (continueShopping) {
727      continueShopping.addEventListener('click', (e) => {
728        e.preventDefault();
729        e.stopPropagation();
730        hideCart();
731      });
732    }
733    
734    // Close cart when clicking outside
735    document.addEventListener('mousedown', (e) => {
736      // Skip if cart is not visible
737      if (!isCartVisible) return;
738      
739      const cartContainer = document.querySelector('#cart-container');
740      if (!cartContainer) return;
741      
742      const navShopifyCart = document.querySelector('#nav-shopify-cart');
743      
744      // Check if the click was on an add to cart button or within the cart
745      const isAddToCartButton = e.target.closest('.add-to-cart-button') || 
746                               e.target.closest('.buy-now-cta') ||
747                               e.target.closest('.toc-add-to-cart-button') ||
748                               e.target.closest('.article-add-to-cart-button') ||
749                               e.target.closest('.rec-product-atc-1') ||
750                               e.target.closest('.rec-product-atc-2') ||
751                               e.target.closest('.rec-product-atc-3') ||
752                               e.target.closest('#careplan-select-cta') ||
753                               e.target.closest('.careplan-atc');
754      
755      const isCartToggle = navShopifyCart && navShopifyCart.contains(e.target);
756      const isInsideCart = cartContainer.contains(e.target);
757      
758      // Only hide the cart if the click was outside the cart and not on the cart toggle or an add to cart button
759      if (!isInsideCart && !isCartToggle && !isAddToCartButton) {
760        hideCart();
761      }
762    });
763  };
764
765  return {
766    init: function (configuration, optionalElements) {
767      config = configuration;
768      elements = optionalElements || {};
769
770      // Initialize event listeners
771      initializeEventListeners();
772
773      // Load initial cart data
774      const initialCart = initializeCart();
775      if (initialCart) {
776        window.ShopifyAPIClient.getCart(initialCart.id).then(result => {
777          if (result.data?.cart) {
778            updateCartState(result.data.cart);
779          } else {
780            clearExpiredCart();
781          }
782        }).catch(error => {
783          console.error('Error loading initial cart:', error);
784        });
785      }
786    },
787
788    addToCart,
789    refreshCart: async function () {
790      if (!cart) return;
791      try {
792        const result = await window.ShopifyAPIClient.getCart(cart.id);
793        if (result.data?.cart) {
794          updateCartState(result.data.cart);
795        } else {
796          clearExpiredCart();
797        }
798      } catch (e) {
799        console.error('Error refreshing cart:', e);
800      }
801    },
802    showCart,
803    hideCart,
804    toggleCart,
805    updateCartState,
806    registerSellingPlanGroupNames: function (map) {
807      Object.assign(sellingPlanGroupNames, map);
808    }
809  };
810})(); 

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.