1/** 2 * Quick Filters - Combined Handler 3 * 4 * Three components: 5 * 1. Quick Filter Tag Handler - Connects HTML elements with class "quickFilterTag" to facets 6 * 2. Quick Search Tag Handler - Makes elements with class "quickSearchTag" toggleable search triggers 7 * 3. Quick Price Tag Handler - Makes elements with class "quickPriceTag" toggleable price range filters 8 * 9 * All only initialize on listing pages (universal_pageType === 'listing') 10 */ 11 12(function() { 13 'use strict'; 14 15 // ========================================== 16 // Quick Filter Tag Handler (Facets) 17 // ========================================== 18 19 // Store all facet/buttonDiv pairs for clearing 20 var facetButtonPairs = []; 21 22 // Function to update div appearance based on facet state (shared function) 23 function updateButtonAppearance(div, facet) { 24 // Remove only the quickFilter state classes, preserve user's custom classes 25 div.classList.remove('quickFilterTagSelected', 'quickFilterTagAvailable', 'quickFilterTagUnavailable'); 26 27 // Update selection state 28 facet.isSelected(facet.checkIsSelected()); 29 30 var isSelected = facet.isSelected(); 31 var isAvailable = facet.IsAvailable(); 32 33 // Add appropriate state classes (CSS handles the styling) 34 if (isSelected) { 35 div.classList.add('quickFilterTagSelected'); 36 } else if (isAvailable) { 37 div.classList.add('quickFilterTagAvailable'); 38 } else { 39 div.classList.add('quickFilterTagUnavailable'); 40 } 41 } 42 43 // Wait for the Search object to be available for filter tags 44 function waitForSearchFilterTags() { 45 if (typeof Search !== 'undefined') { 46 initializeFilterTags(); 47 } else { 48 setTimeout(waitForSearchFilterTags, 100); 49 } 50 } 51 52 function initializeFilterTags() { 53 // Check if Search object exists 54 if (typeof Search === 'undefined') { 55 console.warn('Search object not found. Doing nothing.'); 56 return; 57 } 58 59 // Find all elements with quickFilterTag class anywhere on the page 60 var buttonDivs = document.querySelectorAll('.quickFilterTag'); 61 62 if (buttonDivs.length === 0) { 63 console.log('No elements with class "quickFilterTag" found. Doing nothing.'); 64 return; 65 } 66 67 console.log('Found ' + buttonDivs.length + ' element(s) with class "quickFilterTag"'); 68 69 // Function to normalize strings for matching (handles spaces, hyphens, etc.) 70 function normalizeForMatch(str) { 71 if (!str) return ''; 72 return str.toLowerCase().trim() 73 .replace(/\s+/g, ' ') // Normalize multiple spaces 74 .replace(/[-\s]/g, ''); // Remove spaces and hyphens for comparison 75 } 76 77 // Helper function to check if a facet matches 78 function facetMatches(facet, buttonTextLower, buttonTextNormalized) { 79 var facetName = facet.Name() ? facet.Name().toLowerCase() : ''; 80 var facetUrlFragment = facet.FacetURLFragment() ? facet.FacetURLFragment().toLowerCase() : ''; 81 var facetNameNormalized = normalizeForMatch(facetName); 82 var facetUrlFragmentNormalized = normalizeForMatch(facetUrlFragment); 83 84 // Try exact matches first 85 if (facetName === buttonTextLower || 86 facetUrlFragment === buttonTextLower || 87 facetNameNormalized === buttonTextNormalized || 88 facetUrlFragmentNormalized === buttonTextNormalized) { 89 return true; 90 } 91 92 // Try partial matches (indexOf) with both original and normalized ( I was being too forgiving - Connor) 93 // if (facetName.indexOf(buttonTextLower) >= 0 || 94 // facetUrlFragment.indexOf(buttonTextLower) >= 0 || 95 // facetNameNormalized.indexOf(buttonTextNormalized) >= 0 || 96 // facetUrlFragmentNormalized.indexOf(buttonTextNormalized) >= 0) { 97 // return true; 98 // } 99 100 return false; 101 } 102 103 // Recursive function to search through facets and sub-facets
104 function searchFacetsRecursive(facets, buttonTextLower, buttonTextNormalized) { 105 if (!facets || facets.length === 0) { 106 return null; 107 } 108 109 for (var i = 0; i < facets.length; i++) { 110 var facet = facets[i]; 111 112 // Check if this facet matches 113 if (facetMatches(facet, buttonTextLower, buttonTextNormalized)) { 114 return facet; 115 } 116 117 // Recursively search SubFacetOptions 118 if (facet.SubFacetOptions && facet.SubFacetOptions().length > 0) { 119 var found = searchFacetsRecursive(facet.SubFacetOptions(), buttonTextLower, buttonTextNormalized); 120 if (found) { 121 return found; 122 } 123 } 124 } 125 126 return null; 127 } 128 129 // Function to find a facet by name (case-insensitive partial match, searches recursively) 130 function findFacetByName(buttonText) { 131 if (!Search.FacetDefinitions || Search.FacetDefinitions().length === 0) { 132 return null; 133 } 134 135 var buttonTextLower = buttonText.toLowerCase().trim(); 136 var buttonTextNormalized = normalizeForMatch(buttonText); 137 138 // Search through all facet definitions 139 for (var i = 0; i < Search.FacetDefinitions().length; i++) { 140 var facetDef = Search.FacetDefinitions()[i]; 141 142 if (facetDef.FacetOptions && facetDef.FacetOptions().length > 0) { 143 var found = searchFacetsRecursive(facetDef.FacetOptions(), buttonTextLower, buttonTextNormalized); 144 if (found) { 145 return found; 146 } 147 } 148 } 149 150 return null; 151 } 152 153 // Connect each div to its matching facet 154 var connectedCount = 0; 155 156 for (var i = 0; i < buttonDivs.length; i++) { 157 var buttonDiv = buttonDivs[i]; 158 159 // Get facet name from data-facet-name attribute only 160 var facetName = buttonDiv.getAttribute('data-facet-name'); 161 162 if (!facetName || !facetName.trim()) { 163 console.warn('Div #' + (i + 1) + ' has no data-facet-name attribute, skipping'); 164 continue; 165 } 166 167 facetName = facetName.trim(); 168 169 var facet = findFacetByName(facetName); 170 171 if (!facet) { 172 console.warn('No facet found matching: "' + facetName + '"'); 173 continue; 174 } 175 176 console.log('Connected div "' + facetName + '" to facet: ' + facet.Name()); 177 connectedCount++; 178 179 // Use IIFE to create proper closure for each button/facet pair 180 (function(btn, fct) { 181 // Store the pair for later clearing 182 facetButtonPairs.push({ button: btn, facet: fct }); 183 184 // Set initial appearance 185 updateButtonAppearance(btn, fct); 186 187 // Clone to remove existing event listeners 188 var newDiv = btn.cloneNode(true); 189 // Preserve all classes from original (including quickFilterTag) 190 newDiv.className = btn.className; 191 btn.parentNode.replaceChild(newDiv, btn); 192 btn = newDiv; 193 194 // Update the stored reference to the new button 195 var pairIndex = facetButtonPairs.length - 1; 196 facetButtonPairs[pairIndex].button = btn; 197 198 // Add click handler with proper closure 199 btn.addEventListener('click', function(e) { 200 e.preventDefault(); 201 e.stopPropagation(); 202 203 if (fct.IsAvailable() || fct.checkIsSelected()) { 204 // Clear search tags when facet is selected (price tags can work alongside) 205 clearAllTags();
206 207 fct.selectFacet(); 208 209 // Update appearance after a short delay to allow state to update 210 setTimeout(function() { 211 updateButtonAppearance(btn, fct); 212 }, 100); 213 } 214 }); 215 216 // Subscribe to facet state changes with proper closure 217 fct.isSelected.subscribe(function() { 218 updateButtonAppearance(btn, fct); 219 }); 220 221 fct.IsAvailable.subscribe(function() { 222 updateButtonAppearance(btn, fct); 223 }); 224 })(buttonDiv, facet); 225 } 226 227 if (connectedCount === 0) { 228 console.warn('No div elements were successfully connected to facets.'); 229 } else { 230 console.log('Successfully connected ' + connectedCount + ' div element(s) to facets.'); 231 } 232 } 233 234 // Function to clear all selected facets 235 function clearAllFacets() { 236 if (typeof Search === 'undefined' || !Search.FacetDefinitions) { 237 return; 238 } 239 240 // Function to recursively find and deselect all selected facets 241 function deselectFacetsRecursive(facets) { 242 if (!facets || facets.length === 0) { 243 return; 244 } 245 246 for (var i = 0; i < facets.length; i++) { 247 var facet = facets[i]; 248 249 // If facet is selected, deselect it by toggling (selectFacet toggles) 250 if (facet.checkIsSelected && facet.checkIsSelected()) { 251 facet.selectFacet(); 252 } 253 254 // Recursively process SubFacetOptions 255 if (facet.SubFacetOptions && facet.SubFacetOptions().length > 0) { 256 deselectFacetsRecursive(facet.SubFacetOptions()); 257 } 258 } 259 } 260 261 // Clear all facets from FacetDefinitions 262 var facetDefs = Search.FacetDefinitions(); 263 if (facetDefs && facetDefs.length > 0) { 264 for (var i = 0; i < facetDefs.length; i++) { 265 var facetDef = facetDefs[i]; 266 if (facetDef.FacetOptions && facetDef.FacetOptions().length > 0) { 267 deselectFacetsRecursive(facetDef.FacetOptions()); 268 } 269 } 270 } 271 272 // Update all quickFilterTag button appearances after a short delay 273 setTimeout(function() { 274 for (var i = 0; i < facetButtonPairs.length; i++) { 275 var pair = facetButtonPairs[i]; 276 if (pair && pair.button && pair.facet) { 277 updateButtonAppearance(pair.button, pair.facet); 278 } 279 } 280 }, 100); 281 282 // Trigger filter update 283 if (Search.FilterProducts) { 284 Search.FilterProducts(); 285 } 286 287 console.log('All facets cleared'); 288 } 289 290 // ========================================== 291 // Quick Search Tag Handler (Search Terms) 292 // ========================================== 293 294 // Track active search term (only one at a time) 295 let activeSearchTerm = null; 296 let isUpdatingFromTag = false; // Flag to prevent clearing when tag updates search 297 298 // Wait for the Search object to be available for search tags 299 function waitForSearchSearchTags() { 300 if (typeof Search !== 'undefined' && Search.CurrentSearchTerm) { 301 initializeSearchTags(); 302 } else { 303 setTimeout(waitForSearchSearchTags, 100); 304 } 305 } 306 307 function initializeSearchTags() { 308 // Find all elements with the quickSearchTag class 309 const searchTags = document.querySelectorAll('.quickSearchTag'); 310 311 // If no search tags exist, don't initialize 312 if (searchTags.length === 0) { 313 console.log('No quickSearchTag elements found, skipping initialization'); 314 return; 315 } 316
317 searchTags.forEach(function(tag) { 318 // Use data-search-term if available, otherwise use text content 319 tag.dataset.searchTerm = tag.dataset.searchTerm || tag.textContent.trim(); 320 321 // Add click handler for toggle functionality 322 tag.addEventListener('click', function() { 323 toggleTag(this); 324 }); 325 }); 326 327 console.log(`Initialized ${searchTags.length} toggleable search tags`); 328 329 // Add listener for search input changes to clear active tags 330 setupSearchInputListener(); 331 } 332 333 function toggleTag(tag) { 334 const searchTerm = tag.dataset.searchTerm; 335 const isActive = tag.classList.contains('active'); 336 337 if (isActive) { 338 // Remove tag 339 removeTag(tag, searchTerm); 340 } else { 341 // Deactivate any currently active tag first 342 if (activeSearchTerm) { 343 const currentActiveTag = document.querySelector('.quickSearchTag.active'); 344 if (currentActiveTag) { 345 removeTag(currentActiveTag, activeSearchTerm); 346 } 347 } 348 // Add new tag 349 addTag(tag, searchTerm); 350 } 351 352 // Update search 353 updateSearch(); 354 } 355 356 function addTag(tag, searchTerm) { 357 // Clear all regular filters (facets) when search filter is selected (price tags can work alongside) 358 clearAllFacets(); 359 360 // Set as active term 361 activeSearchTerm = searchTerm; 362 363 // Visual feedback 364 tag.classList.add('active'); 365 366 console.log(`Tag added: "${searchTerm}"`); 367 } 368 369 function removeTag(tag, searchTerm) { 370 // Clear active term 371 activeSearchTerm = null; 372 373 // Visual feedback 374 tag.classList.remove('active'); 375 376 console.log(`Tag removed: "${searchTerm}"`); 377 } 378 379 function updateSearch() { 380 try { 381 // Set the search term using the existing Search object 382 Search.CurrentSearchTerm(activeSearchTerm || ''); 383 384 console.log(`Active term: "${activeSearchTerm || 'none'}"`); 385 386 // Trigger the existing filter function 387 Search.FilterProducts(); 388 389 // Update the search input field if it exists 390 const searchInput = document.getElementById('searchTermTxt'); 391 if (searchInput) { 392 // Set flag to prevent clearing when we update the search input 393 isUpdatingFromTag = true; 394 searchInput.value = activeSearchTerm || ''; 395 396 // Trigger focus-within behavior 397 searchInput.focus(); 398 setTimeout(() => { 399 searchInput.blur(); 400 // Reset flag after a short delay 401 setTimeout(() => { 402 isUpdatingFromTag = false; 403 }, 50); 404 }, 100); 405 } 406 407 console.log(`Search updated with term: "${activeSearchTerm || 'none'}"`); 408 409 } catch (error) { 410 console.error('Error updating search:', error); 411 } 412 } 413 414 function setupSearchInputListener() { 415 const searchInput = document.getElementById('searchTermTxt'); 416 if (searchInput) { 417 // Listen for input changes 418 searchInput.addEventListener('input', function() { 419 // Only clear tag if user manually typed (not from tag update) 420 if (this.value.trim() !== '' && activeSearchTerm && !isUpdatingFromTag) { 421 clearActiveTag(); 422 } 423 }); 424 425 // Listen for focus events (when user clicks on search bar) 426 searchInput.addEventListener('focus', function() { 427 // Only clear tag if user manually focused (not from tag update) 428 if (activeSearchTerm && !isUpdatingFromTag) { 429 clearActiveTag(); 430 } 431 }); 432 } 433 } 434 435 function clearActiveTag() { 436 if (activeSearchTerm) { 437 const activeTag = document.querySelector('.quickSearchTag.active'); 438 if (activeTag) {
439 removeTag(activeTag, activeSearchTerm); 440 console.log('Active tag cleared due to search input usage'); 441 } 442 } 443 } 444 445 function clearAllTags() { 446 // Clear active term 447 activeSearchTerm = null; 448 449 // Reset all tags visually 450 const allTags = document.querySelectorAll('.quickSearchTag'); 451 allTags.forEach(tag => { 452 tag.classList.remove('active'); 453 }); 454 455 // Clear search 456 updateSearch(); 457 console.log('All tags cleared'); 458 } 459 460 // Expose clearAllTags globally for easy access 461 window.quickSearchTag_clearAllTags = clearAllTags; 462 463 // ========================================== 464 // Quick Price Tag Handler (Price Ranges) 465 // ========================================== 466 467 // Track active price range (only one at a time) 468 let activePriceRange = null; 469 let isUpdatingFromPriceTag = false; // Flag to prevent clearing when tag updates price 470 471 // Wait for the Search object to be available for price tags 472 function waitForSearchPriceTags() { 473 if (typeof Search !== 'undefined' && Search.minPrice && Search.maxPrice) { 474 initializePriceTags(); 475 } else { 476 setTimeout(waitForSearchPriceTags, 100); 477 } 478 } 479 480 function initializePriceTags() { 481 // Find all elements with the quickPriceTag class 482 const priceTags = document.querySelectorAll('.quickPriceTag'); 483 484 // If no price tags exist, don't initialize 485 if (priceTags.length === 0) { 486 console.log('No quickPriceTag elements found, skipping initialization'); 487 return; 488 } 489 490 priceTags.forEach(function(tag) { 491 // Get min and max from data attributes (required) 492 const priceMin = tag.getAttribute('data-price-min'); 493 const priceMax = tag.getAttribute('data-price-max'); 494 495 if (!priceMin || !priceMax) { 496 console.warn('quickPriceTag missing data-price-min or data-price-max attribute, skipping'); 497 return; 498 } 499 500 // Store the price range in dataset for easy access 501 tag.dataset.priceMin = parseFloat(priceMin); 502 tag.dataset.priceMax = parseFloat(priceMax); 503 504 // Add click handler for toggle functionality 505 tag.addEventListener('click', function() { 506 togglePriceTag(this); 507 }); 508 }); 509 510 console.log(`Initialized ${priceTags.length} toggleable price tags`); 511 512 // Add listener for price filter input changes to clear active tags 513 setupPriceInputListener(); 514 } 515 516 function togglePriceTag(tag) { 517 const priceMin = parseFloat(tag.dataset.priceMin); 518 const priceMax = parseFloat(tag.dataset.priceMax); 519 const isActive = tag.classList.contains('active'); 520 521 if (isActive) { 522 // Remove price filter 523 removePriceTag(tag); 524 } else { 525 // Deactivate any currently active price tag first 526 if (activePriceRange) { 527 const currentActiveTag = document.querySelector('.quickPriceTag.active'); 528 if (currentActiveTag) { 529 removePriceTag(currentActiveTag); 530 } 531 } 532 // Add new price filter 533 addPriceTag(tag, priceMin, priceMax); 534 } 535 536 // Update price filter 537 updatePriceFilter(); 538 } 539 540 function addPriceTag(tag, priceMin, priceMax) { 541 // Set as active price range 542 activePriceRange = { min: priceMin, max: priceMax }; 543 544 // Visual feedback 545 tag.classList.add('active'); 546 547 console.log(`Price tag added: £${priceMin} - £${priceMax}`); 548 } 549 550 function removePriceTag(tag) { 551 // Clear active price range 552 activePriceRange = null; 553 554 // Visual feedback 555 tag.classList.remove('active'); 556 557 console.log('Price tag removed'); 558 } 559 560 function updatePriceFilter() { 561 try { 562 if (typeof Search === 'undefined') { 563 console.error('Search object not available for price filter update'); 564 return; 565 } 566 567 if (activePriceRange) { 568 // Set the price range using the existing Search object 569 Search.minPrice(activePriceRange.min); 570 Search.maxPrice(activePriceRange.max); 571 Search.FilterByPrice(true); 572 573 console.log(`Price filter set: £${activePriceRange.min} - £${activePriceRange.max}`); 574 } else { 575 // Clear price filter 576 Search.FilterByPrice(false); 577 console.log('Price filter cleared'); 578 } 579 580 // Update the price filter input fields if they exist 581 const priceLowerInput = document.getElementById('priceFilterLower'); 582 const priceUpperInput = document.getElementById('priceFilterUpper'); 583 584 if (priceLowerInput && priceUpperInput) { 585 // Set flag to prevent clearing when we update the price inputs 586 isUpdatingFromPriceTag = true; 587 588 if (activePriceRange) { 589 priceLowerInput.value = activePriceRange.min.toFixed(2); 590 priceUpperInput.value = activePriceRange.max.toFixed(2); 591 } else { 592 priceLowerInput.value = ''; 593 priceUpperInput.value = ''; 594 } 595 596 // Reset flag after a short delay 597 setTimeout(() => { 598 isUpdatingFromPriceTag = false; 599 }, 100); 600 } 601 602 // Update the slider if it exists and has been initialized 603 const sliderElement = document.getElementById('sliderPrice'); 604 if (sliderElement && sliderElement.noUiSlider && activePriceRange) { 605 try { 606 sliderElement.noUiSlider.set([activePriceRange.min, activePriceRange.max]); 607 } catch (e) { 608 console.warn('Could not update price slider:', e); 609 } 610 } 611 612 // Trigger the existing filter function 613 Search.FilterProducts(); 614 615 } catch (error) { 616 console.error('Error updating price filter:', error); 617 } 618 } 619 620 function setupPriceInputListener() { 621 const priceLowerInput = document.getElementById('priceFilterLower'); 622 const priceUpperInput = document.getElementById('priceFilterUpper'); 623 624 if (priceLowerInput && priceUpperInput) { 625 // Listen for input changes 626 const handlePriceInputChange = function() { 627 // Only clear tag if user manually typed (not from tag update) 628 if (!isUpdatingFromPriceTag && activePriceRange) { 629 const lowerVal = parseFloat(priceLowerInput.value) || 0; 630 const upperVal = parseFloat(priceUpperInput.value) || 0; 631 632 // Check if the values differ from the active range 633 if (Math.abs(lowerVal - activePriceRange.min) > 0.01 || 634 Math.abs(upperVal - activePriceRange.max) > 0.01) { 635 clearActivePriceTag(); 636 } 637 } 638 }; 639 640 priceLowerInput.addEventListener('input', handlePriceInputChange); 641 priceUpperInput.addEventListener('input', handlePriceInputChange); 642 643 // Listen for focus events (when user clicks on price inputs) 644 const handlePriceInputFocus = function() { 645 // Only clear tag if user manually focused (not from tag update) 646 if (activePriceRange && !isUpdatingFromPriceTag) { 647 clearActivePriceTag(); 648 } 649 }; 650 651 priceLowerInput.addEventListener('focus', handlePriceInputFocus); 652 priceUpperInput.addEventListener('focus', handlePriceInputFocus); 653 } 654 } 655 656 function clearActivePriceTag() { 657 if (activePriceRange) { 658 const activeTag = document.querySelector('.quickPriceTag.active'); 659 if (activeTag) { 660 removePriceTag(activeTag); 661 console.log('Active price tag cleared due to price input usage'); 662 } 663 } 664 } 665 666 function clearAllPriceTags() { 667 // Clear active price range 668 activePriceRange = null; 669 670 // Reset all price tags visually 671 const allPriceTags = document.querySelectorAll('.quickPriceTag');
672 allPriceTags.forEach(tag => { 673 tag.classList.remove('active'); 674 }); 675 676 // Clear price filter 677 updatePriceFilter(); 678 console.log('All price tags cleared'); 679 } 680 681 // Expose clearAllPriceTags globally for easy access 682 window.quickPriceTag_clearAllPriceTags = clearAllPriceTags; 683 684 // ========================================== 685 // Initialization 686 // ========================================== 687 688 // Check if we should initialize (only on listing pages) 689 function shouldInitialize() { 690 return typeof universal_pageType !== 'undefined' && universal_pageType === 'listing'; 691 } 692 693 // Initialize when DOM is ready and only on listing pages 694 function initializeIfReady() { 695 if (shouldInitialize()) { 696 if (document.readyState === 'loading') { 697 document.addEventListener('DOMContentLoaded', function() { 698 waitForSearchFilterTags(); 699 waitForSearchSearchTags(); 700 waitForSearchPriceTags(); 701 }); 702 } else { 703 waitForSearchFilterTags(); 704 waitForSearchSearchTags(); 705 waitForSearchPriceTags(); 706 } 707 } 708 } 709 710 // Start initialization 711 initializeIfReady(); 712 713})();
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.