Subversion Repositories oidplus

Rev

Rev 829 | Rev 839 | Go to most recent revision | View as "text/javascript" | Blame | Compare with Previous | Last modification | View Log | RSS feed

  1. /*
  2.  * OIDplus 2.0
  3.  * Copyright 2019 - 2022 Daniel Marschall, ViaThinkSoft
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17.  
  18. /*jshint esversion: 6 */
  19.  
  20. // $('#html').jstree();
  21.  
  22. var bs5Utils = undefined;
  23.  
  24. var current_node = "";
  25. var popstate_running = false;
  26. // DEFAULT_LANGUAGE will be set by oidplus.min.js.php
  27. // language_messages will be set by oidplus.min.js.php
  28. // language_tblprefix will be set by oidplus.min.js.php
  29. // csrf_token will be set by oidplus.min.js.php
  30. // samesite_policy will bet set by oidplus.min.js.php
  31.  
  32. var pageChangeCallbacks = [];
  33. var pageChangeRequestCallbacks = [];
  34.  
  35. var ajaxPageLoadedCallbacks = [];
  36.  
  37. var oidplus_menu_width = 450; // In pixels. You can change this at runtime because of the glayoutWorkaroundB() workaround
  38.  
  39. function isInternetExplorer() {
  40.         // see also includes/functions.inc.php
  41.         var ua = window.navigator.userAgent;
  42.         return ((ua.indexOf("MSIE ") > 0) || (ua.indexOf("Trident/") > 0));
  43. }
  44.  
  45. String.prototype.explode = function (separator, limit) {
  46.         // https://stackoverflow.com/questions/4514323/javascript-equivalent-to-php-explode
  47.         const array = this.split(separator);
  48.         if (limit !== undefined && array.length >= limit) {
  49.                 array.push(array.splice(limit - 1).join(separator));
  50.         }
  51.         return array;
  52. };
  53.  
  54. String.prototype.htmlentities = function () {
  55.         return this.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');//"
  56. };
  57.  
  58. String.prototype.html_entity_decode = function () {
  59.         return $('<textarea />').html(this).text();
  60. };
  61.  
  62. if (!String.prototype.replaceAll) {
  63.         /**
  64.          * String.prototype.replaceAll() polyfill
  65.          * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
  66.          * @author Chris Ferdinandi
  67.          * @license MIT
  68.          */
  69.         String.prototype.replaceAll = function(str, newStr){
  70.                 // If a regex pattern
  71.                 if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') {
  72.                         return this.replace(str, newStr);
  73.                 }
  74.                 // If a string
  75.                 return this.replace(new RegExp(str, 'g'), newStr);
  76.         };
  77. }
  78.  
  79. if (!String.prototype.startsWith) {
  80.         // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith#polyfill
  81.         Object.defineProperty(String.prototype, 'startsWith', {
  82.                 value: function(search, rawPos) {
  83.                         var pos = rawPos > 0 ? rawPos|0 : 0;
  84.                         return this.substring(pos, pos + search.length) === search;
  85.                 }
  86.         });
  87. }
  88.  
  89. function getMeta(metaName) {
  90.         const metas = $('meta[name='+metaName+']');
  91.         return (metas.length == 0) ? '' : metas[0].content;
  92. }
  93.  
  94. function getOidPlusSystemTitle() {
  95.         return getMeta('OIDplus-SystemTitle'); // do not translate
  96. }
  97.  
  98. function combine_systemtitle_and_pagetitle(systemtitle, pagetitle) {
  99.         // Please also change the function in index.php
  100.         if (systemtitle == pagetitle) {
  101.                 return systemtitle;
  102.         } else {
  103.                 return pagetitle + ' - ' + systemtitle;
  104.         }
  105. }
  106.  
  107. function getSystemUrl(relative) {
  108.         var url = new URL(window.location.href);
  109.         if (relative) {
  110.                 return url.pathname;
  111.         } else {
  112.                 return url.href.substr(0, url.href.length-url.search.length);
  113.         }
  114. }
  115.  
  116. function getTreeLoadURL() {
  117.         var url = new URL(window.location.href);
  118.         var goto = url.searchParams.get("goto");
  119.         return (goto != null) ? "ajax.php?csrf_token="+encodeURIComponent(csrf_token)+"&action=tree_load&anticache="+Date.now()+"&goto="+encodeURIComponent(goto)
  120.                               : "ajax.php?csrf_token="+encodeURIComponent(csrf_token)+"&action=tree_load&anticache="+Date.now();
  121. }
  122.  
  123. function reloadContent() {
  124.         // window.location.href = "?goto="+encodeURIComponent(current_node);
  125.         if (openOidInPanel(current_node, false)) {
  126.                 if(!$('#oidtree').jstree(true).get_node(current_node)) {
  127.                         // Avoid that a language change at "oidplus:srvreg_status" won't redirect the user to "oidplus:srv_registration" because of the reselection during refresh
  128.                         $('#oidtree').jstree("deselect_all");
  129.                 }
  130.  
  131.                 $('#oidtree').jstree("refresh");
  132.         }
  133. }
  134.  
  135. function x_rec(x_data, i) {
  136.         $('#oidtree').jstree('open_node', x_data[i], function(e, data) {
  137.                 if (i+1 < x_data.length) {
  138.                         x_rec(x_data, i+1);
  139.                 } else {
  140.                         popstate_running = true; // don't call openOidInPanel again
  141.                         try {
  142.                                 $('#oidtree').jstree('select_node', x_data[i]);
  143.                         } catch (err) {
  144.                                 popstate_running = false;
  145.                         } finally {
  146.                                 popstate_running = false;
  147.                         }
  148.                 }
  149.         });
  150. }
  151.  
  152. function performCloseQueryCB() {
  153.         for (var i=0; i<pageChangeRequestCallbacks.length; i++) {
  154.                 if (!pageChangeRequestCallbacks[i][0](pageChangeRequestCallbacks[i][1])) return false;
  155.         }
  156.         pageChangeRequestCallbacks = [];
  157.         return true; // may close
  158. }
  159.  
  160. function performCloseCB() {
  161.         for (var i=0; i<pageChangeCallbacks.length; i++) {
  162.                 pageChangeCallbacks[i][0](pageChangeCallbacks[i][1]);
  163.         }
  164.         pageChangeCallbacks = [];
  165. }
  166.  
  167. function openOidInPanel(id, reselect/*=false*/, anchor/*=''*/, force/*=false*/) {
  168.         reselect = (typeof reselect === 'undefined') ? false : reselect; // do not translate
  169.         anchor = (typeof anchor === 'undefined') ? '' : anchor; // do not translate
  170.         force = (typeof force === 'undefined') ? false : force; // do not translate
  171.  
  172.         var mayClose = performCloseQueryCB();
  173.         if (!force && !mayClose) return false;
  174.  
  175.         performCloseCB();
  176.  
  177.         $.xhrPool.abortAll();
  178.  
  179.         if (reselect) {
  180.                 $('#oidtree').jstree('deselect_all');
  181.  
  182.                 popstate_running = true; // don't call openOidInPanel during tree selection
  183.                 try {
  184.                         // If the node is already loaded in the tree, select it
  185.                         if (!$('#oidtree').jstree('select_node', id)) {
  186.                                 // If the node is not loaded, then we try to search it.
  187.                                 // If it can be found, then open all parent nodes and select the node
  188.                                 $.ajax({
  189.                                         url:"ajax.php",
  190.                                         method:"POST",
  191.                                         beforeSend: function(jqXHR, settings) {
  192.                                                 //$.xhrPool.abortAll();
  193.                                                 $.xhrPool.add(jqXHR);
  194.                                         },
  195.                                         complete: function(jqXHR, text) {
  196.                                                 $.xhrPool.remove(jqXHR);
  197.                                         },
  198.                                         data:{
  199.                                                 csrf_token:csrf_token,
  200.                                                 action:"tree_search",
  201.                                                 search:id,
  202.                                                 anticache:Date.now()
  203.                                         },
  204.                                         error:function(jqXHR, textStatus, errorThrown) {
  205.                                                 if (errorThrown == "abort") return;
  206.                                                 console.error("Tree search failed");
  207.                                                 console.error(_L("Error: %1",errorThrown));
  208.                                         },
  209.                                         success:function(data) {
  210.                                                 if ("error" in data) {
  211.                                                         console.error("Tree search failed");
  212.                                                         console.error(data);
  213.                                                 } else if ((data instanceof Array) && (data.length > 0)) {
  214.                                                         x_rec(data, 0);
  215.                                                 } else {
  216.                                                         console.error("Tree search failed");
  217.                                                         console.error(data);
  218.                                                 }
  219.                                         }
  220.                                 });
  221.                         }
  222.                 } catch (err) {
  223.                         popstate_running = false;
  224.                 } finally {
  225.                         popstate_running = false;
  226.                 }
  227.         }
  228.  
  229.         // This loads the actual content
  230.  
  231.         // document.title = ""; // <-- we may not do this, otherwise Firefox won't
  232.         //                            show titles in the browser history (right-click
  233.         //                            on back-button), although document.title() is
  234.         //                            set inside the AJAX-callback [Firefox bug?!]
  235.  
  236.         $('#real_title').html("&nbsp;");
  237.         $('#real_content').html(_L("Loading..."));
  238.         $('#static_link').attr("href", "index.php?goto="+encodeURIComponent(id));
  239.         $("#gotoedit").val(id);
  240.  
  241.         // Normal opening of a description
  242.         $.ajax({
  243.                 url:"ajax.php",
  244.                 method:"GET",
  245.                 beforeSend: function(jqXHR, settings) {
  246.                         //$.xhrPool.abortAll();
  247.                         $.xhrPool.add(jqXHR);
  248.                 },
  249.                 complete: function(jqXHR, text) {
  250.                         $.xhrPool.remove(jqXHR);
  251.                 },
  252.                 data:{
  253.                         csrf_token:csrf_token,
  254.                         action:"get_description",
  255.                         id:id,
  256.                         anticache:Date.now()
  257.                 },
  258.                 error:function(jqXHR, textStatus, errorThrown) {
  259.                         if (errorThrown == "abort") return;
  260.                         alertError(_L("Failed to load content: %1",errorThrown));
  261.                         console.error(_L("Error: %1",errorThrown));
  262.                 },
  263.                 success:function(data) {
  264.                         if ("error" in data) {
  265.                                 alertError(_L("Failed to load content: %1",data.error));
  266.                                 console.error(data.error);
  267.                         } else if (data.status >= 0) {
  268.                                 data.id = id;
  269.  
  270.                                 var state = {
  271.                                         "node_id":id,
  272.                                         "titleHTML":(data.icon ? '<img src="'+data.icon+'" width="48" height="48" alt="'+data.title.htmlentities()+'"> ' : '') + data.title.htmlentities(),
  273.                                         "textHTML":data.text,
  274.                                         "staticlinkHREF":"index.php?goto="+encodeURIComponent(id),
  275.                                 };
  276.                                 if (current_node != id) {
  277.                                         window.history.pushState(state, data.title, "?goto="+encodeURIComponent(id));
  278.                                 } else {
  279.                                         window.history.replaceState(state, data.title, "?goto="+encodeURIComponent(id));
  280.                                 }
  281.  
  282.                                 document.title = combine_systemtitle_and_pagetitle(getOidPlusSystemTitle(), data.title);
  283.  
  284.                                 if (data.icon) {
  285.                                         $('#real_title').html('<img src="'+data.icon+'" width="48" height="48" alt="'+data.title.htmlentities()+'"> ' + data.title.htmlentities());
  286.                                 } else {
  287.                                         $('#real_title').html(data.title.htmlentities());
  288.                                 }
  289.                                 $('#real_content').html(data.text);
  290.                                 document.title = combine_systemtitle_and_pagetitle(getOidPlusSystemTitle(), data.title);
  291.                                 current_node = id;
  292.  
  293.                                 ajaxPageLoadedCallbacks.forEach(
  294.                                         function(listener) {
  295.                                                 if (typeof listener == 'function') listener();
  296.                                         }
  297.                                 );
  298.  
  299.                                 if (anchor != '') {
  300.                                         jumpToAnchor(anchor);
  301.                                 }
  302.                         } else {
  303.                                 alertError(_L("Failed to load content: %1",data.status));
  304.                                 console.error(data);
  305.                         }
  306.                 }
  307.         });
  308.  
  309.         return true;
  310. }
  311.  
  312. // This function opens the "parentID" node, and then selects the "childID" node (which should be beneath the parent node)
  313. function openAndSelectNode(childID, parentID) {
  314.         if ($('#oidtree').jstree(true).get_node(parentID)) {
  315.                 $('#oidtree').jstree('open_node', parentID, function(e, data) { // open parent node
  316.                         if ($('#oidtree').jstree(true).get_node(childID)) { // is the child there?
  317.                                 $('#oidtree').jstree('deselect_all').jstree('select_node', childID); // select it
  318.                         } else {
  319.                                 // This can happen if the content page contains brand new items which are not in the treeview yet
  320.                                 $("#gotoedit").val(childID);
  321.                                 window.location.href = "?goto="+encodeURIComponent(childID);
  322.                         }
  323.                 }, true);
  324.         } else {
  325.                 // This should usually not happen
  326.                 $("#gotoedit").val(childID);
  327.                 window.location.href = "?goto="+encodeURIComponent(childID);
  328.         }
  329. }
  330.  
  331. $(window).on("popstate", function(e) {
  332.         if (!performCloseQueryCB()) {
  333.                 // TODO: does not work!!! The "back/forward" action will be cancelled, but the browser still thinks it was successful,
  334.                 // so if you do it again, you will then jump 2 pages back, etc!
  335.                 // This does also not help:
  336.                 //window.history.pushState(e.originalEvent.state, e.originalEvent.title, e.originalEvent.url);
  337.                 //window.history.forward();
  338.                 return;
  339.         }
  340.  
  341.         popstate_running = true;
  342.         try {
  343.                 var data = e.originalEvent.state;
  344.  
  345.                 current_node = data.node_id;
  346.                 $("#gotoedit").val(current_node);
  347.                 $('#oidtree').jstree('deselect_all').jstree('select_node', data.node_id);
  348.                 $('#real_title').html(data.titleHTML);
  349.                 $('#real_content').html(data.textHTML);
  350.                 $('#static_link').attr("href", data.staticlinkHREF);
  351.                 document.title = combine_systemtitle_and_pagetitle(getOidPlusSystemTitle(), data.titleHTML.html_entity_decode());
  352.         } catch (err) {
  353.                 popstate_running = false;
  354.         } finally {
  355.                 popstate_running = false;
  356.         }
  357. });
  358.  
  359. $(document).ready(function () {
  360.         /*
  361.         window.onbeforeunload = function(e) {
  362.                 // TODO: This won't be called because TinyMCE overrides it??
  363.                 // TODO: when the user accepted the query in performCloseQueryCB(), then the message will be shown again by the browser!
  364.                 if (!performCloseQueryCB()) {
  365.                         // Cancel the event
  366.                         e.preventDefault(); // If you prevent default behavior in Mozilla Firefox prompt will always be shown
  367.                         // Chrome requires returnValue to be set
  368.                         e.returnValue = '';
  369.                 } else {
  370.                         // the absence of a returnValue property on the event will guarantee the browser unload happens
  371.                         delete e['returnValue'];
  372.                 }
  373.         };
  374.         */
  375.  
  376.         if (typeof oidplus_menu_width_uservalue !== 'undefined') {
  377.                 oidplus_menu_width = oidplus_menu_width_uservalue;
  378.         }
  379.  
  380.         // --- JsTree
  381.  
  382.         $('#oidtree')
  383.         .jstree({
  384.                 plugins: ['massload','search','conditionalselect'],
  385.                 'core' : {
  386.                         'data' : {
  387.                                 "url" : getTreeLoadURL(),
  388.                                 "data" : function (node) {
  389.                                         return { "id" : node.id };
  390.                                 }
  391.                         },
  392.                         "multiple": false
  393.                 },
  394.                 'conditionalselect' : function (node) {
  395.                         if (node.original.conditionalselect !== undefined) {
  396.                                 return eval(node.original.conditionalselect);
  397.                         } else {
  398.                                 return performCloseQueryCB();
  399.                         }
  400.                 },
  401.         })
  402.         .on('ready.jstree', function (e, data) {
  403.                 var url = new URL(window.location.href);
  404.                 var goto = url.searchParams.get("goto");
  405.                 if (goto == null) goto = "oidplus:system"; // the page was not called with ?goto=...
  406.                 $("#gotoedit").val(goto);
  407.  
  408.                 // By setting current_node, select_node() will not cause ajax.php?action=get_description to load (since we already loaded the first static content via PHP, for search engines mainly)
  409.                 // But then we need to set the history state manually
  410.                 current_node = goto;
  411.                 window.history.replaceState({
  412.                         "node_id":goto,
  413.                         "titleHTML":$('#real_title').html(),
  414.                         "textHTML":$('#real_content').html(),
  415.                         "staticlinkHREF":"index.php?goto="+encodeURIComponent(goto),
  416.                 }, $('#real_title').html(), "?goto="+encodeURIComponent(goto));
  417.  
  418.                 if (goto != null) data.instance.select_node([goto]);
  419.  
  420.                 setTimeout(glayoutWorkaroundAC, 100);
  421.                 setTimeout(glayoutWorkaroundB, 100);
  422.         })
  423.         .on('select_node.jstree', function (node, selected, event) {
  424.                 mobileNavClose();
  425.  
  426.                 var id = selected.node.id;
  427.                 if ((!popstate_running) && (current_node != id)) {
  428.                         // 4th argument: we force the reload (because in the
  429.                         // conditional select above, we already asked if
  430.                         // tinyMCE needs to be saved)
  431.                         openOidInPanel(id, false, '', true);
  432.                 }
  433.         });
  434.  
  435.         // --- Layout
  436.  
  437.         $("#system_title_menu")[0].style.display = "block";
  438.  
  439.         var tmpObjectTree = _L("OBJECT TREE").replace(/(.{1})/g,"$1<br>");
  440.         tmpObjectTree = tmpObjectTree.substring(0, tmpObjectTree.length-"<br>".length);
  441.  
  442.         $('#oidtree').addClass('ui-layout-west');
  443.         $('#content_window').addClass('ui-layout-center');
  444.         $('#system_title_bar').addClass('ui-layout-north');
  445.         glayout = $('#frames').layout({
  446.                 north__size:                  40,
  447.                 north__slidable:              false,
  448.                 north__closable:              false,
  449.                 north__resizable:             false,
  450.                 west__size:                   oidplus_menu_width,
  451.                 west__spacing_closed:         20,
  452.                 west__togglerLength_closed:   230,
  453.                 west__togglerAlign_closed:    "center",
  454.                 west__togglerContent_closed:  tmpObjectTree,
  455.                 west__togglerTip_closed:      _L("Open & Pin Menu"),
  456.                 west__sliderTip:              _L("Slide Open Menu"),
  457.                 west__slideTrigger_open:      "mouseover",
  458.                 center__maskContents:         true, // IMPORTANT - enable iframe masking
  459.                 onresize_start:                           function() { if (typeof handle_glayout_onresize_start == 'function') handle_glayout_onresize_start(); }
  460.         });
  461.  
  462.         $("#gotobox").addClass("mobilehidden");
  463.         $("#languageBox").addClass("mobilehidden");
  464.         $("#gotobox")[0].style.display = "block";
  465.         $('#gotoedit').keypress(function(event) {
  466.                 var keycode = (event.keyCode ? event.keyCode : event.which);
  467.                 if (keycode == '13') {
  468.                         gotoButtonClicked();
  469.                 }
  470.         });
  471.  
  472.         if (typeof Bs5Utils !== "undefined") {
  473.                 Bs5Utils.defaults.toasts.position = 'top-center';
  474.                 Bs5Utils.defaults.toasts.stacking = true;
  475.                 bs5Utils = new Bs5Utils();
  476.         }
  477. });
  478.  
  479. // can be overridden if necessary
  480. var handle_glayout_onresize_start = undefined;
  481.  
  482. function glayoutWorkaroundAC() {
  483.         // "Bug A": Sometimes, the design is completely destroyed after reloading the page. It does not help when glayout.resizeAll()
  484.         //          is called at the beginning (e.g. during the ready function), and it does not help if we wait 500ms.
  485.         //          So we do it all the time. It has probably something to do with slow loading times, since the error
  486.         //          does only appear when the page is "blank" for a short while while it is loading.
  487.         glayout.resizeAll();
  488.  
  489.         // "Bug C": With Firefox (And sometimes with Chrome), there is a gap between the content-window (including scroll bars)
  490.         //          and the right corner of the screen. Removing the explicit width solves this problem.
  491.         $("#content_window")[0].style.removeProperty("width");
  492.  
  493.         setTimeout(glayoutWorkaroundAC, 100);
  494. }
  495.  
  496. function glayoutWorkaroundB() {
  497.         // "Bug B": Sometimes, after reload, weird space between oidtree and content window, because oidtree has size of 438px
  498.         $("#oidtree")[0].style.width = oidplus_menu_width + "px";
  499. }
  500.  
  501. function mobileNavClose() {
  502.         if ($("#system_title_menu").is(":hidden")) {
  503.                 return;
  504.         }
  505.  
  506.         $("#oidtree").slideUp("medium").promise().done(function() {
  507.                 $("#oidtree").addClass("ui-layout-west");
  508.                 $("#oidtree").show();
  509. //              $("#gotobox").hide();
  510. //              $("#languageBox").hide();
  511.                 $("#gotobox").addClass("mobilehidden");
  512.                 $("#languageBox").addClass("mobilehidden");
  513.         });
  514.         $("#system_title_menu").removeClass("active");
  515. }
  516.  
  517. function mobileNavOpen() {
  518.         $("#oidtree").hide();
  519.         $("#oidtree").removeClass("ui-layout-west");
  520.         $("#oidtree").slideDown("medium");
  521. //      $("#gotobox").show();
  522. //      $("#languageBox").show();
  523.         $("#gotobox").removeClass("mobilehidden");
  524.         $("#languageBox").removeClass("mobilehidden");
  525.         $("#system_title_menu").addClass("active");
  526. }
  527.  
  528. function mobileNavButtonClick(sender) {
  529.         if ($("#oidtree").hasClass("ui-layout-west")) {
  530.                 mobileNavOpen();
  531.         } else {
  532.                 mobileNavClose();
  533.         }
  534. }
  535.  
  536. function mobileNavButtonHover(sender) {
  537.         sender.classList.toggle("hover");
  538. }
  539.  
  540. function gotoButtonClicked() {
  541.         openOidInPanel($("#gotoedit").val(), true);
  542. }
  543.  
  544. function jumpToAnchor(anchor) {
  545.         window.location.href = "#" + anchor;
  546. }
  547.  
  548. function getCookie(cname) {
  549.         // Source: https://www.w3schools.com/js/js_cookies.asp
  550.         var name = cname + "=";
  551.         var decodedCookie = decodeURIComponent(document.cookie);
  552.         var ca = decodedCookie.split(';');
  553.         for(var i = 0; i <ca.length; i++) {
  554.                 var c = ca[i];
  555.                 while (c.charAt(0) == ' ') {
  556.                         c = c.substring(1);
  557.                 }
  558.                 if (c.indexOf(name) == 0) {
  559.                         return c.substring(name.length, c.length);
  560.                 }
  561.         }
  562.         return undefined;
  563. }
  564.  
  565. function setCookie(cname, cvalue, exdays, path) {
  566.         var d = new Date();
  567.         d.setTime(d.getTime() + (exdays*24*60*60*1000));
  568.         var expires = exdays == 0 ? "" : "; expires="+d.toUTCString();
  569.         document.cookie = cname + "=" + cvalue + expires + ";path=" + path + ";SameSite=" + samesite_policy;
  570. }
  571.  
  572. function setLanguage(lngid) {
  573.         setCookie('LANGUAGE', lngid, 0/*Until browser closes*/, location.pathname);
  574.  
  575.         $(".lng_flag").each(function(){
  576.                 $(this).addClass("picture_ghost");
  577.         });
  578.         $("#lng_flag_"+$.escapeSelector(lngid)).removeClass("picture_ghost");
  579.  
  580.         if (isInternetExplorer()) {
  581.                 // Internet Explorer has problems with sending new cookies to new AJAX requests, so we reload the page completely
  582.                 window.location.reload();
  583.         } else {
  584.                 // TODO: Small detail: The "Go" button also needs to be re-translated
  585.                 reloadContent();
  586.                 mobileNavClose();
  587.         }
  588. }
  589.  
  590. function getCurrentLang() {
  591.         // Note: If the argument "?lang=" is used, PHP will automatically set a Cookie, so it is OK when we only check for the cookie
  592.         var lang = getCookie('LANGUAGE'); // do not translate
  593.         return (typeof lang != 'undefined') ? lang : DEFAULT_LANGUAGE; // do not translate
  594. }
  595.  
  596. function _L() {
  597.         var args = Array.prototype.slice.call(arguments);
  598.         var str = args.shift().trim();
  599.  
  600.         var tmp = "";
  601.         if (typeof language_messages[getCurrentLang()] == 'undefined') { // do not translate
  602.                 tmp = str;
  603.         } else {
  604.                 var msg = language_messages[getCurrentLang()][str];
  605.                 if (typeof msg != 'undefined') { // do not translate
  606.                         tmp = msg;
  607.                 } else {
  608.                         tmp = str;
  609.                 }
  610.         }
  611.  
  612.         tmp = tmp.replace('###', language_tblprefix);
  613.  
  614.         var n = 1;
  615.         while (args.length > 0) {
  616.                 var val = args.shift();
  617.                 tmp = tmp.replace("%"+n, val);
  618.                 n++;
  619.         }
  620.  
  621.         tmp = tmp.replace("%%", "%");
  622.  
  623.         return tmp;
  624. }
  625.  
  626. function show_waiting_anim() {
  627.         $("#loading").show();
  628. }
  629.  
  630. function hide_waiting_anim() {
  631.         $("#loading").hide();
  632. }
  633.  
  634. /* Mini-framework to abort all AJAX requests if a new request is made */
  635.  
  636. $.xhrPool = [];
  637. $.xhrPool.add = function(jqXHR) {
  638.         $.xhrPool.push(jqXHR);
  639. }
  640. $.xhrPool.remove = function(jqXHR) {
  641.         var index = $.xhrPool.indexOf(jqXHR);
  642.         if (index > -1) {
  643.                 $.xhrPool.splice(index, 1);
  644.         }
  645. };
  646. $.xhrPool.abortAll = function() {
  647.         var calls = Array.from($.xhrPool);
  648.         $.each(calls, function(key, value) {
  649.                 value.abort();
  650.         });
  651. }
  652.  
  653. /* Misc functions */
  654.  
  655. function isNull(val, def) {
  656.         // For compatibility with Internet Explorer, use isNull(a,b) instead of a??b
  657.         if (val == null) {
  658.                 // since null==undefined, this also works with undefined
  659.                 return def;
  660.         } else {
  661.                 return val;
  662.         }
  663. }
  664.  
  665. /* Individual alert types */
  666. /* TODO: alert() blocks program flow, but alertSuccess() not! Will the mass change have negative effects (e.g. a redirect without the user seeing the toast)? */
  667.  
  668. function alertSuccess(txt) {
  669.         if (typeof bs5Utils !== "undefined") {
  670.                 bs5Utils.Snack.show('success', _L(txt), delay = 5000, dismissible = true);
  671.         } else {
  672.                 alert(txt);
  673.         }
  674. }
  675.  
  676. function alertWarning(txt) {
  677.         // TODO: as toast?
  678.         alert(txt);
  679. }
  680.  
  681. function alertError(txt) {
  682.         // TODO: as toast?
  683.         alert(txt);
  684. }
  685.