PageSourceSearch

https://www.cribgochoutdoor.com/sitespecific/CustomContent/quickfilters/quickfilters.js?v5

js cribgochoutdoor.com collected 2026-10-09 18:23:11 UTC 28,028 bytes, 713 lines download raw bytes

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.