1/** 2 * Shopify TOC Module 3 * Handles all TOC page product functionality including price display and cart operations. 4 * Optimized for pages with many products. 5 */ 6 7window.ShopifyTOC = (function () { 8 'use strict'; 9 10 let config = null; 11 let elements = null; 12 let productCache = {}; 13 let currentTab = 'tabs-1'; 14 let isProcessing = false; 15 let pendingProductIds = []; 16 17 // Format price with tax calculation 18 const formatPrice = (amount, currencyCode) => { 19 if (!amount) return ''; 20 21 let numericAmount = parseFloat(amount); 22 23 // Apply tax if configured 24 if (config && config.shopifyConfig) { 25 const taxIncluded = config.shopifyConfig.shopify_tax_included; 26 const taxRate = parseFloat(config.shopifyConfig.shopify_tax_rate); 27 28 if (taxIncluded === 'True' && taxRate && taxRate > 0) { 29 numericAmount = numericAmount * (1 + (taxRate / 100)); 30 } 31 } 32 33 // Format the price with appropriate currency symbol 34 const formatter = new Intl.NumberFormat(config?.shopifyConfig?.lang || 'en-US', { 35 style: 'decimal', 36 minimumFractionDigits: 2, 37 maximumFractionDigits: 2 38 }); 39 40 return formatter.format(numericAmount); 41 }; 42 43 // Get currency symbol based on currency code 44 const getCurrencySymbol = (currencyCode) => { 45 if (!currencyCode) return '$'; // Default to $ if no currency code 46 47 // Check if we have a currency symbol in the config 48 if (config && config.shopifyConfig && config.shopifyConfig.currency_symbol) { 49 return config.shopifyConfig.currency_symbol; 50 } 51 52 // Fallback mapping based on currency code 53 const currencyMap = { 54 'USD': '$', 55 'AUD': 'A$', 56 'SGD': 'S$', 57 'GBP': '£', 58 'EUR': 'â¬', 59 }; 60 61 return currencyMap[currencyCode] || '$'; 62 }; 63 64 // Create Add to Cart button for product variant 65 const createAddToCartButton = (variant) => { 66 const button = document.createElement('button'); 67 button.className = 'toc-add-to-cart-button'; 68 button.textContent = 'Add to cart'; 69 70 // Disable button if variant is not available 71 if (!variant.availableForSale) { 72 button.disabled = true; 73 button.classList.add('disabled'); 74 button.textContent = 'Out of Stock'; 75 } else { 76 // Add click handler 77 button.addEventListener('click', async (event) => { 78 // Prevent event propagation to avoid document click handler 79 event.preventDefault(); 80 event.stopPropagation(); 81 82 try { 83 await window.ShopifyCart.addToCart(variant.id, 1); 84 } catch (error) { 85 console.error('Error adding TOC product to cart:', error); 86 } 87 }); 88 } 89 90 return button; 91 }; 92 93 // Process a single product 94 const processProduct = async (productId, container) => { 95 try { 96 const resolvedGid = container.getAttribute('data-shopify-gid'); 97 const isNidMode = config.shopifyProductLookup === 'nid'; 98 const useResolvedGid = isNidMode && resolvedGid; 99 100 // In NID mode, skip products that have no resolved GID â they are not in 101 // the sync table and the legacy Shopify ID is not a valid fallback. 102 if (isNidMode && !resolvedGid) { 103 return; 104 } 105 106 const cacheKey = useResolvedGid ? resolvedGid : productId; 107 108 // Check cache first 109 if (productCache[cacheKey]) { 110 renderProduct(productCache[cacheKey], container); 111 return; 112 } 113 114 const result = useResolvedGid 115 ? await window.ShopifyAPIClient.getProduct(resolvedGid) 116 : await window.ShopifyAPIClient.getProduct(`gid://shopify/Product/${productId}`); 117 118 if (!result.data?.product) { 119 console.warn(`TOC product not found: ${useResolvedGid ? 'gid=' + resolvedGid : productId}`); 120 return; 121 } 122 123 const product = result.data.product; 124 125 // Cache the product data 126 productCache[cacheKey] = product; 127 128 // Render the product 129 renderProduct(product, container); 130 } catch (error) { 131 console.error(`Error processing TOC product ${productId}:`, error); 132 } 133 }; 134 135 // Render product in container 136 const renderProduct = (product, container) => { 137 // Get first variant 138 const variant = product.variants.edges[0]?.node; 139 140 if (!variant) { 141 console.warn(`No variant found for TOC product: ${product.title}`); 142 return; 143 } 144 145 // Clear existing content 146 container.innerHTML = ''; 147 148 // Create price element 149 const priceElement = document.createElement('div'); 150 priceElement.className = 'toc-price'; 151 // Adding this for JSON-LD schema purpose 152 var availability = ''; 153 if (!variant.availableForSale || (variant.quantityAvailable !== null && variant.quantityAvailable <= 0)
153) { 154 availability = 'OutOfStock'; 155 } 156 else{ 157 availability = 'InStock'; 158 } 159 const currencyCode = variant.price.currencyCode; 160 const currencySymbol = getCurrencySymbol(currencyCode); 161 const formattedPrice = formatPrice(variant.price.amount, currencyCode); 162 163 // Find the first variant with a valid sale price (compareAtPrice > price). 164 // The first variant may not have compareAtPrice even when others do (NID mode). 165 const saleVariant = product.variants.edges.find( 166 e => e.node.compareAtPrice && parseFloat(e.node.compareAtPrice.amount) > parseFloat(e.node.price.amount) 167 )?.node; 168 169 // Check if there's a compare-at price that is actually lower than the current price 170 if (saleVariant) { 171 const formattedCompareAtPrice = formatPrice(saleVariant.compareAtPrice.amount, currencyCode); 172 priceElement.innerHTML = ` 173 <span class="product-sale">Sale </span> 174 <span class="product-currency-code sale-currency" data-currencycode=${currencyCode}>${currencySymbol}</span><span class="shopify-actual-price on-sale" data-availability=${availability}>${formattedPrice}</span> 175 <span class="product-currency-code" data-currencycode=${currencyCode}>${currencySymbol}</span><span class="product-sale-compare">${formattedCompareAtPrice}</span> 176 `; 177 } else { 178 // If no compare-at price, just show the regular price 179 priceElement.innerHTML = `<span class="product-currency-code" data-currencycode=${currencyCode}>${currencySymbol}</span><span class="shopify-actual-price" data-availability=${availability}>${formattedPrice}</span>`; 180 } 181 182 // For Singapore locale, add Add to Cart button 183 if (config.shopifyConfig.lang === 'en-sg') { 184 const addToCartButton = createAddToCartButton(variant); 185 container.appendChild(addToCartButton); 186 } 187 188 // Add price element 189 container.appendChild(priceElement); 190 }; 191 192 // Process visible products in the current tab 193 const processVisibleProducts = async () => { 194 if (isProcessing) return; 195 196 isProcessing = true; 197 198 try { 199 const activeTab = document.getElementById(currentTab); 200 if (!activeTab) { 201 return; 202 } 203 204 // Find all product containers in the active tab 205 const productContainers = activeTab.querySelectorAll('.toc-shopify-cta'); 206 207 // Process products in batches for better performance 208 const batchSize = 5; 209 const batches = []; 210 211 for (let i = 0; i < productContainers.length; i += batchSize) { 212 batches.push(Array.from(productContainers).slice(i, i + batchSize)); 213 } 214 215 for (const batch of batches) { 216 await Promise.all(batch.map(container => { 217 const productId = container.id; 218 const hasGid = container.getAttribute('data-shopify-gid'); 219 if (productId || hasGid) { 220 return processProduct(productId, container); 221 } 222 return Promise.resolve(); 223 })); 224 225 // Small delay between batches to avoid blocking the UI 226 await new Promise(resolve => setTimeout(resolve, 10)); 227 } 228 } finally { 229 isProcessing = false; 230 231 // Process any pending products that were added while processing 232 if (pendingProductIds.length > 0) { 233 const productIds = [...pendingProductIds]; 234 pendingProductIds = []; 235 236 for (const productId of productIds) { 237 const container = document.getElementById(productId); 238 if (container) { 239 await processProduct(productId, container); 240 } 241 } 242 } 243 } 244 }; 245 246 // Handle tab change 247 const handleTabChange = (tabId) => { 248 currentTab = tabId; 249 processVisibleProducts(); 250 }; 251 252 // Initialize tab event listeners 253 const initializeTabListeners = () => { 254 // Find tab links 255 const tabLinks = document.querySelectorAll('a[href^="#tabs-"]'); 256 257 tabLinks.forEach(link => { 258 link.addEventListener('click', () => { 259 const tabId = link.getAttribute('href').substring(1); 260 handleTabChange(tabId); 261 }); 262 }); 263 264 // Check if there's a hash in the URL 265 if (window.location.hash && window.location.hash.startsWith('#tabs-')) { 266 currentTab = window.location.hash.substring(1); 267 } else if (tabLinks.length > 0) { 268 // FAMS-511: If no hash or tabs-1 doesn't exist, use first available tab 269 currentTab = tabLinks[0].getAttribute('href').substring(1); 270 } 271 272 // Process initial tab 273 processVisibleProducts(); 274 }; 275 276 // Add a product to be processed 277 const addProduct = (productId) => { 278 if (isProcessing) { 279 pendingProductIds.push(productId); 280 } else { 281 const container = document.getElementById(productId); 282 if (container) { 283 processProduct(productId, container); 284 } 285 } 286 }; 287 288 return { 289 init: function (shopifyConfig, optionalElements) { 290 config = shopifyConfig; 291 elements = optionalElements || {}; 292 293 // Initialize tab listeners 294 initializeTabListeners(); 295 }, 296 297 // Public method to add a product 298 addProduct, 299 300 // Public method to process products in a specific tab 301 processTab: handleTabChange 302 }; 303})();
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.