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">–</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.