1/** 2 * Swatchly Frontend JS 3 */ 4;( function ( $ ) { 5 "use strict"; 6 7 if ( typeof swatchly_params === 'undefined' ) { 8 return false; 9 } 10 11 /* Builder Compatibility Utilities 12 ======================================================= */ 13 var builderCompat = { 14 isOxygen: function() { 15 return Boolean(Number(swatchly_params.is_oxygen_builder)); 16 }, 17 18 findProductContainer: function($element) { 19 // Try standard .product first 20 var $container = $element.closest('.product'); 21 if ($container.length) { 22 return $container; 23 } 24 25 // Oxygen Builder: look for ct-div-block with product-related class 26 if (this.isOxygen()) { 27 $container = $element.closest('.ct-div-block[class*="roduct"]'); 28 if ($container.length) { 29 return $container; 30 } 31 $container = $element.closest('.ct-div-block'); 32 if ($container.length) { 33 return $container; 34 } 35 } 36 37 // Final fallback 38 return $element.closest('.product'); 39 } 40 }; 41 42 /* Tooltip JS 43 ======================================================= */ 44 function addTooltip(){ 45 var image_src = $(this).data('tooltip_image'), 46 text = $(this).data('tooltip_text'); 47 48 if( !image_src && !text ){ 49 return; 50 } 51 52 var $tooltip = $('<div class="swatchly-tooltip"></div>'); 53 54 if(text){ 55 $tooltip.append( $('<span class="swatchly-tooltip-text"></span>').text( text ) ); 56 } 57 58 if(image_src){ 59 $tooltip.append( $('<img />').attr( 'src', image_src ) ); 60 } 61 62 $(this).append($tooltip); 63 } 64 65 function removeTooltip(e){ 66 $('.swatchly-tooltip').remove(); 67 } 68 69 $(document).ready(function() { 70 $(document).on( 'mouseover', 'div.swatchly-swatch,a.swatchly-swatch', addTooltip ); 71 $(document).on('mouseleave', 'div.swatchly-swatch,a.swatchly-swatch', removeTooltip); 72 }); 73 74 /* For both Single & Product loop 75 ======================================================= */ 76 $(document).ready(function(){ 77 $('.swatchly-hide-this-variation-row').closest('tr').addClass('swatchly_d_none'); 78 }); 79 80 /* Product Loop JS 81 ======================================================= */ 82 var product_loop = { 83 /* 84 * Some thems make the full product loop item linked to the product details. 85 * Preventing the user from clicking on the product loop item so the swatches will be clickable. 86 */ 87 prevent_click: function(){ 88 $('.swatchly_loop_variation_form').on( 'click', function(e){ 89 if( $(e.target).is('.swatchly-more-button') || $(e.target).closest('.swatchly-more-button').length ){ 90 return; 91 } 92 93 e.preventDefault(); 94 e.stopPropagation(); 95 e.stopImmediatePropagation(); 96 }); 97 }, 98 init_variation_form: function(){ 99 var enable_swatches = Boolean(Number(swatchly_params.enable_swatches)); 100 101 if(enable_swatches){ 102 $( '.swatchly_loop_variation_form' ).swatchly_loop_variation_form(); 103 } 104 } 105 } 106 107 $(document).ready(function(){ 108 $('.swatchly_loop_variation_form').addClass('swatchly_loaded_on_ready'); 109 product_loop.prevent_click(); 110 }); 111 112 // Reset the changes 113 $.fn.reset = function (event){ 114 $(this).find('.swatchly_ajax_add_to_cart').removeClass('alt disabled wc-variation-is-unavailable swatchly_found_variation added'); 115 116 if(event === 'click'){ 117 var $button_text = $(this).find('.swatchly_ajax_add_to_cart').data('select_options_text') 118 $(this).find('.swatchly_ajax_add_to_cart').text($button_text); 119 } 120 121 // remove veiw cart button 122 $(this).find('.added_to_cart.wc-forward').remove(); 123 $(this).remove_out_of_stock(); 124 125 // hide reset button 126 $(this).find('.reset_variations').attr('style', 'display: none !important'); 127 }; 128 129 // Reset to the initial price 130 $.fn.reset_to_default_price = function(){ 131 $(this).find('.price, .wc-block-components-product-price').first().removeClass('swatchly_d_none'); 132 $(this).find('.swatchly_price').remove(); 133 }; 134 135 // Don't show the out of stock element 136 $.fn.remove_out_of_stock = function (){ 137 if($(this).find('.swatchly_pl.swatchly_out_of_stock')){ 138 $(this).find('.swatchly_pl.swatchly_out_of_stock').remove(); 139 } 140 } 141 142 // Find the product image selector 143 $.fn.get_product_image_selector = function(){ 144 var $product_thumbnail = '', 145 product_thumbnail_selector = swatchly_params.product_thumbnail_selector,
146 $productContainer = builderCompat.findProductContainer($(this)); 147 148 // custom selector priority first 149 if(product_thumbnail_selector){ 150 $product_thumbnail = $(this).find(product_thumbnail_selector); 151 152 if( !$product_thumbnail.length && $productContainer.find(product_thumbnail_selector).length ){ 153 $product_thumbnail = $productContainer.find(product_thumbnail_selector); 154 } 155 156 return $product_thumbnail; 157 } 158 159 // Check if product has slider 160 var has_slider = false; 161 if($(this).find('.gallery-image').length){ 162 has_slider = true; 163 return; 164 } 165 166 // look for default wc image selector 167 $product_thumbnail = $(this).find('img.attachment-woocommerce_thumbnail'); 168 169 // ocean theme support 170 if($product_thumbnail.length === 0){ 171 $product_thumbnail = $(this).find('img.woo-entry-image-main'); 172 173 // look for other default seletors 174 if($product_thumbnail.length === 0){ 175 $product_thumbnail = $(this).find('img.attachment-woocommerce_thumbnail'); 176 177 if($product_thumbnail.length === 0){ 178 $product_thumbnail = $(this).find('img.wp-post-image'); 179 180 // Oxygen builder support 181 if($product_thumbnail.length === 0 && builderCompat.isOxygen()){ 182 $product_thumbnail = $productContainer.find('.ct-image'); 183 } 184 185 if($product_thumbnail.length === 0){ 186 $product_thumbnail = $productContainer.find('img').first(); 187 } 188 } 189 } 190 } 191 192 // Support WL Universal grid image slider 193 if($(this).find('.ht-product-image-slider').length !== 0 ){ 194 $product_thumbnail = $(this).find('.slick-slide.ht-slider-first-item img'); 195 } 196 197 return $product_thumbnail; 198 }; 199 200 // Backup product image properties 201 // Needed when reset variation & show the default image 202 $.fn.backup_product_image = function(){ 203 var $product_thumbnail = $(this).get_product_image_selector(); 204 205 // If the product thumbail does not exists, do nothing 206 if(!$product_thumbnail){ 207 return; 208 } 209 210 // Clone & set default image's properties 211 var backup_attributes = { 212 "data-backup_alt": $product_thumbnail.attr('alt'), 213 "data-backup_src": $product_thumbnail.attr('src'), 214 "data-backup_width": $product_thumbnail.attr('width'), 215 "data-backup_height": $product_thumbnail.attr('height') 216 } 217 218 if( $product_thumbnail.attr('srcset') ) { 219 backup_attributes["data-backup_srcset"] = $product_thumbnail.attr( 'srcset' ); 220 backup_attributes["data-backup_sizes"] = $product_thumbnail.attr( 'sizes' ); 221 } 222 223 $product_thumbnail.attr(backup_attributes); 224 }; 225 226 // Change the product image when variation found 227 $.fn.change_image = function(variation){ 228 // image selector 229 var $product_thumbnail = $(this).get_product_image_selector(); 230 231 // If the product thumbail does not exists, do nothing 232 if(!$product_thumbnail){ 233 return; 234 } 235 236 var attributes = { 237 alt: variation.image.alt, 238 src: variation.image.thumb_src, 239 width: variation.image.thumb_src_w, 240 height: variation.image.thumb_src_h 241 }; 242 243 if( $product_thumbnail.attr('srcset') ) { 244 attributes.srcset = variation.image.srcset; 245 attributes.sizes = variation.image.sizes; 246 } 247 248 // Finally change/update image 249 $product_thumbnail.attr(attributes); 250 251 // Support WL Universal grid image slider 252 if( $(this).find('.ht-product-image-slider').length !== 0 ){ 253 $(this).find('.ht-product-image-slider').slick('slickGoTo', 0); 254 } 255 }; 256 257 // Reset to the default image when click on "reset" button 258 $.fn.reset_to_default_image = function(){ 259 // Image selector 260 var $product_thumbnail = $(this).get_product_image_selector(); 261 262 // Get backup attributes before reset 263 var backup_attributes = { 264 alt: $product_thumbnail.attr('data-backup_alt'), 265 src: $product_thumbnail.attr('data-backup_src'), 266 width: $product_thumbnail.attr('data-backup_width'), 267 height: $product_thumbnail.attr('data-backup_height') 268 } 269 270 if( $product_thumbnail.attr('srcset') ) { 271 backup_attributes["srcset"] = $product_thumbnail.attr( 'data-backup_srcset' ); 272 backup_attributes["sizes"] = $product_thumbnail.attr( 'data-backup_sizes' ); 273 } 274 275 // Finally reset the image 276 $product_thumbnail.attr(backup_attributes); 277
278 // Support WL Universal grid image slider 279 if( $(this).find('.ht-product-image-slider').length !== 0 ){ 280 $(this).find('.ht-product-image-slider').slick('slickGoTo', 0); 281 } 282 }; 283 284 // Change add to cart button text 285 $.fn.change_add_to_cart_text = function(){ 286 var original_cart_text = $(this).text(), 287 new_cart_text = $(this).data('add_to_cart_text'); 288 289 $(this).html($(this).html().replace(original_cart_text, new_cart_text)); 290 }; 291 292 // Function for each loop variation form 293 $.fn.swatchly_loop_variation_form = function(){ 294 // Classic themes use `.price`; block themes' Product Price block uses `.wc-block-components-product-price`. 295 var $price_selector = '.price, .wc-block-components-product-price'; 296 297 return this.each( function(){ 298 var $el_variation_form = $( this ), 299 $el_product = builderCompat.findProductContainer($el_variation_form), 300 $el_ajax_add_to_cart = $el_product.find('.swatchly_ajax_add_to_cart'); 301 302 $el_product.backup_product_image(); 303 304 // hide reset button by default 305 $el_product.find('.reset_variations').attr('style', 'display: none !important'); 306 307 $el_variation_form.on('found_variation', function(e, variation){ 308 $el_product.reset(); 309 310 var availability_html = variation.availability_html, 311 is_in_stock = variation.is_in_stock; 312 313 // If out of stcok 314 if(!is_in_stock){ 315 $(this).append(`<div class="swatchly_pl swatchly_out_of_stock">${availability_html}</div>`); 316 } else { 317 $el_product.remove_out_of_stock(); 318 } 319 320 // show reset buton once a vaiation found 321 $el_product.find('.reset_variations').attr('style', ''); 322 323 // Update price 324 if( !$el_product.find('.swatchly_price').length ){ 325 if($(variation.price_html).length){ 326 $el_product.find($price_selector).addClass('swatchly_d_none').after( $(variation.price_html).addClass('swatchly_price') ); 327 } 328 } else { 329 $el_product.find('.swatchly_price').remove(); 330 $el_product.find($price_selector).addClass('swatchly_d_none').after($(variation.price_html).addClass('swatchly_price')); 331 } 332 333 // Update cart text 334 if( $el_ajax_add_to_cart.length ){ 335 $el_ajax_add_to_cart.change_add_to_cart_text('found_variation'); 336 } 337 338 // Update Image 339 $el_product.change_image(variation); 340 341 // For ajax add to cart 342 // Manually generate selected variation attributes 343 var selected_variation = {}, 344 variations = $(this).find( 'select[name^=attribute]' ); 345 if ( !variations.length) { 346 variations = $(this).find( '[name^=attribute]:checked' ); 347 } 348 if ( !variations.length) { 349 variations = $(this).find( 'input[name^=attribute]' ); 350 } 351 352 variations.each( function() { 353 var $this_item = $( this ), 354 attribute_name = $this_item.attr( 'name' ), 355 attribute_value = $this_item.val(), 356 index, 357 attribute_tax_name; 358 $this_item.removeClass( 'error' ); 359 if ( attribute_value.length === 0 ) { 360 index = attribute_name.lastIndexOf( '_' ); 361 attribute_tax_name = attribute_name.substring( index + 1 ); 362 $this_item.addClass( 'required error' ); 363 } else { 364 selected_variation[attribute_name] = attribute_value; 365 } 366 }); 367 368 if($el_ajax_add_to_cart.length){ 369 $el_ajax_add_to_cart.addClass('swatchly_found_variation'); 370 } 371 $el_ajax_add_to_cart.attr('data-variation_id', variation.variation_id); 372 $el_ajax_add_to_cart.attr('data-variation', JSON.stringify(selected_variation)); 373 374 if($el_ajax_add_to_cart.hasClass('added')){ 375 $el_ajax_add_to_cart.removeClass('added'); 376 } 377 378 }).on('click','.reset_variations', function(e){ 379 $el_product.reset('click'); 380 $el_product.remove_out_of_stock(); 381 $el_product.reset_to_default_price(); 382 $el_product.reset_to_default_image(); 383 384 }); 385 }); 386 }; 387 388 // Product loop init 389 $(window).on('load', function(){ 390 product_loop.init_variation_form(); 391 } ); 392 393 /*===== End of Product Loop JS ======*/ 394 395 396 /* Single product page JS 397 ======================================================= */ 398 var single_product = { 399 init: function(){ 400 var enable_swatches = Boolean(Number(swatchly_params.enable_swatches)), 401 sp_override_global = Boolean(Number(swatchly_params.sp_override_global)), 402 pl_override_global = Boolean(Number(swatchly_params.pl_override_global)), 403 enable_variation_url = Boolean(Number(swatchly_params.enable_variation_url)), 404 enable_rv_variations = Boolean(Number(swatchly_params.enable_rv_variations)), 405 enable_sp_variation_url = Boolean(Number(swatchly_params.enable_sp_variation_url)), 406 enable_sp_rv_variations = Boolean(Number(swatchly_params.enable_sp_rv_variations)), 407 enable_pl_variation_url = Boolean(Number(swatchly_params.enable_pl_variation_url)), 408 enable_pl_rv_variations = Boolean(Number(swatchly_params.enable_pl_rv_variations)), 409 is_product = Boolean(Number(swatchly_params.is_product)), 410 deselect_on_click = Boolean(Number(swatchly_params.deselect_on_click)), 411 show_selected_attribute_name = Boolean(Number(swatchly_params.show_selected_attribute_name)), 412 variation_label_separator = swatchly_params.variation_label_separator; 413 414 const rv_variations_config = { 415 storage_key: 'swatchly_rv_variations', 416 }; 417 418 const loopVariationUrlParams = ({event, value, paramName}) => { 419 const product = builderCompat.findProductContainer($(event.target))[0]; 420 if (!product) return; 421 422 // Get the product URL from data attribute (theme-agnostic approach) 423 const variationForm = product.querySelector('.swatchly_loop_variation_form') || 424 product.querySelector('.variations_form') || 425 event.target.closest('.swatchly_loop_variation_form'); 426 const productUrlFromData = variationForm ? variationForm.getAttribute('data-product_url') : null; 427 428 // Fallback to selector-based approach for backwards compatibility 429 const productLink = product.querySelector('.woocommerce-loop-product__link') || 430 product.querySelector('.bundled_product_permalink') || 431 product.querySelector('a[href*="/product/"]') || 432 product.querySelector('a'); 433 434 // Determine the product URL pathname 435 let productUrlPathname = null; 436 if (productUrlFromData) { 437 try { 438 productUrlPathname = new URL(productUrlFromData).pathname; 439 } catch (e) { 440 // Invalid URL from data attribute, will use fallback 441 } 442 } 443 if (!productUrlPathname && productLink) { 444 try { 445 productUrlPathname = new URL(productLink.href).pathname; 446 } catch (e) { 447 // Invalid URL from link 448 } 449 } 450 451 const links = product.querySelectorAll('a');
452 Array.from(links).forEach(link => { 453 if(!$(link)?.attr('href')) return; 454 try { 455 const url = new URL(link.href); 456 // Match links by comparing pathname OR if link contains /product/ (backwards compat) 457 const shouldUpdate = productUrlPathname 458 ? (url.pathname === productUrlPathname) 459 : (url.href.indexOf('/product/') !== -1); 460 461 if(shouldUpdate) { 462 if(value) { 463 if(!url.searchParams.get(paramName)) { 464 url.searchParams.append(paramName, value); 465 } else { 466 url.searchParams.set(paramName, value) 467 } 468 } else { 469 url.searchParams.delete(paramName) 470 } 471 link.href = url.toString(); 472 } 473 } catch (e) { 474 // Ignore invalid URLs 475 } 476 }); 477 }, 478 singleVariationUrlParams = ({value, paramName}) => { 479 const url = new URL(location); 480 if(value) { 481 if(!url.searchParams.get(paramName)) { 482 url.searchParams.append(paramName, value); 483 } else { 484 url.searchParams.set(paramName, value) 485 } 486 } else { 487 url.searchParams.delete(paramName) 488 } 489 history.pushState({}, '', url.href); 490 }; 491 492 /** 493 * Recently Viewed Variations 494 * Update selected variations from local storage 495 */ 496 const updateRecentlyViewedVariations = ($el_variation_form) => { 497 const rv_variations = localStorage.getItem(rv_variations_config?.storage_key); 498 if(rv_variations){ 499 const parsed_rv_variations = JSON.parse(rv_variations); 500 if(parsed_rv_variations[$el_variation_form.data('product_id')]){ 501 const selected_variations = parsed_rv_variations[$el_variation_form.data('product_id')]; 502 $el_variation_form.find('select').each(function(){ 503 const $el_default_select = $(this); 504 const product_attributes = $el_default_select[0].name; 505 if(selected_variations[product_attributes]){ 506 $el_default_select.val(selected_variations[product_attributes]); 507 $el_default_select.change(); 508 const swatchly_swatch = $el_default_select.closest('td.value').find('.swatchly-swatch[data-attr_value="' + selected_variations[product_attributes] + '"]'); 509 if(swatchly_swatch.length){ 510 swatchly_swatch.addClass('swatchly-selected').siblings('div.swatchly-swatch').removeClass('swatchly-selected'); 511 } 512 } 513 }); 514 } 515 } 516 }; 517 518 if( enable_swatches){ 519 $.fn.swatchly_variation_form = function(){ 520 return this.each( function(){ 521 var $el_variation_form = $( this ); 522 523 /** 524 * Recently Viewed Variations 525 * Update selected variations from local storage 526 */ 527 // Global setting (when overrides are not active) 528 if(enable_rv_variations && !sp_override_global && !pl_override_global){ 529 updateRecentlyViewedVariations($el_variation_form); 530 } 531 // Single product page with override active 532 else if(enable_sp_rv_variations && is_product && sp_override_global){ 533 updateRecentlyViewedVariations($el_variation_form); 534 } 535 // Product listing page with override active 536 else if(enable_pl_rv_variations && !is_product && pl_override_global){ 537 updateRecentlyViewedVariations($el_variation_form); 538 } 539 540 // Actions while select a swatch 541 $el_variation_form.on( 'click', 'div.swatchly-swatch', function ( e ) { 542 var $el_swatch = $( this ), 543 $el_default_select = $el_swatch.closest( '.value' ).find( 'select' ), 544 value = $el_swatch.attr( 'data-attr_value' ); 545 546 if( !deselect_on_click ){ 547 // Add selected class & remove siblings selected class 548 $el_swatch.addClass('swatchly-selected').siblings('div.swatchly-swatch').removeClass('swatchly-selected'); 549 550 // Show selected variation name 551 if( show_selected_attribute_name ){ 552 if($el_swatch.closest('tr').find('.swatchly_selected_variation_name').length){ 553 $el_swatch.closest('tr').find('.swatchly_selected_variation_name').text( variation_label_separator + $el_swatch.data('attr_label') ); 554 } else { 555 $el_swatch.closest('tr').find('.label label').append( $('<span class="swatchly_selected_variation_name"></span>').text( variation_label_separator + $el_swatch.data('attr_label') ) ); 556 } 557 } 558 559 } else { 560 561 if($el_swatch.hasClass('swatchly-selected')){ 562 563 // Remove selection 564 $el_swatch.removeClass('swatchly-selected'); 565 566 // Change select field value to empty 567 value = ''; 568 569 // Remove selected variation name 570 $el_swatch.closest('tr').find('.swatchly_selected_variation_name').text( '' ); 571 572 } else { 573 574 // Add selected class & remove siblings selected class 575 $el_swatch.addClass('swatchly-selected').siblings('div.swatchly-swatch').removeClass('swatchly-selected'); 576 577 // Show selected variation name 578 if( show_selected_attribute_name ){ 579 if($el_swatch.closest('tr').find('.swatchly_selected_variation_name').length){ 580 $el_swatch.closest('tr').find('.swatchly_selected_variation_name').text( variation_label_separator + $el_swatch.data('attr_label') ); 581 } else { 582 $el_swatch.closest('tr').find('.label label').append( $('<span class="swatchly_selected_variation_name"></span>').text( variation_label_separator + $el_swatch.data('attr_label') ) ); 583 } 584 } 585 586 } 587 } 588 589 /** 590 * Recently Viewed Variations 591 * Save selected variations in local storage 592 */ 593 if((enable_rv_variations && !sp_override_global && !pl_override_global) || 594 (enable_sp_rv_variations && is_product && sp_override_global) || 595 (enable_pl_rv_variations && !is_product && pl_override_global)){ 596 const product_attributes = $el_default_select[0].name; 597 const product_id = $el_variation_form.data('product_id'); 598 const rv_variations = localStorage.getItem(rv_variations_config?.storage_key); 599 if(rv_variations){ 600 const parsed_rv_variations = JSON.parse(rv_variations); 601 if(parsed_rv_variations[product_id]){ 602 const selected_variations = parsed_rv_variations[product_id]; 603 localStorage.setItem(rv_variations_config?.storage_key, JSON.stringify({ 604 ...parsed_rv_variations, 605 [product_id]: { 606 ...selected_variations, 607 [product_attributes]: value 608 } 609 })); 610 } else { 611 localStorage.setItem(rv_variations_config?.storage_key, JSON.stringify({ 612 ...parsed_rv_variations, 613 [product_id]: { 614 [product_attributes]: value 615 } 616 })); 617 } 618 } else { 619 localStorage.setItem(rv_variations_config?.storage_key, JSON.stringify({ 620 [product_id]: { 621 [product_attributes]: value 622 } 623 })); 624 } 625 } 626 627 $el_default_select.val(value); 628 $el_default_select.change(); 629 630 }) 631 .on('change', '.value select', function(e) { 632 const QUICKVIEW_SELECTORS = [ 633 '.woosq-popup', 634 '.qqvfw', 635 '.acoqvw_quickview', 636 '.quickviewwoo-product', 637 '.adfy-quick-view-modal-content', 638 '.swal2-content', 639 '.merchant-quick-view-content', 640 '.cawqv-modal', 641 '.xoo-qv-modal', 642 '.woolentor-quickview-modal', 643 '#wcqv_contend', 644 '.yith-quick-view', 645 '.quickswish-modal', 646 '.wd-popup', 647 ]; 648 649 const element = $(this)[0]; 650 const paramName = element.dataset.attribute_name; 651 const value = element.value; 652 653 // Handle product loop variation URL 654 if ((pl_override_global && enable_pl_variation_url) || (!pl_override_global && enable_variation_url)) { 655 const isInProductLoop = e.target.closest('.products') && !e.target.closest('.summary'); 656 const isBundleProduct = e.target.closest('.bundled_product'); 657 // Fallback: check if inside swatchly_loop_variation_form (works regardless of theme structure) 658 const isInSwatchlyLoopForm = e.target.closest('.swatchly_loop_variation_form') && !e.target.closest('.summary'); 659 660 if ((isInProductLoop || isBundleProduct || isInSwatchlyLoopForm) && !e.target.closest(QUICKVIEW_SELECTORS.join(', '))) { 661 loopVariationUrlParams({ 662 event: e, 663 value, 664 paramName, 665 }); 666 } 667 } 668 669 // Handle single product variation URL 670 if ((sp_override_global && enable_sp_variation_url) || (!sp_override_global && enable_variation_url)) { 671 const isInSingleProduct = is_product && e.target.closest('.summary'); 672 if (isInSingleProduct && !e.target.closest([...QUICKVIEW_SELECTORS, '.bundle_form'].join(', '))) { 673 singleVariationUrlParams({ 674 value, 675 paramName, 676 }); 677 } 678 } 679 }) 680 .on( 'woocommerce_update_variation_values', function() { 681 setTimeout( function() { 682 683 // Loop through each variation row 684 $el_variation_form.find( 'tbody tr' ).each( function() { 685 var $tr = $(this), 686 values = []; 687 688 // Set default attribute label 689 if( show_selected_attribute_name && !$tr.find('.swatchly_selected_variation_name').length ){ 690 var default_attr_label = $tr.find('.swatchly-type-wrap').attr('data-default_attr_value'); 691 692 if(default_attr_label){ 693 $tr.find('.label label').append( $('<span class="swatchly_selected_variation_name"></span>').text( variation_label_separator + default_attr_label ) ); 694 } else { 695 $tr.find('.label label').append('<span class="swatchly_selected_variation_name"></span>'); 696 } 697 698 } 699 700 var $select = $tr.find('select'); 701 var attribute_name = $select.data('attribute_name') || $select.attr('name'); 702 var variationData = $el_variation_form.data('product_variations'); 703 704 // Collect chosen values of OTHER attributes so we respect multi-attribute constraints 705 var chosenAttributes = {}; 706 $el_variation_form.find('.variations select').each(function(){ 707 var $s = $(this), 708 name = $s.data('attribute_name') || $s.attr('name'), 709 val = $s.val() || ''; 710 if( name && name !== attribute_name ){ 711 chosenAttributes[ name ] = val; 712 } 713 }); 714 715 // Primary path: consult product_variations JSON for combo validity/purchasability. 716 // Out-of-stock combos stay disabled by default (previous/existing behavior) unless 717 // the merchant opted into "enable_out_of_stock_swatch_selection", in which case 718 // out-of-stock-but-valid combos stay clickable so customers can still click through 719 // to see price + "out of stock" message, matching core WooCommerce. 720 if( Array.isArray(variationData) && variationData.length ){ 721 var allowOutOfStockSelection = window.swatchly_params ? Boolean(Number(swatchly_params.enable_out_of_stock_swatch_selection)) : false;
722 $select.find('option').each(function(index, option){ 723 if( option.value === '' ){ return; } 724 var value = option.value; 725 var hasAvailable = false; 726 for( var i = 0; i < variationData.length; i++ ){ 727 var v = variationData[i]; 728 if( !v || !v.attributes ){ continue; } 729 if( !v.is_purchasable ){ continue; } 730 if( !allowOutOfStockSelection && !v.is_in_stock ){ continue; } 731 // Variation's value for THIS attribute ('' means "Any") 732 var vAttrVal = v.attributes[ attribute_name ]; 733 if( vAttrVal !== '' && vAttrVal !== value ){ continue; } 734 // Other chosen attributes must be compatible 735 var mismatch = false; 736 for( var otherName in chosenAttributes ){ 737 var chosen = chosenAttributes[ otherName ]; 738 if( chosen === '' ){ continue; } 739 var otherVal = v.attributes[ otherName ]; 740 if( otherVal !== '' && otherVal !== chosen ){ mismatch = true; break; } 741 } 742 if( !mismatch ){ hasAvailable = true; break; } 743 } 744 if( hasAvailable ){ 745 values.push( value ); 746 } 747 }); 748 } else { 749 // Fallback (e.g. AJAX variation mode): use the select's current options, 750 // excluding ones WooCommerce has disabled. 751 $select.find('option:not(:disabled)').each(function(index, option){ 752 if( option.value !== '' ){ 753 values.push( option.value ); 754 } 755 }); 756 757 // Secondary fallback: if select has no options (stripped by another plugin), 758 // read from the stored attribute_html reference set. 759 if( values.length === 0 ){ 760 var attrHtml = $select.data('attribute_html'); 761 if( attrHtml ){ 762 $(attrHtml).filter('option').each(function(index, option){ 763 if( option.value !== '' ){ 764 values.push( option.value ); 765 } 766 }); 767 } 768 } 769 } 770 771 // If values is STILL empty, don't disable anything - 772 // showing all enabled is better than all disabled 773 if( values.length === 0 ){ 774 $tr.find( 'div.swatchly-swatch' ).removeClass('swatchly-disabled'); 775 } else { 776 // Disable unavailable swatches 777 $tr.find( 'div.swatchly-swatch' ).each( function() { 778 var $el_swatch = $( this ), 779 value = $el_swatch.attr( 'data-attr_value' ); 780 781 if( values.indexOf( value ) == -1 ){ 782 $el_swatch.addClass('swatchly-disabled'); 783 } else { 784 $el_swatch.removeClass('swatchly-disabled'); 785 } 786 }); 787 } 788 789 }); // tbody tr each 790 }, 100 ); // timeout 791 792 // Update price for product loop 793 var $price_selector = '.price, .wc-block-components-product-price', 794 $el_product = builderCompat.findProductContainer($el_variation_form); 795 if(!swatchly_params.is_product){ 796 if($el_product.find('.swatchly_price').length){ 797 $el_product.find($price_selector).removeClass('swatchly_d_none'); 798 $el_product.find('.swatchly_price').remove(); 799 } 800 } 801 }) 802 .on('found_variation', function(e, variation){ 803 // some user use single product add to cart button in the product loop 804 // so we need to change the image for the product loop 805 if( !swatchly_params.is_product ){ 806 var $product_thumbnail = $(this).get_product_image_selector(); 807 808 let $el_product = builderCompat.findProductContainer($(this)); 809 $el_product.backup_product_image(); 810 811 $product_thumbnail.attr('src', variation.image.url); 812 $product_thumbnail.attr('srcset', variation.image.srcset); 813 } 814 }) 815 .on( 'click', '.reset_variations', function () { 816 const url = new URL(location); 817 $el_variation_form.find( '.value select' ).each(function() { 818 url.searchParams.delete($(this)[0].name); 819 }) 820 history.pushState({}, '', url.href); 821 $el_variation_form.find( '.swatchly-selected' ).removeClass( 'swatchly-selected' ); 822 $el_variation_form.find( '.swatchly-disabled' ).removeClass( 'swatchly-disabled' ); 823 $el_variation_form.find('.swatchly_selected_variation_name').text( '' ); 824 825 // some user use single product add to cart button in the product loop 826 // so we need to reset the image for the product loop 827 if( !swatchly_params.is_product ){ 828 let $el_product = builderCompat.findProductContainer($(this)); 829 $el_product.reset_to_default_image() 830 } 831 832 /** 833 * Recently Viewed Variations 834 * reset selected variations from local storage 835 */ 836 if((enable_rv_variations && !sp_override_global && !pl_override_global) || 837 (enable_sp_rv_variations && is_product && sp_override_global) || 838 (enable_pl_rv_variations && !is_product && pl_override_global)){ 839 const rv_variations = localStorage.getItem(rv_variations_config?.storage_key); 840 if(rv_variations){ 841 const parsed_rv_variations = JSON.parse(rv_variations); 842 if(parsed_rv_variations[$el_variation_form.data('product_id')]){ 843 delete parsed_rv_variations[$el_variation_form.data('product_id')]; 844 localStorage.setItem(rv_variations_config?.storage_key, JSON.stringify(parsed_rv_variations)); 845 } 846 } 847 } 848 }); // on click div.swatchly-swatch 849 }); 850 } 851 852 const initVariationForms = () => { 853 const variationForm = $('.variations_form:not(.swatchly_initialized):not(.bundled_item_cart_content)'); 854 const bundleForm = $('.bundle_form:not(.swatchly_initialized)'); 855 if(variationForm.length) { 856 variationForm 857 .addClass('swatchly_variation_form swatchly_initialized') 858 .swatchly_variation_form(); 859 } 860 if(bundleForm.length) { 861 bundleForm 862 .addClass('swatchly_variation_form swatchly_initialized') 863 .swatchly_variation_form(); 864 } 865 }; 866 867 // Do stuffs for each variations form 868 $(document).ready(initVariationForms) 869 870 // All major quick view plugin support 871 const QUICKVIEW_KEYWORDS = [ 872 'woosq_quickview', 873 'quickswish_product', 874 'wqv_popup_content', 875 'acoqvw_get_quickview', 876 'qode-quick-view-for-woocommerce', 877 'quickviewwoo', 878 'ca-quick-view', 879 'xoo_qv_ajax', 880 'wcqv_get_product', 881 ]; 882 $(document).ajaxComplete(function (event, request, settings) { 883 if(QUICKVIEW_KEYWORDS.some(word => settings?.url?.includes(word) || settings?.data?.includes(word))) { 884 initVariationForms(); 885 } 886 }); 887 const QUICKVIEW_EVENTS = [ 888 'addonifyQuickViewModalContentLoaded', 889 'gpls-arcw-quick-view-buy-now-for-woocommerce-quick-view-popup-rendered', 890 'yith_quick_view_loaded', 891 'merchant.quickview.ajax.loaded', 892 'wdQuickViewOpen', 893 ]; 894 // Handle event-based quick view plugins
895 QUICKVIEW_EVENTS.forEach(eventName => { 896 const target = eventName.startsWith('merchant') ? window : document; 897 $(target).on(eventName, initVariationForms); 898 }); 899 } 900 } 901 } 902 903 // Single product init 904 single_product.init(); 905 906 907 /* Third party plugin/theme's compatibility 908 ======================================================= */ 909 910 /** 911 * 1. annasta Woocommerce Product Filters 912 */ 913 $( document ).on( 'awf_after_ajax_products_update', function() { 914 product_loop.prevent_click(); 915 product_loop.init_variation_form(); 916 single_product.init(); 917 }); 918 919 /** 920 * 2. Jet Smart Filters 921 */ 922 $( document ).on( 'jet-filter-content-rendered', function() { 923 product_loop.prevent_click(); 924 product_loop.init_variation_form(); 925 single_product.init(); 926 }); 927 928 /** 929 * 3. Woolentor Support 930 */ 931 $( document ).on('woolentor_quick_view_rendered', function(){ 932 product_loop.prevent_click(); 933 product_loop.init_variation_form(); 934 single_product.init(); 935 }); 936 937 /** 938 * 4. infiniteScroll Support 939 * It's oly work under document.ready and on 'append.infiniteScroll' 940 */ 941 $(document).ready(function(){ 942 $('.products').on('append.infiniteScroll', function(){ 943 product_loop.prevent_click(); 944 product_loop.init_variation_form(); 945 single_product.init(); 946 }); 947 }); 948 949 /** 950 * 5. YITH infiniteScroll Support 951 */ 952 $(document).on('yith_infs_added_elem', function(){ 953 product_loop.prevent_click(); 954 product_loop.init_variation_form(); 955 single_product.init(); 956 }); 957 958 /** 959 * 6. Barn2 WCF Product Filter 960 */ 961 $(window).on('load', function(){ 962 if( $('.wcf-filter-form').length && typeof window.wp.hooks.addAction !== 'undefined' ){ 963 window.wp.hooks.addAction( 'wcpf.filteringDone', 'namespace', function( config, filteredDOM ){ 964 product_loop.prevent_click(); 965 product_loop.init_variation_form(); 966 single_product.init(); 967 } ); 968 } 969 }); 970 971 /** 972 * 7. Astra Pro quick view Support 973 */ 974 $( document ).on('ast_quick_view_loader_stop', function(){ 975 product_loop.prevent_click(); 976 product_loop.init_variation_form(); 977 single_product.init(); 978 }); 979 980 /** 981 * Themes compatibility 982 * 1. Airi theme 983 * It is intended to add compatibility for the airi theme's infinite scroll 984 */ 985 $( document ).on('ajaxComplete', function (event, jqxhr, settings) { 986 $('.swatchly_loop_variation_form:not(.swatchly_loaded_on_ready)').each(function(){ 987 product_loop.prevent_click(); 988 product_loop.init_variation_form(); 989 single_product.init(); 990 }); 991 }); 992 993 /** 994 * 8. Block-theme Query Loop pagination (WP Interactivity API) + generic fetch-based DOM replacement. 995 * jQuery's ajaxComplete does not fire for native fetch(), so new swatches on paginated 996 * pages never get initialized. Watch the DOM for added variation/loop forms and reinit. 997 */ 998 if ( typeof window.MutationObserver !== 'undefined' ) { 999 var swatchlyReinitScheduled = false; 1000 var swatchlyReinit = function() { 1001 swatchlyReinitScheduled = false; 1002 if ( ! document.querySelector( 1003 '.variations_form:not(.swatchly_initialized), .swatchly_loop_variation_form:not(.swatchly_loaded_on_ready), .variations_form:not(.swatchly_wc_initialized)' 1004 ) ) { 1005 return; 1006 } 1007 try { 1008 // Initialize WooCommerce's own VariationForm on any new .variations_form 1009 // (needed for native-fetch pagination since ajaxComplete does not fire). 1010 if ( typeof window.wc_add_to_cart_variation_params !== 'undefined' ) { 1011 $( '.variations_form:not(.swatchly_wc_initialized)' ).each( function() { 1012 $( this ).addClass( 'swatchly_wc_initialized' ).wc_variation_form(); 1013 }); 1014 } 1015 product_loop.prevent_click(); 1016 product_loop.init_variation_form(); 1017 single_product.init(); 1018 } catch ( e ) { /* noop */ } 1019 }; 1020 var swatchlyScheduleReinit = function() { 1021 if ( swatchlyReinitScheduled ) { return; } 1022 swatchlyReinitScheduled = true; 1023 ( window.requestAnimationFrame || function( fn ){ setTimeout( fn, 16 ); } )( swatchlyReinit ); 1024 }; 1025 var swatchlyDomObserver = new MutationObserver( function( mutations ) { 1026 for ( var i = 0; i < mutations.length; i++ ) { 1027 var added = mutations[ i ].addedNodes; 1028 if ( ! added || ! added.length ) { continue; } 1029 for ( var j = 0; j < added.length; j++ ) { 1030 var node = added[ j ]; 1031 if ( node.nodeType !== 1 ) { continue; } 1032 if ( 1033 ( node.matches && node.matches( '.variations_form, .swatchly_loop_variation_form' ) ) || 1034 ( node.querySelector && node.querySelector( '.variations_form, .swatchly_loop_variation_form' ) ) 1035 ) {
1036 swatchlyScheduleReinit(); 1037 return; 1038 } 1039 } 1040 } 1041 }); 1042 $( document ).ready( function() { 1043 if ( document.body ) { 1044 swatchlyDomObserver.observe( document.body, { childList: true, subtree: true } ); 1045 } 1046 }); 1047 } 1048})(jQuery);
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.