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.