PageSourceSearch

https://www.hydraulikkteknikk.no/themes/javascript/gca.func.search…=202603121224.pagespeed.ce.XZmhC4-_Df.js

js hydraulikkteknikk.no collected 2026-10-06 16:21:11 UTC 50,504 bytes, 1,335 lines download raw bytes

1class GcaSearchBase {
2    constructor() {
3        this.query;
4        this.initialPageSize;
5        this.pageSize;
6        this.orderBy;
7        this.orderDir;
8        this.position;
9        this.tab;
10        this.lastShowMore;
11        this.paginationEnabled;
12        this.hasMore;
13        this.dispatcherType;
14        this.dispatcherId;
15        this.elementsInRow;
16        this.elementsInRowMobile;
17        this.context;
18        this.extraParams;
19        this._urlMapper = this._populateUrlParamsMapper();
20    }
21
22    setup(query, dispatcherType, dispatcherId, context, size, orderBy, orderDir, tab, position, entityId, elementsInRow, elementsInRowMobile) {
23        return new Promise((resolve, reject) => {
24            this.query               = query;
25            this.dispatcherType      = dispatcherType;
26            this.dispatcherId        = dispatcherId;
27            this.entityId            = entityId;
28            this.elementsInRow       = elementsInRow;
29            this.elementsInRowMobile = elementsInRowMobile
30            this.context             = context;
31            this.initialPageSize     = size;
32            this.pageSize            = size;
33            this.orderBy             = orderBy;
34            this.orderDir            = orderDir;
35            this.tab                 = tab;
36            this.position            = position;
37            this.lastShowMore        = new Date().getTime();
38            this.paginationEnabled   = false;
39            this.hasMore             = true;
40            this.extraParams         = null;
41
42            this.setupFilterEvents();
43            this.setupTabEvent();
44            this.setupShowMoreEvent();
45            this.setupScrollEvent();
46            resolve();
47        });
48    }
49
50    _populateUrlParamsMapper() {
51        let urlParamsMapper = {};
52        let urlParamsToMap = ["select", "delete", "having", "union"];
53        urlParamsToMap.forEach(function (paramValue) {
54            let val1 = paramValue;
55            let val2 = paramValue.toUpperCase();
56            let val3 = paramValue.charAt(0).toUpperCase() + paramValue.slice(1);
57
58            urlParamsMapper[val1] = "__" + val1
59            urlParamsMapper[val2] = "__" + val2
60            urlParamsMapper[val3] = "__" + val3
61        });
62        // Map specific symbols:
63        urlParamsMapper["&"] = "__and__";
64
65        return urlParamsMapper;
66    }
67
68    _mapParamToUrl(val) {
69        let newVal = val;
70        if(typeof val === "string") {
71            Object.keys(this._urlMapper).forEach(key => {
72                if(val.includes(key)) {
73                    newVal = val.replace(key, this._urlMapper[key]);
74                }
75            })
76        }
77        return newVal;
78    }
79
80    _mapParamFromUrl(val) {
81        let newVal = val;
82        if(this._isMappedUrlParameter(val)) {
83            Object.keys(this._urlMapper).forEach(key => {
84                let mappedVal = this._urlMapper[key];
85                if(val.includes(mappedVal)) {
86                    newVal = val.replace(mappedVal, key);
87                }
88            })
89        }
90        return newVal;
91    }
92
93    _containsParamValue(groupValues, value) {
94        let included = false;
95        if(groupValues !== undefined && groupValues.length > 0) {
96            groupValues.forEach(v => {
97                let groupValue = this._mapParamFromUrl(v);
98                if(groupValue == value) {
99                    included = true;
100                    return included;
101                }
102            })
103        }
104        return included;
105    }
106
107    _isMappedUrlParameter(val) {
108        let mapped = false;
109        if(val && val.includes("__")) {
110            Object.values(this._urlMapper).forEach(mappedValue => {
111                if(val.includes(mappedValue)) {
112                    mapped = true;
113                }
114            })
115        }
116        return mapped;
117    }
118
119
120    pushSearchToGoogle(query, type) {
121        if (query && query.trim().length > 0) {
122            jQuery.ajax({
123                dataType: "json",
124                url: "/ajax?action=allowTrack&recipient=google&service=analytics&event=search",
125                cache: false,
126                type: "get"
127            }).done(function (allowed) {
128                if (allowed === true) {
129                    dataLayer.push({
130                        'event': 'search',
131                        'search_term': query,
132                        'search_type': type
133                    });
134                }
135            });
136        }
137    }
138
139    doSearch(facetGroups) {
140        gca.showPageLoader();
141
142        let self            = this;
143        let body            = {};
144        body.query          = self.query;
145        body.dispatcherType = self.dispatcherType;
146        body.dispatcherId   = self.dispatcherId;
147        body.context        = self.context;
148        body.facetGroups    = facetGroups;
149        body.filters        = self.getFilterParams();
150        body.size           = self.pageSize;
151        body.orderBy        = self.orderBy;
152        body.orderDir       = self.orderDir;
153        body.extraParams    = self.getExtraParams();
154
155        // Remove any old filtering params..
156        if (body.facetGroups !== undefined) {
157            body.facetGroups.forEach(function(group) {
158                gca.removeURLParameterWithoutReload(group.name);
159            });
160        }
161
162        let hasFilters = false;
163        for (let i in body.filters) {
164            let filter = body.filters[i];
165            if (filter.values.length > 0 || filter.ranges.length > 0) {
166                hasFilters = true;
167                break;
168            }
169        }
170
171        let hasUrlFilterParams = false;
172        let urlFilters = {};
173        if (!hasFilters) {
174            const queryString = window.location.search;
175            const urlParams = new URLSearchParams(queryString);
176            const urlParamsSize = new Set(urlParams.keys()).size;
177            if (urlParamsSize > 1 && urlParams.has("filtered") && urlParams.get("filtered") === "true") {
178                urlParams.entries().forEach(function (entry) {
179                    let key = entry[0];
180                    if(key.startsWith("attribute.") || key === "vendor") { // TODO: Handle all fixed facet options. Available in Tool/facade. As this is a JS file, we should populate a list of fixed system facet groups by Ajax request once during init/setup.
181                        let value = entry[1];
182                        let type = "terms";
183                        if(value.split(":").length === 3 && value.split(":")[2] === "__range") {
184                            type = "range";
185                        }
186                        if (urlFilters[key] === undefined) {
187                            urlFilters[key] = {
188                                key: key,
189                                type: type,
190                                values: [],
191                                ranges: []
192                            };
193                        }
194
195                        if(type === "range") {
196                            const gteVal  = value.split(":")[0];
197                            const lteVal  = value.split(":")[1];
198                            let range = {};
199                            range["gte"]  = gteVal;
200                            range["lte"]  = lteVal;
201                            urlFilters[key].ranges.push(range);
202                        }
203                        else {
204                            urlFilters[key].values.push( self._mapParamFromUrl(value) );
205                        }
206                        gca.removeURLParameterWithoutReload(key);
207                        hasUrlFilterParams = true;
208                    }
209                })
210                body.filters = Object.values(urlFilters);
211            }
212        }
213
214        fetch ("/ajax?action=search&tab=" + self.tab,{
215            method: 'POST',
216            headers: {
217                'Content-Type': 'application/json',
218            },
219            body: JSON.stringify(body)
220        })
221        .then(response => response.text())
222        .then(data => {
223            self.pushSearchToGoogle(self.query, "default")
224            gca.hidePageLoader();
225            self.showResults(data);
226
227            if(body.filters && body.filters.length > 0) {
228                gca.updateURLParameterWithoutReload("filtered", "true");
229                body.filters.forEach(function(entry) {
230                    if (!entry.key.startsWith("_")) {
231                        if (entry.type === "range") {
232                            entry.ranges.forEach(function(range) {
233                                gca.addURLParameterWithoutReload(entry.key, range.gte + ":" + range.lte + ":__range");
234                            });
235                        }
236                        else {
237                            entry.values.forEach(function(value) {
238                                gca.addURLParameterWithoutReload(entry.key, self._mapParamToUrl(value));
239                            });
240                        }
241                    }
242                })
243            }
244            else {
245                gca.removeURLParameterWithoutReload("filtered");
246            }
247            if(hasUrlFilterParams) {
248                body.filters.forEach(function (entry) {
249                    const group = entry.key
250
251                    const facetGroupSelector = ".search-facet-group [data-facet-group='" + group + "']";
252                    gca.waitForElement(facetGroupSelector).then((elm) => {
253                        const allButtons = document.querySelectorAll(facetGroupSelector);
254                        allButtons.forEach(function (el) {
255                            const type  = el.getAttribute("data-facet-group-type");
256
257                            if (type === "range") {
258                                const rangeValues = entry.ranges
259                                if(rangeValues && rangeValues[0] !== undefined) {
260                                    const rangeInput = document.querySelector(facetGroupSelector);
261                                    const gteInput = rangeInput.querySelector("input[data-facet-group-range-type='gte']");
262                                    const lteInput = rangeInput.querySelector("input[data-facet-group-range-type='lte']");
263
264                                    rangeInput.setAttribute("data-facet-checked", "true");
265                                    gteInput.value = rangeValues[0]["gte"];
266                                    lteInput.value  = rangeValues[0]["lte"];
267                                }
268                            }
269                            else {
270                                let value   = null;
271                                try {
272                                    value = el.getAttribute("data-facet-value")
273                                }
274                                catch (e) {
275                                    console.log(e);
276                                }
277                                const groupValues = entry.values
278                                if (groupValues !== undefined && groupValues.includes(value)) {
279                                    el.setAttribute("data-facet-checked", "true");
280                                }
281                            }
282                        })
283                    });
284                })
285            }
286        })
287        .catch((error) => {
288            console.error('Error:', error);
289        });
290    }
291
292    doIncrementalSearch(query, target) {
293        let self            = this;
294        let body            = {};
295        body.query          = query;
296        body.dispatcherType = self.dispatcherType;
297        body.dispatcherId   = self.dispatcherId;
298        body.context        = "incremental";
299        body.filters        = [];
300        body.size           = 5;
301        body.extraParams    = self.getExtraParams();
302
303        fetch ("/ajax?action=searchIncremental",{
304            method: 'POST',
305            headers: {
306                'Content-Type': 'application/json',
307            },
308            body: JSON.stringify(body)
309        })
310        .then(response => response.text())
311        .then((data) => {
312            this.renderIncrementalResults(data, target)
313            self.pushSearchToGoogle(query, "incremental")
314        })
315        .catch((error) => {
316            console.error('Error:', error);
317        });
318    }
319
320    doFilter(facetGroups, offCanvas) {
321        gca.showPageLoader();
322
323        let self            = this;
324        let body            = {};
325        body.query          = self.query;
326        body.dispatcherType = self.dispatcherType;
327        body.dispatcherId   = self.dispatcherId;
328        body.context        = self.context;
329        body.facetGroups    = facetGroups;
330        if (offCanvas) {
331            body.filters = self.getOffCanvasFilterParams();
332        }
333        else {
334            body.filters = self.getFilterParams();
335        }
336        body.size           = self.pageSize;
337        body.orderBy        = self.orderBy;
338        body.orderDir       = self.orderDir;
339        body.extraParams    = self.getExtraParams();
340
341        // Remove any old filtering params..
342        if (body.facetGroups !== undefined) {
343            body.facetGroups.forEach(function(group) {
344                gca.removeURLParameterWithoutReload(group.name.toLowerCase());
345            });
346        }
347
348        let hasFilters = false;
349        for (let i in body.filters) {
350            let filter = body.filters[i];
351            if (filter.values.length > 0 || filter.ranges.length > 0) {
352                hasFilters = true;
353                break;
354            }
355        }
356
357        if (!hasFilters) {
358            gca.removeURLParameterWithoutReload("filtered");
359            location.reload();
360            return;
361        }
362
363        let filter    = {};
364        filter.key    = "__page";
365        filter.values = [];
366        filter.values.push(self.dispatcherId);
367        body.filters.push(filter);
368
369        fetch ("/ajax?action=filter&id=" + this.entityId + "&sort=" + self.orderBy + "&dir=" + this.orderDir + "&view=" + gca.getListView(this.entityId) + "&row=" + this.elementsInRow + "&rowMobile=" + this.elementsInRowMobile, {
370            method: 'POST',
371            headers: {
372                'Content-Type': 'application/json',
373            },
374            body: JSON.stringify(body)
375        })
376            .then(response => response.text())
377            .then(data => {
378                gca.hidePageLoader();
379                self.showResults(data);
380
381                gca.updateURLParameterWithoutReload("filtered", "true");
382                body.filters.forEach(function(entry) {
383                    if (!entry.key.startsWith("_")) {
384                        if (entry.type === "range") {
385                            entry.ranges.forEach(function(range) {
386                                gca.addURLParameterWithoutReload(entry.key.toLowerCase(), range.gte + ":" + range.lte + ":__range");
387                            });
388                        }
389                        else {
390                            entry.values.forEach(function(value) {
391                                if (typeof value === "string") {
392                                    gca.addURLParameterWithoutReload(entry.key.toLowerCase(), self._mapParamToUrl(value.toLowerCase()));
393                                }
394                                else {
395                                    gca.addURLParameterWithoutReload(entry.key.toLowerCase(), self._mapParamToUrl(value));
396                                }
397                            });
398                        }
399                    }
400                })
401            })
402            .catch((error) => {
403                console.error('Error:', error);
404            });
405    }
406
407    doSort() {
408        const sortFieldSelect = $("[data-target='sortFields'] :selected");
409        gca.updateURLParameterWithoutReload("sort", sortFieldSelect.data("field"));
410        gca.updateURLParameterWithoutReload("dir", sortFieldSelect.data("direction"));
411        location.reload();
412    }
413
414    updateOffCanvasFilter(facetGroups) {
415        let self            = this;
416        let body        = {};
417        body.query          = self.query;
418        body.dispatcherType = self.dispatcherType;
419        body.dispatcherId   = self.dispatcherId;
420        body.context        = self.context;
421        body.facetGroups    = facetGroups;
422        body.filters        = self.getOffCanvasFilterParams();
423        body.size           = self.pageSize;
424        body.orderBy        = self.orderBy;
425        body.orderDir       = self.orderDir;
426        body.extraParams    = self.getExtraParams();
427
428        let filter    = {};
429        filter.key    = "__page";
430        filter.values = [];
431        filter.values.push(self.dispatcherId);
432        body.filters.push(filter);
433
434        fetch ("/ajax?action=updateOffCanvasFilter",{
435            method: 'POST',
436            headers: {
437                'Content-Type': 'application/json',
438            },
439            body: JSON.stringify(body)
440        })
441            .then(response => response.text())
442            .then(data => {
443                let htmlContent = jQuery.parseHTML(data);
444                const offCanvasBody  = $(htmlContent).find("div#offcanvas-body").html();
445                $("#offcanvas-search-facets-body").html(offCanvasBody);
446            })
447            .catch((error) => {
448                console.error('Error:', error);
449            });
450    }
451
452    getFilterFacets() {
453        let self            = this;
454        let body            = {};
455        body.query          = self.query;
456        body.dispatcherType = self.dispatcherType;
457        body.dispatcherId   = self.dispatcherId;
458        body.context        = self.context;
459        body.filters        = self.getFilterParams();
460        body.size           = self.pageSize;
461        body.orderBy        = self.orderBy;
462        body.orderDir       = self.orderDir;
463        body.extraParams    = self.getExtraParams();
464
465        let filter    = {};
466        filter.key    = "__page";
467        filter.values = [];
468        filter.values.push(self.dispatcherId);
469
470        body.filters.push(filter);
471
472        fetch ("/ajax?action=getFilterFacets",{
473            method: 'POST',
474            headers: {
475                'Content-Type': 'application/json',
476            },
477            body: JSON.stringify(body)
478        })
479            .then(response => response.text())
480            .then(data => {
481                gca.hidePageLoader();
482                self.showFacets(data);
483            })
484            .catch((error) => {
485                console.error('Error:', error);
486            });
487    }
488
489    getFilterParams() {
490        let filters = {};
491        let values  = {};
492        let ranges  = {};
493
494        jQuery.each($("[data-facet-group]"), function(i, el) {
495            el = $(el);
496
497            const name    = encodeURIComponent(el.data('facet-group'));
498            const type    = el.data("facet-group-type");
499            const value   = el.data('facet-value');
500            const checked = el.attr('data-facet-checked');
501
502            if (values[name] === undefined) {
503                values[name] = [];
504            }
505            if (ranges[name] === undefined) {
506                ranges[name] = [];
507            }
508
509            if ((type === "terms" || type === 'range') && checked === 'true') {
510                // Create filter if not already done...
511                if (filters[name] === undefined) {
512                    filters[name] = {
513                        key: name,
514                        type: type,
515                        values: [],
516                        ranges: []
517                    };
518                }
519
520                // Add value or range...
521                if (type === "terms" && checked === 'true') {
522                    filters[name].values.push(value);
523                }
524                else if (type === 'range' && checked === 'true') {
525                    const gte = el.find("[data-facet-group-range-type='gte']").val();
526                    const lte = el.find("[data-facet-group-range-type='lte']").val();
527
528                    if (gte !== undefined && gte.length > 0 && lte !== undefined && lte.length > 0) {
529                        let range = {};
530                        range["gte"] = gte;
531                        range["lte"]  = lte;
532                        filters[name].ranges.push(range);
533                    }
534                }
535            }
536        });
537
538        return Object.values(filters);
539    }
540
541    getOffCanvasFilterParams() {
542        let filters = {};
543        let values  = {};
544        let ranges  = {};
545
546        jQuery.each($("[data-offcanvas-facet-group]"), function(i, el) {
547            el = $(el);
548
549            const name    = encodeURIComponent(el.data('offcanvas-facet-group'));
550            const type    = el.data("offcanvas-facet-group-type");
551            const value   = el.data('offcanvas-facet-value');
552            const checked = el.attr('data-offcanvas-facet-checked');
553
554            if (values[name] === undefined) {
555                values[name] = [];
556            }
557            if (ranges[name] === undefined) {
558                ranges[name] = [];
559            }
560
561            if ((type === "terms" && checked === 'true') || (type === 'range')) {
562                // Create filter if not already done...
563                if (filters[name] === undefined) {
564                    filters[name] = {
565                        key: name,
566                        type: type,
567                        values: [],
568                        ranges: []
569                    };
570                }
571
572                // Add value or range...
573                if (type === "terms" && checked === 'true') {
574                    filters[name].values.push(value);
575                }
576                else if (type === 'range' && checked === 'true') {
577                    const gte = el.find("[data-offcanvas-facet-group-range-type='gte']").val();
578                    const lte = el.find("[data-offcanvas-facet-group-range-type='lte']").val();
579
580                    if (gte !== undefined && gte.length > 0 && lte !== undefined && lte.length > 0) {
581                        let range = {};
582                        range["gte"] = gte;
583                        range["lte"]  = lte;
584                        filters[name].ranges.push(range);
585                    }
586                }
587            }
588        });
589
590        return Object.values(filters);
591    }
592
593    getFacetGroups() {
594        let facetGroups = {};
595        let values  = {};
596        let ranges  = {};
597
598        let facetGroupElements = document.querySelectorAll(".search-facet-group")
599        facetGroupElements.forEach(function (el) {
600            let facetGroupBtn   = el.querySelector("button[id^='facet_']")
601            let facetGroupName = facetGroupBtn.id.replace("facet_", "")
602            facetGroups[facetGroupName] = {
603                name: facetGroupName,
604                description: facetGroupBtn.getAttribute("title"),
605                type: el.getAttribute("data-facet-group-type"),
606                facets: [],
607                ranges: [],
608                extraParams: {
609                    hidden: el.getAttribute("data-facet-group-hidden")
610                }
611            }
612        });
613
614        jQuery.each($("[data-facet-group]"), function(i, el) {
615            el = $(el);
616
617            const name    = encodeURIComponent(el.data('facet-group'));
618            const type    = el.data("facet-group-type");
619            const value   = el.data('facet-value');
620            const descr   = el.text().trim();
621            const checked = el.attr('data-facet-checked');
622            const hidden  = el.data('facet-group-hidden');
623
624            if (values[name] === undefined) {
625                values[name] = [];
626            }
627            if (ranges[name] === undefined) {
628                ranges[name] = [];
629            }
630
631            if (type === "terms" || type === 'range') {
632                // Create filter if not already done...
633                if (facetGroups[name] === undefined) {
634                    facetGroups[name] = {
635                        name: name,
636                        description: document.getElementById('facet_' + name).getAttribute('title'),
637                        type: type,
638                        facets: [],
639                        ranges: [],
640                        extraParams: {
641                            hidden: hidden
642                        }
643                    };
644                }
645
646                // Add value or range...
647                if (type === "terms") {
648                    let facet = {};
649                    facet["value"] = value;
650                    facet["description"] = descr;
651                    facet["checked"] = checked;
652                    facetGroups[name].facets.push(facet);
653                }
654                else if (type === 'range') {
655                    const gte = el.find("[data-facet-group-range-type='gte']").val();
656                    const lte  = el.find("[data-facet-group-range-type='lte']").val();
657
658                    if (gte !== undefined && gte.length > 0 && lte !== undefined && lte.length > 0) {
659                        let range = {};
660                        range["max"] = gte;
661                        range["min"]  = lte;
662                        range["checked"] = checked;
663                        facetGroups[name].ranges.push(range);
664                    }
665                }
666            }
667        });
668
669        return Object.values(facetGroups);
670    }
671
672    getOffcanvasFacetGroups() {
673        let facetGroups = {};
674        let values  = {};
675        let ranges  = {};
676
677        let facetGroupElements = document.querySelectorAll(".accordion-search-facets")
678        facetGroupElements.forEach(function (el) {
679            let facetGroupBtn   = el.querySelector("div[id^='offcanvas-facet_']")
680            let facetGroupName = facetGroupBtn.id.replace("offcanvas-facet_", "")
681            facetGroups[facetGroupName] = {
682                name: facetGroupName,
683                description: facetGroupBtn.getAttribute("title"),
684                type: el.getAttribute("data-offcanvas-facet-group-type"),
685                facets: [],
686                ranges: []
687            }
688        });
689
690        jQuery.each($("[data-offcanvas-facet-group]"), function(i, el) {
691            el = $(el);
692
693            const name    = encodeURIComponent(el.data('offcanvas-facet-group'));
694            const type    = el.data("offcanvas-facet-group-type");
695            const value   = el.data('offcanvas-facet-value');
696            const descr   = el.text().trim();
697            const checked = el.attr('data-offcanvas-facet-checked');
698
699            if (values[name] === undefined) {
700                values[name] = [];
701            }
702            if (ranges[name] === undefined) {
703                ranges[name] = [];
704            }
705
706            if (type === "terms" || type === 'range') {
707                // Create filter if not already done...
708                if (facetGroups[name] === undefined) {
709                    facetGroups[name] = {
710                        name: name,
711                        description: document.getElementById('offcanvas-facet_' + name.replaceAll(".", "-")).getAttribute('title'),
712                        type: type,
713                        facets: [],
714                        ranges: []
715                    };
716                }
717
718                // Add value or range...
719                if (type === "terms") {
720                    let facet = {};
721                    facet["value"] = value;
722                    facet["description"] = descr;
723                    facet["checked"] = checked;
724                    facetGroups[name].facets.push(facet);
725                }
726                else if (type === 'range') {
727                    const gte = el.find("[data-offcanvas-facet-group-range-type='gte']").val();
728                    const lte = el.find("[data-offcanvas-facet-group-range-type='lte']").val();
729
730                    if (gte !== undefined && gte.length > 0 && lte !== undefined && lte.length > 0) {
731                        let range = {};
732                        range["max"] = gte;
733                        range["min"]  = lte;
734                        range["checked"] = checked;
735                        facetGroups[name].ranges.push(range);
736                    }
737                }
738            }
739        });
740
741        return Object.values(facetGroups);
742    }
743
744    getExtraParams() {
745        return this.extraParams;
746    }
747
748    loadExtraParams() {
749        const self = this;
750        return fetch("/ajax?action=getExtraSearchRequestParams")
751        .then(function (response) {
752            return response.json();
753        })
754        .then(function (data) {
755            self.extraParams = data;
756            return self.extraParams;
757        });
758    }
759
760    loadShowMoreProductsButton() {
761        fetch("/ajax?" + new URLSearchParams({
762            action: "getShowMoreProductsButton",
763            context: "search",
764        })).then(function (response) {
765            return response.text();
766        }).then(function (data) {
767            $("[data-target='showMoreProductsButton']").html(data.trim())
768        }).catch(function (err) {
769            console.warn('Something went wrong.', err);
770        });
771    }
772
773    showResults(data) {
774        const content = jQuery.parseHTML(data);
775        const results = $(content).find("div#results").html();
776        const facets  = $(content).find("div#facets").html();
777        const hasMore = $(content).find("div#hasMore").html();
778
779        $("[data-target='search-results']").html(results);
780        this.showFacets(facets);
781
782        const querySuggestionDiv = $(content).find('div#querySuggestion');
783        if (querySuggestionDiv.length > 0) {
784            $("[data-ajax_target='querySuggestion']").html(querySuggestionDiv.html());
785        }
786
787        this.hasMore = hasMore !== undefined && hasMore.trim() === "true";
788
789        if (this.hasMore) {
790            this.loadShowMoreProductsButton();
791        }
792
793        gca.loadProductData();
794        gca.loadFavoritesStuff();
795        gca.executeCustomFunctions();
796        gca.pushProductListsToGoogle();
797    }
798
799    showFacets(data) {
800        $("[data-target='search-facets']").html(data);
801        this.initRangeSlider();
802    }
803
804    showMoreProducts() {
805        let self = this;
806        let now = new Date().getTime();
807        if (now - self.lastShowMore > 500 && self.hasMore) {
808            self.pageSize = self.pageSize + self.initialPageSize;
809            let facetGroups = self.getFacetGroups();
810            if (self.dispatcherType === "search") {
811                self.doSearch(facetGroups);
812            }
813            else {
814                self.doFilter(facetGroups);
815            }
816            self.lastShowMore = now;
817        }
818
819        gca.updateURLParameterWithoutReload("size", self.pageSize);
820    }
821
822    setupKeyHandlerForIncrementalResults(input) {
823        let self     = this;
824        let target   = input.getAttribute("data-content-target");
825        let targetEl = this.getIncrementalResultsContainer(target);
826
827        document.addEventListener("keydown", handleKeys);
828
829        function handleKeys(e) {
830            if (input.isEqualNode(e.target)) {
831                const $selectedEl = $(targetEl).find(".selected");
832
833                if (e.code === "ArrowDown") {
834                    if ($selectedEl !== undefined && $selectedEl.length > 0) {
835                        $selectedEl.removeClass("selected");
836                        $selectedEl.next().addClass("selected");
837                    }
838                    else {
839                        const $selectEl = $(targetEl).find(".incremental-product-container").first();
840                        $selectEl.addClass("selected");
841                    }
842                }
843                else if (e.code === "ArrowUp") {
844                    if ($selectedEl !== undefined && $selectedEl.length > 0) {
845                        $selectedEl.removeClass("selected");
846                        $selectedEl.prev().addClass("selected");
847                    }
848                    else {
849                        const $selectEl = $(targetEl).find(".incremental-product-container").last();
850                        $selectEl.addClass("selected");
851                    }
852                }
853                else if (e.code === "Enter") {
854                    e.preventDefault();
855                    if ($selectedEl !== undefined && $selectedEl.length > 0) {
856                        location.href = $selectedEl.find("a").attr("href");
857                    }
858                }
859                else if (e.code === "Escape") {
860                    self.hideIncrementalResults(target);
861                }
862            }
863        }
864    }
865
866    renderIncrementalResults(data, target) {
867        const el = this.getIncrementalResultsContainer(target);
868        el.innerHTML = data;
869        this.showIncrementalResults(target);
870    }
871
872    showIncrementalResults(target) {
873        const el = this.getIncrementalResultsContainer(target);
874        el.style.display = "block";
875    }
876
877    hideIncrementalResults(target) {
878        const el = this.getIncrementalResultsContainer(target);
879        if(el) el.style.display = "none";
880    }
881
882    getIncrementalResultsContainer(target) {
883        return document.querySelectorAll("[data-content='" + target + "']")[0];
884    }
885
886    isIncrementalResultsVisible(target) {
887        const el = this.getIncrementalResultsContainer(target);
888        return el.style.display !== "none";
889    }
890
891    setupIncrementalSearchEvent() {
892        let self     = this;
893        const inputs = document.querySelectorAll("[data-action='incremental-search']");
894
895        if (inputs !== undefined && inputs.length > 0) {
896            for (const input of inputs) {
897                let target = input.getAttribute("data-content-target");
898
899                const process = (e) => {
900                    if (e.code === "Enter") {
901                        input.parentElement.parentElement.submit();
902                    }
903                    else if (e.code !== "Escape" && e.code.indexOf("Arrow") < 0) {
904                        self.doIncrementalSearch(input.value, target);
905                    }
906                };
907
908                const handler  = gca.debounced(500, process);
909                const targetEl = this.getIncrementalResultsContainer(target);
910
911                input.addEventListener("keyup", handler);
912                input.onclick = function() {
913                    self.showIncrementalResults(target);
914                };
915                this.setupKeyHandlerForIncrementalResults(input);
916
917                document.addEventListener("click", (event) => {
918                    if (self.isIncrementalResultsVisible(target)) {
919                        const withinBoundaries = event.composedPath().includes(targetEl) || event.composedPath().includes(input);
920                        if (!withinBoundaries) {
921                            self.hideIncrementalResults(target);
922                        }
923                    }
924                });
925            }
926        }
927    }
928
929    setupFilterEvents() {
930        let self = this;
931        let container = $("[data-target='search-facets']");
932
933        const queryString = window.location.search;
934        const urlParams = new URLSearchParams(queryString);
935        const urlParamsSize = new Set(urlParams.keys()).size;
936        if (urlParamsSize > 1 && urlParams.has("filtered") && urlParams.get("filtered") === "true" && self.dispatcherType !== "search") {
937            gca.showPageLoader();
938
939            gca.waitForElement("[data-action='search-filter']").then((elm) => {
940                let foundFilterParams = false;
941                const allButtons = document.querySelectorAll("[data-facet-group]");
942                allButtons.forEach(function (el) {
943                    const group = el.getAttribute("data-facet-group").toLowerCase();
944                    const type  = el.getAttribute("data-facet-group-type");
945                    let value   = null;
946
947                    try {
948                        value = el.getAttribute("data-facet-value").toLowerCase()
949                    }
950                    catch (e) {
951                        console.log(e);
952                    }
953
954                    if (urlParams.has(group)) {
955                        const groupValues = urlParams.getAll(group);
956                        if (type === "range") {
957                            const rawVal   = groupValues[0];
958                            const gteVal   = rawVal.split(":")[0];
959                            const lteVal   = rawVal.split(":")[1];
960                            // const postfix  = rawVal.split(":")[2]; Not in use here, but indicator for range type range (is in use for search filter url params to interpret range type)
961
962                            const rangeInput = document.querySelector("[data-facet-group='" + group + "']");
963                            const gteInput = rangeInput.querySelector("input[data-facet-group-range-type='gte']");
964                            const lteInput = rangeInput.querySelector("input[data-facet-group-range-type='lte']");
965                            rangeInput.setAttribute("data-facet-checked", "true");
966
967                            gteInput.value = gteVal;
968                            lteInput.value  = lteVal;
969                            foundFilterParams = true;
970                        }
971                        else {
972                            if (self._containsParamValue(groupValues, value)) {
973                                el.setAttribute("data-facet-checked", "true");
974                                foundFilterParams = true;
975                            }
976                        }
977                    }
978                })
979
980                if (foundFilterParams) {
981                    let facetGroups = self.getFacetGroups();
982                    self.doFilter(facetGroups);
983                }
984            });
985        }
986
987        container.on("click", "[data-action='search-filter']", function() {
988            let facetGroups = self.getFacetGroups()
989            if ($(this).data('facet-checked')) {
990                $(this).attr('data-facet-checked', false);
991            }
992            else {
993                $(this).attr('data-facet-checked', true);
994            }
995            if (self.dispatcherType === "search") {
996                self.doSearch(facetGroups);
997            }
998            else {
999                self.doFilter(facetGroups);
1000            }
1001        });
1002
1003        container.on("click", "[data-action='do-filter-range']", function(e) {
1004            $(this).closest("[data-facet-group]").attr('data-facet-checked', true);
1005            if (self.dispatcherType === "search") {
1006                self.doSearch(self.getFacetGroups());
1007            }
1008            else {
1009                self.doFilter(self.getFacetGroups());
1010            }
1011        });
1012
1013        container.on("click", "[data-action='do-filter']", function(e) {
1014            let facetGroups = self.getFacetGroups();
1015            self.doFilter(facetGroups, true)
1016        });
1017
1018        container.on("click", "[data-action='uncheck-filter']", function() {
1019            let group = $(this).attr('data-facet-group');
1020            let value = $(this).attr('data-facet-value');
1021            container.find("[data-action='search-filter'][data-facet-group='" + group + "'][data-facet-value='" + value + "']").click();
1022
1023            if (value === "") {
1024                const rangeGroup = container.find("[data-facet-group='" + group + "'][data-facet-group-type='range']");
1025                if (rangeGroup !== undefined) {
1026                    rangeGroup.attr('data-facet-checked', false);
1027                    if (self.dispatcherType === "search") {
1028                        self.doSearch(self.getFacetGroups());
1029                    }
1030                    else {
1031                        self.doFilter(self.getFacetGroups());
1032                    }
1033                }
1034            }
1035        });
1036
1037        container.on("click", "[data-action='offcanvas-search-filter']", function() {
1038            const el = $(this);
1039            if (el.attr('data-offcanvas-facet-checked') === "true") {
1040                el.find("i").addClass("fa-square").removeClass("fa-check-square");
1041                el.attr('data-offcanvas-facet-checked', false);
1042            }
1043            else {
1044                el.attr('data-offcanvas-facet-checked', true);
1045                el.find("i").addClass("fa-check-square").removeClass("fa-square");
1046            }
1047            let facetGroups = self.getOffcanvasFacetGroups();
1048            self.updateOffCanvasFilter(facetGroups);
1049        });
1050
1051        container.on("click", "[data-action='offcanvas-do-filter-range']", function(e) {
1052            $(this).closest("[data-offcanvas-facet-group]").attr('data-offcanvas-facet-checked', true);
1053            let facetGroups = self.getOffcanvasFacetGroups();
1054            self.doFilter(facetGroups, true)
1055        });
1056
1057        container.on("click", "[data-action='offcanvas-clear-filter']", function() {
1058            jQuery.each($("[data-offcanvas-facet-group]"), function(i, el) {
1059                el = $(el);
1060                el.find("i").addClass("fa-square").removeClass("fa-check-square");
1061                el.attr('data-offcanvas-facet-checked', false);
1062            });
1063            let facetGroups = self.getOffcanvasFacetGroups();
1064            self.doFilter(facetGroups, true)
1065        });
1066
1067        container.on("click", "[data-facet-group-range-type='gte'], [data-facet-group-range-type='lte'], [data-offcanvas-facet-group-range-type='gte'], [data-offcanvas-facet-group-range-type='lte'] ", function() {
1068            $(this).select();
1069        });
1070
1071        container.on("click", "[data-action='clear-all-filters']", function() {
1072            const { origin, pathname } = location;
1073            location.href = origin + pathname;
1074        });
1075    }
1076
1077    setupTabEvent() {
1078        let self = this;
1079        $(document).on("click", "a[role=tab]", function() {
1080            self.tab = $(this).attr("href").replace("#tab-","");
1081            gca.updateURLParameterWithoutReload("tab", self.tab);
1082        });
1083    }
1084
1085    setupShowMoreEvent() {
1086        const self = this;
1087        $("[data-target='search-results']").on("click", "[data-action='showMoreProducts'][data-context='search']", function() {
1088            self.paginationEnabled = true;
1089            self.showMoreProducts();
1090            $(this).hide();
1091        })
1092    }
1093
1094    setupScrollEvent() {
1095        let self = this;
1096        $(window).scroll(function() {
1097            let currentPosition = $(window).scrollTop();
1098            self.position = currentPosition;
1099
1100            if (self.paginationEnabled && (currentPosition + $(window).height() >
1100 $(document).height() - $('section#footer').height())) {
1101                let productTab = $("#tab-products");
1102                if (productTab.length === 0 || productTab.hasClass("active")) {
1103                    self.showMoreProducts();
1104                }
1105            }
1106        });
1107    }
1108
1109    toggleBreadcrumbs(event) {
1110        let self = $(event);
1111        let icon = self.children(".toggle-breadcrumb");
1112
1113        if (icon.hasClass('fa-angle-down')) {
1114            icon.removeClass('fa-angle-down').addClass('fa-angle-up')
1115        } else {
1116            icon.removeClass('fa-angle-up').addClass('fa-angle-down')
1117        }
1118
1119        self.closest(".category-paths").find('.hidden-breadcrumb').slideToggle();
1120    }
1121
1122    filterFacetSearch(self) {
1123        const facets = $(self).closest("ul.dropdown-menu").children("li.facet-group-type-terms");
1124
1125        facets.each(function () {
1126            let facet = $(this);
1127            facet.text().toLowerCase().includes(self.value.toLowerCase()) ? facet.show() : facet.hide();
1128        });
1129    }
1130
1131    filterFacetShowMore(event) {
1132        let self = $(event);
1133        let label = self.children("span");
1134        let icon = self.children("i");
1135        const facets = $(self).closest("ul.dropdown-menu").children("li.facet-group-type-terms");
1136
1137        if (icon.hasClass('fa-angle-down')) {
1138            icon.removeClass('fa-angle-down').addClass('fa-angle-up')
1139            label.text(self.data("title-see-less"))
1140        } else {
1141            icon.removeClass('fa-angle-up').addClass('fa-angle-down')
1142            label.text(self.data("title-see-more"))
1143        }
1144
1145        facets.toggleClass("d-flex");
1146    }
1147
1148    initRangeSlider() {
1149        gca.waitForElement("[data-facet-group-type='range']").then(() => gcaRangeSlider.init());
1150    }
1151
1152}
1153
1154
1155class GcaRangeSlider {
1156    _observer;
1157    _ranges;
1158    constructor() {
1159        this._observer = null;
1160        this._ranges = new Map();
1161    }
1162
1163    init() {
1164        // init any facets currently in the DOM
1165        this.initAll();
1166
1167        // Watch the search-facet template for swaps (AJAX re-renders)
1168        this.observeFacetsRoot();
1169
1170        // Hydrate again on BFCache/back/forward restores
1171        // window.addEventListener('pageshow', () => this.initAll());
1172    }
1173
1174    observeFacetsRoot() {
1175        // Container/element that gets updated from filtering
1176        const root = document.querySelector('.search-facet-groups');
1177        if (!root) return;
1178
1179        // Avoid duplicate observers
1180        if (this._observer) this._observer.disconnect();
1181
1182        this._observer = new MutationObserver(muts => {
1183            for (const m of muts) {
1184                m.addedNodes.forEach(n => {
1185                    if (n.nodeType !== 1) return;  // elements only
1186                    if (n.matches(".collapse-search-facets")) {
1187                        this.initAll(n);               // <- scans n for facets and calls initOne()
1188                    }
1189                });
1190            }
1191        });
1192
1193        this._observer.observe(root, { childList: true, subtree: true });
1194    }
1195
1196    // Find range-facet under a given DOM root - either whole page or under certain div
1197    initAll(ctx = document) {
1198        ctx.querySelectorAll("li[data-facet-group][data-facet-group-type='range']")
1199            .forEach(li => this.initOne(li));
1200    }
1201
1202    initOne(li) {
1203        if (li.dataset.rangeInitialized === "1") return;
1204        li.dataset.rangeInitialized = "1";
1205
1206        const minNum   = li.querySelector('input[data-facet-group-range-type="gte"]');
1207        const maxNum   = li.querySelector('input[data-facet-group-range-type="lte"]');
1208        const applyBtn = li.querySelector('[data-action="do-filter-range"]');
1209        const root     = li.querySelector('.vl-facet-range');
1210        if (!minNum || !maxNum || !root) return;
1211
1212        const minR = root.querySelector('.vl-range-min');
1213        const maxR = root.querySelector('.vl-range-max');
1214        const progress = root.querySelector('.vl-progress');
1215
1216        // ----- keep a stable absolute domain across re-renders -----
1217        const facetKey = li.getAttribute('data-facet-group');
1218
1219        // Set constant MIN and MAX price range for dots to be placed correctly in relation to this values
1220        let ABS_MIN = parseFloat(li.getAttribute('data-abs-min'));
1221        let ABS_MAX = parseFloat(li.getAttribute('data-abs-max'));
1222        let range = this._ranges.get(facetKey);
1223        if(range === undefined) {
1224            this._ranges.set(facetKey, new GcaRange(ABS_MIN, ABS_MAX));
1225        }
1226        else {
1227            ABS_MIN = range.min;
1228            ABS_MAX = range.max;
1229        }
1230
1231        // FORCE the inputs to the absolute domain so thumbs paint correctly
1232        minR.min = maxR.min = ABS_MIN;
1233        minR.max = maxR.max = ABS_MAX;
1234
1235        // Maths used on multiple functions
1236        // In case of STEPS being implemented - default is 1
1237        // GAP to avoid overlapping dot overlapping can be changed here
1238        const MIN  = ABS_MIN;
1239        const MAX  = ABS_MAX;
1240        const STEP = parseFloat(minR.step || maxR.step || 1);
1241        const GAP  = STEP;
1242
1243        const clamp = (v, lo, hi) => Math.min(Math.max(v, lo), hi);
1244        const pct   = v => ((v - MIN) / (MAX - MIN)) * 100;
1245        const roundToStep = v => Math.round(v / STEP) * STEP;
1246
1247        // Paint colored part of the slider
1248        const paint = () => {
1249            const l = pct(+minR.value);
1250            const r = pct(+maxR.value);
1251            const w = Math.max(0, r - l);
1252            progress.style.left  = l + '%';
1253            progress.style.width = w + '%'; // CSS must be left+width (no right)
1254        };
1255
1256        const syncFromRanges = (e) => {
1257            const fromMin = e && e.target === minR;
1258            let lo = +minR.value, hi = +maxR.value;
1259
1260            lo = roundToStep(lo);
1261            hi = roundToStep(hi);
1262
1263            // Dot gap
1264            if (hi - lo < GAP)  {
1265                if (fromMin) {
1266                    lo = hi - GAP;
1267                } else {
1268                    hi = lo + GAP;
1269                }
1270            }
1271
1272            // Avoid overlapping
1273            lo = clamp(lo, MIN, MAX - GAP);
1274            hi = clamp(hi, MIN + GAP, MAX);
1275
1276            minR.value = lo; maxR.value = hi;
1277            minNum.value = lo; maxNum.value = hi;
1278            paint();
1279        };
1280
1281        const syncFromNumbers = () => {
1282            let lo = parseFloat(minNum.value), hi = parseFloat(maxNum.value);
1283            if (isNaN(lo)) lo = MIN; if (isNaN(hi)) hi = MAX;
1284
1285            lo = roundToStep(lo); hi = roundToStep(hi);
1286            if (hi - lo < GAP) lo = hi - GAP;
1287
1288            lo = clamp(lo, MIN, MAX - GAP);
1289            hi = clamp(hi, MIN + GAP, MAX);
1290
1291            minR.value = lo; maxR.value = hi;
1292            minNum.value = lo; maxNum.value = hi;
1293            paint();
1294        };
1295
1296        const hydrateFromQuery = () => {
1297            const params = new URLSearchParams(location.search);
1298            const raw = params.get(facetKey);
1299
1300            if (!raw) {
1301                syncFromNumbers();
1302                return;
1303            }
1304
1305            const [a,b] = String(raw).split(':');
1306            const lo = parseFloat(a), hi = parseFloat(b);
1307
1308            if (Number.isFinite(lo) && Number.isFinite(hi)) {
1309                minNum.value = lo; maxNum.value = hi;
1310            }
1311            syncFromNumbers(); // normalize & paint
1312        };
1313
1314        // events
1315        minR.addEventListener('input',  e => syncFromRanges(e));
1316        maxR.addEventListener('input',  e => syncFromRanges(e));
1317        minNum.addEventListener('change', syncFromNumbers);
1318        maxNum.addEventListener('change', syncFromNumbers);
1319
1320        if (applyBtn) {
1321            // before AJAX filter runs, push last thumb values into number inputs
1322            applyBtn.addEventListener('click', () => {
1323                minNum.value = minR.value;
1324                maxNum.value = maxR.value;
1325            });
1326        }
1327
1328        // first paint
1329        hydrateFromQuery();
1330        paint();
1331        requestAnimationFrame(paint); // covers "inserted while collapsed" cases
1332    }
1333}
1334const gcaRangeSlider = new GcaRangeSlider();
1335

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.