PageSourceSearch

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

js fluke.com collected 2026-09-24 08:51:05 UTC 9,952 bytes, 303 lines download raw bytes

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&nbsp;</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.