PageSourceSearch

https://www.retailnews.dk/js/default.js?t=1790252416360

js retailnews.dk collected 2026-09-25 04:07:35 UTC 58,684 bytes, 1,841 lines download raw bytes

1
2function createSeenSet() {
3    return typeof WeakSet !== "undefined" ? new WeakSet() : new Set();
4}
5
6function getStack(err) {
7    if (err && err.stack) {
8        return err.stack;
9    }
10
11    // fallback – force stack
12    try {
13        throw new Error(err && err.message ? err.message : String(err));
14    } catch (e) {
15        return e.stack;
16    }
17}
18
19function serializeError(err, seen) {
20    if (!err) {
21        return "Unknown error";
22    }
23
24    seen = seen || createSeenSet();
25
26    if (typeof err === "object") {
27        if (seen.has(err)) {
28            return "[Circular error reference]";
29        }
30        seen.add(err);
31    }
32
33    let parts = [];
34
35    parts.push("Message: " + (err.message || String(err)));
36
37    parts.push("Stack:\n" + getStack(err));
38
39    if (err.cause) {
40        parts.push("Cause:\n" + serializeError(err.cause, seen));
41    }
42
43    return parts.join("\n\n");
44}
45
46window.logRemotely = function (msg) {
47    try {
48        $.ajax(window.contextPath + "/rest/logs", {
49            method: "post",
50            data: {
51                msg: msg,
52                t: window.startupTimestamp
53            }
54        });
55    } catch (e) {
56        console.log(e);
57    }
58};
59
60window.logErrorRemotely = function (err) {
61    try {
62        let parts = [];
63
64        let msg = serializeError(err);
65        if (!msg) {
66            msg = "Unknown error (serialization failed)";
67        }
68
69        parts.push(msg);
70
71        parts.push("Location:\n" + document.location);
72
73        parts.push("Browser:\n" + navigator.userAgent);
74
75        window.logRemotely(parts.join("\n\n"));
76    } catch (e) {
77        console.log(e);
78    }
79};
80
81// countdown ###################################################################
82function delayWhile(element, wt, event) {
83    var originalText = element.value;
84    var count = 0;
85    var waitText = (!wt) ? 'Sending' : wt;
86    var originalWaitText = waitText;
87    __animateText();
88    function __animateText() {
89        if (count > 3) {
90            count = 0;
91            waitText = originalWaitText + '.';
92        } else {
93            waitText = waitText + '.';
94        }
95        element.value = waitText;
96        if ((event === 'disabled' && element.disabled) ||
97                (event === 'enabled' && !element.disabled) ||
98                (event === 'not_hidden' && element.style.display !== 'none') ||
99                (event === 'hidden' && element.style.display === 'none')) {
100            count++;
101            window.setTimeout(__animateText, 500);
102        } else {
103            window.clearTimeout();
104            element.value = originalText;
105        }
106    }
107}
108/**
109 * Counts down seconds. Once <code>0</code> is reached, a click event is fired
110 * on the <code>b</code> element. While counting down, innerHTML of <code>e</code>
111 * is set with the current number of seconds left. The count down starts with
112 * the innerHTML value of <code>e</code>. If this value isn't a usable number,
113 * this method will fire the click event right away instead.
114 * @param e an element, whos innerHTML value is used as starting point for the
115 * count down and whos innerHTML value is updated each second, while the count
116 * down takes place
117 * @param b an element, whos click event is triggered once the count down reach
118 * <code>0</code> or if an invalid starting point for the count down was detected;
119 * this element probably should be limited to an input button or submit button
120 */
121function countDownToClick(e, b) {
122    var s = e.innerHTML;
123    if (!(s >= 0)) {
124        s = 0;
125    }
126    __countDown();
127    function __countDown() {
128        if (s == 0) {
129            b.click();
130        } else {
131            e.innerHTML = s--;
132            window.setTimeout(__countDown, 1000);
133        }
134    }
135}
136
137/**
138 * Convert an ID to something jQuery understands. This is done by prepending
139 * # and escaping : and . characters.
140 */
141function jq(id) {
142    return "#" + id.replace(/(:|\.)/g, "\\$1");
143}
144
145const mediaWidths = ["xs", "sm", "md", "lg"];
146function mediaWidth() {
147    try {
148        let found;
149        $("#media-width-detection").children().each(function () {
150            if ($(this).css("display") !== "none") {
151                found = $(this);
152                return false;
153            }
154            return true;
155        });
156        return "" + found.attr("class").slice(-2);
157    } catch (e) {
158        console.log(e);
159        return "";
160    }
161}
162function mediaWidthIndex() {
163    return mediaWidths.indexOf(mediaWidth());
164}
165function xs() {
166    return mediaWidth() === "xs";
167}
168function sm() {
169    return mediaWidth() === "sm";
170}
171function md() {
172    return mediaWidth() === "md";
173}
174function lg() {
175    return mediaWidth() === "lg";
176}
177function minXs() {
178    const index = mediaWidthIndex();
179    return index !== -1 && index >= mediaWidths.indexOf("xs");
180}
181function minSm() {
182    const index = mediaWidthIndex();
183    return index !== -1 && index >= mediaWidths.indexOf("sm");
184}
185function minMd() {
186    const index = mediaWidthIndex();
187    return index !== -1 && index >= mediaWidths.indexOf("md");
188}
189function minLg() {
190    const index = mediaWidthIndex();
191    return index !== -1 && index >= mediaWidths.indexOf("lg");
192}
193function maxXs() {
194    const index = mediaWidthIndex();
195    return index !== -1 && index <= mediaWidths.indexOf("xs");
196}
197function maxSm() {
198    const index = mediaWidthIndex();
199    return index !== -1 && index <= mediaWidths.indexOf("sm");
200}
201function maxMd() {
202    const index = mediaWidthIndex();
203    return index !== -1 && index <= mediaWidths.indexOf("md");
204}
205function maxLg() {
206    const index = mediaWidthIndex();
207    return index !== -1 && index <= mediaWidths.indexOf("lg");
208}
209
210var updateMessage = function ($form, $container, message) {
211    var local = message.elementId;
212    var type = local ? "span" : "div";
213    var alertDiv = "<" + type + " class=\"ajax-alert alert alert-" + message.severity + "\" role=\"alert\">" + message.message + "</" + type + ">";
214    if (local) {
215        $form.find("*[name='" + message.elementId + "']").before(alertDiv);
216    } else {
217        ($container && $container.length ? $container : $form).before(alertDiv);
218    }
219};
220var updateMessages = function ($form, $container, messages) {
221    $form.find(".ajax-alert").remove();
222    if (messages) {
223        for (var i = 0; i < messages.length; i++) {
224            updateMessage($form, $container, messages[i]);
225        }
226    }
227};
228var extractFormData = function ($form) {
229    var data = {};
230    $form.find("input[type!='submit']").each(function (i, v) {
231        var name = $(this).attr("name");
232        const inputs = $form.find("input[name="+name+"]");
233        if (inputs.length > 1 || inputs.filter("[data-type=array]").length) {
234            if (!data[name]) {
235                data[name] = [];
236            }
237            if ($(this).attr("type") !== "checkbox" || $(this).prop("checked")) {
238                data[name].push($(this).val());
239            }
240        }
241        else if ($(this).attr("type") === "checkbox") {
242            data[name] = $(this).prop("checked");
243        } else {
244            data[name] = $(this).val();
245        }
246    });
247    return data;
248};
249
250function jwtRequest(ajaxFn) {
251    $.get(window.contextPath + '/rest/tokens', function(data) { ajaxFn($.parseJSON(data).token); });
252}
253
254$.fn.extend({
255    hasAttribute: function (name) {
256        var attr = $(this).attr(name);
257        return typeof attr !== typeof undefined && attr !== false;
258    }
259});
260
261var EventBus = new function () {
262    var listeners = [];
263    return {
264        register: function (name, callback) {
265            var index = listeners.push({
266                name: name,
267                callback: callback
268            });
269            return {
270                unregister: function () {
271                    listeners.splice(index, 1, null);
272                }
273            }
274        },
275        broadcast: function (name) {
276            for (var i = 0; i < listeners.length; i++) {
277                var listener = listeners[i];
278                if (listener !== null && listener.name === name) {
279                    listener.callback();
280                }
281            }
282        }
283    };
284};
285
286function logIn(username, password, remember, onSuccess, onFailure) {
287    var req = {
288        url: window.contextPath + "/rest/persons/login",
289        method: "POST",
290        cache: false,
291        data: {
292            username: username,
293            password: password,
294            remember: remember
295        }
296    };
297
298    $.ajax(req)
299        .done(function (data, textStatus, jqXHR) {
300            var redirect = jqXHR.getResponseHeader("X-REDIRECT");
301            if (redirect) {
302                location.assign(redirect);
303            } else {
304                location.reload();
305            }
306            if (onSuccess) {
307                onSuccess();
308            }
309        })
310        .fail(function (jqXHR) {
311            if (onFailure) {
312                onFailure(JSON.parse(jqXHR.getResponseHeader("X-MESSAGES")));
313            }
314        });
315}
316
317$.fn.extend({
318    collapsibleFactbox: function () {
319        const elem = $(this);
320        elem.css("display", "block");
321        const getElemHeight = () => {
322            let elemHeight = 0;
323            elem.children().each(function () {
324                elemHeight += $(this).outerHeight(true);
325            });
326            return elemHeight;
327        }
328        if (getElemHeight() > 240) {
329            const icon = $("<i class=\"fa-light fa-angle-down\"></i>");
330            const button = $("<a href=\"#\" class=\"toggle-btn\"></a>");
331            button.append(icon);
332            elem.append(button);
333            const getButtonHeight = () => {
334                return button.outerHeight(true);
335            }
336            const resizeEvent = function () {
337                // Need to wait for animation to finish and fire multiple times to adjust content as the animation plays.
338                for (var time = 50; time <= 300; time += 50) {
339                    setTimeout(fireWindowResizeEvent, time);
340                }
341            };
342            const open = function () {
343                elem.removeClass("collapsed").addClass("uncollapsed");
344                elem.css("max-height", Math.ceil(1.1 * (getElemHeight() + getButtonHeight())) + "px");
345                icon.removeClass("fa-angle-down").addClass("fa-angle-up");
346                resizeEvent();
347            };
348            const close = function () {
349                elem.removeClass("uncollapsed").addClass("collapsed");
350                elem.css("max-height", "240px");
351                icon.removeClass("fa-angle-up").addClass("fa-angle-down");
352                resizeEvent();
353            };
354            close();
355            button.click(function () {
356                try {
357                    if (elem.hasClass("uncollapsed")) {
358                        close();
359                    } else {
360                        open();
361                    }
362                } finally {
363                    return false;
364                }
365            });
366        }
367    },
368    progress: function (options) {
369        var $element = $(this);
370        $element.append('<span class="pg-bar" style="width: 0%; height:2px; display: block; background-color: #337ab7"></span>');
371        $element.find("span.pg-bar").animate({
372            width: "100%"
373        }, options.duration, "linear", options.complete);
374    },
375    mailingListForm: function (options) {
376        var $form = $(this);
377        $form.submit(function(e) {
378            e.preventDefault();
379            $form = $(this);
380            var $container = options && options.container ? options.container : null;
381
382            jwtRequest(function(token) {
383                var url;
384                if (options && options.editions) {
385                    url = window.contextPath + "/rest/recipient/edition";
386                } else {
387                    url = window.contextPath + "/rest/recipient/subscribeMailinglists";
388                }
389                var req = {
390                    url: url,
391                    method: "POST",
392                    cache: false,
393                    data: extractFormData($form),
394                    headers: {"token": token}
395                };
396
397                $.ajax(req)
398                    .done(function (data, textStatus, jqXHR) {
399                        // Show success message
400                        updateMessages($form, $container, JSON.parse(jqXHR.getResponseHeader("X-MESSAGES")));
401
402                        // GTM: Custom newsletter_sign_up event
403                        window.dataLayer = window.dataLayer || [];
404                        for (var mailingList of jqXHR.responseJSON.mailingLists) {
405                            window.dataLayer.push({
406                                "event": "newsletter_signup",
407                                "recipientCode": jqXHR.responseJSON.recipientCode,
408                                "newsletter": mailingList,
409                                "source": options.source
410                            });
411                        }
412
413                        // Notify Salespanel of new newsletter subscription.
414                        (async function() {
415                            // Wait for salespanel promise to be fulfilled (either loaded or not).
416                            var salespanelLoaded = await window.salespanelPromise.catch(message => {
417                                // Salespanel did not load in time or was rejected by consent.
418                                console.warn(message);
419                                return false;
420                            });
421
422                            if (!salespanelLoaded) {
423                                return;
424                            }
425
426                            $salespanel.push(["set", "activity:customActivity", "website", "Nyhedsbrev sign-up"]);
427                        })();
428
429                        if ($container) {
430                            $container.hide();
431                        }
432
433                        if (typeof ga !== 'undefined' && ga) {
434                            if (options && options.source) {
435                                ga('send', 'event', 'Mailing List', 'subscribe', options.source);
436                            } else {
437                                ga('send', 'event', 'Mailing List', 'subscribe');
438                            }
439                        }
440                        if (options && options.success) {
441                            options.success();
442                        }
443                    })
444                    .fail(function (jqXHR) {
445                        // Push all messages to front-end
446                        updateMessages($form, $container, JSON.parse(jqXHR.getResponseHeader("X-MESSAGES")));
447                        if (options && options.failed) {
448                            options.failed();
449                        }
450                    })
451            });
452        });
453    },
454    hashDropdown: function (hash, options) {
455
456        var $link = $(this);
457        var dropdownId = jq($link.attr("data-target"));
458        var $dropdown = $(dropdownId);
459
460        var show = function () {
461            if (options && options.opening) {
462                options.opening($link, $dropdown);
463            }
464            if (options && options.slide) {
465                $dropdown.slideDown("fast", function () {
466                    if (options && options.opened) {
467                        options.opened($link, $dropdown);
468                    }
469                });
470            } else {
471                $dropdown.show();
472                if (options && options.opened) {
473                    options.opened($link, $dropdown);
474                }
475            }
476        };
477        var hide = function () {
478            if (!$dropdown.is(":hidden")) {
479                if (options && options.closing) {
480                    options.closing($link, $dropdown);
481                }
482                if (options && options.slide) {
483                    $dropdown.slideUp("fast", function () {
484                        if (options && options.closed) {
485                            options.closed($link, $dropdown);
486                        }
487                    });
488                } else {
489                    $dropdown.hide();
490                    if (options && options.closed) {
491                        options.closed($link, $dropdown);
492                    }
493                }
494            } else if (options && options.closed) {
495                options.closed($link, $dropdown);
496            }
497        };
498
499        var isHash = function () {
500            return window.location.hash.substring(1) == hash;
501        };
502
503        var hashEvent = function () {
504            if (isHash()) {
505                show();
506            } else {
507                hide();
508            }
509        };
510
511        var open = function () {
512            if (!isHash()) {
513                window.location.hash = hash;
514            }
515        };
516        var close = function () {
517            if (isHash()) {
518                window.location.hash = "close"; // Don't jump to the top of the page.
519            }
520        };
521        var toggle = function () {
522            if (isHash()) {
523                close();
524            } else {
525                open();
526            }
527            return false;
528        };
529
530
531        var bind = function () {
532            hashEvent();
533            $(window).bind("hashchange", hashEvent);
534            $link.bind("click", toggle);
535        };
536        var unbind = function () {
537            close();
538            $(window).unbind("hashchange", hashEvent);
539            $link.unbind("click", toggle);
540        };
541
542        bind();
543
544        $(document).mouseup(function (e) {
545            if (!$(e.target).closest(dropdownId).length
546                    && !$(e.target).closest(jq($link.attr("id"))).length) {
547                close();
548            }
549        });
550        $(document).keyup(function (e) {
551            if (e.keyCode === 27) { // ESC
552                close();
553            }
554        });
555
556        return {
557            open: open,
558            close: close,
559            toggle: toggle,
560            bind: bind,
561            unbind: unbind
562        };
563    },
564    hoverDropdown: function (options) {
565        var $link = $(this);
566        var $dropdown = $($link.attr("data-target"));
567        if (!$dropdown.length) {
568            return;
569        }
570        var openTimeout;
571        var closeTimeout;
572        var open = function () {
573            openTimeout = setTimeout(function () {
574                $link.addClass("open");
575                $link.append("<sub><sub></sub></sub>");
576                $dropdown.show();
577                if (options && options.opened) {
578                    options.opened();
579                }
580            }, 100);
581        };
582        var closeNow = function () {
583            if (!$dropdown.is(":hidden")) {
584                $link.removeClass("open");
585                $link.children("sub").remove();
586                $dropdown.hide();
587            }
588            if (options && options.closed) {
589                options.closed();
590            }
591        };
592        var close = function () {
593            // .is(":hover") is broke in our jquery version
594            //if (!$link.is(":hover") && !$dropdown.is(":hover")) {
595            // for now, use this workaround
596            if (!$link.hasClass(".hover") && !$dropdown.hasClass(".hover")) {
597                closeTimeout = setTimeout(function () {
598                    closeNow();
599                }, 110);
600            }
601        };
602        $link.mouseover(function () {
603            $link.addClass("hover");
604            if (closeTimeout) {
605                clearTimeout(closeTimeout);
606            }
607            open();
608        }).mouseout(function () {
609            $link.removeClass("hover");
610            if (openTimeout) {
611                clearTimeout(openTimeout);
612            }
613            close();
614        });
615        $dropdown.mouseover(function () {
616            $dropdown.addClass("hover");
617            if (closeTimeout) {
618                clearTimeout(closeTimeout);
619            }
620        }).mouseout(function () {
621            $dropdown.removeClass("hover");
622            close();
623        });
624        $(document).keyup(function (e) {
625            if (e.keyCode === 27) { // ESC
626                closeNow();
627            }
628        });
629    },
630    brickwork: function (customOptions) {
631        var container = $(this);
632        var options = {
633            gutter: 0,
634            brickSelector: ".brick",
635            mortarSelector: ".filler",
636            size: 12
637//            size: function () {
638//                switch (mediaWidth()) {
639//                    case "xs": return 6;
640//                    case "sm": return 9;
641//                    default: return 12;
642//                }
643//            }
644        };
645        $.extend(options, customOptions);
646        var size = $.isFunction(options.size) ? options.size() : options.size;
647        var maxWidth = container.width() - ((size - 1) * options.gutter);
648        var minWidth = maxWidth / size;
649
650        var matrix = [];
651
652        container.css("position", "relative");
653
654        var position = function (item, width, height) {
655            for (var y = 0; y < matrix.length; y++) {
656                x:for (var x = 0; x < matrix[y].length; x++) {
657                    try {
658                        if (matrix[y][x]) {
659                            for (var y2 = 0; y2 < height; y2++) {
660                                if (!matrix[y + y2]) {
661                                    continue x;
662                                }
663                                for (var x2 = 0; x2 < width; x2++) {
664                                    if (!matrix[y + y2][x + x2]) {
665                                        continue x;
666                                    }
667                                }
668                            }
669                            for (var y2 = 0; y2 < height; y2++) {
670                                for (var x2 = 0; x2 < width; x2++) {
671                                    matrix[y + y2][x + x2] = false;
672                                }
673                            }
674                            item.css("position", "absolute");
675                            item.css("left", (x * (minWidth + options.gutter)) + "px");
676                            item.css("top", (y * (minWidth + options.gutter)) + "px");
677                            item.show();
678                            return true;
679                        }
680                    } catch (err) {
681                        console.log(err);
682                        continue x;
683                    }
684                }
685            }
686            return false;
687        };
688
689        var getWidth = function(item) {
690            var mws = ["xs", "sm", "md", "lg"];
691            var mw = mediaWidth();
692            for (var i = $.inArray(mw, mws); i >= 0; i--) {
693                if (item.hasAttribute("data-" + mws[i] + "-width")) {
694                    return item.attr("data-" + mws[i] + "-width");
695                }
696            }
697            return item.attr("data-width");
698        };
699
700        var getHeight = function(item) {
701            var mws = ["xs", "sm", "md", "lg"];
702            var mw = mediaWidth();
703            for (var i = $.inArray(mw, mws); i >= 0; i--) {
704                if (item.hasAttribute("data-" + mws[i] + "-height")) {
705                    return item.attr("data-" + mws[i] + "-height");
706                }
707            }
708            return item.attr("data-height");
709        };
710
711        var items = container.children(options.brickSelector + ":not(" + options.mortarSelector + "):not([data-" + mediaWidth() + "-hidden=true])");
712        items.each(function () {
713            var item = $(this);
714            var width = getWidth(item);
715            var height = getHeight(item);
716            if (width > size) {
717                width = size;
718            }
719            item.css("width", ((width * minWidth) + ((width - 1) * options.gutter)) + "px");
720            item.css("height", ((height * minWidth) + ((height - 1) * options.gutter)) + "px");
721            while (!position(item, width, height)) {
722                var row = [];
723                for (var j = 0; j < size; j++) {
724                    row[j] = true;
725                }
726                matrix.push(row);
727            }
728        });
729
730        var fillers = container.children(options.mortarSelector);
731        // Hide fillers
732        fillers.each(function () {
733            $(this).hide();
734        });
735        fillers.each(function () {
736            var item = $(this);
737//            var width = parseInt(item.attr("data-width"));
738//            var height = parseInt(item.attr("data-height"));
739            var width = getWidth(item);
740            var height = getHeight(item);
741            item.css("width", ((width * minWidth) + ((width - 1) * options.gutter)) + "px");
742            item.css("height", ((height * minWidth) + ((height - 1) * options.gutter)) + "px");
743            return position(item, width, height);
744        });
745
746        container.css("height", (matrix.length * (minWidth + options.gutter)) + "px");
747    },
748    stickyKid: function (options) {
749        const opts = {
750            marginTop: 0
751        };
752        $.extend(opts, options);
753
754        const $element = $(this);
755        const $parent = $element.parent();
756        const $window = $(window);
757
758        let windowHeight, elementHeight, parentHeight, offsetTop, parentOffsetTop, marginLeft, left, top, oldScrollTop;
759
760        const onScrollUp = function (scrollTop, distance) {
761
762            // The offset of the parent can change if content above it changes. If it does,
763            // it means that the offsetTop of our element changes, so set that as well.
764            parentOffsetTop = $parent.offset().top;
765            offsetTop = parentOffsetTop;
766
767            // The height of the parent can change if its height depends on elements that render as you scroll up.
768            parentHeight = $parent.outerHeight();
769            elementHeight = $element.outerHeight();
770
771            if (top < 0 && scrollTop > offsetTop) {
772                top = top + distance;
773                if (top > scrollTop) {
774                    top = scrollTop;
775                }
776            } else if (top + distance <= opts.marginTop) {
777                top = top + distance;
778            } else if (scrollTop + opts.marginTop > offsetTop) {
779                top = opts.marginTop;
780            } else {
781                top = offsetTop - scrollTop;
782            }
783
784            // Switch between absolute and fixed position for better visual stability of the sticky element.
785            if (top > opts.marginTop) {
786                $element.css("position", "absolute");
787                $element.css("top", "0");
788                $element.css("bottom", "auto");
789            } else if (scrollTop + top + elementHeight >= parentOffsetTop + parentHeight) {
790                $element.css("position", "absolute");
791                $element.css("top", "auto");
792                $element.css("bottom", "0");
793            } else {
794                $element.css("position", "fixed");
795                $element.css("top", top + "px");
796                $element.css("bottom", "auto");
797            }
798        };
799
800        const onScrollDown = function (scrollTop, distance) {
801
802            // The offset of the parent can change if content above it changes. If it changes,
803            // we must adjust the top value to match the change, or it will be off and can cause our
804            // element to make sudden jumps.
805            if (parentOffsetTop != $parent.offset().top) {
806                top += ($parent.offset().top - parentOffsetTop);
807                parentOffsetTop = $parent.offset().top;
808            }
809
810            // The height of the parent can change if its height depends on elements that render as you scroll down.
811            parentHeight = $parent.outerHeight();
812            elementHeight = $element.outerHeight();
813
814            let oldTop = top;
815
816            // If we can move the scroll distance without setting a top that is less than the margin we are required
817            // to have, or if we still cannot see the bottom of our element, we should move the distance.
818            // This basically what moves the element, where the following steps locks the element to the top or bottom.
819            if (top - distance > opts.marginTop || top - distance + elementHeight >= windowHeight) {
820                top = top - distance;
821            }
822            // If we can set the top to the top margin without making the combined height of the top margin and
823            // element height taller than the window, just do that.
824            // This locks the element to the top, when we have plenty of room to show the entire element.
825            else if (opts.marginTop + elementHeight < windowHeight) {
826                top = opts.marginTop;
827            }
828            // Otherwise, simple set the top so we can see the bottom of the element.
829            // This locks the element to the bottom, when we are scrolled down and the top is far from the top margin.
830            // We do however need to make sure that we don't move the element below the bottom of the parent.
831            else {
832                top = Math.min(
833                    windowHeight - elementHeight,
834                    parentOffsetTop + parentHeight - scrollTop - elementHeight
835                );
836            }
837
838            // Switch between absolute and fixed position for better visual stability of the sticky element.
839            // Part 1) is for elements that are longer than the window height.
840            // Part 2) is for elements that are shorter than the window height and locks them to the bottom of the
841            //         parent, when they are touching the bottom of the parent.
842            // Part 3) is for elements that are shorter than the window height and sets a top value that puts them
843            //         in the right spot.
844            // console.log("top: " + top);
845            if (top > windowHeight - elementHeight && (scrollTop + windowHeight - elementHeight < parentOffsetTop && top > opts.marginTop) || top > opts.marginTop) {
846                $element.css("position", "absolute");
847                $element.css("top", "0");
848                $element.css("bottom", "auto");
849            } else if (scrollTop + top + elementHeight >= parentOffsetTop + parentHeight) {
850                $element.css("position", "absolute");
851                $element.css("top", "auto");
852                $element.css("bottom", "0");
853            } else {
854                $element.css("position", "fixed");
855                $element.css("top", top + "px");
856                $element.css("bottom", "auto");
857            }
858        };
859
860        const onScrollHorizontal = function () {
861            if ($element.css("position") == "absolute") {
862                $element.css("left", "0");
863            } else {
864                $element.css("left", (left - $window.scrollLeft()) + "px");
865            }
866        };
867
868        const init = function () {
869            if (!$element.is(":visible")) {
870                return;
871            }
872            windowHeight = $window.height();
873            elementHeight = $element.outerHeight();
874            $parent.css("min-height", $element.outerHeight() + "px");
875            $parent.css("position", "relative");
876            marginLeft = parseInt($element.css("margin-left").replace("px", ""));
877            $element.css("position", "relative").css("left", "");
878            left = $element.offset().left - marginLeft;
879            $element.css("position", "absolute").css("top", "0"); // move to the top to get the top offset
880            offsetTop = $element.offset().top;
881            $element.css("position", "fixed");
882            parentOffsetTop = $parent.offset().top;
883            parentHeight = $parent.outerHeight();
884
885            top = offsetTop;
886
887            oldScrollTop = $window.scrollTop();
888            onScrollDown(oldScrollTop, oldScrollTop); // distance == oldScrollTop because we are virtually scrolling from the top to the current position
889
890            // Send a scroll event after init is completed. If the page was reloaded while scroll down, the page will
891            // not trigger a scroll event after this component is initialized, even though the user might appear scrolled
892            // down on the page. This can lead to un-desired results such as banners not measuring viewable impressions,
893            // if you've used this component to make banners sticky.
894            window.dispatchEvent(new Event("scroll"));
895        };
896        init();
897
898        $window.resize(init);
899
900        $window.scroll(function () {
901            const newScrollTop = $window.scrollTop();
902            if (oldScrollTop > newScrollTop) {
903                onScrollUp(newScrollTop, oldScrollTop - newScrollTop);
904            } else if (oldScrollTop < newScrollTop) {
905                onScrollDown(newScrollTop, newScrollTop - oldScrollTop);
906            }
907            oldScrollTop = newScrollTop;
908            onScrollHorizontal();
909        });
910
911        return {
912            reinitialize: init,
913            reposition: function () {
914                window.dispatchEvent(new Event("scroll"));
915            }
916        };
917    },
918    testClicks: function (variation) {
919        var $this = $(this);
920        var uuid = null;
921        var promise = $.ajax(window.contextPath + "/rest/tests", {
922            cache: false,
923            type: "POST",
924            data: {
925                variation: variation
926            },
927            dataType: "text",
928            success: function (data) {
929                uuid = data;
930            },
931            error: function (jqXHR, textStatus, errorThrown) {
932                console.log(jqXHR);
933                console.log(textStatus);
934                console.log(errorThrown);
935            }
936        });
937        $.when(promise).then(function () {
938            if (uuid != null) {
939                $this.find("a").click(function () {
940                    $.ajax(window.contextPath + "/rest/tests", {
941                        cache: false,
942                        type: "PUT",
943                        data: {
944                            uuid: uuid
945                        },
946                        error: function (jqXHR, textStatus, errorThrown) {
947                            console.log(jqXHR);
948                            console.log(textStatus);
949                            console.log(errorThrown);
950                        }
951                    });
952                });
953            }
954        });
955    }
956});
957
958$(function () {
959    var adjustFilterOptions_callback = {};
960    var $options = $(".collapsible-search-filter").find("h6");
961    var mw;
962
963    $options.append(" <i class=\"fa-light fa-angle-down ms-2 d-inline d-md-none\">");
964
965    var resetPos = function ($next) {
966        $next.css("left", "0px").css("width", "100%");
967    };
968
969    var toggle = function ($this) {
970        var $next = $this.next();
971        // No toggling on desktops.
972        if (!xs()) {
973            return;
974        }
975        var open = function (pos) {
976            $next.addClass("show");
977
978            setTimeout(function () {
979                $(document).click(function (e) {
980                    if (!$(e.target).closest($next).length) {
981                        if ($next.is(":visible")) {
982                            close();
983                        }
984                    }
985                });
986                $(document).keydown(function (e) {
987                    if (e.which === 27) {
988                        close();
989                    }
990                });
991            }, 0);
992
993            pos();
994
995            adjustFilterOptions_callback[$this.attr("id")] = pos;
996        };
997        var close = function () {
998            $next.removeClass("show");
999
1000            $(document).unbind("click").unbind("keydown");
1001
1002            resetPos($next);
1003
1004            delete adjustFilterOptions_callback[$this.attr("id")];
1005        };
1006        if ($next.hasClass("dropdown-menu")) {
1007
1008            // The bootstrap row margin in pixels.
1009            var MARGIN = 10;
1010
1011            // Call open with a function that positions the filter options.
1012            open(function () {
1013                resetPos($next);
1014
1015                var winWidth = $(window).width();
1016
1017                $next.css("width", (winWidth * 0.6) + "px");
1018
1019                var thisWidth = $this.outerWidth();
1020                var nextWidth = $next.outerWidth();
1021                var left = (thisWidth - nextWidth) / 4;
1022
1023                if ($next.offset().left + left < MARGIN) {
1024                    left = 0;
1025                }
1026
1027                var right = $next.offset().left + left + nextWidth;
1028                if (right > winWidth - MARGIN) {
1029                    left -= (right - (winWidth - MARGIN));
1030                }
1031
1032                $next.css("left", left + "px");
1033
1034                return {
1035                    remove: function () {
1036                        close();
1037                    }
1038                };
1039            });
1040        } else {
1041            close();
1042        }
1043    };
1044
1045    $options.click(function () {
1046        toggle($(this));
1047    });
1048
1049    var adjust = function () {
1050        if (xs()) {
1051            if (mw !== mediaWidth()) {
1052                mw = mediaWidth();
1053                $options.addClass("btn btn-primary mt-2 me-2 mb-0");
1054            }
1055            for (var key in adjustFilterOptions_callback) {
1056                adjustFilterOptions_callback[key]();
1057            }
1058            $options.next().addClass("dropdown-menu");
1059        } else {
1060            if (mw === "xs") {
1061                mw = mediaWidth();
1062                $options.removeClass("btn btn-primary mt-2 me-2 mb-0");
1063            }
1064            for (var key in adjustFilterOptions_callback) {
1065                adjustFilterOptions_callback[key]().remove();
1066                delete adjustFilterOptions_callback[key];
1067            }
1068            $options.next().removeClass("dropdown-menu").removeClass("show"); // Make everything normal again.
1069            resetPos($options.next());
1070        }
1071    };
1072
1073    $(window).resize(function () {
1074        adjust();
1075    });
1076    adjust();
1077});
1078
1079// JSF ajax loader for buttons and links
1080var btnAjaxLoaderMap201701101057 = [];
1081var btnAjaxLoader = function(data, src) {
1082    var $btn = $(jq(data.source.id));
1083    if (data.status == "begin") {
1084        $btn.prop("disabled", true);
1085        var old = btnAjaxLoaderMap201701101057[data.source.id];
1086        if (!old) {
1087            old = {
1088                backgroundImage: $btn.css("background-image"),
1089                backgroundRepeat: $btn.css("background-repeat"),
1090                backgroundPosition: $btn.css("background-position"),
1091                paddingRight: $btn.css("padding-right")
1092            };
1093            btnAjaxLoaderMap201701101057[data.source.id] = old;
1094        }
1095        $btn.css("background-image", "url(" + src + ")");
1096        $btn.css("background-repeat", "no-repeat");
1097        $btn.css("background-position", "90% 50%");
1098        $btn.css("padding-right", (parseInt(old["paddingRight"]) + 24) + "px");
1099    } else if (data.status == "complete") {
1100        $btn.prop("disabled", false);
1101        var old = btnAjaxLoaderMap201701101057[data.source.id];
1102        if (old) {
1103            $btn.css("background-image", old.backgroundImage);
1104            $btn.css("background-repeat", old.backgroundRepeat);
1105            $btn.css("background-position", old.backgroundPosition);
1106            $btn.css("padding-right", old.paddingRight);
1107            delete btnAjaxLoaderMap201701101057[data.source.id];
1108        }
1109    }
1110};
1111var btnAjaxLoaderBlack = function(data) {
1112    btnAjaxLoader(data, "//f.nordiskemedier.dk/ajax-loader/transparent-black-16x16.svg");
1113};
1114var btnAjaxLoaderWhite = function(data) {
1115    btnAjaxLoader(data, "//f.nordiskemedier.dk/ajax-loader/transparent-white-16x16.svg");
1116};
1117
1118/**
1119 * Save a state (JSON object) in the client's session storage (if available)
1120 * and set a URL hash with the name of the saved object. In the session storage
1121 * the name of the saved object includes the current URL path to avoid naming
1122 * conflicts.
1123 */
1124var statefulHash = {
1125    /**
1126     * Save state and set a URL hash with the name where the state can be
1127     * retrieved from the client's session storage.
1128     * @param {String} hash
1129     * @param {Object} state
1130     */
1131    saveState: function (hash, state) {
1132        if (typeof(Storage) !== "undefined") {
1133            if (hash.indexOf("#") === -1) {
1134                hash = "#" + hash;
1135            }
1136            if (history.replaceState) {
1137                history.replaceState(null, null, hash);
1138            } else {
1139                location.hash = hash;
1140            }
1141            sessionStorage.setItem(window.location.pathname + location.hash, JSON.stringify(state));
1142        }
1143    },
1144    /**
1145     * Restore and return the state for the current URL hash.
1146     * @returns {Array|Object} the found state; null if no state was found
1147     */
1148    restoreState: function (hash) {
1149        if (typeof(Storage) !== "undefined") {
1150            if (hash.indexOf("#") === -1) {
1151                hash = "#" + hash;
1152            }
1153            if (hash === location.hash) {
1154                var stateStr = sessionStorage.getItem(window.location.pathname + location.hash);
1155                if (stateStr) {
1156                    return JSON.parse(stateStr);
1157                }
1158            }
1159        }
1160        return null;
1161    }
1162};
1163
1164
1165
1166
1167var Bundle = (function() {
1168
1169    var _cache = {};
1170    var _bus = [];
1171
1172    function init(keys, readyFunc) {
1173        var load = true;
1174        for (var i in _bus) {
1175            if (JSON.stringify(_bus[i].keys) === JSON.stringify(keys)) {
1176                load = false;
1177                break;
1178            }
1179        }
1180        if (load) {
1181            load = false;
1182            for (var i in keys) {
1183                var key = keys[i];
1184                if (!_cache.hasOwnProperty(key)) {
1185                    load = true;
1186                    break;
1187                }
1188            }
1189        }
1190
1191        _bus.push({
1192            keys: keys,
1193            readyFunc: readyFunc
1194        });
1195
1196        _evaluateBus(keys);
1197        if (_bus.length > 0 && load) {
1198            _load(keys, _evaluateBus);
1199        }
1200    }
1201
1202    function _evaluateBus(keys) {
1203        var bundle = _build(keys);
1204        if (bundle) {
1205            for (var i in _bus) {
1206                var e = _bus[i];
1207                if (JSON.stringify(e.keys) === JSON.stringify(keys)) {
1208                    var readyFunc = e.readyFunc;
1209                    delete _bus[i];
1210                    readyFunc(bundle);
1211                }
1212            }
1213        }
1214    }
1215
1216    function _build(keys) {
1217        var bundle = {};
1218        for (var i in keys) {
1219            var key = keys[i];
1220            if (_cache.hasOwnProperty(key)) {
1221                bundle[key] = _cache[key];
1222            } else {
1223                return null;
1224            }
1225        }
1226        return bundle;
1227    }
1228
1229    function _load(keys, callback) {
1230        var keysParam = keys.join();
1231        var request = new XMLHttpRequest();
1232        request.open("GET", window.contextPath + "/rest/bundle?keys=" + keysParam, true);
1233        request.onload = function () {
1234            if (request.status === 200) {
1235                var data = JSON.parse(this.response);
1236                for (var key in data) {
1237                    _cache[key] = data[key];
1238                }
1239                callback(keys);
1240            }
1241        };
1242        request.send();
1243    }
1244
1245    return {
1246        init: init
1247    };
1248
1249})();
1250
1251var fireWindowResizeEvent = function () {
1252    if (window.dispatchEvent) {
1253        // Modern way
1254        try {
1255            window.dispatchEvent(new Event("resize"));
1256        }
1257        // IE11 way
1258        catch (err) {
1259            console.log("'Event' not supported. Trying 'createEvent'.");
1260            var resizeEvent = window.document.createEvent("UIEvents");
1261            resizeEvent.initUIEvent("resize", true, false, window, 0);
1262            window.dispatchEvent(resizeEvent);
1263        }
1264    } else {
1265        $(window).resize(); // Does not fire events added with window.addEventListener
1266    }
1267};
1268
1269
1270
1271window.CvrRestApi = function (authorization, authorizationScheme) {
1272
1273    var accessToken = null;
1274    var renewing = false;
1275    var queue = [];
1276
1277    var completeQueued = function () {
1278        var queued = queue.splice(0, queue.length);
1279        for (var i = 0; i < queued.length; i++) {
1280            queued[i]();
1281        }
1282    };
1283
1284    var renew = function (done) {
1285        if (renewing) {
1286            queue.push(done);
1287        } else {
1288            renewing = true;
1289            $.ajax(window.cvrRestApiAddress + "/access-tokens", {
1290                cache: false,
1291                type: "POST",
1292                success: function (data) {
1293                    accessToken = data;
1294                    done();
1295                    completeQueued();
1296                },
1297                complete: function () {
1298                    renewing = false;
1299                }
1300            });
1301        }
1302    };
1303
1304    var execute = function (options) {
1305        if (!options.headers) {
1306            options.headers = {};
1307        }
1308        options.headers[authorization] = authorizationScheme + " " + accessToken;
1309        $.ajax(options);
1310    };
1311
1312    var ajax = function (options) {
1313        var job = function () {
1314            var renewed = false;
1315            var existingError = options.error;
1316            options.error = function (jqXHR, textStatus, errorThrown) {
1317                if (jqXHR.status == 401 && !renewed) {
1318                    renewed = true;
1319                    renew(function () {
1320                        execute(options);
1321                    });
1322                }
1323                if (typeof existingError == "function") {
1324                    existingError(jqXHR, textStatus, errorThrown);
1325                }
1326            };
1327            execute(options);
1328        };
1329        if (accessToken == null) {
1330            renew(job);
1331        } else {
1332            job();
1333        }
1334    };
1335
1336    return {
1337        ajax: ajax
1338    }
1339};
1340var SlugUtil = function () {
1341
1342    var chars = {
1343        "\u00C0": "A",
1344        "\u00C1": "A",
1345        "\u00C2": "A",
1346        "\u00C3": "A",
1347        "\u00C4": "A",
1348        "\u00C5": "A",
1349        "\u00C6": "AE",
1350        "\u00C7": "C",
1351        "\u00C8": "E",
1352        "\u00C9": "E",
1353        "\u00CA": "E",
1354        "\u00CB": "E",
1355        "\u00CC": "I",
1356        "\u00CD": "I",
1357        "\u00CE": "I",
1358        "\u00CF": "I",
1359        "\u00D0": "D",
1360        "\u00D1": "N",
1361        "\u00D2": "O",
1362        "\u00D3": "O",
1363        "\u00D4": "O",
1364        "\u00D5": "O",
1365        "\u00D6": "O",
1366        "\u00D8": "O",
1367        "\u00D9": "U",
1368        "\u00DA": "U",
1369        "\u00DB": "U",
1370        "\u00DC": "U",
1371        "\u00DD": "Y",
1372        "\u00DF": "s",
1373        "\u00E0": "a",
1374        "\u00E1": "a",
1375        "\u00E2": "a",
1376        "\u00E3": "a",
1377        "\u00E4": "a",
1378        "\u00E5": "a",
1379        "\u00E6": "ae",
1380        "\u00E7": "c",
1381        "\u00E8": "e",
1382        "\u00E9": "e",
1383        "\u00EA": "e",
1384        "\u00EB": "e",
1385        "\u00EC": "i",
1386        "\u00ED": "i",
1387        "\u00EE": "i",
1388        "\u00EF": "i",
1389        "\u00F1": "n",
1390        "\u00F2": "o",
1391        "\u00F3": "o",
1392        "\u00F4": "o",
1393        "\u00F5": "o",
1394        "\u00F6": "o",
1395        "\u00F8": "o",
1396        "\u00F9": "u",
1397        "\u00FA": "u",
1398        "\u00FB": "u",
1399        "\u00FC": "u",
1400        "\u00FD": "y",
1401        "\u00FF": "y",
1402        "\u0100": "A",
1403        "\u0101": "a",
1404        "\u0102": "A",
1405        "\u0103": "a",
1406        "\u0104": "A",
1407        "\u0105": "a",
1408        "\u0106": "C",
1409        "\u0107": "c",
1410        "\u0108": "C",
1411        "\u0109": "c",
1412        "\u010A": "C",
1413        "\u010B": "c",
1414        "\u010C": "C",
1415        "\u010D": "c",
1416        "\u010E": "D",
1417        "\u010F": "d",
1418        "\u0110": "D",
1419        "\u0111": "d",
1420        "\u0112": "E",
1421        "\u0113": "e",
1422        "\u0114": "E",
1423        "\u0115": "e",
1424        "\u0116": "E",
1425        "\u0117": "e",
1426        "\u0118": "E",
1427        "\u0119": "e",
1428        "\u011A": "E",
1429        "\u011B": "e",
1430        "\u011C": "G",
1431        "\u011D": "g",
1432        "\u011E": "G",
1433        "\u011F": "g",
1434        "\u0120": "G",
1435        "\u0121": "g",
1436        "\u0122": "G",
1437        "\u0123": "g",
1438        "\u0124": "H",
1439        "\u0125": "h",
1440        "\u0126": "H",
1441        "\u0127": "h",
1442        "\u0128": "I",
1443        "\u0129": "i",
1444        "\u012A": "I",
1445        "\u012B": "i",
1446        "\u012C": "I",
1447        "\u012D": "i",
1448        "\u012E": "I",
1449        "\u012F": "i",
1450        "\u0130": "I",
1451        "\u0131": "i",
1452        "\u0132": "IJ",
1453        "\u0133": "ij",
1454        "\u0134": "J",
1455        "\u0135": "j",
1456        "\u0136": "K",
1457        "\u0137": "k",
1458        "\u0139": "L",
1459        "\u013A": "l",
1460        "\u013B": "L",
1461        "\u013C": "l",
1462        "\u013D": "L",
1463        "\u013E": "l",
1464        "\u013F": "L",
1465        "\u0140": "l",
1466        "\u0141": "l",
1467        "\u0142": "l",
1468        "\u0143": "N",
1469        "\u0144": "n",
1470        "\u0145": "N",
1471        "\u0146": "n",
1472        "\u0147": "N",
1473        "\u0148": "n",
1474        "\u0149": "n",
1475        "\u014C": "O",
1476        "\u014D": "o",
1477        "\u014E": "O",
1478        "\u014F": "o",
1479        "\u0150": "O",
1480        "\u0151": "o",
1481        "\u0152": "OE",
1482        "\u0153": "oe",
1483        "\u0154": "R",
1484        "\u0155": "r",
1485        "\u0156": "R",
1486        "\u0157": "r",
1487        "\u0158": "R",
1488        "\u0159": "r",
1489        "\u015A": "S",
1490        "\u015B": "s",
1491        "\u015C": "S",
1492        "\u015D": "s",
1493        "\u015E": "S",
1494        "\u015F": "s",
1495        "\u0160": "S",
1496        "\u0161": "s",
1497        "\u0162": "T",
1498        "\u0163": "t",
1499        "\u0164": "T",
1500        "\u0165": "t",
1501        "\u0166": "T",
1502        "\u0167": "t",
1503        "\u0168": "U",
1504        "\u0169": "u",
1505        "\u016A": "U",
1506        "\u016B": "u",
1507        "\u016C": "U",
1508        "\u016D": "u",
1509        "\u016E": "U",
1510        "\u016F": "u",
1511        "\u0170": "U",
1512        "\u0171": "u",
1513        "\u0172": "U",
1514        "\u0173": "u",
1515        "\u0174": "W",
1516        "\u0175": "w",
1517        "\u0176": "Y",
1518        "\u0177": "y",
1519        "\u0178": "Y",
1520        "\u0179": "Z",
1521        "\u017A": "z",
1522        "\u017B": "Z",
1523        "\u017C": "z",
1524        "\u017D": "Z",
1525        "\u017E": "z",
1526        "\u017F": "s",
1527        "\u0192": "f",
1528        "\u01A0": "O",
1529        "\u01A1": "o",
1530        "\u01AF": "U",
1531        "\u01B0": "u",
1532        "\u01CD": "A",
1533        "\u01CE": "a",
1534        "\u01CF": "I",
1535        "\u01D0": "i",
1536        "\u01D1": "O",
1537        "\u01D2": "o",
1538        "\u01D3": "U",
1539        "\u01D4": "u",
1540        "\u01D5": "U",
1541        "\u01D6": "u",
1542        "\u01D7": "U",
1543        "\u01D8": "u",
1544        "\u01D9": "U",
1545        "\u01DA": "u",
1546        "\u01DB": "U",
1547        "\u01DC": "u",
1548        "\u01FA": "A",
1549        "\u01FB": "a",
1550        "\u01FC": "AE",
1551        "\u01FD": "ae",
1552        "\u01FE": "O",
1553        "\u01FF": "o"
1554    };
1555
1556    var convert = function (val, whitespace) {
1557        var res = val.toLowerCase().trim();
1558        for (var c in chars) {
1559            if (chars.hasOwnProperty(c)) {
1560                res = res.replace(new RegExp(c, "g"), chars[c]); // replace characters
1561            }
1562        }
1563        res = res.replace(new RegExp("[^\\s^\\w]+", "g"), ""); // remove unsupported characters
1564        res = res.trim();
1565        res = res.replace(new RegExp("(\\s)+", "g"), whitespace); // replace whitespace
1566        return res;
1567    };
1568
1569    return {
1570        convert: function (val, whitespace) {
1571            return convert(val, whitespace);
1572        },
1573        withUnderscores: function (val) {
1574            return convert(val, "_");
1575        },
1576        withDashes: function (val) {
1577            return convert(val, "-");
1578        }
1579    };
1580};
1581
1582const ArticleBookmarks = (function () {
1583    let _cache = {};
1584    let _pendingKeys = new Set();
1585    let _bus = [];
1586    let _subscriptions = {}; // Store key-specific change callbacks
1587
1588    window.addEventListener("pageshow", (event) => {
1589        if (event.persisted) {
1590            _cache = {};
1591            _pendingKeys.clear();
1592        }
1593    });
1594
1595    function init(bookmarkService, keys, readyFunc, changeFunc) {
1596        const missingKeys = [];
1597
1598        keys.forEach((key) => {
1599            if (!_cache.hasOwnProperty(key) && !_pendingKeys.has(key)) {
1600                missingKeys.push(key);
1601                _pendingKeys.add(key);
1602            }
1603        });
1604
1605        _bus.push({
1606            keys,
1607            readyFunc,
1608        });
1609
1610        // Register change callback if provided
1611        if (changeFunc) {
1612            keys.forEach((key) => {
1613                if (!_subscriptions[key]) {
1614                    _subscriptions[key] = [];
1615                }
1616                _subscriptions[key].push(changeFunc);
1617            });
1618        }
1619
1620        if (missingKeys.length > 0) {
1621            _scheduleLoad(bookmarkService);
1622        } else {
1623            _evaluateBus();
1624        }
1625    }
1626
1627    function _isInBus(key) {
1628        return _bus.some((entry) => entry.keys.includes(key));
1629    }
1630
1631    function _scheduleLoad(bookmarkService) {
1632        if (_scheduleLoad.timeout) {
1633            clearTimeout(_scheduleLoad.timeout);
1634        }
1635
1636        _scheduleLoad.timeout = setTimeout(() => {
1637            const allMissingKeys = Array.from(
1638                new Set(_bus.flatMap((entry) => entry.keys.filter((key) => !_cache.hasOwnProperty(key))))
1639            );
1640
1641            if (allMissingKeys.length > 0) {
1642                _load(bookmarkService, allMissingKeys, _evaluateBus);
1643            }
1644        }, 250);
1645    }
1646
1647    function _evaluateBus() {
1648        for (let i = _bus.length - 1; i >= 0; i--) {
1649            const entry = _bus[i];
1650            const bundle = _build(entry.keys);
1651            if (bundle) {
1652                const { readyFunc } = entry;
1653                _bus.splice(i, 1);
1654                readyFunc(bundle);
1655            }
1656        }
1657    }
1658
1659    function _build(keys) {
1660        const bundle = {};
1661        for (const key of keys) {
1662            if (_cache.hasOwnProperty(key)) {
1663                bundle[key] = _cache[key];
1664            } else {
1665                return null;
1666            }
1667        }
1668        return bundle;
1669    }
1670
1671    function _load(bookmarkService, keys, callback) {
1672        bookmarkService.checkArticles(keys).then((data) => {
1673            Object.keys(data).forEach((key) => {
1674                _updateCache(key, data[key]); // Use a central function for cache updates
1675                _pendingKeys.delete(key);
1676            });
1677            callback();
1678        });
1679    }
1680
1681    function set(bookmarkService, key, value) {
1682        const action = value
1683            ? bookmarkService.createArticle(key)
1684            : bookmarkService.removeArticle(key);
1685
1686        return action
1687            .then(() => {
1688                _updateCache(key, value); // Use the cache update function
1689                _evaluateBus();
1690            })
1691            .catch((error) => {
1692                console.error(`Failed to ${value ? "set" : "remove"} bookmark for key ${key}:`, error);
1693                throw error;
1694            });
1695    }
1696
1697    function _updateCache(key, value) {
1698        if (_cache[key] !== value) {
1699            _cache[key] = value;
1700            if (_subscriptions[key]) {
1701                _subscriptions[key].forEach((callback) => callback(value));
1702            }
1703        }
1704    }
1705
1706    return {
1707        init,
1708        set,
1709    };
1710})();
1711
1712const PostBookmarks = (function () {
1713    const _cache = {};
1714    const _bus = [];
1715    const _subscriptions = {}; // Store key-specific change callbacks
1716
1717    function init(bookmarkService, keys, readyFunc, changeFunc) {
1718        const pendingKeys = [];
1719
1720        keys.forEach((key) => {
1721            if (!_cache.hasOwnProperty(key) && !_isInBus(key)) {
1722                pendingKeys.push(key);
1723            }
1724        });
1725
1726        _bus.push({
1727            keys,
1728            readyFunc,
1729        });
1730
1731        // Register change callback if provided
1732        if (changeFunc) {
1733            keys.forEach((key) => {
1734                if (!_subscriptions[key]) {
1735                    _subscriptions[key] = [];
1736                }
1737                _subscriptions[key].push(changeFunc);
1738            });
1739        }
1740
1741        if (pendingKeys.length > 0) {
1742            _scheduleLoad(bookmarkService);
1743        } else {
1744            _evaluateBus();
1745        }
1746    }
1747
1748    function _isInBus(key) {
1749        return _bus.some((entry) => entry.keys.includes(key));
1750    }
1751
1752    function _scheduleLoad(bookmarkService) {
1753        if (_scheduleLoad.timeout) {
1754            clearTimeout(_scheduleLoad.timeout);
1755        }
1756
1757        _scheduleLoad.timeout = setTimeout(() => {
1758            const allPendingKeys = Array.from(
1759                new Set(_bus.flatMap((entry) => entry.keys.filter((key) => !_cache.hasOwnProperty(key))))
1760            );
1761
1762            if (allPendingKeys.length > 0) {
1763                _load(bookmarkService, allPendingKeys, _evaluateBus);
1764            }
1765        }, 250);
1766    }
1767
1768    function _evaluateBus() {
1769        for (let i = _bus.length - 1; i >= 0; i--) {
1770            const entry = _bus[i];
1771            const bundle = _build(entry.keys);
1772            if (bundle) {
1773                const { readyFunc } = entry;
1774                _bus.splice(i, 1);
1775                readyFunc(bundle);
1776            }
1777        }
1778    }
1779
1780    function _build(keys) {
1781        const bundle = {};
1782        for (const key of keys) {
1783            if (_cache.hasOwnProperty(key)) {
1784                bundle[key] = _cache[key];
1785            } else {
1786                return null;
1787            }
1788        }
1789        return bundle;
1790    }
1791
1792    function _load(bookmarkService, keys, callback) {
1793        bookmarkService.checkPosts(keys).then((data) => {
1794            Object.keys(data).forEach((key) => {
1795                _updateCache(key, data[key]); // Use a central function for cache updates
1796            });
1797            callback();
1798        });
1799    }
1800
1801    function set(bookmarkService, key, value) {
1802        const action = value
1803            ? bookmarkService.createPost(key)
1804            : bookmarkService.removePost(key);
1805
1806        return action
1807            .then(() => {
1808                _updateCache(key, value); // Use the cache update function
1809                _evaluateBus();
1810            })
1811            .catch((error) => {
1812                console.error(`Failed to ${value ? "set" : "remove"} bookmark for key ${key}:`, error);
1813                throw error;
1814            });
1815    }
1816
1817    function _updateCache(key, value) {
1818        if (_cache[key] !== value) {
1819            _cache[key] = value;
1820            if (_subscriptions[key]) {
1821                _subscriptions[key].forEach((callback) => callback(value));
1822            }
1823        }
1824    }
1825
1826    return {
1827        init,
1828        set,
1829    };
1830})();
1831
1832
1833
1834// Enable for Vue components.
1835window.logIn = logIn;
1836window.Bundle = Bundle;
1837window.EventBus = EventBus;
1838window.fireWindowResizeEvent = fireWindowResizeEvent;
1839window.mediaWidth = mediaWidth;
1840window.ArticleBookmarks = ArticleBookmarks;
1841window.PostBookmarks = PostBookmarks;

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.