PageSourceSearch

https://shop.skinnyraven.com/assets/scripts/shared/colorSwatches.js

js skinnyraven.com collected 2026-09-26 01:39:14 UTC 15,536 bytes, 406 lines download raw bytes

1/**
2 * Color Swatch Component
3 * 
4 * Provides interactive color variant swatches for product grids.
5 * Features hover preview, click navigation, intelligent sorting, and performance optimization.
6 * 
7 * Usage:
8 *   colorSwatches.init(containerSelector, options);
9 *   colorSwatches.load();
10 */
11var colorSwatches = (function() {
12  var cache = {};
13  var containerSelector = '#ProductList';
14  var options = {};
15  
16  return {
17    /**
18     * Initialize the color swatch component
19     * @param {string} selector - jQuery selector for the product grid container
20     * @param {object} opts - Optional configuration
21     */
22    init: function(selector, opts) {
23      containerSelector = selector || '#ProductList';
24      options = opts || {};
25      cache = {};
26    },
27    
28    /**
29     * Load color swatches for all products in the container
30     */
31    load: function() {
32      // Guard against missing dependencies
33      if (typeof $ === 'undefined') {
34        return;
35      }
36      
37      var productsToFetch = [];
38      var containers = [];
39      
40      $('.colorswatches[data-total-colors]').each(function() {
41        var container = $(this);
42        var totalColors = parseInt(container.attr('data-total-colors'));
43        var productName = container.attr('data-product-name');
44        
45        // Handle single-color products - render single swatch from existing product image
46        if (totalColors === 1) {
47          var $productCard = container.closest('.product');
48          var $productImage = $productCard.find('.image');
49          var currentBg = $productImage.css('background-image');
50          
51          // Extract the image URL from background-image
52          var imageUrl = null;
53          if (currentBg && currentBg !== 'none') {
54            var urlMatch = currentBg.match(/url\(["']?([^"')]+)["']?\)/);
55            if (urlMatch && urlMatch[1]) {
56              imageUrl = urlMatch[1];
57            }
58          }
59          
60          if (imageUrl) {
61            // Create a single swatch using the product's current image
62            var singleColorVariant = [{
63              url: imageUrl,
64              color: productName,
65              isPrimary: 1
66            }];
67            colorSwatches.render(container, singleColorVariant, true);
68          }
69        } else if (totalColors > 1 && productName) {
70          // Check cache first
71          if (cache[productName]) {
72            colorSwatches.render(container, cache[productName]);
73          } else {
74            if (productsToFetch.indexOf(productName) === -1) {
75              productsToFetch.push(productName);
76            }
77            containers.push({ name: productName, container: container });
78          }
79        }
80      });
81      
82      // Batch fetch all needed variants in one API call
83      if (productsToFetch.length > 0) {
84        // Try service.call first, fall back to $.ajax
85        var makeRequest = function(onSuccess, onError) {
86          if (typeof service !== 'undefined' && typeof service.call === 'function') {
87            service.call("product/colors/batch", { names: productsToFetch }, onSuccess);
88          } else if (typeof $.ajax === 'function') {
89            $.ajax({
90              url: '/api/product/colors/batch',
91              method: 'POST',
92              contentType: 'application/json',
93              data: JSON.stringify({ names: productsToFetch }),
94              success: onSuccess,
95              error: onError
96            });
97          }
98        };
99        
100        makeRequest(
101          function(result) {
102            if (result && result.data) {
103              // Cache the results
104              Object.assign(cache, result.data);
105              
106              // Render swatches for each container
107              containers.forEach(function(item) {
108                var variants = result.data[item.name];
109                if (variants && variants.length > 0) {
110                  colorSwatches.render(item.container, variants);
111                }
112              });
113              
114              // Set up event delegation
115              colorSwatches.setupEvents();
116            }
117          },
118          function(error) {
119            // Silently fail - swatches are enhancement, not critical
120            if (typeof console !== 'undefined' && console.warn) {
121              console.warn('Color swatches failed to load:', error);
122            }
123          }
124        );
125      } else if (containers.length === 0) {
126        // All cached, still need to set up event delegation
127        colorSwatches.setupEvents();
128      }
129    },
130    
131    /**
132     * Render color swatches for a product
133     * @param {jQuery} container - The .colorswatches container element
134     * @param {Array} colors - Array of color variant objects
135     * @param {boolean} isSingleColor - Optional flag indicating this is a single-color product
136     */
137    render: function(container, colors, isSingleColor) {
138      // Guard against missing dependencies
139      if (typeof products === 'undefined' || typeof products.getSize !== 'function' || typeof products.getCanonical !== 'function') {
140        return;
141      }
142      
143      var MAX_VISIBLE = 6;
144      
145      // Don't show swatches if no colors
146      if (!colors || colors.length === 0) {
147        container.empty().hide();
148        return;
149      }
150      
151      container.empty();
152      
153      // Get the currently displayed product image to identify which variant is showing
154      var $productCard = container.closest('.product');
155      var $productImage = $productCard.find('.image');
156      var currentBg = $productImage.css('background-image');
157      
158      // Extract the image filename from the background-image URL
159      var currentImageFile = null;
160      if (currentBg && currentBg !== 'none') {
161        var urlMatch = currentBg.match(/url\(["']?([^"')]+)["']?\)/);
162        if (urlMatch && urlMatch[1]) {
163          var urlParts = urlMatch[1].split('/');
164          currentImageFile = urlParts[urlParts.length - 1];
165        }
166      }
167      
168      // Sort colors array with priority: isPrimary first, then currently displayed, then original order
169      var sortedColors = colors.slice(); // Copy array
170      if (currentImageFile) {
171        // Extract just the SKU part (e.g., "1104093-IBMR" from "(1104093-IBMR)_(8).jpg")
172        var extractSKU = function(filename) {
173          if (!filename) return '';
174          return filename
175            .replace(/\.[^.]+$/, '')  // Remove extension
176            .replace(/[()]/g, '')      // Remove parentheses
177            .replace(/_.*$/, '')       // Remove underscore suffix
178            .trim();
179        };
180        
181        var currentSKU = extractSKU(currentImageFile);
182        
183        sortedColors.sort(function(a, b) {
184          var aFile = a.url ? a.url.split('/').pop() : '';
185          var bFile = b.url ? b.url.split('/').pop() : '';
186          var aSKU = extractSKU(aFile);
187          var bSKU = extractSKU(bFile);
188          
189          var aIsPrimary = a.isPrimary === 1 || a.isPrimary === true;
190          var bIsPrimary = b.isPrimary === 1 || b.isPrimary === true;
191          var aIsCurrent = currentSKU && aSKU.includes(currentSKU);
192          var bIsCurrent = currentSKU && bSKU.includes(currentSKU);
193          
194          // Priority 1: isPrimary flag (store's explicit choice)
195          if (aIsPrimary && !bIsPrimary) return -1;
196          if (!aIsPrimary && bIsPrimary) return 1;
197          
198          // Priority 2: Currently displayed variant
199          if (aIsCurrent && !bIsCurrent) return -1;
200          if (!aIsCurrent && bIsCurrent) return 1;
201          
202          // Priority 3: Maintain original order
203          return 0;
204        });
205      }
206      
207      // Store total color count on container for overflow calculation
208      container.attr('data-total-colors', colors.length);
209      
210      // Show up to 6 swatches
211      var visibleColors = sortedColors.slice(0, MAX_VISIBLE);
212      
213      visibleColors.forEach(function(colorVariant, index) {
214        try {
215          // products.getSize() converts to CDN URLs and handles retina displays
216          // Use 75px for swatches (150px on 2x displays)
217          var swatchThumbUrl = products.getSize(75, colorVariant.url);
218          // Use 380px for preview (will be 760px on 2x displays) to match product tile size
219          var previewUrl = products.getSize(380, colorVariant.url);
220          
221          var swatchClass = 'colorswatch';
222          if (isSingleColor) {
223            swatchClass += ' single-color';
224          }
225          
226          var swatch = $('<span class="' + swatchClass + '"></span>')
227            .attr('style', 'background-image: url("' + swatchThumbUrl + '")')
228            .attr('title', colorVariant.color || '')
229            .attr('data-product-url', isSingleColor ? '' : products.getCanonical(colorVariant))
230            .attr('data-preview-url', previewUrl);
231          
232          container.append(swatch);
233        } catch (e) {
234          // Skip this swatch if any error occurs
235          if (typeof console !== 'undefined' && console.warn) {
236            console.warn('Failed to render swatch:', e);
237          }
238        }
239      });
240      
241      // Add "+X more" indicator if there are additional colors
242      if (colors.length > MAX_VISIBLE) {
243        var remaining = colors.length - MAX_VISIBLE;
244        var moreIndicator = $('<span class="colorswatch more" data-original-count="' + remaining + '">+' + remaining + '</span>');
245        container.append(moreIndicator);
246      }
247      
248      // Apply responsive overflow behavior to prevent wrapping
249      colorSwatches.handleOverflow(container);
250    },
251    
252    /**
253     * Handle overflow by hiding swatches that would wrap to a second line
254     * @param {jQuery} container - The .colorswatches container element
255     */
256    handleOverflow: function(container) {
257      if (!container || container.length === 0) return;
258      
259      var swatches = container.find('.colorswatch');
260      if (swatches.length === 0) return;
261      
262      // Get total color count from container data attribute
263      var totalColorCount = parseInt(container.attr('data-total-colors')) || swatches.not('.more').length;
264      
265      // Show all swatches first to measure
266      swatches.show();
267      
268      var containerWidth = container.width();
269      var gap = parseInt(container.css('gap')) || 6;
270      var totalWidth = 0;
271      var visibleCount = 0;
272      var moreIndicator = container.find('.colorswatch.more');
273      var hasMoreIndicator = moreIndicator.length > 0;
274      var originalHiddenCount = parseInt(moreIndicator.attr('data-original-count')) || 0;
275      
276      // Measure the "more" indicator width if it exists
277      var moreWidth = 0;
278      if (hasMoreIndicator) {
279        moreWidth = moreIndicator.outerWidth() + gap;
280      }
281      
282      // Calculate how many swatches fit in one line
283      var colorSwatches = swatches.not('.more');
284      colorSwatches.each(function(index) {
285        var $swatch = $(this);
286        var swatchWidth = $swatch.outerWidth();
287        
288        var projectedWidth = totalWidth + swatchWidth + (visibleCount > 0 ? gap : 0);
289        
290        // Check if we need to reserve space for the more indicator
291        var willNeedMoreIndicator = (index < colorSwatches.length - 1) || originalHiddenCount > 0;
292        
293        if (willNeedMoreIndicator && projectedWidth + moreWidth > containerWidth) {
294          // This swatch won't fit with the more indicator - hide it
295          $swatch.hide();
296        } else if (projectedWidth > containerWidth) {
297          // This swatch won't fit
298          $swatch.hide();
299        } else {
300          // This swatch fits
301          totalWidth = projectedWidth;
302          visibleCount++;
303        }
304      });
305      
306      // Calculate total hidden count: originally hidden + newly hidden by overflow
307      var visibleColorCount = colorSwatches.filter(':visible').length;
308      var totalHiddenCount = totalColorCount - visibleColorCount;
309      
310      // Update or create the "+X more" indicator
311      if (totalHiddenCount > 0) {
312        if (moreIndicator.length > 0) {
313          moreIndicator.text('+' + totalHiddenCount).show();
314        } else {
315          // Create new more indicator
316          var newMoreIndicator = $('<span class="colorswatch more">+' + totalHiddenCount + '</span>');
317          container.append(newMoreIndicator);
318        }
319      } else if (moreIndicator.length > 0) {
320        // All swatches are visible, remove the indicator
321        moreIndicator.remove();
322      }
323    },
324    
325    /**
326     * Set up event delegation for hover and click interactions
327     */
328    setupEvents: function() {
329      // Guard against missing jQuery
330      if (typeof $ === 'undefined') {
331        return;
332      }
333      
334      var container = $(containerSelector);
335      if (!container.length) {
336        return;
337      }
338      
339      // Remove old delegated events if any
340      container.off('mouseenter.swatches mouseleave.swatches click.swatches');
341      
342      // Hover to preview variant image (exclude single-color swatches)
343      container.on('mouseenter.swatches', '.colorswatch:not(.more):not(.single-color)', function(e) {
344        var $swatch = $(this);
345        var $productCard = $swatch.closest('.product');
346        var $productImage = $productCard.find('.image');
347        var previewUrl = $swatch.data('preview-url');
348        
349        // Store original image if not already stored
350        if (!$productImage.data('original-image')) {
351          var currentBg = $productImage.css('background-image');
352          $productImage.data('original-image', currentBg);
353        }
354        
355        // Swap to variant image
356        if (previewUrl) {
357          $productImage.css('background-image', 'url("' + previewUrl + '")');
358        }
359      });
360      
361      // Restore original image on mouse leave (exclude single-color swatches)
362      container.on('mouseleave.swatches', '.colorswatch:not(.more):not(.single-color)', function(e) {
363        var $swatch = $(this);
364        var $productCard = $swatch.closest('.product');
365        var $productImage = $productCard.find('.image');
366        var originalImage = $productImage.data('original-image');
367        
368        if (originalImage) {
369          $productImage.css('background-image', originalImage);
370        }
371      });
372      
373      // Click to navigate to variant product page (exclude single-color swatches)
374      container.on('click.swatches', '.colorswatch:not(.more):not(.single-color)', function(e) {
375        e.stopPropagation();
376        e.preventDefault();
377        var url = $(this).data('product-url');
378        if (url) {
379          window.location = url;
380        }
381      });
382    },
383    
384    /**
385     * Clear the cache (useful for page transitions)
386     */
387    clearCache: function() {
388      cache = {};
389    }
390  };
391})();
392
393// Handle window resize to recalculate overflow
394if (typeof $ !== 'undefined') {
395  var resizeTimer;
396  $(window).on('resize', function() {
397    clearTimeout(resizeTimer);
398    resizeTimer = setTimeout(function() {
399      $('.colorswatches').each(function() {
400        if (typeof colorSwatches !== 'undefined' && colorSwatches.handleOverflow) {
401          colorSwatches.handleOverflow($(this));
402        }
403      });
404    }, 150);
405  });
406}

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.