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.