PageSourceSearch

https://plannersy.com/wp-content/plugins/listeo-core/assets/js/drilldown.js?ver=1.9.40

js plannersy.com collected 2026-10-04 03:13:24 UTC 22,599 bytes, 621 lines download raw bytes

1/* ----------------- Start Document ----------------- */
2(function ($) {
3  "use strict";
4
5  $(document).ready(function () {
6    
7      // Global default categories if none are provided (optional)
8      var defaultCategories = [
9        {
10          label: "Default",
11          children: [{ label: "Item 1" }, { label: "Item 2" }],
12        },
13      ];
14
15      // Helper function to check if an item has real children (not just duplicates)
16      function hasRealChildren(item) {
17        if (!item.children || item.children.length === 0) {
18          return false;
19        }
20        
21        // If there's only one child and it's essentially the same as the parent, treat as leaf
22        if (item.children.length === 1) {
23          var child = item.children[0];
24          // Check if the child is a duplicate of the parent
25          if (child.value === item.value && child.id === item.id) {
26            return false;
27          }
28          // Also check if it's an "All in X" pattern
29          if (child.label === "All in " + item.label) {
30            return false;
31          }
32        }
33        
34        return true;
35      }
36
37      // Set up each drilldown menu instance
38      $(".drilldown-menu").each(function () {
39        var $menu = $(this);
40        var selectedItems = [];
41        var menuStack = []; // Array to keep track of drilldown levels
42        var initialized = false; //
43        // Add this option - read from data attribute or default to false
44        var singleSelect = $menu.data("single-select") === true;
45
46        // Read categories from the data attribute; fallback to defaultCategories if needed.
47        var categories = $menu.data("categories");
48        if (typeof categories === "string") {
49          try {
50            categories = JSON.parse(categories);
51          } catch (e) {
52            categories = defaultCategories;
53          }
54        } else if (!categories) {
55          categories = defaultCategories;
56        }
57
58        // Cache commonly used elements within this menu
59        var $menuToggle = $menu.find(".menu-toggle");
60        var $menuPanel = $menu.find(".menu-panel");
61        var $menuLevelsContainer = $menu.find(".menu-levels");
62        var $menuLabel = $menu.find(".menu-label");
63        var $menuLabelText = $menu.data("label");
64        var $resetButton = $menu.find(".reset-button");
65
66        // Recursive function to check if an item (or any descendant) matches the search term
67        function itemMatchesSearch(item, searchTerm) {
68          if ($.trim(searchTerm) === "") return true;
69          var lowerSearch = searchTerm.toLowerCase();
70          if (item.label.toLowerCase().indexOf(lowerSearch) !== -1) {
71            return true;
72          }
73          if (item.children && item.children.length > 0) {
74            for (var i = 0; i < item.children.length; i++) {
75              if (itemMatchesSearch(item.children[i], searchTerm)) {
76                return true;
77              }
78            }
79          }
80          return false;
81        }
82
83        // Initialize the menu at the root level
84        function initMenu() {
85          menuStack = [];
86          menuStack.push({ data: categories, parent: null });
87          $menuLevelsContainer.empty();
88          var $levelElement = createMenuLevel(categories, 0);
89          $menuLevelsContainer.append($levelElement);
90          updateMenuLevelPosition();
91          updateMenuHeight();
92          initializePreselectedValues();
93        }
94
95        // Create a new menu level element for the given data
96        function createMenuLevel(data, levelIndex) {
97          var $levelDiv = $("<div/>")
98            .addClass("menu-level")
99            .attr("data-level", levelIndex);
100
101          // Add a "Back" button if not at the root level
102          if (levelIndex > 0) {
103            var $backButton = $("<button/>")
104              .addClass("back-button")
105              .text(listeo_core.back)
106              .on("click", function (e) {
107                e.stopPropagation();
108                drillUp();
109              });
110            $levelDiv.append($backButton);
111          }
112
113          // Add a search input field
114          var $searchInput = $("<input/>", {
115            type: "text",
116            placeholder: listeo_core.search,
117            class: "menu-search",
118          }).on("input", function () {
119            filterMenuLevel($levelDiv, $searchInput.val());
120          });
121          $levelDiv.append($searchInput);
122
123          // Create a container for menu items
124          var $itemsContainer = $("<div/>").addClass("menu-items");
125          $levelDiv.append($itemsContainer);
126
127          // Iterate over the items and create each menu item element
128          $.each(data, function (i, item) {
129            var $itemDiv = $("<div/>")
130              .addClass("menu-item")
131              .attr("data-label", item.label);
132
133            // Add value attribute if it exists
134            if (item.value) {
135              $itemDiv.attr("data-value", item.value);
136            }
137            if (item.id) {
138              $itemDiv.attr("data-id", item.id);
139            }
140            // Store the entire item object for use in search filtering
141            $itemDiv.data("item", item);
142            var $labelSpan = $("<span/>")
143              .addClass("item-label")
144              .text(item.label);
145            $itemDiv.append($labelSpan);
146
147            // FIXED: Use hasRealChildren instead of just checking if children exist
148            if (hasRealChildren(item)) {
149              // Item with subcategories: add an arrow and set up drilldown
150              var $arrowSpan = $("<span/>").addClass("arrow");
151              $itemDiv.append($arrowSpan);
152              $itemDiv.on("click", function (e) {
153                e.stopPropagation();
154                drillDown(item);
155              });
156            } else {
157              // Leaf item: toggle selection on click
158              $itemDiv.on("click", function (e) {
159                e.stopPropagation();
160                // Remove 'active' class from any .category-item
161                $(".category-item").removeClass("active");
162                toggleSelection(item, $itemDiv);
163              });
164              if (isSelected(item)) {
165                $itemDiv.addClass("selected");
166              }
167            }
168            $itemsContainer.append($itemDiv);
169          });
170
171          return $levelDiv;
172        }
173
174        // Modified filter function that checks parent items and their descendants.
175        // It also highlights matches using <mark>.
176        function filterMenuLevel($levelDiv, searchTerm) {
177          var $itemsContainer = $levelDiv.find(".menu-items");
178          var $items = $itemsContainer.find(".menu-item");
179          var anyVisible = false;
180          $levelDiv.find(".no-results").remove();
181
182          $items.each(function () {
183            var $item = $(this);
184            var itemObj = $item.data("item"); // get the complete data object
185            var label = itemObj.label;
186            var lowerSearch = $.trim(searchTerm).toLowerCase();
187            // Determine if there is a direct match in the label
188            var directMatch =
189              lowerSearch !== "" &&
190              label.toLowerCase().indexOf(lowerSearch) > -1;
191            // Determine if the item or any descendant matches
192            var matches = itemMatchesSearch(itemObj, searchTerm);
193            if (matches) {
194              $item.css("display", "flex");
195              anyVisible = true;
196              var $labelSpan = $item.find(".item-label");
197              // Reset any previous highlighting and classes
198              $item.removeClass("child-match");
199              $labelSpan.text(label);
200              if ($.trim(searchTerm) !== "") {
201                if (directMatch) {
202                  // Highlight the matching substring in the label
203                  var regex = new RegExp(
204                    "(" + escapeRegExp(searchTerm) + ")",
205                    "gi"
206                  );
207                  $labelSpan.html(label.replace(regex, "<mark>$1</mark>"));
208                } else {
209                  // No direct match—but a descendant matches: add a special class
210                  $item.addClass("child-match");
211                }
212              }
213            } else {
214              $item.css("display", "none");
215            }
216          });
217          if (!anyVisible) {
218            var $noResults = $("<div/>")
219              .addClass("no-results")
220              .text("No results");
221            $itemsContainer.append($noResults);
222          }
223          updateMenuHeight();
224        }
225
226        // Utility function to escape regex special characters
227        function escapeRegExp(string) {
228          return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
229        }
230
231        // Drill down into a submenu for the given category.
232        // Also, propagate the parent's search term to the child's search field.
233        function drillDown(category) {
234          if (!category.children || category.children.length === 0) return;
235          // Get parent's search term from the current active level.
236          var $currentLevel = $menuLevelsContainer.children().last();
237          var parentSearchTerm = $currentLevel.find(".menu-search").val();
238          menuStack.push({ data: category.children, parent: category });
239          var levelIndex = menuStack.length - 1;
240          var $newLevel = createMenuLevel(category.children, levelIndex);
241          // Propagate parent's search term to child's search input and filter.
242          $newLevel.find(".menu-search").val(parentSearchTerm);
243          filterMenuLevel($newLevel, parentSearchTerm);
244          $menuLevelsContainer.append($newLevel);
245          updateMenuLevelPosition();
246          setTimeout(updateMenuHeight, 0);
247        }
248
249        // Return to the previous menu level
250        function drillUp() {
251          if (menuStack.length <= 1) return;
252          menuStack.pop();
253          $menuLevelsContainer.children().last().remove();
254          updateMenuLevelPosition();
255          updateMenuHeight();
256        }
257
258        function findItemByValue(categories, value) {
259          for (var i = 0; i < categories.length; i++) {
260            var item = categories[i];
261            // Check if this item matches
262            if ("#submit-listing-form".length) {
263              if (item.id == value) {
264                return item;
265              }
266            } else {
267              if (
268                item.value === value ||
269                (!item.value && item.label === value)
270              ) {
271                return item;
272              }
273            }
274
275            // Check children if they exist
276            if (item.children && item.children.length > 0) {
277              var found = findItemByValue(item.children, value);
278              if (found) return found;
279            }
280          }
281          return null;
282        }
283
284        // Make drilldown control functions available globally
285        if (!window.ListeoDrilldown) window.ListeoDrilldown = {};
286
287        window.ListeoDrilldown[$menu.attr("id")] = {
288          initMenu: initMenu,
289          selectById: function (categoryId) {
290            initMenu();
291
292            setTimeout(function () {
293              const item = findItemByValue(categories, categoryId);
294              if (item) {
295                // Fake a jQuery element just to pass to toggleSelection
296                const $fake = $("<div>")
297                  .addClass("menu-item")
298                  .attr("data-id", categoryId);
299                toggleSelection(item, $fake);
300              } else {
301                // reset the menu if no item found
302                resetSelections();
303              }
304            }, 20);
305          },
306        };
307
308        function initializePreselectedValues() {
309          selectedItems = []; // Clear existing selections
310
311          // Get the original input class and name
312          var $originalInput = $menu.find("input.drilldown-values");
313          var inputName = $menu.data("name");
314
315          // Get all existing array inputs
316          var $existingInputs = $menu.find('input[name="' + inputName + '[]"]');
317
318          $existingInputs.each(function () {
319            var value = $(this).val();
320            if (value) {
321              var item = findItemByValue(categories, value.trim());
322              if (item) {
323                selectedItems.push(item);
324              }
325            }
326          });
327
328          if (selectedItems.length > 0) {
329            updateMainButton();
330            // Ensure hidden inputs are created for preselected items
331            updateHiddenInput();
332          }
333        }
334        initializePreselectedValues();
335
336        function updateHiddenInput() {
337          // Find the hidden input by class within this menu instance
338          var $originalInput = $menu.find("input.drilldown-values");
339
340          var inputName = $menu.data("name");
341
342          // First remove any existing array inputs
343          $menu.find('input[name="' + inputName + '[]"]').remove();
344
345          // Create new hidden inputs for each selected value
346          selectedItems.forEach(function (item) {
347            if ($("#submit-listing-form").length) {
348              var value = item.id;
349            } else {
350              var value = item.value || item.label;
351            }
352
353            $("<input>", {
354              type: "hidden",
355              name: inputName + "[]",
356              value: value,
357              "data-label": item.label,
358              class: "drilldown-generated", // Add a class to identify generated inputs
359            }).appendTo($menu);
360          });
361          var target = $("div#listeo-listings-container");
362          target.triggerHandler("update_results", [1, false]);
363
364          $menu.trigger("drilldown-updated");
365
366          // Verify inputs were created
367        }
368
369        // Update the container's transform to slide to the active level
370        function updateMenuLevelPosition() {
371          var levelIndex = menuStack.length - 1;
372          $menuLevelsContainer.css(
373            "transform",
374            "translateX(-" + levelIndex * 100 + "%)"
375          );
376        }
377
378        // Update the panel height to match the active level's n
378atural height
379        function updateMenuHeight() {
380          var $levels = $menuLevelsContainer.children();
381          if ($levels.length === 0) return;
382          var $activeLevel = $levels.last();
383          $menuPanel.height($activeLevel[0].scrollHeight);
384        }
385
386        // Toggle selection of a leaf item.
387        // Also update the main button with highlighting of the current search term.
388        function toggleSelection(item, $itemDiv) {
389          var index = selectedItems.findIndex(function (selected) {
390            return item.value && selected.value
391              ? selected.value === item.value
392              : selected.label === item.label;
393          });
394
395          if (index > -1) {
396            // Deselect
397            selectedItems.splice(index, 1);
398            $itemDiv.removeClass("selected");
399
400            // Also remove from any other instances of this item in other levels
401            $menu
402              .find(
403                '.menu-item[data-value="' + (item.value || item.label) + '"]'
404              )
405              .removeClass("selected");
406            if (item.id) {
407              $menu
408                .find('.menu-item[data-id="' + item.id + '"]')
409                .removeClass("selected");
410            }
411          } else {
412            // Select
413            if (singleSelect) {
414              // Remove 'selected' class from all items
415              $menu.find(".menu-item.selected").removeClass("selected");
416              // Clear the array
417              selectedItems = [];
418            }
419
420            // Check if item already exists before pushing
421            if (!isSelected(item)) {
422              selectedItems.push(item);
423            }
424            $itemDiv.addClass("selected");
425
426            // Also add to any other instances of this item in other levels
427            $menu
428              .find(
429                '.menu-item[data-value="' + (item.value || item.label) + '"]'
430              )
431              .addClass("selected");
432            if (item.id) {
433              $menu
434                .find('.menu-item[data-id="' + item.id + '"]')
435                .addClass("selected");
436            }
437          }
438
439          updateMainButton();
440          updateHiddenInput();
441        }
442
443        // Check if an item is already selected
444        // function isSelected(item) {
445        //   var exists = false;
446        //   $.each(selectedItems, function (i, sel) {
447        //     if (sel.label === item.label) {
448        //       exists = true;
449        //       return false;
450        //     }
451        //   });
452        //   return exists;
453        // }
454
455        function isSelected(item) {
456          return selectedItems.some(function (selected) {
457            if ($("#submit-listing-form").length) {
458              if (item.id && selected.id) {
459                return selected.id === item.id;
460              }
461            } else {
462              if (item.value && selected.value) {
463                return selected.value === item.value;
464              }
465            }
466
467            return selected.label === item.label;
468          });
469        }
470
471        // Update the main button text.
472        // If a search term is active in the current level, highlight it in the label.
473        function updateMainButton() {
474          var searchTerm =
475            $menuLevelsContainer.children().last().find(".menu-search").val() ||
476            "";
477          if (selectedItems.length === 0) {
478            $menuLabel.html($menuLabelText);
479            $resetButton.hide();
480            $menuToggle.removeClass("dd-chosen"); // Remove class when no selection
481          } else if (selectedItems.length === 1) {
482            var label = selectedItems[0].label;
483            if ($.trim(searchTerm) !== "") {
484              var regex = new RegExp(
485                "(" + escapeRegExp(searchTerm) + ")",
486                "gi"
487              );
488              label = label.replace(regex, "<mark>$1</mark>");
489            }
490            $menuLabel.html(label);
491            $resetButton.show();
492            $menuToggle.addClass("dd-chosen"); // Add class when selection exists
493          } else {
494            var label = selectedItems[0].label;
495            if ($.trim(searchTerm) !== "") {
496              var regex = new RegExp(
497                "(" + escapeRegExp(searchTerm) + ")",
498                "gi"
499              );
500              label = label.replace(regex, "<mark>$1</mark>");
501            }
502            $menuLabel.html(label + " +" + (selectedItems.length - 1));
503            $resetButton.show();
504            $menuToggle.addClass("dd-chosen"); // Add class when selection exists
505          }
506        }
507
508        // Replace the existing resetSelections function with this fixed version:
509        function resetSelections() {
510          selectedItems = [];
511
512          // Remove selected class from ALL menu items in ALL levels, not just the current panel
513          $menu.find(".menu-item.selected").removeClass("selected");
514
515          // Also remove from any cached/hidden levels
516          $menuLevelsContainer
517            .find(".menu-item.selected")
518            .removeClass("selected");
519
520          // Debug logging
521          console.log("$menuLabel:", $menuLabel);
522          console.log("$menuLabelText:", $menuLabelText);
523          console.log("Current label HTML:", $menuLabel.html());
524
525          // Force update the main button label to original text
526           if ($menuLabelText) {
527             $menuLabel.html($menuLabelText);
528           } else {
529             // Fallback - try to get original text from data attribute or use default
530             var originalText =
531               $menu.data("label") ||
532               $menu.attr("data-label") ||
533               "Select an option";
534             console.log("Using fallback text:", originalText);
535             $menuLabel.html(originalText);
536           }
537          $resetButton.hide();
538          $menuToggle.removeClass("dd-chosen");
539
540          updateHiddenInput();
541
542          // Optional: Close the menu after reset
543          // closeMenu();
544        }
545
546        // Open the menu and initialize it; also close any other open menus
547        function openMenu() {
548          // Close all other menus on the page
549
550          $(".drilldown-menu")
551            .not($menu)
552            .each(function () {
553              $(this).find(".menu-panel").removeClass("open");
554              $(this).find(".menu-toggle").removeClass("dd-active"); // Remove class from other menus
555            });
556
557          if ($.fn.selectpicker) {
558            // For Bootstrap 4+
559            $(".bootstrap-select.show").each(function () {
560              $(this).removeClass("show");
561              $(this).find(".dropdown-menu").removeClass("show");
562            });
563
564            // For older Bootstrap versions
565            $(".bootstrap-select.open").each(function () {
566              $(this).removeClass("open");
567              $(this).find(".dropdown-menu").removeClass("open");
568            });
569          }
570          $menuPanel.addClass("open");
571          $menuToggle.addClass("dd-active"); // Add class when menu is opened
572          initMenu();
573        }
574
575        // Close the menu
576        function closeMenu() {
577          $menuPanel.removeClass("open");
578          $menuToggle.removeClass("dd-active"); // Remove class when menu is closed
579        }
580
581        // Toggle the menu when clicking the main button
582        $menuToggle.on("click", function (e) {
583          e.stopPropagation();
584          if ($menuPanel.hasClass("open")) {
585            closeMenu();
586          } else {
587            openMenu();
588          }
589        });
590
591        // Reset selections when clicking the reset button
592        $resetButton.on("click", function (e) {
593          e.stopPropagation();
594          resetSelections();
595          $(".category-item").removeClass("active");
596        });
597
598        // Close this menu if clicking outside it
599        $(document).on("click", function (e) {
600          if (!$menu.is(e.target) && $menu.has(e.target).length === 0) {
601            closeMenu();
602          }
603        });
604      });
605
606      window.selectDrilldownCategoryById = function (categoryId) {
607        $(".drilldown-menu").each(function () {
608          var $menu = $(this);
609          var $matchingItem = $menu.find('.menu-item[data-id="${categoryId}"]');
610          console.log("Matching item:", $matchingItem);
611          if ($matchingItem.length) {
612            $matchingItem.trigger("click");
613            console.log("Item clicked:", $matchingItem.text());
614            // Open the menu if needed and close it after selection
615         
616          }
617        });
618      };
619    });
620  
621})(this.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.