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.