PageSourceSearch

https://www.tibnor.dk/_ui/tibnor/common/js/acc.refinements.js

js tibnor.dk collected 2026-09-30 06:04:48 UTC 25,770 bytes, 659 lines download raw bytes

1ACC.refinements = {
2
3    _autoload: [
4        "handlebarsHelpers",  
5        "handleHandlebarsFacets",
6        "bindProdListStockInfo",
7        "bindGeneral",
8        ["bindMoreLessToggles", $(".js-handlebars-facets").length !== 0]
9    ],
10
11    currentProdList: [],
12    mobileOpenFacet: [],
13
14    bindGeneral: function() {
15        localStorage.removeItem('fullFacetsList');
16    },
17
18    bindProdListStockInfo: function () {
19        if ($(".js-product-stock").length) {
20            $(".js-product-stock").each(function () {
21                var code = $(this).data("code");
22                if(code) {
23                    ACC.refinements.currentProdList.push(code.toString());
24                }
25            });
26            ACC.refinements.getProdStocks();
27        }
28    },
29
30    getProdStocks: function (sku) {
31        var codes = sku || ACC.refinements.currentProdList.join(",");
32        if(codes && ACC.userLogged) {
33            $.ajax({
34                type: "GET",
35                url: ACC.config.encodedContextPath + "/rest/products/product/" + codes + "/availability",
36                contentType: "application/json",
37                headers: {
38                    CSRFToken: ACC.config.CSRFToken,
39                },
40                success: function (data) {
41                    $(".js-product-stock:empty").each(function () {
42                        var code = $(this).data("code");
43
44                        if(code && !sku || !code && sku ) {
45                            var codeOrSku = sku || code.toString();
46                            var stock = data.find(function (item) {
47                                return item.product.code === codeOrSku;
48                            })?.product.stock.stockLevelStatus.code;
49
50                            if(stock) {
51                                // outOfStock | lowStock | inStock
52                                var out = stock === "outOfStock";
53                                var typeVal = out ? "excl" : "tick";
54                                var colorType = out ? "blue" : stock === "inStock" ? "green" : "yellow";
55
56                                $(this)
57                                    .text(ACC.text[stock])
58                                    .prepend(ACC.global.getSvgStockInfo(typeVal, colorType))
59                                    .removeClass("hidden");
60                            }
61                        }
62                    });
63                },
64                error: function (data) {
65                    ACC.navigation.handleSessionError(data, 400);
66                }
67            });
68        }
69    },
70
71    bindMoreLessToggles: function () {
72        $(document).on("click", ".js-more-handlebars-facets", function (e) {
73            e.preventDefault();
74
75            var container = $(this).parents(".js-facet");
76            container.find(".js-facet-list li:hidden").show().attr('data-more', 'open');
77            $(this).parent().hide().next().show();
78        });
79        $(document).on("click", ".js-less-handlebars-facets", function (e) {
80            e.preventDefault();
81
82            var container = $(this).parents(".js-facet");
83            container.find(".js-facet-list li[data-more]").hide().removeAttr('data-more');
84            $(this).parent().hide().prev().show();
85
86            var fromTop = container.offset().top - $('.js-mainHeader').height();
87            $('html, body').animate({scrollTop: fromTop}, 100);
88        });
89
90    },
91
92    handlebarsFacetsMethods: {
93        handleLocalStocked: (data) => {
94            const localStocked = data.facets?.find(({ code }) => code === 'inLocalStorage');
95            const localStockedBtn = $('body').find('.js-plp-local-stocked-btn');
96            localStockedBtn.addClass('hidden');
97
98            if(!localStocked) {
99                return
100            }
101
102            const trueVal = localStocked.values.find(({ code }) => code === 'true');
103            if(!trueVal) {
104                return
105            }
106            
107            localStockedBtn
108              .attr("data-value", trueVal.query.value)
109              .attr("data-url", trueVal.query.url)
110              .removeClass("hidden")
111              .prev()
112              .prop("checked", trueVal?.selected);
113        },
114        handleFacets: (data, id) => {
115            const d = {
116              ...data,
117              facets: data.facets
118                ?.filter(
119                  (f) =>
120                    (f.slider && f.values.length > 1) ||
121                    ["type", "grade", "surface", "lowCO2Impact"].includes(f.code)
122                )
123                .sort((a, b) => {
124                  const nameA = a.name.toLowerCase();
125                  const nameB = b.name.toLowerCase();
126                  const aS = a.slider;
127                  const bS = b.slider;
128                  const eq = (aS || bS) && bS - aS;
129                  return eq !== 0
130                    ? eq
131                    : nameA > nameB
132                    ? 1
133                    : nameB > nameA
134                    ? -1
135                    : 0;
136                })
137                .map((f) => {
138                  const topVal = f.values.splice(0, ACC.config.facetTopValues);
139                  const values = [
140                    ...topVal,
141                    ...f.values.sort((a, b) => {
142                      const nameA = a.name.toLowerCase();
143                      const nameB = b.name.toLowerCase();
144                      const aS = a.selected;
145                      const bS = b.selected;
146                      return f.slider
147                        ? 0
148                        : aS || bS
149                        ? bS - aS
150                        : nameA > nameB
151                        ? 1
152                        : nameB > nameA
153                        ? -1
154                        : 0;
155                    }),
156                  ];
157                  return { ...f, values };
158                }),
159            };
160            const { handlebarsTpl, handleLocalStocked } = ACC.refinements.handlebarsFacetsMethods;
161
162            handlebarsTpl(d, 'facets');
163            
164            d.facets = data.facets.filter(f => f.code === 'category');
165            handlebarsTpl(d, 'facetsCat');
166            handleLocalStocked(data);
167
168            const val = $("body").find('[data-facet-id="' + id + '"] .js-facet-values');
169            if (window.innerWidth >= 1024) {
170              val.removeClass("hidden").prev().addClass("open");
171            }
172        },
173        handlebarsTpl: (d, id, nextPage) => {
174            const tpl = $('#' + id + 'Handlebars').html()
175            const template = Handlebars.compile(tpl);
176            const { displayPrices, showPrices, userLogged } = ACC;
177            let data = {
178              ...d,
179              displayPrices,
180              showPrices,
181              userLogged
182            };
183            const facetObj = template({data: data});
184
185            if(nextPage) {
186                $(facetObj).insertBefore('.js-handlebars-newProducts');
187            } else {
188                $('.js-handlebars-' + id).html(facetObj);
189            }
190            if(id === 'facets') {
191                ACC.refinements.sliderRange(data.facets, data.breadcrumbs);
192            }
193            if(id === 'products') {
194                ACC.product.handleListAttrsLength();
195            }
196        },
197
198        handlebarsAjaxCall: (url, callback, callbackError, trackResults = true, again) => {
199            if(url) {
200                if(url.includes('/search/')) {
201                    url = url.replace('?text', '?q');
202                }
203
204                if (url.includes("/facets")) {
205                    if (localStorage.getItem("fullFacetsList")) {
206                        $(".js-handlebars-facets")
207                          .addClass("progressing")
208                          .prepend("<div class='loader js-loader'></div>");
209                    } else {
210                      if (!url.includes("/search/") && url.includes("q=")) {
211                        const cleanUrl = url.slice(0, url.indexOf("q="));
212
213                        ACC.refinements.handlebarsFacetsMethods.handlebarsAjaxCall(
214                          cleanUrl,
215                          callback,
216                          callbackError,
217                          trackResults,
218                          url
219                        );
220                        return;
221                      }
222                    } 
223                }
224
225                $.ajax({
226                    'type': 'GET',
227                    'url': url,
228                    'contentType': 'application/json',
229                    'headers': {
230                        'CSRFToken': ACC.config.CSRFToken
231                    },
232                    success: function (data) {
233                        // for facets call
234                        if (url.includes('/facets')) {
235                            if (!(url.includes('q='))) {
236                                localStorage.setItem('fullFacetsList', JSON.stringify(data));
237                            }
238                            if(again) {
239                                ACC.refinements.handlebarsFacetsMethods.handlebarsAjaxCall(
240                                    again,
241                                    callback,
242                                    callbackError,
243                                    trackResults
244                                );
245                                return;
246                            }
247                        }
248
249                        if (typeof callback == 'function')
250                            callback(data);
251
252                        // for results
253                        if (url.includes('/results')) {
254                            ACC.refinements.currentProdList = [];
255                            data.results.forEach(function(r){
256                                ACC.refinements.currentProdList.push(r.code)
257                            })
258                            if (ACC.userLogged && ACC.config.searchMode === 'variant') {
259                                ACC.refinements.getProdStocks();
260                            }
261                            if (trackResults) {
262                                ACC.track.trackProductsListGA4(data, location.search)
263                            }
264
265                            const { sort, originalQuery } = data.pagination;
266                            ACC.refinements.setSortSelect(originalQuery);
267
268                            if(originalQuery) {
269                                $('.js-handlebars-clean-all-facets').attr('data-sort', sort);
270                            }
271                            ACC.refinements.handlebarsFacetsMethods.handleCleanAllBtn();
272                        }
273
274                        if (url.includes('/facets')) {
275                            if (window.innerWidth < 1024) {
276                                $('.js-facet-values').removeClass('hidden')
277                                $('.js-facet').each(function(){
278                                    const facetId = $(this).data('facetId');
279
280                                    if(ACC.refinements.mobileOpenFacet.find(id => id === facetId)){
281                                        $(this).addClass('active');
282                                    }
283                                })
284                                ACC.refinements.mobileOpenFacet = [];
285                            }
286                        }
287                    },
288                    error: function () {
289                        if (typeof callbackError == 'function') 
290                            callbackError();
291                    },
292                    complete: function() {
293                        $(".js-handlebars-facets")
294                            .removeClass("progressing")
295                            .find("js-loader")
296                            .remove();
297                    }
298                });
299            }
300        },
301
302        handleCleanAllBtn: () => {
303            const btn = $('.js-handlebars-clean-all-facets');
304            const sorts = ACC.refinements.getSortSelect();
305            const isCampaign = sorts.includes(ACC.variables.CAMPAIGN_STRING);
306            const params = sorts.split(':')
307
308            customToggleClass(btn, 'hidden', !(params.length > (isCampaign ? 4 : 2)))
309        },
310
311        buildUrl: (dest) => {
312            const search = location.search.replace('?text=', '?q=')
313            return location.pathname + dest + search;
314        },
315
316        updateCallUrl: (url, dest) => {
317            return url?.replace('?q=', dest + '?q=');
318        }
319    },
320
321    handleHandlebarsFacets: function(){
322        const {
323          handlebarsAjaxCall,
324          handlebarsTpl,
325          updateCallUrl,
326          handleCleanAllBtn,
327          buildUrl,
328          handleFacets,
329        } = ACC.refinements.handlebarsFacetsMethods;
330        
331        if($(".js-handlebars-facets").length !== 0) {
332            handlebarsAjaxCall(buildUrl('/facets'), function (data) {
333                handleFacets(data);
334                handleCleanAllBtn();
335            })
336        }
337        
338        //click on facet
339        $('body').on("click", ".js-handlebars-facet", function (e) {
340            const dataUrl = $(this).attr('data-url');
341            const parentId = $(this).parents('.js-facet').data('facetId')
342
343            if (window.innerWidth < 1024) {
344                $('.js-facet').each(function(){
345                    if($(this).hasClass('active')){
346                        ACC.refinements.mobileOpenFacet.push($(this).data('facetId'))
347                    }
348                })
349            }
350
351            handlebarsAjaxCall(
352                updateCallUrl(dataUrl, '/facets'), 
353                function (data) {
354                    handleFacets(data, parentId);
355                    for (const [type, count] of Object.entries(data.itemsFoundPerSearchItemType)) {
356                        handlebarsTpl({count: count}, 'total-results-' + type);
357                    }
358                }
359            );
360
361            handlebarsAjaxCall(updateCallUrl(dataUrl, '/results'), function (data) {
362                history.pushState(null, null, dataUrl);
363                handlebarsTpl(data, 'products');
364                handlebarsTpl(data, 'pagination');
365            });
366        });
367
368        $('.js-handlebars-pagination').on("click", ".js-pagination-link", function (e) {
369            e.preventDefault();
370            var urlTarget = $(this).attr('href');
371
372            handlebarsAjaxCall(urlTarget.replace('?','/results?'), function (data) {
373                history.pushState(null, null, urlTarget);
374                handlebarsTpl(data, 'products');
375                handlebarsTpl(data, 'pagination');
376                
377                var fromTop = $('.js-mainHeader').height() + 110;
378                $('html, body').animate({scrollTop: fromTop}, 300);
379            });
380        });
381
382        document.addEventListener("click", function (e) {
383            if($('.js-product-facet').length && !$(e.target).hasClass('open')) {
384                if(!$(e.target).parents('.js-facet').length) {
385                    $('body').find('.js-facet-select.open').removeClass('open').next().addClass('hidden');
386                }
387            }
388        }, true);
389
390        $(document).on("click", ".js-facet-select", function () {
391            const clicked = $(this)
392            $('.js-facet-select').each(function() {
393                if($(this)[0] === clicked[0])
394                    $(this).toggleClass('open').next().toggleClass('hidden');
395                else
396                    $(this).removeClass('open').next().addClass('hidden');
397            })
398        });
399        
400        $(document).on("click", ".js-handlebars-clean-all-facets", function (e) {
401            e.preventDefault();
402            const dataSort = $(this).data('sort');
403
404            function urlClear(url) {
405                let dest = (url || '') + '?q=';
406                const isSearch = location.pathname.includes('/search');
407
408                if(isSearch) {
409                    const qParams = ACC.refinements.getSortSelect().split(':');
410                    dest += qParams[0]
411                } else {
412                    dest += ACC.variables.URL_SEPARATOR + dataSort;
413                }
414
415                return location.pathname + dest;
416            }
417
418            handlebarsAjaxCall(urlClear('/facets'), function (data) {
419                handleFacets(data);
420                for (const [type, count] of Object.entries(data.itemsFoundPerSearchItemType)) {
421                    handlebarsTpl({count: count}, 'total-results-' + type);
422                }
423            });
424
425            handlebarsAjaxCall(urlClear('/results'), function (data) {
426                history.pushState(null, null, urlClear());
427                handlebarsTpl(data, 'products');
428                handlebarsTpl(data, 'pagination');
429            });
430        });
431    },
432
433    handlebarsHelpers: function(){
434        Handlebars.registerHelper({
435            inc: function(v1, v2) { return v1 + v2},
436            dec: function(v1, v2) { return  v1 - v2},
437            eq: function(v1, v2) { return  v1 === v2},
438            ne: function(v1, v2) { return  v1 !== v2},
439            lt: function(v1, v2) { return  v1 < v2},
440            gt: function(v1, v2) { return  v1 > v2},
441            not: function(v) { return !v},
442            co2Format: function (v) { return Number(v).toFixed(3)},
443            idFacetCleaner: function (id) {
444                const re = /[\W_]/g;
445                return id.replace(re, '');
446            },
447            getPrimaryImageUrl: function (images, format) {
448                return (
449                  images?.find(
450                    (i) => i.imageType === "PRIMARY" && i.format === format
451                  )?.url || undefined
452                );
453            },
454            paginationStart: function (limit, pagination, halfLimit) {
455                if(limit > pagination.numberOfPages) {
456                    return 1
457                } else if(pagination.currentPage + halfLimit >= limit) {
458                    if(pagination.currentPage + halfLimit < pagination.numberOfPages){
459                        if(pagination.currentPage + 1 - halfLimit > 0){
460                            return pagination.currentPage + 1 - halfLimit
461                        } else {
462                            return 1
463                        }
464                    } else {
465                        return pagination.numberOfPages + 1 - limit
466                    }
467                } else {
468                    return 1
469                }
470            },
471            pagesList: function (limit, start, numOfPages) {
472                var arr = []; 
473                var length  = numOfPages > limit ? limit : numOfPages;
474                for (let i = 0; i < length; i++) {
475                    arr[i] = start + i;
476                }
477                return arr
478            },
479            topVal: function (index, selected) {
480                return !(selected || index < ACC.config.facetTopValues);
481            }, 
482            selectedName: function (arr) {
483                const count = arr.reduce(
484                  (acc, item) => (item.selected ? [...acc, item.name] : acc),
485                  []
486                );
487                return (
488                  (!count.length && ACC.text.selectDefault) ||
489                  (count.length === 1 && count[0]) ||
490                  `${ACC.text.selectSelected} (${count.length})`
491                );
492            }, 
493            showLessMore: function(obj) {
494                return obj.length > ACC.config.facetTopValues
495            },
496            getUrl: function (page) {
497                let { href, search, pathname } = location;
498                const p = "page=";
499                const isPager = href.indexOf(p);
500                const sort = ACC.refinements.getSortSelect();
501
502                if (search) {
503                  if (isPager > -1) {
504                    const filtered = search
505                      .split("&")
506                      .filter((el) => !el.includes(p));
507                    href = pathname + filtered.join("&");
508                  }
509                } else {
510                  href += `?q=${ACC.variables.URL_SEPARATOR}${sort}`;
511                }
512
513                return `${href}&${p}${page}`;
514            },
515            trimString: function (str) {
516                return str.length > 25
517                ? new Handlebars.SafeString(str.substring(0, 20)) + "..."
518                : str;
519            },
520            isLoginOnPriceList: (userLogged, displayPrices, showPrices) =>
521              (userLogged || displayPrices) && showPrices
522        });
523    },
524
525    setSortSelect: (val) => {
526        // set the correct value for sorting
527        $('[id*="sortForm"').each(function() {
528            $(this).find('input[name="q"]').val(val);
529        });
530    },
531
532    getSortSelect: () => $('body').find('[id*="sortForm"] input[name="q"]')[0]?.value, 
533
534    sliderRange: function(facets, breadcrumbs){
535        const range = $(".js-facet-range-slider");
536        
537        let apply;
538        const applySlider = (el) => {
539            if(apply) {
540                clearTimeout(apply);
541            }
542            apply = setTimeout(() => {
543                el.parents('.js-facet-range').find(".js-facet-range-btn").trigger('click');
544            }, 1000);
545        }
546
547        function cleanArr( arr, code) {
548            return arr.split(':').filter((a, i, ar) => a!== code && ar[i-1] !== code).join(':')
549        }
550                
551        function createUrl(url, rangeVal, parent, code) {
552            const btn = parent.find(".js-facet-range-btn");
553            const q = `:${code}:${rangeVal[0]}:${code}:${rangeVal[1]}`;
554            const newUrl = (location.pathname + "?q=" + cleanArr(url, code) + q).replaceAll(":", `${ACC.variables.URL_SEPARATOR}`);
555            
556            btn.attr("data-url", newUrl);
557        }
558
559        if (range.length) {
560            const fullFacetsList = JSON.parse(localStorage.getItem('fullFacetsList'));
561            range.each(function () {
562                const code = $(this).attr('data-code'),
563                    parent = $(this).parent();
564
565                parent.removeClass('hidden');
566
567                const valMin = parent.find(".js-val-range-min"),
568                    valMax = parent.find(".js-val-range-max"),
569                    valUnit = parent.parents('.js-facet').find(".js-val-range-unit"),
570
571                    facet = facets.find(function(facet){
572                        return facet.code === code
573                    }),
574                    fullFacet = fullFacetsList?.facets.find(function(facet){
575                        return facet.code === code
576                    }),
577
578                    values = facet.values,
579                    fullValues = fullFacet?.values,
580                    useValues = fullValues || values,
581
582                    min = useValues?.[0].code * 1,
583                    max = useValues[useValues.length - 1].code * 1,
584
585                    selectedValues = breadcrumbs.filter(breadcrumb => breadcrumb.facetCode === code)
586                        .sort((a, b) =>
586 (a.facetValueCode * 1) - (b.facetValueCode * 1)),
587                    minSelected = selectedValues.length ? selectedValues[0].facetValueCode * 1 : values[0].code * 1,
588                    maxSelected = selectedValues.length ? selectedValues[selectedValues.length - 1].facetValueCode * 1 : values[values.length - 1].code * 1,
589
590                    unit = facet.featureUnit.symbol || '',
591                    url = values[0].query.query.value,
592                    
593                    sliderSelected = [minSelected, maxSelected];
594
595                const slider = $(this).slider({
596                    range: true,
597                    min: min,
598                    max: max + 0.0001, // fix for roundings in floats
599                    step: 0.05,
600                    values: sliderSelected,
601                    slide: function (_, ui) {
602                        valMin.val(ui.values[0]);
603                        valMax.val(ui.values[1]);
604                        createUrl(url, ui.values, parent, code);
605
606                        applySlider($(this));
607                    },
608                    create: function () {
609                        createUrl(url, sliderSelected, parent, code);
610                    },
611                });
612
613                valUnit.text(unit);
614                valMin.val($(this).slider("values", 0));
615                valMax.val($(this).slider("values", 1));
616
617                const rangeInput = $(parent).find( '.js-val-range');
618
619                rangeInput.on('change', function(){
620                    var isMin = $(this).hasClass('js-val-range-min');
621                    var val = $(this).val().replace(',', '.');
622                    var minVal = slider.slider("values", 0);
623                    var maxVal = slider.slider("values", 1);
624
625                    if (isMin) {
626                        if (val > maxVal) {
627                            val = maxVal;
628                        }
629                        if (val < min) {
630                            val = min;
631                        }
632                    } else {
633                        if (val < minVal) {
634                            val = minVal;
635                        }
636                        if (val > max) {
637                            val = max;
638                        }
639                    }
640
641                    arr = [
642                        isMin ? val : minVal,
643                        !isMin ? val : maxVal
644                    ];
645
646                    slider.slider("option", "values", arr);
647                    $(this).val(val);
648                    createUrl(url, arr, parent, code);
649                })
650
651                rangeInput.on('keypress blur', (e) => {
652                    if (e.key === "Enter" || e.type === 'blur') {
653                        applySlider($(this));
654                    }
655                })
656            });
657          }
658    }
659};

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.