1(function ($) { 2 3 var filterOption = { 4 get_category_list($, html, all = false) { 5 let selected_cat = html.find(".dnwoo_product_filter_menu li.active"); 6 if (selected_cat.length == 0 || all) { 7 selected_cat = html.find(".dnwoo_product_filter_menu li"); 8 } 9 10 const get_cat_list = selected_cat 11 .map(function () { 12 // Skip "All Categories" item - it has class "all_categories" or data-id might be undefined/empty 13 if ($(this).hasClass("all_categories")) { 14 return null; 15 } 16 return $(this).data("id"); 17 }) 18 .get() 19 .filter(function(id) { 20 // Filter out null, undefined, and empty values 21 return id !== null && id !== undefined && id !== ''; 22 }); 23 24 return get_cat_list; 25 } 26 }; 27 28 const mobileFilterSidebarTransition = 620; 29 30 // The off-canvas filter panel already uses a very high z-index, but the site 31 // header can still paint over it. That happens when an ancestor of the filter 32 // form creates a stacking context (transform / filter / perspective /
33 // will-change / opacity / position+z-index), which traps the panel's z-index 34 // *inside* that ancestor â so no z-index value can lift it above a header 35 // that lives higher in the DOM. 36 // 37 // Instead of guessing the header's selector (fragile across themes) or 38 // re-parenting the panel to <body> (which breaks the AJAX filtering that 39 // reads inputs via the form wrapper), we walk the panel's ancestor chain up 40 // to <body> and, on each ancestor, neutralise the stacking-context triggers 41 // and force a high z-index. At every level our branch becomes the topmost, so 42 // the panel bubbles above the header at the document root. Originals are 43 // restored on close. 44 const dnwooStackingProps = ["transform", "filter", "perspective", "will-change", "mix-blend-mode"]; 45 46 function dnwoo_lift_offcanvas($sidebar) { 47 window.__dnwooLifted = []; 48 if (!$sidebar || !$sidebar[0]) { 49 return; 50 } 51 let el = $sidebar[0].parentElement; 52 while (el && el !== document.body && el.nodeType === 1) { 53 let cs = window.getComputedStyle(el); 54 let rec = { el: el, inline: {} }; 55 56 dnwooStackingProps.forEach(function (prop) { 57 let val = cs.getPropertyValue(prop); 58 let creates = 59 (prop === "transform" && val !== "none") || 60 (prop === "filter" && val !== "none") || 61 (prop === "perspective" && val !== "none") || 62 (prop === "will-change" && /transform|filter|perspective|opacity/.test(val)) || 63 (prop === "mix-blend-mode" && val !== "normal"); 64 if (creates) { 65 rec.inline[prop] = el.style.getPropertyValue(prop); 66 el.style.setProperty(prop, prop === "will-change" ? "auto" : "none", "important"); 67 } 68 }); 69 70 if (parseFloat(cs.opacity) < 1) { 71 rec.inline.opacity = el.style.getPropertyValue("opacity"); 72 el.style.setProperty("opacity", "1", "important"); 73 } 74 75 // Lift this ancestor above its siblings so the panel's branch wins. 76 rec.inline.position = el.style.getPropertyValue("position"); 77 rec.inline["z-index"] = el.style.getPropertyValue("z-index"); 78 if (cs.position === "static") { 79 el.style.setProperty("position", "relative", "important"); 80 } 81 el.style.setProperty("z-index", "999999990", "important"); 82 83 window.__dnwooLifted.push(rec); 84 el = el.parentElement; 85 } 86 } 87 88 function dnwoo_unlift_offcanvas() { 89 (window.__dnwooLifted || []).forEach(function (rec) { 90 Object.keys(rec.inline).forEach(function (prop) { 91 let prev = rec.inline[prop]; 92 if (prev) { 93 rec.el.style.setProperty(prop, prev); 94 } else { 95 rec.el.style.removeProperty(prop); 96 } 97 }); 98 }); 99 window.__dnwooLifted = []; 100 } 101 102 function close_mobile_filter_sidebar(main_wrapper, on_closed) { 103 let $sidebar = main_wrapper.find(".dnwoo_Pro_filter_menu_left_sidebar_wrapper"); 104 let $overlay = main_wrapper.find(".dnwoo_mobile_filter_overlay"); 105 let done = function () { 106 if (typeof on_closed === "function") { 107 on_closed(); 108 } 109 }; 110 111 if ( 112 !$sidebar.length || 113 (!$sidebar.hasClass("dnwoo_sidebar_open") && !$overlay.hasClass("dnwoo_overlay_active")) 114 ) { 115 done(); 116 return; 117 } 118 119 if ($sidebar.data("dnwooClosing")) { 120 return; 121 } 122 123 $sidebar.data("dnwooClosing", true); 124 $overlay.css({ 125 opacity: 0, 126 transition: "opacity 0.4s ease" 127 }); 128 129 if ($sidebar[0]) { 130 $sidebar[0].style.setProperty( 131 "transition", 132 "left 0.6s cubic-bezier(0.22, 1, 0.36, 1)", 133 "important" 134 ); 135 $sidebar[0].style.setProperty("left", "-100%", "important"); 136 } 137 138 window.setTimeout(function () { 139 $sidebar 140 .removeClass("dnwoo_sidebar_open") 141 .removeAttr("style") 142 .removeData("dnwooClosing"); 143 $overlay 144 .removeClass("dnwoo_overlay_active") 145 .removeAttr("style"); 146 147 if ($(".dnwoo_Pro_filter_menu_left_sidebar_wrapper.dnwoo_sidebar_open").length === 0) { 148 $("body").removeClass("dnwoo_filter_sidebar_open"); 149 // Restore the ancestor styles we changed to lift the panel. 150 dnwoo_unlift_offcanvas(); 151 } 152 153 done(); 154 }, mobileFilterSidebarTransition); 155 } 156 157 $(document).ready(function () { 158 $(".dnwoo_filter_masonry").each(function (index, element) { 159 filtering_option($(element), index); 160 }); 161 162 function hide_reset(_this, element) { 163 let parent = _this.parents(".category-wrapper"); 164 let filter_reset = parent.find(".filter-reset"); 165 let list_handle = element.find(".dnwoo_product_filter_menu li"); 166 let all_inputs = list_handle.find("input:checkbox").filter(":checked"); 167 168 if (all_inputs.length == 0) { 169 filter_reset.addClass("d-none"); 170 } 171 } 172 173 function filtering_option(element, index) { 174 // Expose a refilter hook so add-on UIs (e.g. Layout 2 chips / clear-all) 175 // can re-run the AJAX query after mutating filter state directly in the 176 // DOM, without depending on the optional per-section reset buttons. 177 element.data("dnwooRefilter", function (opts) { 178 get_products($.extend({ html: element }, opts || { filter_changed: true })); 179 }); 180 /** 181 * Filter Data 182 */ 183 let rating = element.find(".ratings li"); 184 let clickable = [rating]; 185 let list_handle = element.find(".dnwoo_product_filter_menu li"); 186 187 /** 188 * Default add active class on 1st li and trigger filtering 189 */ 190 if (!list_handle.hasClass("active")) { 191 list_handle.first().addClass("active"); 192 // Trigger initial filtering to sync products with the first active category 193 get_products({ html: element, filter_changed: true }); 194 } 195 /** 196 * Category Click 197 */ 198 let action = "change"; 199 let show_filter_menu = element.find(".dnwoo_product_filter_wrap").data("show_filter_menu"); 200 // When show_filter_menu is "default", use single category select behavior 201 if (show_filter_menu == "default") { 202 action = "click"; 203 } 204 205 list_handle.on(action, function (e) { 206 e.preventDefault(); 207 let _this = $(this); 208 let $list = element.find(".dnwoo_product_filter_menu");
209 let $single_category = $list.data("single"); 210 let $top = element 211 .find(".dnwoo_product_filter_wrap") 212 .data("show_filter_menu"); 213 reset_filter_block(_this.parents(".category-wrapper")); 214 215 // For "default" mode (single select without checkboxes), only one category can be active 216 if ($top == "default") { 217 // Remove active from all other items 218 list_handle.not(_this).removeClass("active"); 219 // Set this item as active 220 _this.addClass("active"); 221 get_products({ html: element, list_this: _this, filter_changed: true, reveal_results: true }); 222 return; 223 } 224 225 if (_this.hasClass("all_categories") && $single_category == "off") { 226 let all_checked = _this.find("input"); 227 let all_inputs = list_handle.find("input"); 228 if (all_checked.is(":checked")) { 229 list_handle.addClass("active"); 230 all_inputs.prop("checked", true); 231 } else { 232 all_inputs.prop("checked", false); 233 list_handle.removeClass("active"); 234 } 235 get_products({ 236 html: element, 237 reset: "all_categories", 238 list_this: _this, 239 reveal_results: true 240 }); 241 242 return; 243 } 244 245 action_for_all_list(element, _this); 246 247 if ($single_category == "on") { 248 if (_this.find("input").is(":checked")) { 249 // In single mode, uncheck all other categories first 250 list_handle.not(_this).removeClass("active"); 251 $list 252 .find('input[type="checkbox"]') 253 .not(_this.find("input")) 254 .prop("checked", false); 255 } 256 } 257 258 if (_this.find("input").prop("type") == "checkbox") { 259 if (_this.find("input").is(":checked")) { 260 _this.addClass("active"); 261 // sub category parent category check 262 if (_this.hasClass("parent")) { 263 let parent = _this.data("filter"); 264 let child_li = element.find( 265 '.dnwoo_product_filter_menu li[data-parent="' + parent + '"]' 266 ); 267 // Only auto-select children if NOT in single category mode 268 if ($single_category !== "on" && $top !== "default") { 269 child_li.addClass("active"); 270 child_li.find("input").prop("checked", true); 271 } 272 } 273 // sub category child category un-check 274 if (_this.data("parent")) { 275 let parent = _this.data("parent"); 276 let parent_li = element.find( 277 '.dnwoo_product_filter_menu li[data-filter="' + parent + '"]' 278 ); 279 280 // If in single category mode, uncheck parent and siblings 281 if ($single_category == "on" || $top == "default") { 282 // Uncheck parent 283 parent_li.find("input").prop("checked", false); 284 parent_li.removeClass("active"); 285 // Uncheck sibling sub-categories 286 let sibling_cats = _this.parent("ul.sub_categories").find("li").not(_this); 287 sibling_cats.removeClass("active"); 288 sibling_cats.find("input").prop("checked", false); 289 } else { 290 // In multi-select mode, check parent if all children are selected 291 let child_cat = _this.parent("ul.sub_categories").find("li"); 292 if (child_cat.length == child_cat.find("input:checked").length) { 293 parent_li.find("input").prop("checked", true); 294 parent_li.addClass("active"); 295 } 296 } 297 } 298 } else { 299 _this.removeClass("active"); 300 // sub category parent category un-check 301 if (_this.hasClass("parent")) { 302 let parent = _this.data("filter"); 303 let child_li = element.find( 304 '.dnwoo_product_filter_menu li[data-parent="' + parent + '"]' 305 ); 306 child_li.removeClass("active"); 307 child_li.find("input").prop("checked", false); 308 } 309 310 // sub category child category un-check 311 if (_this.data("parent")) { 312 let parent = _this.data("parent"); 313 let parent_li = element.find( 314 '.dnwoo_product_filter_menu li[data-filter="' + parent + '"]' 315 ); 316 parent_li.find("input").prop("checked", false); 317 parent_li.removeClass("active"); 318 } 319 320 // reset filter 321 let $if_all_active = element 322 .find(".dnwoo_product_filter_menu li.all_categories") 323 .find("input"); 324 if ($if_all_active.is(":checked")) { 325 $if_all_active.removeClass("active"); 326 $if_all_active.prop("checked", false); 327 } 328 } 329 } else { 330 _this.toggleClass("active"); 331 } 332 333 get_products({ html: element, list_this: _this, filter_changed: true, reveal_results: true }); 334 }); 335 336 /** 337 * All reset 338 */ 339 let all_clear = element.find(".all_clear"); 340 all_clear.on("click", function (e) { 341 e.preventDefault(); 342 let section = element.find(".options-section"); 343 // Hide every section's Reset link. This used to name only rating and 344 // category, so the attribute / availability / price links stayed on 345 // screen after a Clear All. 346 reset_filter_block(element, "all_reset"); 347 reset_block(section, element); 348 // reset_block() only clears checkboxes and .active classes; the price 349 // slider keeps its state in a closure, so its thumbs and number inputs 350 // survived Clear All and went stale against the unfiltered results. 351 element.find(".slidecontainer").each(function () { 352 let price_reset = $(this).data("dnwooPriceReset"); 353 if (typeof price_reset === "function") { 354 price_reset(true); // silent â the get_products() below re-queries once 355 } 356 }); 357 let class_name = $(this).attr("class"); 358 get_products({ reset: class_name, html: element }); 359 }); 360 361 /** 362 * Pagination Click 363 */ 364 element 365 .find(".dnwoo_pages_wrapper") 366 .on("click", ".pagination li:not(.pagination-dots)", function (e) { 367 e.preventDefault(); 368 let class_name = $(this).attr("class"); 369 if (class_name !== "load_more") { 370 element.find(".pagination li").removeClass("active"); 371 } 372 $(this).addClass("active"); 373 374 // Preserve current price range filter values when paginating 375 let priceParams = {}; 376 const $container = element.find(".slidecontainer"); 377 if ($container.length > 0) { 378 const $minInput = $container.find('.dnwoo-pricerange-min'); 379 const $maxInput = $container.find('.dnwoo-pricerange-max');
380 if ($minInput.length && $maxInput.length) { 381 const minVal = parseFloat($minInput.val()); 382 const maxVal = parseFloat($maxInput.val()); 383 if (!isNaN(minVal)) priceParams.min = minVal; 384 if (!isNaN(maxVal)) priceParams.max = maxVal; 385 } 386 } 387 388 get_products({ html: element, page_style: class_name, ...priceParams }); 389 }); 390 391 clickable.forEach(item => { 392 item.on("click", function (e) { 393 e.preventDefault(); 394 reset_filter_block($(this).parents(".rating-wrapper")); 395 item.removeClass("active"); 396 $(this).addClass("active"); 397 get_products({ html: element, filter_changed: true }); 398 }); 399 }); 400 401 /** 402 * Sorting dropdown change - use AJAX instead of page reload 403 */ 404 element.find(".dnwoo-orderby").on("change", function (e) { 405 e.stopImmediatePropagation(); 406 var selectedValue = $(this).val(); 407 var html = element.find(".dnwoo_product_filter_wrap"); 408 409 // Update data attributes with the new sort value 410 html.data("orderby", selectedValue); 411 html.data("order", "ASC"); 412 html.data("meta_key", ""); 413 // Mark that the visitor explicitly chose a sort so it overrides the 414 // module's Product View Type ordering. 415 html.data("user_orderby", selectedValue); 416 417 get_products({ html: element, filter_changed: true }); 418 return false; 419 }); 420 421 /** 422 * Attribute click 423 */ 424 element.find(".options-wrapper").each((i, item) => { 425 $(item) 426 .find("div") 427 .on("click", function (e) { 428 e.preventDefault(); 429 $(item) 430 .find("div") 431 .removeClass("active"); 432 $(this).addClass("active"); 433 reset_filter_block($(this).parents(".attribute-wrapper")); 434 get_products({ html: element, filter_changed: true }); 435 }); 436 }); 437 438 /** 439 * Attribute checkbox layout (multi-select) - brand, color, size 440 */ 441 element 442 .find(".attribute-wrapper .options-wrapper input[type='checkbox']") 443 .on("change", function (e) { 444 let $item = $(this).closest("[data-term_id]"); 445 $item.toggleClass("active", $(this).is(":checked")); 446 reset_filter_block($(this).parents(".attribute-wrapper")); 447 get_products({ html: element, filter_changed: true }); 448 }); 449 450 /** 451 * Availability toggle (in stock / on sale / out of stock) 452 */ 453 element 454 .find(".availability-wrapper input[type='checkbox']") 455 .on("change", function (e) { 456 let $item = $(this).closest("[data-availability]"); 457 $item.toggleClass("active", $(this).is(":checked")); 458 reset_filter_block($(this).parents(".availability-wrapper")); 459 get_products({ html: element, filter_changed: true }); 460 }); 461 462 /** 463 * Single Reset 464 */ 465 element.find(".filter-reset").on("click", function (e) { 466 let _this = $(this); 467 let section = _this.siblings(".options-section"); 468 let param = { html: element }; 469 // reset category 470 if (_this.parents(".category-wrapper").length > 0) { 471 param.reset = "category_reset"; 472 reset_block(section, element); 473 } 474 // reset rating 475 if (_this.parents(".rating-wrapper").length > 0) { 476 element.find(".ratings li").removeClass("active"); 477 element.find("div").removeClass("active"); 478 } 479 // reset attribute filters - brand, color, size (button & checkbox layouts) 480 if (_this.parents(".attribute-wrapper").length > 0) { 481 let attribute_wrapper = _this.parents(".attribute-wrapper"); 482 attribute_wrapper.find(".active").removeClass("active"); 483 attribute_wrapper.find("input[type='checkbox']").prop("checked", false); 484 } 485 // reset availability filter 486 if (_this.parents(".availability-wrapper").length > 0) { 487 let availability_wrapper = _this.parents(".availability-wrapper"); 488 availability_wrapper.find(".active").removeClass("active"); 489 availability_wrapper.find("input[type='checkbox']").prop("checked", false); 490 } 491 // reset price range â the slider owns its state, so hand off to its own 492 // reset hook, which restores the thumbs/inputs and re-queries itself. 493 let $price_wrapper = _this.parents(".price-range-wrapper"); 494 if ($price_wrapper.length > 0) { 495 let price_handled = false;
496 $price_wrapper.find(".slidecontainer").each(function () { 497 let price_reset = $(this).data("dnwooPriceReset"); 498 if (typeof price_reset === "function") { 499 price_reset(); 500 price_handled = true; 501 } 502 }); 503 if (price_handled) { 504 _this.addClass("d-none"); 505 return; 506 } 507 } 508 _this.addClass("d-none"); 509 get_products(param); 510 }); 511 512 /** 513 * Dual Cursor Price Range Slider 514 */ 515 element.find(".slidecontainer").each((i, item) => { 516 const $container = $(item); 517 const $wrapper = $container.find('.dnwoo-pricerange-wrapper'); 518 const $track = $wrapper.find('.dnwoo-pricerange-track'); 519 const $fill = $wrapper.find('.dnwoo-pricerange-fill'); 520 const $thumbMin = $wrapper.find('.dnwoo-pricerange-thumb-min'); 521 const $thumbMax = $wrapper.find('.dnwoo-pricerange-thumb-max'); 522 const $minValueInput = $container.find('input.dnwoo-pricerange-min-value'); 523 const $maxValueInput = $container.find('input.dnwoo-pricerange-max-value'); 524 const $minInput = $container.find('.dnwoo-pricerange-min'); 525 const $maxInput = $container.find('.dnwoo-pricerange-max'); 526 527 // Get min/max bounds from data attributes or defaults 528 // Use the max value from either input since they share the same bounds 529 const bounds = { 530 min: parseFloat($minInput.data('min')) || 0.01, 531 max: parseFloat($minInput.data('max')) || parseFloat($maxInput.data('max')) || 1000 532 }; 533 534 // Initialize values - use default values from data attributes or fallback 535 // Store original float values for input fields, but round for slider display 536 let values = { 537 min: parseFloat($minInput.val()) || parseFloat($minInput.data('default')) || bounds.min,
538 max: parseFloat($maxInput.val()) || parseFloat($maxInput.data('default')) || bounds.max 539 }; 540 541 // Initialize typing state and timer 542 let inputTimer; 543 let isTyping = false; 544 545 // This section's "Reset" link ships hidden (`filter-reset d-none`, see 546 // FilterOptions::reset_html). Every other section reveals its own via 547 // reset_filter_block() from its change handler; the price slider never 548 // did, so the link stayed invisible. Show it once the range is narrowed 549 // and hide it again when it is back to the full bounds. 550 const $priceSection = $container.closest(".price-range-wrapper"); 551 function syncResetLink() { 552 let narrowed = values.min > bounds.min || values.max < bounds.max; 553 reset_filter_block($priceSection, narrowed ? "" : "all_reset"); 554 } 555 556 function commitPrice() { 557 syncResetLink(); 558 get_products({ html: element, min: values.min, max: values.max }); 559 } 560 561 // `values`, `bounds` and updateSlider() are closure-scoped, so the 562 // "Reset" / "Clear All" handlers outside cannot move the thumbs. Expose 563 // a reset hook the same way filtering_option() exposes dnwooRefilter. 564 // Pass silent = true when the caller issues its own get_products(). 565 $container.data("dnwooPriceReset", function (silent) { 566 clearTimeout(inputTimer); 567 isTyping = false; 568 values.min = bounds.min; 569 values.max = bounds.max; 570 updateSlider(); 571 syncResetLink(); 572 if (!silent) { 573 get_products({ html: element }); 574 } 575 }); 576 577 // Initialize slider positions 578 updateSlider(); 579 syncResetLink(); 580 581 function updateSlider(skipInputUpdate = false, fromSliderInteraction = false) { 582 const minPercent = ((values.min - bounds.min) / (bounds.max - bounds.min)) * 100; 583 const maxPercent = ((values.max - bounds.min) / (bounds.max - bounds.min)) * 100; 584 585 $thumbMin.css('left', minPercent + '%'); 586 $thumbMax.css('left', maxPercent + '%'); 587 $fill.css({ 588 'left': minPercent + '%', 589 'width': (maxPercent - minPercent) + '%' 590 }); 591 592 // Only update input fields if not skipping (to preserve empty state) and not typing 593 if (!skipInputUpdate && !isTyping) { 594 // Always display integer values in the visible input fields 595 $minValueInput.val(Math.round(values.min)); 596 $maxValueInput.val(Math.round(values.max)); 597 // Keep actual values (can be float) in hidden inputs for server 598 $minInput.val(values.min); 599 $maxInput.val(values.max); 600 } 601 } 602 603 function updateSliderOnly() { 604 const minPercent = ((values.min - bounds.min) / (bounds.max - bounds.min)) * 100; 605 const maxPercent = ((values.max - bounds.min) / (bounds.max - bounds.min)) * 100; 606 607 $thumbMin.css('left', minPercent + '%'); 608 $thumbMax.css('left', maxPercent + '%'); 609 $fill.css({ 610 'left': minPercent + '%', 611 'width': (maxPercent - minPercent) + '%' 612 }); 613 } 614 615 function handleDrag($thumb, type) { 616 let isDragging = false; 617 618 $thumb.on('mousedown touchstart', function(e) { 619 e.preventDefault(); 620 isDragging = true; 621 $('body').addClass('dnwoo-dragging'); 622 }); 623 624 $(document).on('mousemove touchmove', function(e) { 625 if (!isDragging) return; 626 627 const rect = $wrapper[0].getBoundingClientRect(); 628 const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX; 629 const percent = Math.max(0, Math.min(100, ((clientX - rect.left) / rect.width) * 100)); 630 const value = bounds.min + (percent / 100) * (bounds.max - bounds.min); 631 632 // Round values to integers when dragging 633 const roundedValue = Math.round(value); 634 635 if (type === 'min') { 636 values.min = Math.max(Math.round(bounds.min), Math.min(roundedValue, values.max)); 637 } else { 638 values.max = Math.min(Math.round(bounds.max), Math.max(roundedValue, values.min)); 639 } 640 641 updateSlider(); 642 }); 643 644 $(document).on('mouseup touchend', function() { 645 if (isDragging) { 646 isDragging = false;
647 $('body').removeClass('dnwoo-dragging'); 648 // Ensure values are integers when sending to server after dragging 649 values.min = Math.round(values.min); 650 values.max = Math.round(values.max); 651 // Update display with rounded values 652 updateSlider(); 653 // Trigger filtering after drag ends with integer values 654 commitPrice(); 655 } 656 }); 657 } 658 659 // Initialize drag handlers 660 handleDrag($thumbMin, 'min'); 661 handleDrag($thumbMax, 'max'); 662 663 // Handle manual input changes 664 $minValueInput.on('input', function() { 665 const inputVal = $(this).val(); 666 isTyping = true; 667 668 // Allow empty value - set to bounds.min internally but keep field empty 669 if (inputVal === '') { 670 values.min = bounds.min; 671 $minInput.val(values.min); // Update hidden input 672 // Without this the thumb stays where it was while the field reads 673 // empty â the track and the inputs disagree until the next drag. 674 updateSliderOnly(); 675 clearTimeout(inputTimer); 676 inputTimer = setTimeout(() => { 677 commitPrice(); 678 isTyping = false; 679 }, 500); 680 return; 681 } 682 683 // Validate that input contains only numbers, decimal point, and is well-formed 684 if (!/^[0-9]*\.?[0-9]*$/.test(inputVal)) { 685 return; // Ignore invalid input 686 } 687 688 const newValue = parseFloat(inputVal); 689 if (!isNaN(newValue) && newValue >= 0) { 690 // Only enforce minimum bound, let user set any value up to the absolute max 691 values.min = Math.max(bounds.min, Math.min(newValue, 999999)); 692 // Ensure min doesn't exceed max - if it does, update max to match 693 if (values.min > values.max) { 694 values.max = values.min; 695 $maxValueInput.val(values.max); 696 } 697 // Update hidden input and slider, but don't override the visible input 698 $minInput.val(values.min); 699 updateSliderOnly(); // Update slider position without touching input values 700 701 // Debounce the filtering 702 clearTimeout(inputTimer); 703 inputTimer = setTimeout(() => { 704 commitPrice(); 705 isTyping = false; 706 }, 500); 707 } 708 }); 709 710 $maxValueInput.on('input', function() { 711 const inputVal = $(this).val(); 712 isTyping = true; 713 714 // Allow empty value - set to bounds.max internally but keep field empty 715 if (inputVal === '') { 716 values.max = bounds.max; 717 $maxInput.val(values.max); // Update hidden input 718 // See the min handler: keep the track in sync with the cleared field. 719 updateSliderOnly(); 720 clearTimeout(inputTimer); 721 inputTimer = setTimeout(() => { 722 commitPrice(); 723 isTyping = false; 724 }, 500); 725 return; 726 } 727 728 // Validate that input contains only numbers, decimal point, and is well-formed 729 if (!/^[0-9]*\.?[0-9]*$/.test(inputVal)) { 730 return; // Ignore invalid input 731 } 732 733 const newValue = parseFloat(inputVal); 734 if (!isNaN(newValue) && newValue >= 0) { 735 // Allow any reasonable value for max, ensure it's not less than min 736 values.max = Math.max(values.min, Math.min(newValue, 999999)); 737 // Update hidden input and slider, but don't override the visible input 738 $maxInput.val(values.max); 739 updateSliderOnly(); // Update slider position without touching input values 740 741 // Debounce the filtering 742 clearTimeout(inputTimer); 743 inputTimer = setTimeout(() => { 744 commitPrice(); 745 isTyping = false; 746 }, 500); 747 } 748 }); 749 750 // Track click to move nearest thumb 751 $track.on('click', function(e) { 752 const rect = this.getBoundingClientRect(); 753 const percent = ((e.clientX - rect.left) / rect.width) * 100; 754 const clickValue = bounds.min + (percent / 100) * (bounds.max - bounds.min); 755 756 const distToMin = Math.abs(clickValue - values.min); 757 const distToMax = Math.abs(clickValue - values.max); 758 759 // Round to integer when clicking on track 760 if (distToMin < distToMax) { 761 values.min = Math.round(Math.max(bounds.min, Math.min(clickValue, values.max))); 762 } else { 763 values.max = Math.round(Math.min(bounds.max, Math.max(clickValue, values.min))); 764 } 765 766 updateSlider(); 767 commitPrice(); 768 }); 769 }); 770 } 771 772 function reset_block(section, element) { 773 let list = element.find(".dnwoo_product_filter_menu li"); 774 list.find("input").prop("checked", false); 775 list.removeClass("active"); 776 section.find("div").removeClass("active"); 777 section.find("li").removeClass("active"); 778 // attribute / brand items (incl. brand checkbox layout) 779 section.find("[data-term_id]").removeClass("active"); 780 // availability toggles 781 section.find("[data-availability]").removeClass("active"); 782 section.find("input[type='checkbox']").prop("checked", false); 783 } 784 785 /** 786 * Filter Reset show / hide 787 */ 788 function reset_filter_block(parent, action = "") { 789 let filter_reset = parent.find(".filter-reset"); 790 791 if (action == "all_reset") { 792 if (!filter_reset.hasClass("d-none")) { 793 filter_reset.addClass("d-none"); 794 } 795 } else { 796 if (filter_reset.hasClass("d-none")) { 797 filter_reset.removeClass("d-none"); 798 } 799 } 800 } 801 802 /** 803 * Clear all list active class 804 * @param {*} _this 805 */ 806 function action_for_all_list(element, _this) { 807 let all_li = element.find("li.all_categories"); 808 if (all_li.hasClass("active")) { 809 all_li.removeClass("active"); 810 } 811 } 812 813 function is_mobile_filter_view() { 814 return window.matchMedia("(max-width: 980px)").matches; 815 } 816 817 function get_fixed_header_offset() { 818 let offset = 12; 819 let adminBar = document.getElementById("wpadminbar"); 820 let mainHeader = document.getElementById("main-header"); 821 822 [adminBar, mainHeader].forEach(function (header) { 823 if (!header) { 824 return; 825 } 826 827 let headerStyle = window.getComputedStyle(header); 828 if (headerStyle.position !== "fixed" && headerStyle.position !== "sticky") { 829 return; 830 } 831 832 let rect = header.getBoundingClientRect(); 833 if (rect.top <= 1 && rect.bottom > offset) { 834 offset = rect.bottom + 12; 835 } 836 }); 837 838 return offset; 839 } 840 841 function reveal_product_results(filter_data, main_wrapper) { 842 if (!filter_data?.reveal_results || !is_mobile_filter_view()) { 843 return; 844 } 845 846 let $productSection = main_wrapper.find(".dnwoo_product_section").first(); 847 if (!$productSection.length) { 848 return; 849 } 850 851 close_mobile_filter_sidebar(main_wrapper, function () { 852 let scrollTop = Math.max(0, $productSection.offset().top - get_fixed_header_offset()); 853 let prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; 854 855 if (prefersReducedMotion) { 856 window.scrollTo(0, scrollTop); 857 } else { 858 $("html, body").stop(true).animate({ scrollTop }, 450); 859 } 860 861 $productSection.addClass("dnwoo_filter_results_revealed"); 862 if (!prefersReducedMotion && typeof Element !== "undefined" && Element.prototype.animate) { 863 $productSection 864 .find(".dnwoo-filter-result, .dnwoo_product_filter_wrap") 865 .each(function () { 866 this.animate( 867 [ 868 { opacity: 0.72, transform: "translateY(12px)" }, 869 { opacity: 1, transform: "translateY(0)" } 870 ], 871 { duration: 550, easing: "ease" } 872 ); 873 }); 874 } 875 window.setTimeout(function () { 876 $productSection.removeClass("dnwoo_filter_results_revealed"); 877 }, 900); 878 }); 879 } 880 881 /** 882 * Get all selected data 883 * @param {*} data 884 * @returns 885 */ 886 function selected_param(data = {}) { 887 let html = data.html; 888 let params = { 889 current_categories: "", 890 rating: "", 891 offset: 0, 892 taxonomies: [] 893 }; 894 895 if (data?.reset && data?.reset == "all_clear") { 896 params.current_categories = filterOption.get_category_list( 897 $, 898 html, 899 true 900 ); 901 return params; 902 } else if ( 903 data?.reset && 904 (data?.reset == "all_categories" || data?.reset == "category_reset") 905 ) { 906 params.current_categories = filterOption.get_category_list( 907 $, 908 html, 909 true 910 ); 911 } else { 912 params.current_categories = filterOption.get_category_list($, html); 913 } 914 915 916 if (params.current_categories.length == 0) { 917 params.current_categories = filterOption.get_category_list( 918 $, 919 html, 920 true 921 ); 922 } 923 924 // "Use Current Page" / hidden-sidebar fallback: when the filter sidebar 925 // is not rendered (e.g. on a product category/tag archive) there are no 926 // category list items to read, so fall back to the current term id that 927 // the server stamped on the product wrap. 928 if (params.current_categories.length == 0) { 929 let pageCat = html.find(".dnwoo_product_filter_wrap").data("current_categories"); 930 if (pageCat !== undefined && pageCat !== null && pageCat !== "") { 931 params.current_categories = String(pageCat) 932 .split(",") 933 .map(function (id) { 934 return id.trim(); 935 }) 936 .filter(Boolean); 937 } 938 } 939 940 // Only reset offset when filters actually change (not on pagination) 941 // Check if this is a pagination action by looking for page_style 942 let isPagination = data?.page_style !== undefined; 943 let shouldResetOffset = !isPagination && ( 944 data?.reset || 945 data?.filter_changed === true 946 ); 947 948 return { 949 current_categories: params.current_categories, 950 rating: data.html.find(".ratings li.active").data("id"), 951 products_number: html.data("products_number") ?? -1, 952 offset: shouldResetOffset ? 0 : (data.html.find(".pagination li.active").data("id") ?? 0), 953 taxonomies: get_taxonomies(html), 954 availability: get_availability(html), 955 min: data.min,
956 max: data.max, 957 price_value: data.price_value 958 }; 959 } 960 961 /** 962 * Get selected availability options (in stock / on sale / out of stock) 963 * @returns {string[]} 964 */ 965 function get_availability(html) { 966 return html 967 .find(".availability-wrapper [data-availability].active") 968 .map(function () { 969 return $(this).data("availability"); 970 }) 971 .get(); 972 } 973 974 /** 975 * Get Attributes 976 * @returns 977 */ 978 function get_taxonomies(html) { 979 let listing = []; 980 let options = html.find(".options-wrapper"); 981 if (options.length > 0) { 982 options.each((i, value) => { 983 let get_listing = $(value) 984 .find(".active") 985 .map(function () { 986 return { 987 term_id: $(this).data("term_id"), 988 taxonomy: $(this).data("taxonomy") 989 }; 990 }) 991 .get(); 992 993 if (get_listing.length > 0) { 994 listing.push(get_listing); 995 } 996 }); 997 } 998 999 return listing; 1000 } 1001 1002 /** 1003 * Ajax call 1004 */ 1005 function get_products(filter_data) { 1006 if (filter_data?.list_this) { 1007 hide_reset(filter_data.list_this, filter_data.html); 1008 } 1009 let main_wrapper = filter_data.html; 1010 let html = main_wrapper.find(".dnwoo_product_filter_wrap"); 1011 let params = selected_param(filter_data); 1012 let pagination_style = html.data("show_pagination") ?? "numbers"; 1013 1014 const data = { 1015 action: "get_filtered_data", 1016 _wpnonce: filter.nonce, 1017 dnwoo_query_products: main_wrapper.find("#dnwoo_query_products").val(), 1018 min: params.min, 1019 max: params.max, 1020 offset: params.offset, 1021 paged: params.offset, 1022 rating: params.rating, 1023 products_number: params.products_number, 1024 taxonomies: params.taxonomies, 1025 availability: params.availability, 1026 current_categories: 1027 "all" !== params.current_categories ? params.current_categories : [], 1028 //default data 1029 products_number: html.data("products_number") ?? -1, 1030 dnwoo_badge_percentage: html.data("dnwoo_badge_percentage") ?? "", 1031 show_badge: html.data("show_badge") ?? "", 1032 hide_out_of_stock: html.data("hide_out_of_stock") ?? "", 1033 thumbnail_size: html.data("thumbnail_size") ?? "woocommerce_thumbnail", 1034 show_price_text: html.data("show_price_text") ?? "on", 1035 show_add_to_cart: html.data("show_add_to_cart") ?? "on", 1036 dnwoo_show_add_to_cart_text: 1037 html.data("dnwoo_show_add_to_cart_text") ?? "", 1038 select_option_text: html.data("select_option_text") ?? "", 1039 show_featured_product: html.data("show_featured_product") ?? "", 1040 show_wishlist_button: html.data("show_wishlist_button") ?? "", 1041 tag: html.data("tag") ?? "div", 1042 show_wishlist_button: html.data("show_wishlist_button") ?? "", 1043 wishlist_text: html.data("wishlist_text") ?? "", 1044 show_compare_button: html.data("show_compare_button") ?? "", 1045 compare_text: html.data("compare_text") ?? "", 1046 show_quickview_button: html.data("show_quickview_button") ?? "", 1047 quickview_text: html.data("quickview_text") ?? "", 1048 order_class: html.data("order_class") ?? "", 1049 dnwoo_badge_featured: html.data("dnwoo_badge_featured") ?? "", 1050 show_sub_categories: html.data("show_sub_categories") ?? "on", 1051 show_pagination: pagination_style, 1052 loadmore_text: html.data("loadmore_text") ?? "numbers", 1053 pagination_alignment: html.data("pagination_alignment"), 1054 dnwoo_badge_sale: html.data("dnwoo_badge_sale") ?? "", 1055 dnwoo_badge_outofstock: html.data("dnwoo_badge_outofstock") ?? "", 1056 masonry_style: html.data("masonry_style") ?? "on", 1057 type: html.data("type") ?? "", 1058 orderby: html.data("orderby") ?? "date", 1059 order: html.data("order") ?? "ASC", 1060 meta_key: html.data("meta_key") ?? "", 1061 // Empty unless the visitor explicitly picked a sort, so the server can 1062 // tell a real sort choice apart from the module's default orderby and 1063 // keep the Product View Type ordering when no sort is selected. 1064 user_orderby: html.data("user_orderby") ?? "", 1065 include_categories: html.data("include_categories") ?? "all", 1066 }; 1067 $.ajax({ 1068 method: "POST", 1069 url: filter.ajax_url, 1070 data, 1071 beforeSend: function (response) { 1072 html.addClass("loading"); 1073 }, 1074 success: function (response) { 1075 html.removeClass("loading"); 1076 let products = response?.data?.products; 1077 let pagination = response?.data?.pagination; 1078 1079 main_wrapper.find('.dnwoo-showing-result').html(response?.data?.showing_result); 1080 if ( 1081 filter_data?.page_style == "loadmore" && 1082 pagination_style == "loadmore" 1083 ) { 1084 html.append(products); 1085 } else { 1086 html.html("").html(products); 1087 } 1088 main_wrapper 1089 .find(".dnwoo_pages_wrapper") 1090 .html("") 1091 .html(pagination); 1092 } 1093 }).then(function () { 1094 let wrapper = html; 1095 if (html.data("masonry_style") !== "on") { 1096 reveal_product_results(filter_data, main_wrapper); 1097 return; 1098 } 1099 html.imagesLoaded(function () { 1100 wrapper.isotope("destroy"); 1101 wrapper.isotope({ 1102 itemSelector: ".dnwoo_product_filter_item", 1103 masonry: { 1104 columnWidth: ".grid-sizer", 1105 gutter: ".gutter-sizer" 1106 } 1107 }); 1108 reveal_product_results(filter_data, main_wrapper); 1109 }); 1110 }); 1111 } 1112 isotope_call(); 1113 function isotope_call() { 1114 const $grid = $(".dnwoo_product_filter_wrap"); 1115 if ($grid.data("masonry_style") !== "on") { 1116 return; 1117 } 1118 let firstMenuItemFilter = $(".dnwoo_product_filter_item"); 1119 if ($grid.length) { 1120 $grid.imagesLoaded(function () { 1121 $grid.each(function () { 1122 $(this).isotope({ 1123 itemSelector: ".dnwoo_product_filter_item", 1124 percentPosition: true, 1125 stagger: 0, 1126 filter: firstMenuItemFilter, 1127 horizontalOrder: true, 1128 masonry: { 1129 columnWidth: ".grid-sizer", 1130 gutter: ".gutter-sizer" 1131 } 1132 }); 1133 }); 1134 }); 1135 1136 $(window).scroll(function () { 1137 $grid.isotope("layout"); 1138 }); 1139 } 1140 } 1141 }); 1142 1143 // Mobile filter sidebar toggle 1144 $(document).ready(function () { 1145 $(".dnwoo_mobile_filter_toggle").on("click", function () { 1146 var $container = $(this).closest("form"); 1147 var $sidebar = $container.find(".dnwoo_Pro_filter_menu_left_sidebar_wrapper");
1148 var $overlay = $container.find(".dnwoo_mobile_filter_overlay"); 1149 1150 $sidebar.addClass("dnwoo_sidebar_open"); 1151 $overlay.addClass("dnwoo_overlay_active"); 1152 $("body").addClass("dnwoo_filter_sidebar_open"); 1153 1154 // Lift the panel out of any ancestor stacking context so its z-index 1155 // actually wins over the site header. 1156 dnwoo_lift_offcanvas($sidebar); 1157 1158 // Apply inline styles to guarantee they override Divi theme CSS 1159 var el = $sidebar[0]; 1160 if (el) { 1161 // Dynamically calculate top based on #main-header bottom edge 1162 var topOffset = 0; 1163 var mainHeader = document.getElementById('main-header'); 1164 if (mainHeader) { 1165 topOffset = mainHeader.getBoundingClientRect().bottom; 1166 } 1167 var sidebarTop = topOffset + 'px'; 1168 var sidebarHeight = 'calc(100vh - ' + topOffset + 'px)'; 1169 1170 el.style.setProperty('position', 'fixed', 'important'); 1171 el.style.setProperty('top', sidebarTop, 'important'); 1172 el.style.setProperty('left', '0', 'important'); 1173 el.style.setProperty('transition', 'left 0.6s cubic-bezier(0.22, 1, 0.36, 1)', 'important'); 1174 el.style.setProperty('width', '85%', 'important'); 1175 el.style.setProperty('max-width', '320px', 'important'); 1176 el.style.setProperty('height', sidebarHeight, 'important'); 1177 el.style.setProperty('display', 'flex', 'important'); 1178 el.style.setProperty('flex-direction', 'column', 'important'); 1179 el.style.setProperty('gap', '20px', 'important'); 1180 el.style.setProperty('z-index', '999999999', 'important'); 1181 el.style.setProperty('overflow-y', 'auto', 'important'); 1182 el.style.setProperty('padding', '50px 20px 20px', 'important'); 1183 el.style.setProperty('background', '#fff', 'important'); 1184 el.style.setProperty('box-shadow', '2px 0 10px rgba(0,0,0,0.1)', 'important'); 1185 el.style.setProperty('box-sizing', 'border-box', 'important'); 1186 el.style.setProperty('color', '#333', 'important'); 1187 } 1188 }); 1189 1190 $(".dnwoo_mobile_filter_close, .dnwoo_mobile_filter_overlay").on("click", function () { 1191 var $container = $(this).closest("form"); 1192 close_mobile_filter_sidebar($container); 1193 }); 1194 }); 1195 1196 // sorting js 1197 $(document).ready(function () { 1198 var $filterButton = $("#dnwoo_pf_sort_filter_default_button"); 1199 var $filterDropdown = $("#dnwoo_pf_sort_filter_dropdown"); 1200 var $selectedFilterText = $("#dnwoo_pf_sort_selected_filter_text"); 1201 var $filterItems = $(".dnwoo_pf_sort_filter_item"); 1202 var $filterContainer = $filterButton.closest('.dnwoo_pf_sort_filter_container'); 1203 var dropdownOpen = false; 1204 // Make the orderby query parameter dynamic 1205 var orderbyParam = $('.dnwoo-orderby').attr('name') || 'orderby0'; 1206 var urlParams = new URLSearchParams(window.location.search); 1207 var orderbyValue = urlParams.get(orderbyParam); 1208 1209 if (orderbyValue) { 1210 $(".dnwoo-orderby").val(orderbyValue); 1211 // Get the option text for the selected value 1212 var orderbyText = $(".dnwoo-orderby option:selected").text(); 1213 $selectedFilterText.text(orderbyText); 1214 } 1215 1216 function openDropdown() { 1217 $filterDropdown.addClass("active"); 1218 $filterButton.addClass("active"); 1219 dropdownOpen = true; 1220 } 1221 1222 function closeDropdown() { 1223 $filterDropdown.removeClass("active"); 1224 $filterButton.removeClass("active"); 1225 dropdownOpen = false; 1226 } 1227 1228 $filterButton.on("click", function (e) { 1229 e.preventDefault(); 1230 if (dropdownOpen) { 1231 closeDropdown(); 1232 } else { 1233 openDropdown(); 1234 } 1235 }); 1236 1237 $filterItems.on("click", function (e) { 1238 e.preventDefault(); 1239 var newValue = $(this).text(); 1240 var dataValue = $(this).data("value"); 1241 $selectedFilterText.text(newValue); 1242 $filterItems.removeClass("selected"); 1243 $(this).addClass("selected"); 1244 // Trigger select option change for #dnwoo-orderby 1245 $(".dnwoo-orderby").val(dataValue).trigger("change"); 1246 closeDropdown(); 1247 }); 1248 1249 $(document).on("click", function (e) { 1250 if (!$filterContainer.is(e.target) && $filterContainer.has(e.target).length === 0 && dropdownOpen) { 1251 closeDropdown(); 1252 } 1253 }); 1254 1255 // Set initial selected item based on the text in $selectedFilterText 1256 var initialText = $selectedFilterText.text(); 1257 var $initialSelectedItem = $filterItems.filter(function () { 1258 return $(this).text() === initialText; 1259 }); 1260 if ($initialSelectedItem.length) { 1261 $initialSelectedItem.addClass("selected"); 1262 } 1263 }); 1264 1265 // ============================================================ 1266 // WooFilter â Layout 2 enhancements (Filter Template = Layout 2) 1267 // Collapsible accordions, active-filter chips, column view-switcher. 1268 // All built on top of the existing markup; gated by the 1269 // .dnwoo_filter_template_two container class so Layout 1 is untouched. 1270 // ============================================================ 1271 $(document).ready(function () { 1272 $(".dnwoo_filter_template_two").each(function () { 1273 var $container = $(this); 1274 if ($container.data("dnwooT2Ready")) { 1275 return; 1276 } 1277 $container.data("dnwooT2Ready", true); 1278 1279 // The module wrapper carries the refilter hook installed by 1280 // filtering_option(). 1281 var $module = $container.closest(".dnwoo_filter_masonry"); 1282 1283 function refilter(opts) { 1284 var fn = $module.data("dnwooRefilter"); 1285 if (typeof fn === "function") { 1286 fn(opts || { filter_changed: true }); 1287 } 1288 } 1289 1290 // ---------- Accordions ---------- 1291 $container 1292 .find(".dnwoo_Pro_filter_menu_left_sidebar_wrapper .dnwoo_filter_section") 1293 .each(function () { 1294 var $section = $(this); 1295 var $heading = $section 1296 .children(".dnwoo_product_filter_sidebar_heading") 1297 .first(); 1298 if (!$heading.length || $section.children(".dnwoo_acc_body").length) { 1299 return; 1300 } 1301 // Wrap everything after the heading into a collapsible body. 1302 $heading.nextAll().wrapAll('<div class="dnwoo_acc_body"></div>'); 1303 $section.addClass("dnwoo_acc_open"); // open by default 1304 // The reset link lives inside the heading; it must not toggle. 1305 $heading.find(".filter-reset").on("click", function (e) { 1306 e.stopPropagation(); 1307 }); 1308 $heading.on("click", function (e) { 1309 if ($(e.target).closest(".filter-reset").length) { 1310 return; 1311 } 1312 $section.toggleClass("dnwoo_acc_open"); 1313 }); 1314 }); 1315 1316 // ---------- Brand search box ---------- 1317 // Match the design reference: a "Search brandsâ¦" input above the brand 1318 // options list that hides non-matching options as you type. Works for 1319 // both the button and checkbox brand layouts. 1320 $container.find(".brand-wrapper").each(function () { 1321 var $brand = $(this); 1322 if ($brand.find(".dnwoo_t2_brand_search").length) { 1323 return; 1324 } 1325 var $options = $brand.find(".options-wrapper").first(); 1326 if (!$options.length) { 1327 return; 1328 } 1329 var $search = $( 1330 '<input type="text" class="dnwoo_t2_brand_search" placeholder="Search brandsâ¦" />' 1331 ); 1332 $options.before($search); 1333 $search.on("input", function () { 1334 var q = $.trim(this.value).toLowerCase(); 1335 $options.children().each(function () { 1336 var $opt = $(this); 1337 var name = ( 1338 $opt.data("name") || $opt.text() || "" 1339 ) 1340 .toString() 1341 .toLowerCase(); 1342 $opt.toggleClass("dnwoo_t2_brand_hidden", q !== "" && name.indexOf(q) === -1); 1343 }); 1344 }); 1345 // Don't let typing/clicks inside the search bubble up to the accordion 1346 // toggle or the chip re-render churn. 1347 $search.on("click", function (e) { 1348 e.stopPropagation(); 1349 }); 1350 }); 1351 1352 // ---------- Top bar + column view-switcher ---------- 1353 var $section = $container.find(".dnwoo_product_section").first(); 1354 var $grid = $container.find(".dnwoo_product_filter_wrap").first(); 1355 1356 if ($section.length && !$section.children(".dnwoo_t2_topbar").length) { 1357 var $topbar = $('<div class="dnwoo_t2_topbar"></div>'); 1358 var $showing = $section.find(".dnwoo-showing-result").first(); 1359 // The sort control's <form class="woocommerce-ordering"> is nested in 1360 // the module's outer <form>, so the HTML parser drops the tag and only 1361 // the inner container survives â look for both. Search the whole 1362 // container, not just the product section: with Show Filter Menu = 1363 // "default" the server renders the sort control before the section 1364 // (next to the category tabs), and it still belongs in the top bar. 1365 var $sort = $container.find(".woocommerce-ordering").first(); 1366 if (!$sort.length) { 1367 $sort = $container.find(".dnwoo_pf_sort_filter_container").first(); 1368 } 1369 1370 if ($showing.length) { 1371 $topbar.append($showing); 1372 } 1373 $topbar.append('<div class="dnwoo_t2_topbar_spacer"></div>'); 1374 if ($sort.length) { 1375 $topbar.append($sort); 1376 } 1377 1378 var viewIcons = { 1379 1: '<svg viewBox="0 0 14 11"><rect x="0" y="0" width="14" height="2" rx="1"/><rect x="0" y="4.5" width="14" height="2" rx="1"/><rect x="0" y="9" width="14" height="2" rx="1"/></svg>', 1380 2: '<svg viewBox="0 0 12 12"><rect x="0" y="0" width="5" height="5" rx=".8"/><rect x="7" y="0" width="5" height="5" rx=".8"/><rect x="0" y="7" width="5" height="5" rx=".8"/><rect x="7" y="7" width="5" height="5" rx=".8"/></svg>', 1381 3: '<svg viewBox="0 0 12 12"><rect x="0" y="0" width="3" height="5" rx=".6"/><rect x="4.5" y="0" width="3" height="5" rx=".6"/><rect x="9" y="0" width="3" height="5" rx=".6"/><rect x="0" y="7" width="3" height="5" rx=".6"/><rect x="4.5" y="7" width="3" height="5" rx=".6"/><rect x="9" y="7" width="3" height="5" rx=".6"/></svg>', 1382 4: '<svg viewBox="0 0 12 12"><rect x="0" y="0" width="2" height="5" rx=".5"/><rect x="3.3" y="0" width="2" height="5" rx=".5"/><rect x="6.7" y="0" width="2" height="5" rx=".5"/><rect x="10" y="0" width="2" height="5" rx=".5"/><rect x="0" y="7" width="2" height="5" rx=".5"/><rect x="3.3" y="7" width="2" height="5" rx=".5"/><rect x="6.7" y="7" width="2" height="5" rx=".5"/><rect x="10" y="7" width="2" height="5" rx=".5"/></svg>' 1383 }; 1384 // Which switcher buttons to render. The server emits the module's 1385 // "Columns in View Panel" setting as a CSV; anything absent or 1386 // unparseable means all four, so markup rendered before this setting 1387 // existed (or by the Divi 4 module) keeps its full switcher. 1388 var allowedCols = (function () { 1389 var raw = $grid.length ? String($grid.data("view_panel_columns") || "") : ""; 1390 var list = raw 1391 .split(",") 1392 .map(function (n) { return parseInt(n, 10); }) 1393 .filter(function (n) { return n >= 1 && n <= 4; }); 1394 return list.length ? list : [1, 2, 3, 4]; 1395 })(); 1396 1397 // Which button starts active must follow the module's Column setting, 1398 // not a hardcoded 3 â otherwise a 4-column grid opens with "3" lit. 1399 // The count is read back off the rendered grid (the Column setting is 1400 // emitted as grid-template-columns); if the grid isn't a CSS grid 1401 // (Masonry Style on) or the count is outside the switcher's 1-4 range, 1402 // no button is pre-lit rather than lighting a wrong one. 1403 var initialCols = (function () { 1404 if (!$grid.length) return 0; 1405 var tpl = window.getComputedStyle($grid[0]).gridTemplateColumns; 1406 if (!tpl || tpl === "none") return 3;
1407 var n = tpl.split(" ").filter(Boolean).length; 1408 // Light a button only if it is both in range and actually rendered â 1409 // e.g. Columns is 3 but "3 Columns" was unticked. Lighting nothing 1410 // beats lighting a button that does not match the grid. 1411 return n >= 1 && n <= 4 && allowedCols.indexOf(n) !== -1 ? n : 0; 1412 })(); 1413 1414 var $views = $('<div class="dnwoo_t2_views"></div>'); 1415 allowedCols.forEach(function (cols) { 1416 var $btn = $( 1417 '<button type="button" class="dnwoo_t2_view_btn" data-cols="' + 1418 cols + 1419 '">' + 1420 viewIcons[cols] + 1421 "</button>" 1422 ); 1423 if (cols === initialCols) { 1424 $btn.addClass("active"); 1425 } 1426 $btn.on("click", function () { 1427 $views.find(".dnwoo_t2_view_btn").removeClass("active"); 1428 $btn.addClass("active"); 1429 $grid 1430 .removeClass("dnwoo_cols_1 dnwoo_cols_2 dnwoo_cols_3 dnwoo_cols_4") 1431 .addClass("dnwoo_cols_" + cols);
1432 // With Masonry Style on, Isotope owns the layout; the class swap 1433 // changed the item/sizer widths, so reposition everything. 1434 if ($grid.data("masonry_style") === "on" && $grid.data("isotope")) { 1435 $grid.isotope("layout"); 1436 } 1437 }); 1438 $views.append($btn); 1439 }); 1440 $topbar.append($views); 1441 $section.prepend($topbar); 1442 1443 // Drop the now-empty original result wrapper, if any. 1444 $section.find(".dnwoo-filter-result").each(function () { 1445 var $r = $(this); 1446 if ($.trim($r.text()) === "" && !$r.children().length) { 1447 $r.remove(); 1448 } 1449 }); 1450 } 1451 1452 // ---------- Active filter chips ---------- 1453 var $chips = $('<div class="dnwoo_t2_chips"></div>'); 1454 if ($section.length) { 1455 var $topbarEl = $section.children(".dnwoo_t2_topbar").first(); 1456 if ($topbarEl.length) { 1457 $topbarEl.after($chips); 1458 } else { 1459 $section.prepend($chips); 1460 } 1461 } 1462 1463 function currencySymbol() { 1464 var $s = $container.find(".dnwoo-currency-symbol").first(); 1465 return $s.length ? $.trim($s.text()) : ""; 1466 } 1467 1468 function addChip(label, onRemove) { 1469 var $chip = $('<div class="dnwoo_t2_chip"></div>').text(label); 1470 $chip.append('<span class="dnwoo_t2_chip_x">Ã</span>'); 1471 $chip.on("click", function () { 1472 onRemove(); 1473 renderChips(); 1474 }); 1475 $chips.append($chip); 1476 } 1477 1478 // FilterOptions::availability_filter renders "In Stock" checked+active by 1479 // default, so chipping every checked item showed an "In Stock" tag before 1480 // the visitor had clicked anything. Chips should reflect the visitor's own 1481 // choices, so only chip availability the visitor has actually toggled. 1482 var availabilityTouched = {}; 1483 $container 1484 .find(".availability-wrapper") 1485 .on("change", 'input[type="checkbox"]', function () { 1486 var key = $(this).closest("[data-availability]").data("availability"); 1487 if (key !== undefined) { 1488 availabilityTouched[String(key)] = true; 1489 } 1490 }); 1491 1492 function renderChips() { 1493 $chips.empty(); 1494 1495 // Categories (skip "All", and unchecked checkbox items). 1496 $container.find(".dnwoo_product_filter_menu li.active").each(function () { 1497 var $li = $(this); 1498 if ($li.hasClass("all_categories")) { 1499 return; 1500 } 1501 var $input = $li.find('input[type="checkbox"]'); 1502 if ($input.length && !$input.is(":checked")) { 1503 return; 1504 } 1505 var label = 1506 $.trim($li.find("label, .check-item").first().text()) || 1507 $.trim($li.text()); 1508 if (!label) { 1509 return; 1510 } 1511 addChip(label, function () { 1512 $li.removeClass("active"); 1513 $input.prop("checked", false); 1514 refilter({ filter_changed: true }); 1515 }); 1516 }); 1517 1518 // Rating 1519 var $rate = $container.find(".ratings li.active").first(); 1520 if ($rate.length) { 1521 addChip("â " + $rate.data("id") + " & up", function () { 1522 $rate.removeClass("active"); 1523 refilter({ filter_changed: true }); 1524 }); 1525 } 1526 1527 // Attribute filters â tags / brand / colour / size (button + checkbox). 1528 $container.find(".attribute-wrapper").each(function () { 1529 $(this) 1530 .find(".options-wrapper .active, .dnwo-checkbox.active") 1531 .each(function () { 1532 var $item = $(this); 1533 var label = $item.data("name") || $.trim($item.text()); 1534 if (!label) { 1535 return; 1536 } 1537 addChip(label, function () { 1538 $item.removeClass("active"); 1539 $item.find('input[type="checkbox"]').prop("checked", false); 1540 refilter({ filter_changed: true }); 1541 }); 1542 }); 1543 }); 1544 1545 // Availability â its own wrapper, so the .attribute-wrapper sweep above 1546 // never saw it and an active In Stock / On Sale produced no chip. 1547 $container 1548 .find(".availability-wrapper [data-availability]") 1549 .each(function () { 1550 var $item = $(this); 1551 var $input = $item.find('input[type="checkbox"]'); 1552 var on = $input.length ? $input.is(":checked") : $item.hasClass("active"); 1553 if (!on || !availabilityTouched[String($item.data("availability"))]) { 1554 return; 1555 } 1556 var label = $item.attr("title") || $.trim($item.text()); 1557 if (!label) { 1558 return; 1559 } 1560 addChip(label, function () { 1561 $item.removeClass("active"); 1562 $input.prop("checked", false); 1563 refilter({ filter_changed: true }); 1564 }); 1565 }); 1566 1567 // Price range 1568 var $min = $container.find(".dnwoo-pricerange-min").first(); 1569 var $max = $container.find(".dnwoo-pricerange-max").first(); 1570 if ($min.length && $max.length) { 1571 var bMin = parseFloat($min.data("min")); 1572 var bMax = parseFloat($min.data("max") || $max.data("max")); 1573 var vMin = parseFloat($min.val()); 1574 var vMax = parseFloat($max.val()); 1575 // Compare what the chip would actually print. The slider's lower bound 1576 // is 0.01 when data-min is 0 (`parseFloat(...) || 0.01` treats 0 as 1577 // falsy), so a raw `vMin > bMin` reported "changed" on an untouched 1578 // slider and left a phantom "â¬0 â â¬752" chip after Clear all. 1579 var changed = 1580 (!isNaN(vMin) && !isNaN(bMin) && Math.round(vMin) > Math.round(bMin)) || 1581 (!isNaN(vMax) && !isNaN(bMax) && Math.round(vMax) < Math.round(bMax)); 1582 if (changed) { 1583 var sym = currencySymbol(); 1584 addChip( 1585 sym + Math.round(vMin) + " â " + sym + Math.round(vMax), 1586 function () { 1587 resetPrice(); 1588 } 1589 ); 1590 } 1591 } 1592 1593 // Clear all 1594 if ($chips.children().length) { 1595 var $clear = $( 1596 '<button type="button" class="dnwoo_t2_chip_clear">â Clear all</button>' 1597 ); 1598 $clear.on("click", function () { 1599 clearAll(); 1600 }); 1601 $chips.append($clear); 1602 } 1603 } 1604 1605 function resetPrice() { 1606 // Delegate to the slider's own reset hook. This used to read the bounds 1607 // straight off data-min / data-max with no fallback, so a module whose 1608 // price bounds render empty gave `Math.round(NaN)` â .val(NaN) leaves a 1609 // number input blank, and the resulting empty-string 'input' event moved 1610 // no thumbs. The slider closure is the only place that knows the real 1611 // bounds (it defaults them when the attributes are missing). 1612 var $slide = $container.find(".slidecontainer").first(); 1613 if (!$slide.length) { 1614 return false;
1615 } 1616 var price_reset = $slide.data("dnwooPriceReset"); 1617 if (typeof price_reset !== "function") { 1618 return false; // let the caller fall back to a plain refilter 1619 } 1620 price_reset(); 1621 return true; 1622 } 1623 1624 function clearAll() { 1625 $container.find(".dnwoo_product_filter_menu li").removeClass("active"); 1626 $container 1627 .find('.dnwoo_product_filter_menu li input[type="checkbox"]') 1628 .prop("checked", false); 1629 $container.find(".ratings li").removeClass("active"); 1630 // Availability deliberately uses its own `.availability-wrapper` rather 1631 // than `.attribute-wrapper` (see FilterOptions::availability_filter), so 1632 // sweeping only `.attribute-wrapper` left In Stock / On Sale checked and 1633 // the results still filtered after a Clear all. 1634 $container 1635 .find(".attribute-wrapper, .availability-wrapper") 1636 .each(function () { 1637 $(this).find(".active").removeClass("active"); 1638 $(this).find('input[type="checkbox"]').prop("checked", false); 1639 }); 1640 // Nothing is selected any more, so no availability counts as clicked. 1641 availabilityTouched = {}; 1642 1643 // resetPrice() refilters on its own; otherwise trigger one refilter. 1644 if (!resetPrice()) { 1645 refilter({ reset: "all_clear", filter_changed: true }); 1646 } 1647 renderChips(); 1648 } 1649 1650 // Re-render chips after any filter interaction (engine handlers, bound on 1651 // the same elements, run first â the timeout lets .active settle). 1652 var chipTimer; 1653 $container.on("click change input", function () { 1654 clearTimeout(chipTimer); 1655 chipTimer = setTimeout(renderChips, 80); 1656 }); 1657 1658 renderChips(); 1659 }); 1660 }); 1661})(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.