PageSourceSearch

https://candeloeventi.it/wp-content/plugins/woo-essential/assets/js/isotope-activation.js?ver=1789403603

js candeloeventi.it collected 2026-10-03 22:10:19 UTC 64,924 bytes, 1,661 lines download raw bytes

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.