1;(function($){ 2"use strict"; 3 4 /** 5 * Senitize HTML 6 */ 7 var woolentorSanitizeHTML = function (str) { 8 if( str ){ 9 return str.replace(/[&<>"']/g, function (c) { 10 switch (c) { 11 case '&': return '&'; 12 case '<': return '<'; 13 case '>': return '>'; 14 case '"': return '"'; 15 case "'": return '''; 16 default: return c; 17 } 18 }); 19 }else{ 20 return ''; 21 } 22 } 23 24 /** 25 * Sanitize Object 26 */ 27 var woolentorSanitizeObject = function (inputObj) { 28 const sanitizedObj = {}; 29 30 for (let key in inputObj) { 31 if (inputObj.hasOwnProperty(key)) { 32 let value = inputObj[key]; 33 34 // Sanitize based on the value type 35 if (typeof value === 'string') { 36 // Sanitize strings to prevent injection 37 sanitizedObj[key] = woolentorSanitizeHTML(value); 38 } else if (typeof value === 'number') { 39 // Ensure numbers are valid (you could also set limits if needed) 40 sanitizedObj[key] = Number.isFinite(value) ? value : 0; 41 } else if (typeof value === 'boolean') { 42 // Keep boolean values as they are 43 sanitizedObj[key] = value; 44 } else { 45 // Handle other types if needed (e.g., arrays, objects) 46 sanitizedObj[key] = value; 47 } 48 } 49 } 50 51 return sanitizedObj; 52 } 53 54 /* 55 * Product Slider 56 */ 57 var WidgetProductSliderHandler = function ($scope, $) { 58 59 var slider_elem = $scope.find('.product-slider').eq(0); 60 61 if (slider_elem.length > 0) { 62 63 slider_elem[0].style.display='block'; 64 65 var settings = woolentorSanitizeObject(slider_elem.data('settings')); 66 var arrows = settings['arrows']; 67 var dots = settings['dots']; 68 var autoplay = settings['autoplay']; 69 var infinite = settings.hasOwnProperty('infinite') ? settings['infinite'] : true; 70 var rtl = settings['rtl']; 71 var autoplay_speed = parseInt(settings['autoplay_speed']) || 3000; 72 var animation_speed = parseInt(settings['animation_speed']) || 300; 73 var fade = settings['fade']; 74 var pause_on_hover = settings['pause_on_hover']; 75 var display_columns = parseInt(settings['product_items']) || 4; 76 var scroll_columns = parseInt(settings['scroll_columns']) || 4; 77 var tablet_width = parseInt(settings['tablet_width']) || 800; 78 var tablet_display_columns = parseInt(settings['tablet_display_columns']) || 2; 79 var tablet_scroll_columns = parseInt(settings['tablet_scroll_columns']) || 2; 80 var mobile_width = parseInt(settings['mobile_width']) || 480; 81 var mobile_display_columns = parseInt(settings['mobile_display_columns']) || 1; 82 var mobile_scroll_columns = parseInt(settings['mobile_scroll_columns']) || 1; 83 84 slider_elem.not('.slick-initialized').slick({ 85 arrows: arrows, 86 prevArrow: '<button type="button" class="slick-prev" aria-label="Previous slide"><i class="fa fa-angle-left" aria-hidden="true"></i></button>', 87 nextArrow: '<button type="button" class="slick-next" aria-label="Next slide"><i class="fa fa-angle-right" aria-hidden="true"></i></button>', 88 dots: dots, 89 infinite: infinite, 90 autoplay: autoplay, 91 autoplaySpeed: autoplay_speed, 92 speed: animation_speed, 93 fade: false, 94 pauseOnHover: pause_on_hover, 95 slidesToShow: display_columns, 96 slidesToScroll: scroll_columns, 97 rtl: rtl, 98 responsive: [ 99 { 100 breakpoint: tablet_width, 101 settings: { 102 slidesToShow: tablet_display_columns, 103 slidesToScroll: tablet_scroll_columns 104 } 105 }, 106 { 107 breakpoint: mobile_width, 108 settings: { 109 slidesToShow: mobile_display_columns, 110 slidesToScroll: mobile_scroll_columns 111 } 112 } 113 ] 114 }); 115 116 // A11y fix: Slick adds role="listbox" to .slick-track which is semantically 117 // incorrect for a carousel and triggers a Lighthouse accessibility failure. 118 slider_elem.find('.slick-track').removeAttr('role'); 119 }; 120 }; 121 122 /* 123 * Custom Tab 124 */ 125 function woolentor_tabs( $tabmenus, $tabpane ){ 126 $tabmenus.on('click', 'a', function(e){ 127 e.preventDefault(); 128 var $this = $(this), 129 $target = $this.attr('href'); 130 $this.addClass('htactive').parent().siblings().children('a').removeClass('htactive'); 131 $( $tabpane + $target ).addClass('htactive').siblings().removeClass('htactive'); 132 133 // slick refresh 134 if( $('.slick-slider').length > 0 ){ 135 var $id = $this.attr('href'); 136 $( $id ).find('.slick-slider').slick('refresh'); 137 } 138 139 }); 140 } 141 142 /* 143 * Universal product 144 */ 145 function productImageThumbnailsSlider( $slider ){ 146 $slider.slick({ 147 dots: true, 148 arrows: true, 149 prevArrow: '<button type="button" class="slick-prev" aria-label="Previous image"><i class="sli sli-arrow-left" aria-hidden="true"></i></button>', 150 nextArrow: '<button type="button" class="slick-next" aria-label="Next image"><i class="sli sli-arrow-right" aria-hidden="true"></i></button>', 151 }); 152 153 // A11y fix: Slick adds role="listbox" to .slick-track which is semantically 154 // incorrect for a carousel and triggers a Lighthouse accessibility failure. 155 $slider.find('.slick-track').removeAttr('role'); 156 } 157 if( $(".ht-product-image-slider").length > 0 ) { 158 productImageThumbnailsSlider( $(".ht-product-image-slider") ); 159 } 160 161 var WidgetThumbnaisImagesHandler = function thumbnailsimagescontroller(){ 162 woolentor_tabs( $(".ht-product-cus-tab-links"), '.ht-product-cus-tab-pane' ); 163 woolentor_tabs( $(".ht-tab-menus"), '.ht-tab-pane' ); 164 165 // Countdown 166 var finalTime, daysTime, hours, minutes, second; 167 $('.ht-product-countdown').each(function() { 168 var $this = $(this), finalDate = $(this).data('countdown'); 169 var customlavel = $(this).data('customlavel'); 170 $this.countdown(finalDate, function(event) { 171 $this.html(event.strftime('<div class="cd-single"><div class="cd-single-inner"><h3>%D</h3><p>'+woolentorSanitizeHTML(customlavel.daytxt)+'</p></div></div><div class="cd-single"><div class="cd-single-inner"><h3>%H</h3><p>'+woolentorSanitizeHTML(customlavel.hourtxt)+'</p></div></div><div class="cd-single"><div class="cd-single-inner"><h3>%M</h3><p>'+woolentorSanitizeHTML(customlavel.minutestxt)+'</p></div></div><div class="cd-single"><div class="cd-single-inner"><h3>%S</h3><p>'+woolentorSanitizeHTML(customlavel.secondstxt)+'</p></div></div>')); 172 }); 173 }); 174 175 } 176 177 /* 178 * Tool Tip 179 */ 180 function woolentor_tool_tips(element, content) { 181 if ( content == 'html' ) { 182 var tipText = element.text(); 183 } else { 184 var tipText = element.attr('title'); 185 } 186 element.on('mouseover', function() { 187 if ( $('.woolentor-tip').length == 0 ) { 188 element.before('<span class="woolentor-tip">' + woolentorSanitizeHTML(tipText) + '</span>'); 189 $('.woolentor-tip').css('transition', 'all 0.5s ease 0s'); 190 $('.woolentor-tip').css('margin-left', 0); 191 } 192 }); 193 element.on('mouseleave', function() { 194 $('.woolentor-tip').remove(); 195 }); 196 } 197 198 /* 199 * Tooltip Render 200 */ 201 var WidgetWoolentorTooltipHandler = function woolentor_tool_tip(){
202 $('a.woolentor-compare').each(function() { 203 woolentor_tool_tips( $(this), 'title' ); 204 }); 205 $('.woolentor-cart a.add_to_cart_button,.woolentor-cart a.added_to_cart,.woolentor-cart a.button').each(function() { 206 woolentor_tool_tips( $(this), 'html'); 207 }); 208 $('a.woolentor-quick-checkout-button').each(function() { 209 woolentor_tool_tips( $(this), 'title' ); 210 }); 211 } 212 213 /* 214 * Product Tab 215 */ 216 var WidgetProducttabsHandler = woolentor_tabs( $(".ht-tab-menus"),'.ht-tab-pane' ); 217 218 /* 219 * Single Product Video Gallery tab 220 */ 221 var WidgetProductVideoGallery = function ( $scope, $ ){ 222 var $tabs = $scope.find('.woolentor-product-video-tabs'), 223 $gallery = $scope.find('.woolentor-product-gallery-video').eq(0); 224 225 woolentor_tabs( $tabs, '.video-cus-tab-pane' ); 226 227 if ( ! $gallery.length ) { 228 return; 229 } 230 231 var defaultData = { srcfull: '', src: '', srcset: '' }; 232 233 // show_variation fires on the add to cart widget's form, which lives outside this 234 // widget, so the listener has to be delegated from document. Namespacing it per 235 // instance keeps it from stacking when Elementor re-renders the widget. 236 var ns = '.wlvideogallery' + ( $scope.data('id') || '' ); 237 238 var currentPane = function(){ 239 return $gallery.find('.video-cus-tab-pane.htactive'); 240 }; 241 242 $(document).off( 'show_variation' + ns ).on( 'show_variation' + ns, '.single_variation_wrap', function ( event, variation ) { 243 244 // Active first tab. Selected by position rather than by #wlvideo-1, the markup 245 // repeats those ids for every widget instance on the page. 246 $gallery.find('.video-cus-tab-pane').removeClass('htactive').eq(0).addClass('htactive'); 247 $tabs.find('li').children('a').removeClass('htactive'); 248 $tabs.find('li').eq(0).children('a').addClass('htactive'); 249 250 var $currentTab = currentPane(), 251 $currentImage = $currentTab.find('img'); 252 253 // Remember the original image so reset_variations can restore it 254 if ( ! defaultData.src && $currentImage.length ) { 255 defaultData.srcfull = $currentImage.attr('src'); 256 defaultData.src = $currentImage.attr('src'); 257 defaultData.srcset = $currentImage.attr('srcset'); 258 } 259 260 if ( $currentImage.length === 0 ) { 261 $currentTab.children('.embed-responsive').css({ "display": "none" }); 262 $currentTab.prepend('<img class="attachment-woocommerce_single size-woocommerce_single" src="' + variation.image.full_src + '" />'); 263 $currentImage = $currentTab.children('img'); 264 } 265 266 if ( $currentTab.children('.embed-responsive').length > 0 ) { 267 $currentTab.children('.embed-responsive').css({ "display": "none" }); 268 $currentTab.children('img').css({ "display": "block" }); 269 } 270 271 if ( $currentImage.length && $.fn.wc_set_variation_attr ) { 272 $currentImage.wc_set_variation_attr( 'src', variation.image.full_src ); 273 $currentImage.wc_set_variation_attr( 'srcset', variation.image.srcset ); 274 $currentImage.wc_set_variation_attr( 'src', variation.image.src ); 275 } 276 277 }); 278 279 // Bound once here. The original nested this inside show_variation, which stacked a 280 // fresh handler every time a variation was picked. 281 $(document).off( 'click' + ns ).on( 'click' + ns, '.variations .reset_variations', function(){ 282 283 var $currentTab = currentPane(), 284 $currentImage = $currentTab.children('img'); 285 286 if ( $currentTab.children('.embed-responsive').length > 0 ) { 287 $currentTab.children('.embed-responsive').css({ "display": "block" }); 288 $currentTab.children('img').css({ "display": "none" }); 289 } 290 291 if ( $currentImage.length && $.fn.wc_set_variation_attr ) {
292 $currentImage.wc_set_variation_attr( 'src', defaultData.srcfull ); 293 $currentImage.wc_set_variation_attr( 'srcset', defaultData.srcset ); 294 } 295 296 }); 297 298 } 299 300 /** 301 * WoolentorAccordion 302 */ 303 var WoolentorAccordion = function ( $scope, $ ){ 304 var accordion_elem = $scope.find('.htwoolentor-faq').eq(0); 305 306 var data_opt = accordion_elem.data('settings'); 307 308 if ( accordion_elem.length > 0 ) { 309 var $id = accordion_elem.attr('id'); 310 new Accordion('#' + $id, { 311 duration: 500, 312 showItem: data_opt.showitem, 313 elementClass: 'htwoolentor-faq-card', 314 questionClass: 'htwoolentor-faq-head', 315 answerClass: 'htwoolentor-faq-body', 316 }); 317 } 318 319 }; 320 321 322 /** 323 * WoolentorProductStock 324 * 325 * show_variation fires on the add to cart widget's form, outside this widget, so the 326 * listener is delegated from document and namespaced per instance to avoid stacking 327 * on re-render. Only the element written to is scoped. 328 */ 329 var WoolentorProductStock = function ( $scope, $ ){ 330 var $status = $scope.find('.woolentor-variable-product-status').eq(0); 331 332 if ( ! $status.length ) { 333 return; 334 } 335 336 var ns = '.wlproductstock' + ( $scope.data('id') || '' ); 337 338 $(document).off( 'show_variation' + ns ).on( 'show_variation' + ns, '.single_variation_wrap', function ( event, variation ) { 339 $status.html( ( variation && variation.availability_html ) ? variation.availability_html : '' ); 340 }); 341 342 $(document).off( 'click' + ns ).on( 'click' + ns, '.variations .reset_variations', function(){ 343 $status.html(''); 344 }); 345 346 }; 347 348 349 /** 350 * WoolentorProductAccordion 351 * 352 * Scoped through $scope, the widget wrapper Elementor passes to element_ready, so no 353 * generated per instance class has to be printed into the markup. 354 */ 355 var WoolentorProductAccordion = function ( $scope, $ ){ 356 var $accordion = $scope.find('.wl_product-accordion').eq(0); 357 358 if ( ! $accordion.length ) { 359 return; 360 } 361 362 $accordion.find('.wl_product-accordion-body').hide(); 363 $accordion.find('.wl_product-accordion-card.active').children('.wl_product-accordion-body').slideDown(); 364 365 $accordion.on('click', '.wl_product-accordion-head', function(e) { 366 e.preventDefault(); 367 368 var $card = $(this).parent('.wl_product-accordion-card'); 369 370 if ( $card.hasClass('active') ) { 371 $card.removeClass('active').children('.wl_product-accordion-body').slide
371Up(); 372 } else { 373 $card.addClass('active').children('.wl_product-accordion-body').slideDown(); 374 $card.siblings('.wl_product-accordion-card').removeClass('active') 375 .children('.wl_product-accordion-body').slideUp(); 376 } 377 }); 378 379 }; 380 381 382 /** 383 * WoolentorHorizontalFilter 384 * 385 * Everything is resolved from $scope, so no per instance id has to be interpolated 386 * into the script. The filter url comes from a data attribute and the preview flag 387 * from the localized settings. 388 */ 389 var WoolentorHorizontalFilter = function ( $scope, $ ){ 390 var $wrap = $scope.find('.woolentor-horizontal-filter-wrap').eq(0); 391 392 if ( ! $wrap.length ) { 393 return; 394 } 395 396 // The pro plugin registers a widget of the same name with its own markup and its own 397 // inline script. data-filter-url is only present on the markup this handler owns, so 398 // without it we must leave the widget alone rather than re-initialise select2 over it. 399 var filter_url = $wrap.attr('data-filter-url'); 400 401 if ( typeof filter_url === 'undefined' ) { 402 return; 403 } 404 405 var current_url = filter_url + '?wlfilter=1'; 406 407 var i18n = ( typeof woolentor_addons !== 'undefined' && woolentor_addons.i18n ) ? woolentor_addons.i18n : {}, 408 selectTxt = i18n.select || 'select', 409 ofTxt = i18n.of || 'of'; 410 411 // Never navigate away while the widget is being edited or previewed. 412 var isPreviewMode = function(){ 413 return ( typeof woolentor_addons !== 'undefined' && !! woolentor_addons.is_preview_mode ); 414 }; 415 416 var goTo = function( url ){ 417 if ( url && ! isPreviewMode() ) { 418 window.location = url; 419 } 420 }; 421 422 // Filter Toggle 423 $wrap.on('click', '.filter-icon', function(e){ 424 e.preventDefault(); 425 $wrap.find('.filter-item').slideToggle(); 426 }); 427 428 if ( $.fn.select2 ) { 429 var $singleDrop = $wrap.find('.woolentor-single-select-drop').eq(0), 430 $multiDrop = $wrap.find('.woolentor-multiple-select-drop').eq(0); 431 432 // select2 only dereferences dropdownParent when the dropdown opens, so handing it 433 // an empty set fails later with an unhelpful error rather than here. 434 if ( $singleDrop.length ) { 435 $wrap.find('select.woolentor-onchange-single-item').select2({ 436 dropdownParent: $singleDrop, 437 }); 438 } 439 440 if ( $multiDrop.length ) { 441 $wrap.find('select.woolentor-onchange-multiple-item').select2({ 442 // closeOnSelect : false, 443 allowHtml: true, 444 allowClear: true, 445 dropdownParent: $multiDrop, 446 }); 447 } 448 } 449 450 $wrap.on('change', '.woolentor-filter-single-item select', function (e) { 451 var output = $(this).siblings('span.select2').find('ul'); 452 var total = e.currentTarget.length; 453 var count = output.find('li').length - 0; 454 if( count >= 3 ) { 455 output.html("<li>" + count + " " + ofTxt + " " + total + " " + selectTxt + "</li>"); 456 } 457 }); 458 459 // Filter product 460 $wrap.on('change', '.woolentor-filter-single-item select.woolentor-onchange-single-item', function () { 461 goTo( current_url + $(this).val() ); 462 return false; 463 }); 464 465 // Price Filter 466 $wrap.on('change', '.woolentor-filter-single-item select.woolentor-price-filter', function(){ 467 var selected = $(this).find('option:selected'), 468 min_price = selected.data('min_price'), 469 max_price = selected.data('max_price'), 470 location = min_price + max_price; 471 472 if ( location ) { 473 goTo( current_url + location ); 474 } 475 }); 476 477 // Texanomies Filter 478 var previouslySelected = [];
479 $wrap.on('change', '.woolentor-filter-single-item select.woolentor-onchange-multiple-item', function () { 480 481 var currentlySelected = $(this).val(); 482 483 if( currentlySelected != null ){ 484 485 if( currentlySelected.length == 0 ){ 486 goTo( current_url ); 487 }else{ 488 var newSelections = currentlySelected.filter(function (element) { 489 return previouslySelected.indexOf(element) == -1; 490 }); 491 previouslySelected = currentlySelected; 492 493 var lastSelected; 494 if (newSelections.length) { 495 // If there are multiple new selections, we'll take the last in the list 496 lastSelected = newSelections.reverse()[0]; 497 } 498 if ( lastSelected ) { 499 goTo( lastSelected ); 500 } 501 } 502 503 }else{ 504 goTo( current_url ); 505 } 506 507 return false; 508 }); 509 510 }; 511 512 513 /** 514 * WoolentorOnePageSlider 515 */ 516 var WoolentorOnePageSlider = function ( $scope, $ ){ 517 518 var slider_elem = $scope.find('.ht-full-slider-area').eq(0); 519 520 if ( slider_elem.length > 0 ) { 521 522 /* Jarallax active */ 523 $('.ht-parallax-active').jarallax({ 524 speed: 0.4, 525 }); 526 527 $('#ht-nav').onePageNav({ 528 currentClass: 'current', 529 changeHash: false, 530 scrollSpeed: 750, 531 scrollThreshold: 0.5, 532 filter: '', 533 easing: 'swing', 534 }); 535 536 /*------ Wow Active ----*/ 537 new WOW().init(); 538 539 /*--------------------- 540 Video popup 541 --------------------- */ 542 $('.ht-video-popup').magnificPopup({ 543 type: 'iframe', 544 mainClass: 'mfp-fade', 545 removalDelay: 160, 546 preloader: false, 547 zoom: { 548 enabled: true, 549 } 550 }); 551 552 } 553 554 }; 555 556 /** 557 * LoadMore Product Ajax Action handeler 558 * @param {String} selectorBtn // LoadMore Button Selector 559 * @param {String} loadMoreWrapper // LoadMore Enable Track Class 560 */ 561 var WooLentorLoadMore = function( selectorBtn, loadMoreWrapper ){ 562 563 selectorBtn.on('click', function(e) { 564 e.preventDefault(); 565 566 const $button = selectorBtn; 567 const $loader = $button.siblings('.woolentor-ajax-loader'); 568 const $grid = $('#' + $button.data('grid-id')); 569 const currentPage = parseInt($button.data('page')); 570 const maxPages = parseInt($button.data('max-pages')); 571 const dataLayout = $grid.attr('data-show-layout'); 572 573 if (currentPage > maxPages) { 574 return; 575 } 576 577 $button.hide(); 578 $loader.show(); 579 580 let settings = loadMoreWrapper.attr( 'data-wl-widget-settings' ); 581 582 // Prepare AJAX data 583 const ajaxData = { 584 action: 'woolentor_load_more_products', 585 nonce: typeof woolentor_addons !== 'undefined' ? woolentor_addons.ajax_nonce : '', 586 page: currentPage, 587 settings: settings, 588 viewlayout: typeof dataLayout === 'undefined' ? '' : dataLayout 589 }; 590 591 // AJAX request to load more products 592 $.ajax({ 593 url: typeof woolentor_addons !== 'undefined' ? woolentor_addons.woolentorajaxurl : '', 594 type: 'POST', 595 data: ajaxData, 596 success: function(response) { 597 if (response.success && response.data.html) { 598 599 // Append new products 600 const $newProducts = $(response.data.html); 601 $grid.append($newProducts); 602 603 // Update page counter 604 $button.data('page', currentPage+1); 605 606 // Show button if more pages available 607 if (currentPage < maxPages) { 608 $button.show(); 609 } else { 610 $button.text($button.data('complete-loadtxt')).prop('disabled', true).show(); 611 } 612 } 613 $loader.hide(); 614 }, 615 error: function(xhr, status, error) { 616 $loader.hide(); 617 $button.show(); 618 console.log("Status:", status, "Error:", error); 619 } 620 }); 621 }); 622 623 } 624 625 var WooLentorInfiniteScroll = function(selectorBtn, productLoadWrapper ){ 626 627 let isLoading = false;
628 const $loader = selectorBtn.find('.woolentor-ajax-loader'); 629 const $grid = $('#' + selectorBtn.data('grid-id')); 630 const paginationArea = productLoadWrapper.find('.woolentor-pagination-infinite'); 631 632 function loadMoreOnScroll() { 633 if (isLoading) return; 634 635 // Calculate trigger point based on product grid bottom position 636 const gridOffset = $grid.offset().top; 637 const gridHeight = $grid.outerHeight(); 638 const gridBottom = gridOffset + gridHeight; 639 const scrollTop = $(window).scrollTop(); 640 const windowHeight = $(window).height(); 641 const triggerPoint = gridBottom - windowHeight - 100; // 100px before grid end 642 643 if (scrollTop >= triggerPoint) { 644 const currentPage = parseInt(selectorBtn.data('page')); 645 const maxPages = parseInt(selectorBtn.data('max-pages')); 646 647 if (currentPage > maxPages) { 648 $(window).off('scroll', loadMoreOnScroll); 649 return; 650 } 651 652 paginationArea.css('margin-top', '30px'); 653 isLoading = true; 654 $loader.show(); 655 656 let settings = productLoadWrapper.attr( 'data-wl-widget-settings' ); 657 const dataLayout = $grid.attr('data-show-layout'); 658 659 // AJAX request to load more products 660 $.ajax({ 661 url: typeof woolentor_addons !== 'undefined' ? woolentor_addons.woolentorajaxurl : '', 662 type: 'POST', 663 data: { 664 action: 'woolentor_load_more_products', 665 nonce: typeof woolentor_addons !== 'undefined' ? woolentor_addons.ajax_nonce : '', 666 page: currentPage, 667 settings: settings, 668 viewlayout: typeof dataLayout === 'undefined' ? '' : dataLayout 669 }, 670 success: function(response) { 671 if (response.success && response.data.html) { 672 // Append new products 673 const $newProducts = $(response.data.html); 674 $grid.append($newProducts); 675 676 // Update page counter 677 selectorBtn.data('page', currentPage + 1); 678 679 // Check if we've reached the last page 680 if (currentPage > maxPages) { 681 $(window).off('scroll', loadMoreOnScroll); 682 selectorBtn.remove(); 683 } 684 } 685 }, 686 complete: function() { 687 $loader.hide(); 688 isLoading = false; 689 paginationArea.css('margin-top', '0'); 690 }, 691 error: function() { 692 $loader.hide(); 693 isLoading = false; 694 } 695 }); 696 } 697 } 698 699 // Bind scroll event 700 $(window).on('scroll', loadMoreOnScroll); 701 702 } 703 704 /** 705 * Quantaty Manager 706 */ 707 var WooLentorQtnManager = function(){ 708 $(document).on('click', '.woolentor-qty-minus', function(e) { 709 e.preventDefault(); 710 const $input = $(this).siblings('.woolentor-qty-input'); 711 const $qtnSelector = $(this).parent('.woolentor-quantity-selector').siblings('.add_to_cart_button'); 712 const currentVal = parseInt($input.val()) || 1; 713 const minVal = parseInt($input.attr('min')) || 1; 714 715 if (currentVal > minVal) { 716 $input.val(currentVal - 1); 717 $qtnSelector.attr('data-quantity', currentVal - 1); 718 $input.trigger('change'); 719 } 720 }); 721 722 $(document).on('click', '.woolentor-qty-plus', function(e) { 723 e.preventDefault(); 724 const $input = $(this).siblings('.woolentor-qty-input'); 725 const $qtnSelector = $(this).parent('.woolentor-quantity-selector').siblings('.add_to_cart_button'); 726 const currentVal = parseInt($input.val()) || 1; 727 const maxVal = parseInt($input.attr('max')) || 999; 728 729 if (currentVal < maxVal) { 730 $input.val(currentVal + 1); 731 $qtnSelector.attr('data-quantity', currentVal + 1); 732 $input.trigger('change'); 733 } 734 }); 735 } 736 737 /** 738 * Grid and View Mode Manager 739 */ 740 var WooLentorViewModeManager = function($selector, $style = 'modern'){ 741 $(document).on('click', '.woolentor-layout-btn', function(e){ 742 e.preventDefault(); 743 744 const $this = $(this); 745 const layout = $this.data('layout'); 746 const $gridContainer = $this.closest('.woolentor-product-grid, .woolentor-filters-enabled').find($selector); 747 748 // Update active button state 749 $this.siblings().removeClass('woolentor-active'); 750 $this.addClass('woolentor-active'); 751 752 // Update grid container layout classes 753 if ($gridContainer.length > 0) { 754 // Remove existing layout classes from container 755 $gridContainer.removeClass('woolentor-layout-grid woolentor-layout-list'); 756 757 // Add new layout class to container 758 $gridContainer.addClass('woolentor-layout-' + layout); 759 $gridContainer.attr('data-show-layout', layout); 760 761 // Update product card classes 762 const $productCards = $gridContainer.find('.woolentor-product-card'); 763 $productCards.removeClass('woolentor-grid-card woolentor-list-card'); 764 765 if (layout === 'grid') { 766 if($style === 'editorial'){ 767 $productCards.removeClass('woolentor-editorial-list-card'); 768 $productCards.addClass('woolentor-editorial-grid-card'); 769 }else if($style === 'magazine'){ 770 $productCards.removeClass('woolentor-magazine-list-card'); 771 $productCards.addClass('woolentor-magazine-grid-card'); 772 }else{ 773 $productCards.addClass('woolentor-grid-card'); 774 } 775 } else if (layout === 'list') { 776 if($style === 'editorial'){ 777 $productCards.removeClass('woolentor-editorial-grid-card'); 778 $productCards.addClass('woolentor-editorial-list-card'); 779 }else if($style === 'magazine'){ 780 $productCards.removeClass('woolentor-magazine-grid-card'); 781 $productCards.addClass('woolentor-magazine-list-card'); 782 }else{ 783 $productCards.addClass('woolentor-list-card'); 784 } 785 } 786 } 787 }); 788 } 789 790 /** 791 * Grid Slider 792 * 793 * @param {jQuery} $container Element carrying data-wl-slider="true". 794 */ 795 var WooLentorGridSlider = function( $container ){ 796 797 if( !$container || !$container.length ){ 798 return; 799 } 800 801 var defaults = { 802 arrows : true, 803 dots : true, 804 infinite : true, 805 autoplay : false, 806 autoplay_speed : 3000, 807 speed : 600, 808 pause_on_hover : true, 809 items : 4, 810 scroll : 1, 811 tablet_width : 750, 812 tablet_items : 3, 813 tablet_scroll : 1, 814 mobile_width : 480, 815 mobile_items : 1, 816 mobile_scroll : 1, 817 rtl : false 818 }; 819 820 var s = $.extend( {}, defaults, $container.data('slider-settings') || {} ); 821 822 // Re-rendering inside the Elementor editor would otherwise stack a 823 // second slick instance on the same element. 824 if( $container.hasClass('slick-initialized') ){ 825 $container.slick('unslick'); 826 } 827 828 $container.slick({ 829 slidesToShow : s.items, 830 slidesToScroll : s.scroll, 831 arrows : s.arrows, 832 prevArrow : '<button type="button" class="woolentor-grid-nav woolentor-grid-nav-prev" aria-label="Previous products"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg></button>', 833 nextArrow : '<button type="button" class="woolentor-grid-nav woolentor-grid-nav-next" aria-label="Next products"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg></button>', 834 dots : s.dots, 835 dotsClass : 'woolentor-grid-dots', 836 infinite : s.infinite, 837 autoplay : s.autoplay, 838 autoplaySpeed : s.autoplay_speed, 839 speed : s.speed, 840 pauseOnHover : s.pause_on_hover, 841 rtl : s.rtl, 842 responsive : [ 843 { 844 breakpoint : s.tablet_width, 845 settings : { 846 slidesToShow : s.tablet_items, 847 slidesToScroll : s.tablet_scroll 848 } 849 }, 850 { 851 breakpoint : s.mobile_width, 852 settings : { 853 slidesToShow : s.mobile_items, 854 slidesToScroll : s.mobile_scroll 855 } 856 } 857 ] 858 }); 859 860 // A11y fix: Slick adds role="listbox" to .slick-track which is 861 // semantically incorrect for a carousel and fails a Lighthouse audit. 862 $container.find('.slick-track').removeAttr('role'); 863 }; 864 865 /** 866 * New Product Grid 867 * @param {*} $scope 868 * @param {*} $ 869 */
870 var WoolentorProductGridModern = function ( $scope, $ ){ 871 // Selector 872 let loadMoreWrapper = $scope.find('.woolentor-ajax-enabled').eq(0); 873 let loadMoreButton = $scope.find('.woolentor-load-more-btn').eq(0); 874 let infiniteScroll = $scope.find('.woolentor-infinite-scroll').eq(0); 875 let layoutList = $scope.find('.woolentor-layout-list').eq(0); 876 877 // LoadMore Button 878 if (loadMoreButton.length > 0) { 879 WooLentorLoadMore(loadMoreButton, loadMoreWrapper); 880 } 881 882 // Infinite Scroll 883 if (infiniteScroll.length > 0) { 884 WooLentorInfiniteScroll(infiniteScroll, loadMoreWrapper); 885 } 886 887 // Quantity selector - using event delegation to handle dynamically loaded products 888 if(layoutList.length > 0){ 889 WooLentorQtnManager(); 890 } 891 892 // View Manager 893 WooLentorViewModeManager('.woolentor-product-grid-modern'); 894 895 // Slider layout 896 WooLentorGridSlider( $scope.find('[data-wl-slider="true"]').eq(0) ); 897 898 } 899 900 /** 901 * New Product Grid - Editorial Style 902 * @param {*} $scope 903 * @param {*} $ 904 */ 905 var WoolentorProductGridEditorial = function ($scope, $){ 906 // Selector 907 let loadMoreWrapper = $scope.find('.woolentor-ajax-enabled').eq(0); 908 let loadMoreButton = $scope.find('.woolentor-load-more-btn').eq(0); 909 let infiniteScroll = $scope.find('.woolentor-infinite-scroll').eq(0); 910 911 // LoadMore Button 912 if (loadMoreButton.length > 0) { 913 WooLentorLoadMore(loadMoreButton, loadMoreWrapper); 914 } 915 916 // Infinite Scroll 917 if (infiniteScroll.length > 0) { 918 WooLentorInfiniteScroll(infiniteScroll, loadMoreWrapper); 919 } 920 921 // View Manager 922 WooLentorViewModeManager('.woolentor-product-grid-editorial','editorial'); 923 924 // Slider layout 925 WooLentorGridSlider( $scope.find('[data-wl-slider="true"]').eq(0) ); 926 927 } 928 929 /** 930 * New Product Grid - Magazine Style 931 * @param {*} $scope 932 * @param {*} $ 933 */ 934 var WoolentorProductGridMagazine = function ($scope, $){ 935 // Selector 936 let loadMoreWrapper = $scope.find('.woolentor-ajax-enabled').eq(0); 937 let loadMoreButton = $scope.find('.woolentor-load-more-btn').eq(0); 938 let infiniteScroll = $scope.find('.woolentor-infinite-scroll').eq(0); 939 940 // LoadMore Button 941 if (loadMoreButton.length > 0) { 942 WooLentorLoadMore(loadMoreButton, loadMoreWrapper); 943 } 944 945 // Infinite Scroll 946 if (infiniteScroll.length > 0) { 947 WooLentorInfiniteScroll(infiniteScroll, loadMoreWrapper); 948 } 949 950 // View Manager 951 WooLentorViewModeManager('.woolentor-product-grid-magazine','magazine'); 952 } 953 954 /** 955 * Add To Cart widget quantity plus/minus. 956 * 957 * Delegated from document so it survives Elementor editor re-renders, and scoped to the 958 * widget class so it cannot double fire alongside the Gutenberg block handler, which 959 * binds the same markup in woolentor-blocks. 960 */ 961 var WooLentorAddToCartQuantity = function(){ 962 var widgetSelector = '.elementor-widget-wl-product-add-to-cart .wl-addto-cart form.cart '; 963 964 $(document).on( 'click', widgetSelector + 'span.wl-quantity-plus, ' + widgetSelector + 'span.wl-quantity-minus', function(){ 965 966 var $this = $( this ), 967 // Grouped products render every row inside its own .wl-quantity-grouped-cal, 968 // so the container tells us which layout we are in without knowing the product type. 969 $grouped = $this.closest( '.wl-quantity-grouped-cal' ), 970 isGrouped = $grouped.length > 0, 971 // :visible matters on variable products, the form can hold more than one qty input. 972 qty = isGrouped ? $grouped.find( '.qty:visible' ) : $this.closest( 'form.cart' ).find( '.qty:visible' ), 973 min_val = isGrouped ? 0 : 1; 974 975 if( ! qty.length ){ 976 return; 977 } 978 979 var val = parseFloat( qty.val() ); 980 var max = parseFloat( qty.attr( 'max' ) ); 981 var min = parseFloat( qty.attr( 'min' ) ); 982 var step = parseFloat( qty.attr( 'step' ) ); 983 984 if( isNaN( val ) ){ 985 val = isGrouped ? 0 : min_val; 986 } 987 if( isNaN( step ) ){ 988 step = 1; 989 } 990 991 if ( $this.is( '.wl-quantity-plus' ) ) { 992 if ( max && ( max <= val ) ) { 993 qty.val( max ); 994 } else { 995 qty.val( val + step ); 996 } 997 } else { 998 if ( min && ( min >= val ) ) { 999 qty.val( min ); 1000 } else if ( val > min_val ) { 1001 qty.val( val - step ); 1002 } 1003 } 1004 1005 qty.trigger( 'change' ); 1006 1007 }); 1008 } 1009 1010 WooLentorAddToCartQuantity(); 1011 1012 /** 1013 * Suggest Price form. 1014 * 1015 * Pure event binding, so a delegated handler is enough and it survives Elementor editor 1016 * re-renders. Scoped to the widget class because the Gutenberg suggest-price block emits
1017 * the same .wl-suggest-price markup with its own script, and an unscoped delegate would 1018 * submit that form twice. 1019 */ 1020 var WooLentorSuggestPrice = function(){ 1021 var root = '.elementor-widget-wl-product-suggest-price .wl-suggest-price'; 1022 1023 $(document).on('click', root + ' .wlopen', function(){ 1024 var $wrap = $(this).closest('.wl-suggest-price'); 1025 $(this).hide(); 1026 $wrap.find('.wlclose').show(); 1027 $wrap.find('.wlsuggest-form').slideDown('slow'); 1028 }); 1029 1030 $(document).on('click', root + ' .wlclose', function(){ 1031 var $wrap = $(this).closest('.wl-suggest-price'); 1032 $(this).hide(); 1033 $wrap.find('.wlopen').show(); 1034 $wrap.find('.wlsuggest-form').slideUp('slow'); 1035 }); 1036 1037 $(document).on('submit', root + ' .wlsuggest-form', function(e){ 1038 e.preventDefault(); 1039 1040 var $form = $(this), 1041 $wrap = $form.closest('.wl-suggest-price'), 1042 $submit = $form.find('.wlsuggest-submit'), 1043 $message = $wrap.find('.wlsendmessage'), 1044 // Read before it is swapped for the loading text, so it can be restored. 1045 submitText = $submit.val(), 1046 loadingText = $form.data('loading-text') || submitText; 1047 1048 $.ajax({ 1049 url: $form.attr('action'), 1050 type: 'POST', 1051 data: $form.serialize(), 1052 1053 beforeSend: function () { 1054 $message.hide(); 1055 $submit.removeClass('added').addClass('loading').val(loadingText); 1056 }, 1057 1058 complete: function () { 1059 $submit.addClass('added').removeClass('loading').val(submitText); 1060 $wrap.find('.wlopen').show(); 1061 $wrap.find('.wlclose').hide(); 1062 $form.slideUp('slow'); 1063 }, 1064 1065 success: function (response) { 1066 var data = ( response && response.data ) ? response.data : {}; 1067 1068 $message.show().html( data.message ); 1069 1070 // Update form token for subsequent submissions (without page refresh) 1071 if ( data.new_token ) { 1072 $form.find('input[name="form_token"]').val( data.new_token ); 1073 } 1074 1075 // Clear form fields after successful submission 1076 if ( response && response.success && ! data.error ) { 1077 $form.find('input[name="wlname"]').val(''); 1078 $form.find('input[name="wlemail"]').val(''); 1079 $form.find('textarea[name="wlmessage"]').val(''); 1080 } 1081 }, 1082 1083 }); 1084 1085 }); 1086 } 1087 1088 WooLentorSuggestPrice(); 1089 1090 /* 1091 * Run this code under Elementor. 1092 */ 1093 $(window).on('elementor/frontend/init', function () { 1094 1095 elementorFrontend.hooks.addAction( 'frontend/element_ready/woolentor-product-tab.default', WidgetProductSliderHandler); 1096 elementorFrontend.hooks.addAction( 'frontend/element_ready/woolentor-product-tab.default', WidgetProducttabsHandler); 1097 1098 elementorFrontend.hooks.addAction( 'frontend/element_ready/woolentor-universal-product.default', WidgetProductSliderHandler); 1099 elementorFrontend.hooks.addAction( 'frontend/element_ready/woolentor-universal-product.default', WidgetWoolentorTooltipHandler); 1100 elementorFrontend.hooks.addAction( 'frontend/element_ready/woolentor-universal-product.default', WidgetThumbnaisImagesHandler); 1101 1102 elementorFrontend.hooks.addAction( 'frontend/element_ready/woolentor-cross-sell-product-custom.default', WidgetProductSliderHandler); 1103 elementorFrontend.hooks.addAction( 'frontend/element_ready/woolentor-cross-sell-product-custom.default', WidgetWoolentorTooltipHandler); 1104 elementorFrontend.hooks.addAction( 'frontend/element_ready/woolentor-cross-sell-product-custom.default', WidgetThumbnaisImagesHandler); 1105 1106 elementorFrontend.hooks.addAction( 'frontend/element_ready/woolentor-upsell-product-custom.default', WidgetProductSliderHandler); 1107 elementorFrontend.hooks.addAction( 'frontend/element_ready/woolentor-upsell-product-custom.default', WidgetWoolentorTooltipHandler); 1108 elementorFrontend.hooks.addAction( 'frontend/element_ready/woolentor-upsell-product-custom.default', WidgetThumbnaisImagesHandler); 1109 1110 elementorFrontend.hooks.addAction( 'frontend/element_ready/woolentor-related-product-custom.default', WidgetProductSliderHandler); 1111 elementorFrontend.hooks.addAction( 'frontend/element_ready/woolentor-related-product-custom.default', WidgetWoolentorTooltipHandler); 1112 elementorFrontend.hooks.addAction( 'frontend/element_ready/woolentor-related-product-custom.default', WidgetThumbnaisImagesHandler); 1113 1114 elementorFrontend.hooks.addAction( 'frontend/element_ready/wl-product-video-gallery.default', WidgetProductVideoGallery ); 1115 1116 elementorFrontend.hooks.addAction( 'frontend/element_ready/wl-brand-logo.default', WidgetProductSliderHandler ); 1117 elementorFrontend.hooks.addAction( 'frontend/element_ready/wl-faq.default', WoolentorAccordion ); 1118 elementorFrontend.hooks.addAction( 'frontend/element_ready/woolentor-accordion-product.default', WoolentorProductAccordion ); 1119 elementorFrontend.hooks.addAction( 'frontend/element_ready/wl-single-product-stock.default', WoolentorProductStock ); 1120 elementorFrontend.hooks.addAction( 'frontend/element_ready/wl-product-horizontal-filter.default', WoolentorHorizontalFilter ); 1121 1122 elementorFrontend.hooks.addAction( 'frontend/element_ready/wl-category-grid.default', WidgetProductSliderHandler ); 1123 elementorFrontend.hooks.addAction( 'frontend/element_ready/wl-testimonial.default', WidgetProductSliderHandler ); 1124 elementorFrontend.hooks.addAction( 'frontend/element_ready/wl-product-grid.default', WidgetProductSliderHandler ); 1125 elementorFrontend.hooks.addAction( 'frontend/element_ready/wl-recently-viewed-products.default', WidgetProductSliderHandler ); 1126 elementorFrontend.hooks.addAction( 'frontend/element_ready/wl-onepage-slider.default', WoolentorOnePageSlider ); 1127 1128 elementorFrontend.hooks.addAction( 'frontend/element_ready/wl-customer-veview.default', WidgetProductSliderHandler ); 1129 1130 elementorFrontend.hooks.addAction( 'frontend/element_ready/woolentor-product-grid-modern.default', WoolentorProductGridModern ); 1131 elementorFrontend.hooks.addAction( 'frontend/element_ready/woolentor-product-grid-luxury.default', WoolentorProductGridModern ); 1132 elementorFrontend.hooks.addAction( 'frontend/element_ready/woolentor-product-grid-editorial.default', WoolentorProductGridEditorial ); 1133 elementorFrontend.hooks.addAction( 'frontend/element_ready/woolentor-product-grid-magazine.default', WoolentorProductGridMagazine ); 1134 1135 }); 1136 1137 1138})(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.