]> git.wh0rd.org - tt-rss.git/blobdiff - js/functions.js
remove ok = confirm() thing
[tt-rss.git] / js / functions.js
old mode 100644 (file)
new mode 100755 (executable)
index c75fb95..29dbe70
@@ -1,8 +1,12 @@
-var loading_progress = 0;
-var sanity_check_done = false;
-var init_params = {};
-var _label_base_index = -1024;
-var notify_hide_timerid = false;
+/* global dijit, __ */
+
+let init_params = {};
+let _label_base_index = -1024;
+let loading_progress = 0;
+let notify_hide_timerid = false;
+
+let hotkey_prefix = 0;
+let hotkey_prefix_pressed = false;
 
 Ajax.Base.prototype.initialize = Ajax.Base.prototype.initialize.wrap(
        function (callOriginal, options) {
@@ -22,75 +26,75 @@ Ajax.Base.prototype.initialize = Ajax.Base.prototype.initialize.wrap(
        }
 );
 
+/* xhr shorthand helpers */
+
+function xhrPost(url, params, complete) {
+       console.log("xhrPost:", params);
+    return new Ajax.Request(url, {
+        parameters: params,
+        onComplete: complete
+    });
+}
+
+function xhrJson(url, params, complete) {
+    return xhrPost(url, params, (reply) => {
+        try {
+            const obj = JSON.parse(reply.responseText);
+            complete(obj);
+        } catch (e) {
+            console.error("xhrJson", e, reply);
+            complete(null);
+        }
+
+    })
+}
+
 /* add method to remove element from array */
 
 Array.prototype.remove = function(s) {
-       for (var i=0; i < this.length; i++) {
+       for (let i=0; i < this.length; i++) {
                if (s == this[i]) this.splice(i, 1);
        }
 };
 
-/* create console.log if it doesn't exist */
-
-if (!window.console) console = {};
-console.log = console.log || function(msg) { };
-console.warn = console.warn || function(msg) { };
-console.error = console.error || function(msg) { };
+function report_error(message, filename, lineno, colno, error) {
+       exception_error(error, null, filename, lineno);
+}
 
-function exception_error(location, e, ext_info) {
-       var msg = format_exception_error(location, e);
+function exception_error(e, e_compat, filename, lineno, colno) {
+       if (typeof e == "string") e = e_compat;
 
-       if (!ext_info) ext_info = false;
+       if (!e) return; // no exception object, nothing to report.
 
        try {
-
-               if (ext_info) {
-                       if (ext_info.responseText) {
-                               ext_info = ext_info.responseText;
-                       }
-               }
+               console.error(e);
+               const msg = e.toString();
 
                try {
-                       new Ajax.Request("backend.php", {
-                               parameters: {op: "rpc", method: "log", logmsg: msg},
-                               onComplete: function (transport) {
-                                       console.log(transport.responseText);
-                               } });
-
-               } catch (eii) {
-                       console.log("Exception while trying to log the error.");
-                       console.log(eii);
-               }
-
-               msg += "<p>"+ __("The error will be reported to the configured log destination.") +
-                       "</p>";
-
-               var content = "<div class=\"fatalError\">" +
-                       "<pre>" + msg + "</pre>";
-
-               content += "<form name=\"exceptionForm\" id=\"exceptionForm\" target=\"_blank\" "+
-                 "action=\"http://tt-rss.org/report.php\" method=\"POST\">";
-
-               content += "<textarea style=\"display : none\" name=\"message\">" + msg + "</textarea>";
-               content += "<textarea style=\"display : none\" name=\"params\">N/A</textarea>";
+                       xhrPost("backend.php",
+                               {op: "rpc", method: "log",
+                                       file: e.fileName ? e.fileName : filename,
+                                       line: e.lineNumber ? e.lineNumber : lineno,
+                                       msg: msg, context: e.stack},
+                               (transport) => {
+                                       console.warn(transport.responseText);
+                               });
 
-               if (ext_info) {
-                       content += "<div><b>Additional information:</b></div>" +
-                       "<textarea name=\"xinfo\" readonly=\"1\">" + ext_info + "</textarea>";
+               } catch (e) {
+                       console.error("Exception while trying to log the error.", e);
                }
 
-               content += "<div><b>Stack trace:</b></div>" +
-                       "<textarea name=\"stack\" readonly=\"1\">" + e.stack + "</textarea>";
+               let content = "<div class='fatalError'><p>" + msg + "</p>";
 
-               content += "</form>";
+               if (e.stack) {
+                       content += "<div><b>Stack trace:</b></div>" +
+                               "<textarea name=\"stack\" readonly=\"1\">" + e.stack + "</textarea>";
+               }
 
                content += "</div>";
 
                content += "<div class='dlgButtons'>";
 
-               content += "<button dojoType=\"dijit.form.Button\""+
-                               "onclick=\"dijit.byId('exceptionDlg').report()\">" +
-                               __('Report to tt-rss.org') + "</button> ";
                content += "<button dojoType=\"dijit.form.Button\" "+
                                "onclick=\"dijit.byId('exceptionDlg').hide()\">" +
                                __('Close') + "</button>";
@@ -99,98 +103,31 @@ function exception_error(location, e, ext_info) {
                if (dijit.byId("exceptionDlg"))
                        dijit.byId("exceptionDlg").destroyRecursive();
 
-               var dialog = new dijit.Dialog({
+               const dialog = new dijit.Dialog({
                        id: "exceptionDlg",
                        title: "Unhandled exception",
                        style: "width: 600px",
-                       report: function() {
-                               if (confirm(__("Are you sure to report this exception to tt-rss.org? The report will include your browser information. Your IP would be saved in the database."))) {
-
-                                       document.forms['exceptionForm'].params.value = $H({
-                                               browserName: navigator.appName,
-                                               browserVersion: navigator.appVersion,
-                                               browserPlatform: navigator.platform,
-                                               browserCookies: navigator.cookieEnabled,
-                                       }).toQueryString();
-
-                                       document.forms['exceptionForm'].submit();
-
-                               }
-                       },
                        content: content});
 
                dialog.show();
 
        } catch (ei) {
-               console.log("Exception while trying to report an exception. Oh boy.");
-               console.log(ei);
-               console.log("Original exception:");
-               console.log(e);
-
-               msg += "\n\nAdditional exception caught while trying to show the error dialog.\n\n" +  format_exception_error('exception_error', ei);
-
-               try {
-                       new Ajax.Request("backend.php", {
-                               parameters: {op: "rpc", method: "log", logmsg: msg},
-                               onComplete: function (transport) {
-                                       console.log(transport.responseText);
-                               } });
-
-               } catch (eii) {
-                       console.log("Third exception while trying to log the error! Seriously?");
-                       console.log(eii);
-               }
-
-               msg += "\n\nThe error will be reported to the configured log destination.";
-
-               alert(msg);
-       }
-
-}
-
-function format_exception_error(location, e) {
-       var msg;
-
-       if (e.fileName) {
-               var base_fname = e.fileName.substring(e.fileName.lastIndexOf("/") + 1);
+               console.error("Exception while trying to report an exception:", ei);
+               console.error("Original exception:", e);
 
-               msg = "Exception: " + e.name + ", " + e.message +
-                       "\nFunction: " + location + "()" +
-                       "\nLocation: " + base_fname + ":" + e.lineNumber;
-
-       } else if (e.description) {
-               msg = "Exception: " + e.description + "\nFunction: " + location + "()";
-       } else {
-               msg = "Exception: " + e + "\nFunction: " + location + "()";
+               alert("Exception occured while trying to report an exception.\n" +
+                       ei.stack + "\n\nOriginal exception:\n" + e.stack);
        }
 
-       console.error("EXCEPTION: " + msg);
-
-       return msg;
 }
 
 function param_escape(arg) {
-       if (typeof encodeURIComponent != 'undefined')
-               return encodeURIComponent(arg);
-       else
-               return escape(arg);
-}
-
-function param_unescape(arg) {
-       if (typeof decodeURIComponent != 'undefined')
-               return decodeURIComponent(arg);
-       else
-               return unescape(arg);
-}
-
-
-function hide_notify() {
-       Element.hide('notify');
+       return encodeURIComponent(arg);
 }
 
 function notify_real(msg, no_hide, n_type) {
 
-       var n = $("notify");
+       const n = $("notify");
 
        if (!n) return;
 
@@ -199,12 +136,11 @@ function notify_real(msg, no_hide, n_type) {
        }
 
        if (msg == "") {
-               if (Element.visible(n)) {
-                       notify_hide_timerid = window.setTimeout("hide_notify()", 0);
+               if (n.hasClassName("visible")) {
+                       notify_hide_timerid = window.setTimeout(function() {
+                               n.removeClassName("visible") }, 0);
                }
                return;
-       } else {
-               Element.show(n);
        }
 
        /* types:
@@ -218,30 +154,40 @@ function notify_real(msg, no_hide, n_type) {
 
        msg = "<span class=\"msg\"> " + __(msg) + "</span>";
 
-       if (n_type == 1) {
-               n.className = "notify";
-       } else if (n_type == 2) {
-               n.className = "notify progress";
-               msg = "<span><img src='images/indicator_white.gif'></span>" + msg;
+       if (n_type == 2) {
+               msg = "<span><img src=\""+getInitParam("icon_indicator_white")+"\"></span>" + msg;
                no_hide = true;
        } else if (n_type == 3) {
-               n.className = "notify error";
-               msg = "<span><img src='images/alert.png'></span>" + msg;
+               msg = "<span><img src=\""+getInitParam("icon_alert")+"\"></span>" + msg;
        } else if (n_type == 4) {
-               n.className = "notify info";
-               msg = "<span><img src='images/information.png'></span>" + msg;
+               msg = "<span><img src=\""+getInitParam("icon_information")+"\"></span>" + msg;
        }
 
-       msg += " <span><img src=\"images/cross.png\" class=\"close\" title=\"" +
+       msg += " <span><img src=\""+getInitParam("icon_cross")+"\" class=\"close\" title=\"" +
                __("Click to close") + "\" onclick=\"notify('')\"></span>";
 
-//     msg = "<img src='images/live_com_loading.gif'> " + msg;
-
        n.innerHTML = msg;
 
-       if (!no_hide) {
-               notify_hide_timerid = window.setTimeout("hide_notify()", 5*1000);
-       }
+       window.setTimeout(function() {
+               // goddamnit firefox
+               if (n_type == 2) {
+               n.className = "notify notify_progress visible";
+                       } else if (n_type == 3) {
+                       n.className = "notify notify_error visible";
+                       msg = "<span><img src='images/alert.png'></span>" + msg;
+               } else if (n_type == 4) {
+                       n.className = "notify notify_info visible";
+               } else {
+                       n.className = "notify visible";
+               }
+
+               if (!no_hide) {
+                       notify_hide_timerid = window.setTimeout(function() {
+                               n.removeClassName("visible") }, 5*1000);
+               }
+
+       }, 10);
+
 }
 
 function notify(msg, no_hide) {
@@ -263,7 +209,7 @@ function notify_info(msg, no_hide) {
 
 function setCookie(name, value, lifetime, path, domain, secure) {
 
-       var d = false;
+       let d = false;
 
        if (lifetime) {
                d = new Date();
@@ -296,9 +242,9 @@ function delCookie(name, path, domain) {
 
 function getCookie(name) {
 
-       var dc = document.cookie;
-       var prefix = name + "=";
-       var begin = dc.indexOf("; " + prefix);
+       const dc = document.cookie;
+       const prefix = name + "=";
+       let begin = dc.indexOf("; " + prefix);
        if (begin == -1) {
            begin = dc.indexOf(prefix);
            if (begin != 0) return null;
@@ -306,7 +252,7 @@ function getCookie(name) {
        else {
            begin += 2;
        }
-       var end = document.cookie.indexOf(";", begin);
+       let end = document.cookie.indexOf(";", begin);
        if (end == -1) {
            end = dc.length;
        }
@@ -325,53 +271,14 @@ function gotoMain() {
        document.location.href = "index.php";
 }
 
-/** * @(#)isNumeric.js * * Copyright (c) 2000 by Sundar Dorai-Raj
-  * * @author Sundar Dorai-Raj
-  * * Email: sdoraira@vt.edu
-  * * This program is free software; you can redistribute it and/or
-  * * modify it under the terms of the GNU General Public License
-  * * as published by the Free Software Foundation; either version 2
-  * * of the License, or (at your option) any later version,
-  * * provided that any use properly credits the author.
-  * * This program is distributed in the hope that it will be useful,
-  * * but WITHOUT ANY WARRANTY; without even the implied warranty of
-  * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-  * * GNU General Public License for more details at http://www.gnu.org * * */
-
-  var numbers=".0123456789";
-  function isNumeric(x) {
-    // is x a String or a character?
-    if(x.length>1) {
-      // remove negative sign
-      x=Math.abs(x)+"";
-      for(var j=0;j<x.length;j++) {
-        // call isNumeric recursively for each character
-        number=isNumeric(x.substring(j,j+1));
-        if(!number) return number;
-      }
-      return number;
-    }
-    else {
-      // if x is number return true
-      if(numbers.indexOf(x)>=0) return true;
-      return false;
-    }
-  }
-
-
 function toggleSelectRowById(sender, id) {
-       var row = $(id);
-       return toggleSelectRow(sender, row);
-}
-
-function toggleSelectListRow(sender) {
-       var row = sender.parentNode;
+       const row = $(id);
        return toggleSelectRow(sender, row);
 }
 
 /* this is for dijit Checkbox */
 function toggleSelectListRow2(sender) {
-       var row = sender.domNode.parentNode;
+       const row = sender.domNode.parentNode;
        return toggleSelectRow(sender, row);
 }
 
@@ -415,28 +322,15 @@ function checkboxToggleElement(elem, id) {
        }
 }
 
-function dropboxSelect(e, v) {
-       for (var i = 0; i < e.length; i++) {
-               if (e[i].value == v) {
-                       e.selectedIndex = i;
-                       break;
-               }
-       }
-}
-
 function getURLParam(param){
        return String(window.location.href).parseQuery()[param];
 }
 
-function closeInfoBox(cleanup) {
-       try {
-               dialog = dijit.byId("infoBox");
+function closeInfoBox() {
+       const dialog = dijit.byId("infoBox");
 
-               if (dialog)     dialog.hide();
+       if (dialog)     dialog.hide();
 
-       } catch (e) {
-               //exception_error("closeInfoBox", e);
-       }
        return false;
 }
 
@@ -445,79 +339,51 @@ function displayDlg(title, id, param, callback) {
 
        notify_progress("Loading, please wait...", true);
 
-       var query = "?op=dlg&method=" +
-               param_escape(id) + "&param=" + param_escape(param);
+       const query = { op: "dlg", method: id, param: param };
 
-       new Ajax.Request("backend.php", {
-               parameters: query,
-               onComplete: function (transport) {
-                       infobox_callback2(transport, title);
-                       if (callback) callback(transport);
-               } });
+       xhrPost("backend.php", query, (transport) => {
+        infobox_callback2(transport, title);
+        if (callback) callback(transport);
+       });
 
        return false;
 }
 
 function infobox_callback2(transport, title) {
-       try {
-               var dialog = false;
+       let dialog = false;
 
-               if (dijit.byId("infoBox")) {
-                       dialog = dijit.byId("infoBox");
-               }
+       if (dijit.byId("infoBox")) {
+               dialog = dijit.byId("infoBox");
+       }
 
-               //console.log("infobox_callback2");
-               notify('');
+       //console.log("infobox_callback2");
+       notify('');
 
-               var content = transport.responseText;
+       const content = transport.responseText;
 
-               if (!dialog) {
-                       dialog = new dijit.Dialog({
-                               title: title,
-                               id: 'infoBox',
-                               style: "width: 600px",
-                               onCancel: function() {
-                                       return true;
-                               },
-                               onExecute: function() {
-                                       return true;
+       if (!dialog) {
+               dialog = new dijit.Dialog({
+                       title: title,
+                       id: 'infoBox',
+                       style: "width: 600px",
+                       onCancel: function() {
+                               return true;
+                       },
+                       onExecute: function() {
+                               return true;
+                       },
+                       onClose: function() {
+                               return true;
                                },
-                               onClose: function() {
-                                       return true;
-                                       },
-                               content: content});
-               } else {
-                       dialog.attr('title', title);
-                       dialog.attr('content', content);
-               }
-
-               dialog.show();
-
-               notify("");
-       } catch (e) {
-               exception_error("infobox_callback2", e);
+                       content: content});
+       } else {
+               dialog.attr('title', title);
+               dialog.attr('content', content);
        }
-}
 
-function filterCR(e, f)
-{
-     var key;
+       dialog.show();
 
-     if(window.event)
-          key = window.event.keyCode;     //IE
-     else
-          key = e.which;     //firefox
-
-       if (key == 13) {
-               if (typeof f != 'undefined') {
-                       f();
-                       return false;
-               } else {
-                       return false;
-               }
-       } else {
-               return true;
-       }
+       notify("");
 }
 
 function getInitParam(key) {
@@ -529,82 +395,75 @@ function setInitParam(key, value) {
 }
 
 function fatalError(code, msg, ext_info) {
-       try {
-
-               if (code == 6) {
-                       window.location.href = "index.php";
-               } else if (code == 5) {
-                       window.location.href = "public.php?op=dbupdate";
-               } else {
+       if (code == 6) {
+               window.location.href = "index.php";
+       } else if (code == 5) {
+               window.location.href = "public.php?op=dbupdate";
+       } else {
 
-                       if (msg == "") msg = "Unknown error";
+               if (msg == "") msg = "Unknown error";
 
-                       if (ext_info) {
-                               if (ext_info.responseText) {
-                                       ext_info = ext_info.responseText;
-                               }
+               if (ext_info) {
+                       if (ext_info.responseText) {
+                               ext_info = ext_info.responseText;
                        }
+               }
 
-                       if (ERRORS && ERRORS[code] && !msg) {
-                               msg = ERRORS[code];
-                       }
+               if (ERRORS && ERRORS[code] && !msg) {
+                       msg = ERRORS[code];
+               }
 
-                       var content = "<div><b>Error code:</b> " + code + "</div>" +
-                               "<p>" + msg + "</p>";
+               let content = "<div><b>Error code:</b> " + code + "</div>" +
+                       "<p>" + msg + "</p>";
 
-                       if (ext_info) {
-                               content = content + "<div><b>Additional information:</b></div>" +
-                                       "<textarea style='width: 100%' readonly=\"1\">" +
-                                       ext_info + "</textarea>";
-                       }
+               if (ext_info) {
+                       content = content + "<div><b>Additional information:</b></div>" +
+                               "<textarea style='width: 100%' readonly=\"1\">" +
+                               ext_info + "</textarea>";
+               }
 
-                       var dialog = new dijit.Dialog({
-                               title: "Fatal error",
-                               style: "width: 600px",
-                               content: content});
+               const dialog = new dijit.Dialog({
+                       title: "Fatal error",
+                       style: "width: 600px",
+                       content: content});
 
-                       dialog.show();
+               dialog.show();
 
-               }
+       }
 
-               return false;
+       return false;
 
-       } catch (e) {
-               exception_error("fatalError", e);
-       }
 }
 
 function filterDlgCheckAction(sender) {
+       const action = sender.value;
 
-       try {
+       const action_param = $("filterDlg_paramBox");
 
-               var action = sender.value;
+       if (!action_param) {
+               console.log("filterDlgCheckAction: can't find action param box!");
+               return;
+       }
 
-               var action_param = $("filterDlg_paramBox");
+       // if selected action supports parameters, enable params field
+       if (action == 4 || action == 6 || action == 7 || action == 9) {
+               new Effect.Appear(action_param, {duration : 0.5});
 
-               if (!action_param) {
-                       console.log("filterDlgCheckAction: can't find action param box!");
-                       return;
-               }
+               Element.hide(dijit.byId("filterDlg_actionParam").domNode);
+               Element.hide(dijit.byId("filterDlg_actionParamLabel").domNode);
+               Element.hide(dijit.byId("filterDlg_actionParamPlugin").domNode);
 
-               // if selected action supports parameters, enable params field
-               if (action == 4 || action == 6 || action == 7) {
-                       new Effect.Appear(action_param, {duration : 0.5});
-                       if (action != 7) {
-                               Element.show(dijit.byId("filterDlg_actionParam").domNode);
-                               Element.hide(dijit.byId("filterDlg_actionParamLabel").domNode);
-                       } else {
-                               Element.show(dijit.byId("filterDlg_actionParamLabel").domNode);
-                               Element.hide(dijit.byId("filterDlg_actionParam").domNode);
-                       }
+               if (action == 7) {
+                       Element.show(dijit.byId("filterDlg_actionParamLabel").domNode);
+               } else if (action == 9) {
+                       Element.show(dijit.byId("filterDlg_actionParamPlugin").domNode);
                } else {
-                       Element.hide(action_param);
+                       Element.show(dijit.byId("filterDlg_actionParam").domNode);
                }
 
-       } catch (e) {
-               exception_error("filterDlgCheckAction", e);
+       } else {
+               Element.hide(action_param);
        }
-
 }
 
 
@@ -613,625 +472,592 @@ function explainError(code) {
 }
 
 function loading_set_progress(p) {
-       try {
-               loading_progress += p;
+       loading_progress += p;
 
-               if (dijit.byId("loading_bar"))
-                       dijit.byId("loading_bar").update({progress: loading_progress});
+       if (dijit.byId("loading_bar"))
+               dijit.byId("loading_bar").update({progress: loading_progress});
 
-               if (loading_progress >= 90)
-                       remove_splash();
+       if (loading_progress >= 90)
+               remove_splash();
 
-       } catch (e) {
-               exception_error("loading_set_progress", e);
-       }
 }
 
 function remove_splash() {
-
-       if (Element.visible("overlay")) {
-               console.log("about to remove splash, OMG!");
-               Element.hide("overlay");
-               console.log("removed splash!");
-       }
-}
-
-function transport_error_check(transport) {
-       try {
-               if (transport.responseXML) {
-                       var error = transport.responseXML.getElementsByTagName("error")[0];
-
-                       if (error) {
-                               var code = error.getAttribute("error-code");
-                               var msg = error.getAttribute("error-msg");
-                               if (code != 0) {
-                                       fatalError(code, msg);
-                                       return false;
-                               }
-                       }
-               }
-       } catch (e) {
-               exception_error("check_for_error_xml", e);
-       }
-       return true;
+       Element.hide("overlay");
 }
 
 function strip_tags(s) {
        return s.replace(/<\/?[^>]+(>|$)/g, "");
 }
 
-function truncate_string(s, length) {
-       if (!length) length = 30;
-       var tmp = s.substring(0, length);
-       if (s.length > length) tmp += "&hellip;";
-       return tmp;
-}
-
 function hotkey_prefix_timeout() {
-       try {
-
-               var date = new Date();
-               var ts = Math.round(date.getTime() / 1000);
-
-               if (hotkey_prefix_pressed && ts - hotkey_prefix_pressed >= 5) {
-                       console.log("hotkey_prefix seems to be stuck, aborting");
-                       hotkey_prefix_pressed = false;
-                       hotkey_prefix = false;
-                       Element.hide('cmdline');
-               }
 
-               setTimeout("hotkey_prefix_timeout()", 1000);
+       const date = new Date();
+       const ts = Math.round(date.getTime() / 1000);
 
-       } catch  (e) {
-               exception_error("hotkey_prefix_timeout", e);
+       if (hotkey_prefix_pressed && ts - hotkey_prefix_pressed >= 5) {
+               console.log("hotkey_prefix seems to be stuck, aborting");
+               hotkey_prefix_pressed = false;
+               hotkey_prefix = false;
+               Element.hide('cmdline');
        }
 }
 
 function uploadIconHandler(rc) {
-       try {
-               switch (rc) {
-                       case 0:
-                               notify_info("Upload complete.");
-                               if (inPreferences()) {
-                                       updateFeedList();
-                               } else {
-                                       setTimeout('updateFeedList(false, false)', 50);
-                               }
-                               break;
-                       case 1:
-                               notify_error("Upload failed: icon is too big.");
-                               break;
-                       case 2:
-                               notify_error("Upload failed.");
-                               break;
-               }
-
-       } catch (e) {
-               exception_error("uploadIconHandler", e);
+       switch (rc) {
+               case 0:
+                       notify_info("Upload complete.");
+                       if (inPreferences()) {
+                               updateFeedList();
+                       } else {
+                               setTimeout('updateFeedList(false, false)', 50);
+                       }
+                       break;
+               case 1:
+                       notify_error("Upload failed: icon is too big.");
+                       break;
+               case 2:
+                       notify_error("Upload failed.");
+                       break;
        }
 }
 
 function removeFeedIcon(id) {
+       if (confirm(__("Remove stored feed icon?"))) {
 
-       try {
+               notify_progress("Removing feed icon...", true);
 
-               if (confirm(__("Remove stored feed icon?"))) {
-                       var query = "backend.php?op=pref-feeds&method=removeicon&feed_id=" + param_escape(id);
+        const query = { op: "pref-feeds", method: "removeicon", feed_id: id };
 
-                       console.log(query);
+               xhrPost("backend.php", query, (transport) => {
+            notify_info("Feed icon removed.");
+            if (inPreferences()) {
+                updateFeedList();
+            } else {
+                setTimeout('updateFeedList(false, false)', 50);
+            }
+        });
+       }
 
-                       notify_progress("Removing feed icon...", true);
+       return false;
+}
 
-                       new Ajax.Request("backend.php", {
-                               parameters: query,
-                               onComplete: function(transport) {
-                                       notify_info("Feed icon removed.");
-                                       if (inPreferences()) {
-                                               updateFeedList();
-                                       } else {
-                                               setTimeout('updateFeedList(false, false)', 50);
-                                       }
-                               } });
+function uploadFeedIcon() {
+       const file = $("icon_file");
+
+       if (file.value.length == 0) {
+               alert(__("Please select an image file to upload."));
+       } else if (confirm(__("Upload new icon for this feed?"))) {
+                       notify_progress("Uploading, please wait...", true);
+                       return true;
                }
 
-               return false;
-       } catch (e) {
-               exception_error("removeFeedIcon", e);
-       }
+       return false;
 }
 
-function uploadFeedIcon() {
+function addLabel(select, callback) {
 
-       try {
+       const caption = prompt(__("Please enter label caption:"), "");
 
-               var file = $("icon_file");
+       if (caption != undefined) {
 
-               if (file.value.length == 0) {
-                       alert(__("Please select an image file to upload."));
-               } else {
-                       if (confirm(__("Upload new icon for this feed?"))) {
-                               notify_progress("Uploading, please wait...", true);
-                               return true;
-                       }
+               if (caption == "") {
+                       alert(__("Can't create label: missing caption."));
+                       return false;
                }
 
-               return false;
+               const query = { op: "pref-labels", method: "add", caption: caption };
+
+               if (select)
+                       Object.extend(query, {output: "select"});
+
+               notify_progress("Loading, please wait...", true);
 
-       } catch (e) {
-               exception_error("uploadFeedIcon", e);
+               xhrPost("backend.php", query, (transport) => {
+            if (callback) {
+                callback(transport);
+            } else if (inPreferences()) {
+                updateLabelList();
+            } else {
+                updateFeedList();
+            }
+        });
        }
+
 }
 
-function addLabel(select, callback) {
+function quickAddFeed() {
+       const query = "backend.php?op=feeds&method=quickAddFeed";
+
+       // overlapping widgets
+       if (dijit.byId("batchSubDlg")) dijit.byId("batchSubDlg").destroyRecursive();
+       if (dijit.byId("feedAddDlg"))   dijit.byId("feedAddDlg").destroyRecursive();
+
+       const dialog = new dijit.Dialog({
+               id: "feedAddDlg",
+               title: __("Subscribe to Feed"),
+               style: "width: 600px",
+               show_error: function(msg) {
+                       const elem = $("fadd_error_message");
+
+                       elem.innerHTML = msg;
+
+                       if (!Element.visible(elem))
+                               new Effect.Appear(elem);
+
+               },
+               execute: function() {
+                       if (this.validate()) {
+                               console.log(dojo.objectToQuery(this.attr('value')));
+
+                               const feed_url = this.attr('value').feed;
+
+                               Element.show("feed_add_spinner");
+                               Element.hide("fadd_error_message");
+
+                               xhrPost("backend.php", this.attr('value'), (transport) => {
+                    try {
+
+                        try {
+                            var reply = JSON.parse(transport.responseText);
+                        } catch (e) {
+                            Element.hide("feed_add_spinner");
+                            alert(__("Failed to parse output. This can indicate server timeout and/or network issues. Backend output was logged to browser console."));
+                            console.log('quickAddFeed, backend returned:' + transport.responseText);
+                            return;
+                        }
+
+                        const rc = reply['result'];
+
+                        notify('');
+                        Element.hide("feed_add_spinner");
+
+                        console.log(rc);
+
+                        switch (parseInt(rc['code'])) {
+                            case 1:
+                                dialog.hide();
+                                notify_info(__("Subscribed to %s").replace("%s", feed_url));
+
+                                updateFeedList();
+                                break;
+                            case 2:
+                                dialog.show_error(__("Specified URL seems to be invalid."));
+                                break;
+                            case 3:
+                                dialog.show_error(__("Specified URL doesn't seem to contain any feeds."));
+                                break;
+                            case 4:
+                                const feeds = rc['feeds'];
+
+                                Element.show("fadd_multiple_notify");
+
+                                const select = dijit.byId("feedDlg_feedContainerSelect");
+
+                                while (select.getOptions().length > 0)
+                                    select.removeOption(0);
+
+                                select.addOption({value: '', label: __("Expand to select feed")});
+
+                                let count = 0;
+                                for (const feedUrl in feeds) {
+                                    select.addOption({value: feedUrl, label: feeds[feedUrl]});
+                                    count++;
+                                }
+
+                                Effect.Appear('feedDlg_feedsContainer', {duration : 0.5});
+
+                                break;
+                            case 5:
+                                dialog.show_error(__("Couldn't download the specified URL: %s").
+                                replace("%s", rc['message']));
+                                break;
+                            case 6:
+                                dialog.show_error(__("XML validation failed: %s").
+                                replace("%s", rc['message']));
+                                break;
+                            case 0:
+                                dialog.show_error(__("You are already subscribed to this feed."));
+                                break;
+                        }
+
+                    } catch (e) {
+                        console.error(transport.responseText);
+                        exception_error(e);
+                    }
+                               });
+                       }
+               },
+               href: query});
 
-       try {
+       dialog.show();
+}
 
-               var caption = prompt(__("Please enter label caption:"), "");
+function createNewRuleElement(parentNode, replaceNode) {
+       const form = document.forms["filter_new_rule_form"];
 
-               if (caption != undefined) {
+       //form.reg_exp.value = form.reg_exp.value.replace(/(<([^>]+)>)/ig,"");
 
-                       if (caption == "") {
-                               alert(__("Can't create label: missing caption."));
-                               return false;
-                       }
+       const query = { op: "pref-filters", method: "printrulename", rule: dojo.formToJson(form) };
 
-                       var query = "?op=pref-labels&method=add&caption=" +
-                               param_escape(caption);
+       xhrPost("backend.php", query, (transport) => {
+               try {
+                       const li = dojo.create("li");
 
-                       if (select)
-                               query += "&output=select";
+                       const cb = dojo.create("input", { type: "checkbox" }, li);
 
-                       notify_progress("Loading, please wait...", true);
+                       new dijit.form.CheckBox({
+                               onChange: function() {
+                                       toggleSelectListRow2(this) },
+                       }, cb);
 
-                       if (inPreferences() && !select) active_tab = "labelConfig";
+                       dojo.create("input", { type: "hidden",
+                               name: "rule[]",
+                               value: dojo.formToJson(form) }, li);
 
-                       new Ajax.Request("backend.php", {
-                               parameters: query,
-                               onComplete: function(transport) {
-                                       if (callback) {
-                                               callback(transport);
-                                       } else if (inPreferences()) {
-                                               updateLabelList();
-                                       } else {
-                                               updateFeedList();
-                                       }
-                       } });
+                       dojo.create("span", {
+                               onclick: function() {
+                                       dijit.byId('filterEditDlg').editRule(this);
+                               },
+                               innerHTML: transport.responseText }, li);
 
+                       if (replaceNode) {
+                               parentNode.replaceChild(li, replaceNode);
+                       } else {
+                               parentNode.appendChild(li);
+                       }
+               } catch (e) {
+                       exception_error(e);
                }
+       });
+}
 
-       } catch (e) {
-               exception_error("addLabel", e);
+function createNewActionElement(parentNode, replaceNode) {
+       const form = document.forms["filter_new_action_form"];
+
+       if (form.action_id.value == 7) {
+               form.action_param.value = form.action_param_label.value;
+       } else if (form.action_id.value == 9) {
+               form.action_param.value = form.action_param_plugin.value;
        }
-}
 
-function quickAddFeed() {
-       try {
-               var query = "backend.php?op=feeds&method=quickAddFeed";
+       const query = { op: "pref-filters", method: "printactionname",
+               action: dojo.formToJson(form) };
 
-               // overlapping widgets
-               if (dijit.byId("batchSubDlg")) dijit.byId("batchSubDlg").destroyRecursive();
-               if (dijit.byId("feedAddDlg"))   dijit.byId("feedAddDlg").destroyRecursive();
+       xhrPost("backend.php", query, (transport) => {
+               try {
+                       const li = dojo.create("li");
 
-               var dialog = new dijit.Dialog({
-                       id: "feedAddDlg",
-                       title: __("Subscribe to Feed"),
-                       style: "width: 600px",
-                       execute: function() {
-                               if (this.validate()) {
-                                       console.log(dojo.objectToQuery(this.attr('value')));
-
-                                       var feed_url = this.attr('value').feed;
-
-                                       Element.show("feed_add_spinner");
-
-                                       new Ajax.Request("backend.php", {
-                                               parameters: dojo.objectToQuery(this.attr('value')),
-                                               onComplete: function(transport) {
-                                                       try {
-
-                                                               var reply = JSON.parse(transport.responseText);
-
-                                                               var rc = reply['result'];
-
-                                                               notify('');
-                                                               Element.hide("feed_add_spinner");
-
-                                                               console.log(rc);
-
-                                                               switch (parseInt(rc['code'])) {
-                                                               case 1:
-                                                                       dialog.hide();
-                                                                       notify_info(__("Subscribed to %s").replace("%s", feed_url));
-
-                                                                       updateFeedList();
-                                                                       break;
-                                                               case 2:
-                                                                       alert(__("Specified URL seems to be invalid."));
-                                                                       break;
-                                                               case 3:
-                                                                       alert(__("Specified URL doesn't seem to contain any feeds."));
-                                                                       break;
-                                                               case 4:
-                                                                       feeds = rc['feeds'];
-
-                                                                       var select = dijit.byId("feedDlg_feedContainerSelect");
-
-                                                                       while (select.getOptions().length > 0)
-                                                                               select.removeOption(0);
-
-                                                                       select.addOption({value: '', label: __("Expand to select feed")});
-
-                                                                       var count = 0;
-                                                                       for (var feedUrl in feeds) {
-                                                                               select.addOption({value: feedUrl, label: feeds[feedUrl]});
-                                                                               count++;
-                                                                       }
-
-                                                                       Effect.Appear('feedDlg_feedsContainer', {duration : 0.5});
-
-                                                                       break;
-                                                               case 5:
-                                                                       alert(__("Couldn't download the specified URL: %s").
-                                                                                       replace("%s", rc['message']));
-                                                                       break;
-                                                               case 6:
-                                                                       alert(__("XML validation failed: %s").
-                                                                                       replace("%s", rc['message']));
-                                                                       break;
-                                                                       break;
-                                                               case 0:
-                                                                       alert(__("You are already subscribed to this feed."));
-                                                                       break;
-                                                               }
-
-                                                       } catch (e) {
-                                                               exception_error("subscribeToFeed", e, transport);
-                                                       }
+                       const cb = dojo.create("input", { type: "checkbox" }, li);
 
-                                               } });
+                       new dijit.form.CheckBox({
+                               onChange: function() {
+                                       toggleSelectListRow2(this) },
+                       }, cb);
 
-                                       }
-                       },
-                       href: query});
+                       dojo.create("input", { type: "hidden",
+                               name: "action[]",
+                               value: dojo.formToJson(form) }, li);
 
-               dialog.show();
-       } catch (e) {
-               exception_error("quickAddFeed", e);
-       }
+                       dojo.create("span", {
+                               onclick: function() {
+                                       dijit.byId('filterEditDlg').editAction(this);
+                               },
+                               innerHTML: transport.responseText }, li);
+
+                       if (replaceNode) {
+                               parentNode.replaceChild(li, replaceNode);
+                       } else {
+                               parentNode.appendChild(li);
+                       }
+
+               } catch (e) {
+                       exception_error(e);
+               }
+       });
 }
 
-function createNewRuleElement(parentNode, replaceNode) {
-       try {
-               var form = document.forms["filter_new_rule_form"];
 
-               form.reg_exp.value = form.reg_exp.value.replace(/(<([^>]+)>)/ig,"");
+function addFilterRule(replaceNode, ruleStr) {
+       if (dijit.byId("filterNewRuleDlg"))
+               dijit.byId("filterNewRuleDlg").destroyRecursive();
+
+       const query = "backend.php?op=pref-filters&method=newrule&rule=" +
+               param_escape(ruleStr);
+
+       const rule_dlg = new dijit.Dialog({
+               id: "filterNewRuleDlg",
+               title: ruleStr ? __("Edit rule") : __("Add rule"),
+               style: "width: 600px",
+               execute: function() {
+                       if (this.validate()) {
+                               createNewRuleElement($("filterDlg_Matches"), replaceNode);
+                               this.hide();
+                       }
+               },
+               href: query});
+
+       rule_dlg.show();
+}
+
+function addFilterAction(replaceNode, actionStr) {
+       if (dijit.byId("filterNewActionDlg"))
+               dijit.byId("filterNewActionDlg").destroyRecursive();
+
+       const query = "backend.php?op=pref-filters&method=newaction&action=" +
+               param_escape(actionStr);
+
+       const rule_dlg = new dijit.Dialog({
+               id: "filterNewActionDlg",
+               title: actionStr ? __("Edit action") : __("Add action"),
+               style: "width: 600px",
+               execute: function() {
+                       if (this.validate()) {
+                               createNewActionElement($("filterDlg_Actions"), replaceNode);
+                               this.hide();
+                       }
+               },
+               href: query});
+
+       rule_dlg.show();
+}
+
+function editFilterTest(query) {
 
-               var query = "backend.php?op=pref-filters&method=printrulename&rule="+
-                       param_escape(dojo.formToJson(form));
+       if (dijit.byId("filterTestDlg"))
+               dijit.byId("filterTestDlg").destroyRecursive();
 
-               console.log(query);
+       var test_dlg = new dijit.Dialog({
+               id: "filterTestDlg",
+               title: "Test Filter",
+               style: "width: 600px",
+               results: 0,
+               limit: 100,
+               max_offset: 10000,
+               getTestResults: function(query, offset) {
+               const updquery = query + "&offset=" + offset + "&limit=" + test_dlg.limit;
 
-               new Ajax.Request("backend.php", {
-                       parameters: query,
-                       onComplete: function (transport) {
+               console.log("getTestResults:" + offset);
+
+               xhrPost("backend.php", updquery, (transport) => {
                                try {
-                                       var li = dojo.create("li");
+                                       const result = JSON.parse(transport.responseText);
 
-                                       var cb = dojo.create("input", { type: "checkbox" }, li);
+                                       if (result && dijit.byId("filterTestDlg") && dijit.byId("filterTestDlg").open) {
+                                               test_dlg.results += result.length;
 
-                                       new dijit.form.CheckBox({
-                                               onChange: function() {
-                                                       toggleSelectListRow2(this) },
-                                       }, cb);
+                                               console.log("got results:" + result.length);
 
-                                       dojo.create("input", { type: "hidden",
-                                               name: "rule[]",
-                                               value: dojo.formToJson(form) }, li);
+                                               $("prefFilterProgressMsg").innerHTML = __("Looking for articles (%d processed, %f found)...")
+                                                       .replace("%f", test_dlg.results)
+                                                       .replace("%d", offset);
 
-                                       dojo.create("span", {
-                                               onclick: function() {
-                                                       dijit.byId('filterEditDlg').editRule(this);
-                                               },
-                                               innerHTML: transport.responseText }, li);
+                                               console.log(offset + " " + test_dlg.max_offset);
 
-                                       if (replaceNode) {
-                                               parentNode.replaceChild(li, replaceNode);
-                                       } else {
-                                               parentNode.appendChild(li);
-                                       }
-                               } catch (e) {
-                                       exception_error("createNewRuleElement", e);
-                               }
-               } });
-       } catch (e) {
-               exception_error("createNewRuleElement", e);
-       }
-}
+                                               for (let i = 0; i < result.length; i++) {
+                                                       const tmp = new Element("table");
+                                                       tmp.innerHTML = result[i];
+                                                       dojo.parser.parse(tmp);
 
-function createNewActionElement(parentNode, replaceNode) {
-       try {
-               var form = document.forms["filter_new_action_form"];
+                                                       $("prefFilterTestResultList").innerHTML += tmp.innerHTML;
+                                               }
 
-               if (form.action_id.value == 7) {
-                       form.action_param.value = form.action_param_label.value;
-               }
+                                               if (test_dlg.results < 30 && offset < test_dlg.max_offset) {
+
+                                                       // get the next batch
+                                                       window.setTimeout(function () {
+                                                               test_dlg.getTestResults(query, offset + test_dlg.limit);
+                                                       }, 0);
 
-               var query = "backend.php?op=pref-filters&method=printactionname&action="+
-                       param_escape(dojo.formToJson(form));
+                                               } else {
+                                                       // all done
 
-               console.log(query);
+                                                       Element.hide("prefFilterLoadingIndicator");
 
-               new Ajax.Request("backend.php", {
-                       parameters: query,
-                       onComplete: function (transport) {
-                               try {
-                                       var li = dojo.create("li");
+                                                       if (test_dlg.results == 0) {
+                                                               $("prefFilterTestResultList").innerHTML = "<tr><td align='center'>No recent articles matching this filter have been found.</td></tr>";
+                                                               $("prefFilterProgressMsg").innerHTML = "Articles matching this filter:";
+                                                       } else {
+                                                               $("prefFilterProgressMsg").innerHTML = __("Found %d articles matching this filter:")
+                                                                       .replace("%d", test_dlg.results);
+                                                       }
 
-                                       var cb = dojo.create("input", { type: "checkbox" }, li);
+                                               }
 
-                                       new dijit.form.CheckBox({
-                                               onChange: function() {
-                                                       toggleSelectListRow2(this) },
-                                       }, cb);
+                                       } else if (!result) {
+                                               console.log("getTestResults: can't parse results object");
 
-                                       dojo.create("input", { type: "hidden",
-                                               name: "action[]",
-                                               value: dojo.formToJson(form) }, li);
+                                               Element.hide("prefFilterLoadingIndicator");
 
-                                       dojo.create("span", {
-                                               onclick: function() {
-                                                       dijit.byId('filterEditDlg').editAction(this);
-                                               },
-                                               innerHTML: transport.responseText }, li);
+                                               notify_error("Error while trying to get filter test results.");
 
-                                       if (replaceNode) {
-                                               parentNode.replaceChild(li, replaceNode);
                                        } else {
-                                               parentNode.appendChild(li);
+                                               console.log("getTestResults: dialog closed, bailing out.");
                                        }
-
                                } catch (e) {
-                                       exception_error("createNewActionElement", e);
+                                       exception_error(e);
                                }
-                       } });
-       } catch (e) {
-               exception_error("createNewActionElement", e);
-       }
-}
 
+                       });
+               },
+               href: query});
 
-function addFilterRule(replaceNode, ruleStr) {
-       try {
-               if (dijit.byId("filterNewRuleDlg"))
-                       dijit.byId("filterNewRuleDlg").destroyRecursive();
+       dojo.connect(test_dlg, "onLoad", null, function(e) {
+               test_dlg.getTestResults(query, 0);
+       });
 
-               var query = "backend.php?op=pref-filters&method=newrule&rule=" +
-                       param_escape(ruleStr);
+       test_dlg.show();
 
-               var rule_dlg = new dijit.Dialog({
-                       id: "filterNewRuleDlg",
-                       title: ruleStr ? __("Edit rule") : __("Add rule"),
-                       style: "width: 600px",
-                       execute: function() {
-                               if (this.validate()) {
-                                       createNewRuleElement($("filterDlg_Matches"), replaceNode);
-                                       this.hide();
-                               }
-                       },
-                       href: query});
+}
 
-               rule_dlg.show();
-       } catch (e) {
-               exception_error("addFilterRule", e);
+function quickAddFilter() {
+       let query = "";
+       if (!inPreferences()) {
+               query = "backend.php?op=pref-filters&method=newfilter&feed=" +
+                       param_escape(getActiveFeedId()) + "&is_cat=" +
+                       param_escape(activeFeedIsCat());
+       } else {
+               query = "backend.php?op=pref-filters&method=newfilter";
        }
-}
 
-function addFilterAction(replaceNode, actionStr) {
-       try {
-               if (dijit.byId("filterNewActionDlg"))
-                       dijit.byId("filterNewActionDlg").destroyRecursive();
+       console.log(query);
 
-               var query = "backend.php?op=pref-filters&method=newaction&action=" +
-                       param_escape(actionStr);
+       if (dijit.byId("feedEditDlg"))
+               dijit.byId("feedEditDlg").destroyRecursive();
 
-               var rule_dlg = new dijit.Dialog({
-                       id: "filterNewActionDlg",
-                       title: actionStr ? __("Edit action") : __("Add action"),
-                       style: "width: 600px",
-                       execute: function() {
-                               if (this.validate()) {
-                                       createNewActionElement($("filterDlg_Actions"), replaceNode);
-                                       this.hide();
-                               }
-                       },
-                       href: query});
+       if (dijit.byId("filterEditDlg"))
+               dijit.byId("filterEditDlg").destroyRecursive();
 
-               rule_dlg.show();
-       } catch (e) {
-               exception_error("addFilterAction", e);
-       }
-}
+       const dialog = new dijit.Dialog({
+               id: "filterEditDlg",
+               title: __("Create Filter"),
+               style: "width: 600px",
+               test: function() {
+                       const query = "backend.php?" + dojo.formToQuery("filter_new_form") + "&savemode=test";
 
-function quickAddFilter() {
-       try {
-               var query = "";
-               if (!inPreferences()) {
-                       query = "backend.php?op=pref-filters&method=newfilter&feed=" +
-                               param_escape(getActiveFeedId()) + "&is_cat=" +
-                               param_escape(activeFeedIsCat());
-               } else {
-                       query = "backend.php?op=pref-filters&method=newfilter";
-               }
-
-               console.log(query);
-
-               if (dijit.byId("feedEditDlg"))
-                       dijit.byId("feedEditDlg").destroyRecursive();
-
-               if (dijit.byId("filterEditDlg"))
-                       dijit.byId("filterEditDlg").destroyRecursive();
-
-               dialog = new dijit.Dialog({
-                       id: "filterEditDlg",
-                       title: __("Create Filter"),
-                       style: "width: 600px",
-                       test: function() {
-                               var query = "backend.php?" + dojo.formToQuery("filter_new_form") + "&savemode=test";
+                       editFilterTest(query);
+               },
+               selectRules: function(select) {
+                       $$("#filterDlg_Matches input[type=checkbox]").each(function(e) {
+                               e.checked = select;
+                               if (select)
+                                       e.parentNode.addClassName("Selected");
+                               else
+                                       e.parentNode.removeClassName("Selected");
+                       });
+               },
+               selectActions: function(select) {
+                       $$("#filterDlg_Actions input[type=checkbox]").each(function(e) {
+                               e.checked = select;
 
-                               if (dijit.byId("filterTestDlg"))
-                                       dijit.byId("filterTestDlg").destroyRecursive();
+                               if (select)
+                                       e.parentNode.addClassName("Selected");
+                               else
+                                       e.parentNode.removeClassName("Selected");
 
-                               var test_dlg = new dijit.Dialog({
-                                       id: "filterTestDlg",
-                                       title: "Test Filter",
-                                       style: "width: 600px",
-                                       href: query});
+                       });
+               },
+               editRule: function(e) {
+                       const li = e.parentNode;
+                       const rule = li.getElementsByTagName("INPUT")[1].value;
+                       addFilterRule(li, rule);
+               },
+               editAction: function(e) {
+                       const li = e.parentNode;
+                       const action = li.getElementsByTagName("INPUT")[1].value;
+                       addFilterAction(li, action);
+               },
+               addAction: function() { addFilterAction(); },
+               addRule: function() { addFilterRule(); },
+               deleteAction: function() {
+                       $$("#filterDlg_Actions li[class*=Selected]").each(function(e) { e.parentNode.removeChild(e) });
+               },
+               deleteRule: function() {
+                       $$("#filterDlg_Matches li[class*=Selected]").each(function(e) { e.parentNode.removeChild(e) });
+               },
+               execute: function() {
+                       if (this.validate()) {
+
+                               const query = dojo.formToQuery("filter_new_form");
+
+                               xhrPost("backend.php", query, (transport) => {
+                                       if (inPreferences()) {
+                                               updateFilterList();
+                                       }
 
-                               test_dlg.show();
-                       },
-                       selectRules: function(select) {
-                               $$("#filterDlg_Matches input[type=checkbox]").each(function(e) {
-                                       e.checked = select;
-                                       if (select)
-                                               e.parentNode.addClassName("Selected");
-                                       else
-                                               e.parentNode.removeClassName("Selected");
+                                       dialog.hide();
                                });
-                       },
-                       selectActions: function(select) {
-                               $$("#filterDlg_Actions input[type=checkbox]").each(function(e) {
-                                       e.checked = select;
+                       }
+               },
+               href: query});
 
-                                       if (select)
-                                               e.parentNode.addClassName("Selected");
-                                       else
-                                               e.parentNode.removeClassName("Selected");
+       if (!inPreferences()) {
+               const selectedText = getSelectionText();
 
-                               });
-                       },
-                       editRule: function(e) {
-                               var li = e.parentNode;
-                               var rule = li.getElementsByTagName("INPUT")[1].value;
-                               addFilterRule(li, rule);
-                       },
-                       editAction: function(e) {
-                               var li = e.parentNode;
-                               var action = li.getElementsByTagName("INPUT")[1].value;
-                               addFilterAction(li, action);
-                       },
-                       addAction: function() { addFilterAction(); },
-                       addRule: function() { addFilterRule(); },
-                       deleteAction: function() {
-                               $$("#filterDlg_Actions li.[class*=Selected]").each(function(e) { e.parentNode.removeChild(e) });
-                       },
-                       deleteRule: function() {
-                               $$("#filterDlg_Matches li.[class*=Selected]").each(function(e) { e.parentNode.removeChild(e) });
-                       },
-                       execute: function() {
-                               if (this.validate()) {
+               var lh = dojo.connect(dialog, "onLoad", function(){
+                       dojo.disconnect(lh);
 
-                                       var query = dojo.formToQuery("filter_new_form");
+                       if (selectedText != "") {
 
-                                       console.log(query);
+                               const feed_id = activeFeedIsCat() ? 'CAT:' + parseInt(getActiveFeedId()) :
+                                       getActiveFeedId();
 
-                                       new Ajax.Request("backend.php", {
-                                               parameters: query,
-                                               onComplete: function (transport) {
-                                                       if (inPreferences()) {
-                                                               updateFilterList();
-                                                       }
+                               const rule = { reg_exp: selectedText, feed_id: [feed_id], filter_type: 1 };
 
-                                                       dialog.hide();
-                                       } });
-                               }
-                       },
-                       href: query});
+                               addFilterRule(null, dojo.toJson(rule));
 
-               if (!inPreferences()) {
-                       var lh = dojo.connect(dialog, "onLoad", function(){
-                               dojo.disconnect(lh);
+                       } else {
 
-                               var query = "op=rpc&method=getlinktitlebyid&id=" + getActiveArticleId();
+                               const query = { op: "rpc", method: "getlinktitlebyid", id: getActiveArticleId() };
 
-                               new Ajax.Request("backend.php", {
-                               parameters: query,
-                               onComplete: function(transport) {
-                                       var reply = JSON.parse(transport.responseText);
+                               xhrPost("backend.php", query, (transport) => {
+                                       const reply = JSON.parse(transport.responseText);
 
-                                       var title = false;
+                                       let title = false;
 
-                                       if (reply && reply) title = reply.title;
+                                       if (reply && reply.title) title = reply.title;
 
                                        if (title || getActiveFeedId() || activeFeedIsCat()) {
 
                                                console.log(title + " " + getActiveFeedId());
 
-                                               var feed_id = activeFeedIsCat() ? 'CAT:' + parseInt(getActiveFeedId()) :
+                                               const feed_id = activeFeedIsCat() ? 'CAT:' + parseInt(getActiveFeedId()) :
                                                        getActiveFeedId();
 
-                                               var rule = { reg_exp: title, feed_id: feed_id, filter_type: 1 };
+                                               const rule = { reg_exp: title, feed_id: [feed_id], filter_type: 1 };
 
                                                addFilterRule(null, dojo.toJson(rule));
                                        }
-
-                               } });
-
-                       });
-               }
-
-               dialog.show();
-
-       } catch (e) {
-               exception_error("quickAddFilter", e);
+                               });
+                       }
+               });
        }
-}
 
-function resetPubSub(feed_id, title) {
+       dialog.show();
 
-       var msg = __("Reset subscription? Tiny Tiny RSS will try to subscribe to the notification hub again on next feed update.").replace("%s", title);
-
-       if (title == undefined || confirm(msg)) {
-               notify_progress("Loading, please wait...");
-
-               var query = "?op=pref-feeds&quiet=1&method=resetPubSub&ids=" + feed_id;
-
-               new Ajax.Request("backend.php", {
-                       parameters: query,
-                       onComplete: function(transport) {
-                               dijit.byId("pubsubReset_Btn").attr('disabled', true);
-                               notify_info("Subscription reset.");
-                       } });
-       }
-
-       return false;
 }
 
-
 function unsubscribeFeed(feed_id, title) {
 
-       var msg = __("Unsubscribe from %s?").replace("%s", title);
+       const msg = __("Unsubscribe from %s?").replace("%s", title);
 
        if (title == undefined || confirm(msg)) {
                notify_progress("Removing feed...");
 
-               var query = "?op=pref-feeds&quiet=1&method=remove&ids=" + feed_id;
-
-               new Ajax.Request("backend.php", {
-                       parameters: query,
-                       onComplete: function(transport) {
+               const query = { op: "pref-feeds", quiet: 1, method: "remove", ids: feed_id };
 
-                                       if (dijit.byId("feedEditDlg")) dijit.byId("feedEditDlg").hide();
+               xhrPost("backend.php", query, (transport) => {
+                       if (dijit.byId("feedEditDlg")) dijit.byId("feedEditDlg").hide();
 
-                                       if (inPreferences()) {
-                                               updateFeedList();
-                                       } else {
-                                               if (feed_id == getActiveFeedId())
-                                                       setTimeout("viewfeed(-5)", 100);
-
-                                               if (feed_id < 0) updateFeedList();
-                                       }
+                       if (inPreferences()) {
+                               updateFeedList();
+                       } else {
+                               if (feed_id == getActiveFeedId())
+                                       setTimeout(function() { viewfeed({feed:-5}) }, 100);
 
-                               } });
+                               if (feed_id < 0) updateFeedList();
+                       }
+               });
        }
 
        return false;
@@ -1240,626 +1066,377 @@ function unsubscribeFeed(feed_id, title) {
 
 function backend_sanity_check_callback(transport) {
 
-       try {
-
-               if (sanity_check_done) {
-                       fatalError(11, "Sanity check request received twice. This can indicate "+
-                     "presence of Firebug or some other disrupting extension. "+
-                               "Please disable it and try again.");
-                       return;
-               }
+       const reply = JSON.parse(transport.responseText);
 
-               var reply = JSON.parse(transport.responseText);
+       if (!reply) {
+               fatalError(3, "Sanity check: invalid RPC reply", transport.responseText);
+               return;
+       }
 
-               if (!reply) {
-                       fatalError(3, "Sanity check: invalid RPC reply", transport.responseText);
-                       return;
-               }
+       const error_code = reply['error']['code'];
 
-               var error_code = reply['error']['code'];
+       if (error_code && error_code != 0) {
+               return fatalError(error_code, reply['error']['message']);
+       }
 
-               if (error_code && error_code != 0) {
-                       return fatalError(error_code, reply['error']['message']);
-               }
+       console.log("sanity check ok");
 
-               console.log("sanity check ok");
+       const params = reply['init-params'];
 
-               var params = reply['init-params'];
+       if (params) {
+               console.log('reading init-params...');
 
-               if (params) {
-                       console.log('reading init-params...');
+               for (const k in params) {
+                       switch (k) {
+                               case "label_base_index":
+                    _label_base_index = parseInt(params[k])
+                                       break;
+                               case "hotkeys":
+                                       // filter mnemonic definitions (used for help panel) from hotkeys map
+                                       // i.e. *(191)|Ctrl-/ -> *(191)
 
-                       for (k in params) {
-                               var v = params[k];
-                               console.log("IP: " + k + " => " + v);
+                    const tmp = [];
+                    for (const sequence in params[k][1]) {
+                        const filtered = sequence.replace(/\|.*$/, "");
+                        tmp[filtered] = params[k][1][sequence];
+                    }
 
-                               if (k == "label_base_index") _label_base_index = parseInt(v);
+                    params[k][1] = tmp;
+                    break;
                        }
 
-                       init_params = params;
-
-                       // PluginHost might not be available on non-index pages
-                       window.PluginHost && PluginHost.run(PluginHost.HOOK_PARAMS_LOADED, init_params);
+            console.log("IP:", k, "=>", params[k]);
                }
 
-               sanity_check_done = true;
+               init_params = params;
 
-               init_second_stage();
-
-       } catch (e) {
-               exception_error("backend_sanity_check_callback", e, transport);
+               // PluginHost might not be available on non-index pages
+               window.PluginHost && PluginHost.run(PluginHost.HOOK_PARAMS_LOADED, init_params);
        }
-}
 
-/*function has_local_storage() {
-       try {
-               return 'sessionStorage' in window && window['sessionStorage'] != null;
-       } catch (e) {
-               return false;
-       }
-} */
-
-function catSelectOnChange(elem) {
-       try {
-/*             var value = elem[elem.selectedIndex].value;
-               var def = elem.getAttribute('default');
-
-               if (value == "ADD_CAT") {
-
-                       if (def)
-                               dropboxSelect(elem, def);
-                       else
-                               elem.selectedIndex = 0;
-
-                       quickAddCat(elem);
-               } */
-
-       } catch (e) {
-               exception_error("catSelectOnChange", e);
-       }
-}
-
-function quickAddCat(elem) {
-       try {
-               var cat = prompt(__("Please enter category title:"));
-
-               if (cat) {
-
-                       var query = "?op=rpc&method=quickAddCat&cat=" + param_escape(cat);
-
-                       notify_progress("Loading, please wait...", true);
-
-                       new Ajax.Request("backend.php", {
-                               parameters: query,
-                               onComplete: function (transport) {
-                                       var response = transport.responseXML;
-                                       var select = response.getElementsByTagName("select")[0];
-                                       var options = select.getElementsByTagName("option");
-
-                                       dropbox_replace_options(elem, options);
-
-                                       notify('');
-
-                       } });
-
-               }
-
-       } catch (e) {
-               exception_error("quickAddCat", e);
-       }
+       init_second_stage();
 }
 
 function genUrlChangeKey(feed, is_cat) {
+       if (confirm(__("Generate new syndication address for this feed?"))) {
 
-       try {
-               var ok = confirm(__("Generate new syndication address for this feed?"));
-
-               if (ok) {
-
-                       notify_progress("Trying to change address...", true);
-
-                       var query = "?op=pref-feeds&method=regenFeedKey&id=" + param_escape(feed) +
-                               "&is_cat=" + param_escape(is_cat);
-
-                       new Ajax.Request("backend.php", {
-                               parameters: query,
-                               onComplete: function(transport) {
-                                               var reply = JSON.parse(transport.responseText);
-                                               var new_link = reply.link;
+               notify_progress("Trying to change address...", true);
 
-                                               var e = $('gen_feed_url');
+               const query = { op: "pref-feeds", method: "regenFeedKey", id: feed, is_cat: is_cat };
 
-                                               if (new_link) {
+               xhrJson("backend.php", query, (reply) => {
+                       const new_link = reply.link;
+                       const e = $('gen_feed_url');
 
-                                                       e.innerHTML = e.innerHTML.replace(/\&amp;key=.*$/,
-                                                               "&amp;key=" + new_link);
+                       if (new_link) {
+                               e.innerHTML = e.innerHTML.replace(/\&amp;key=.*$/,
+                                       "&amp;key=" + new_link);
 
-                                                       e.href = e.href.replace(/\&key=.*$/,
-                                                               "&key=" + new_link);
+                               e.href = e.href.replace(/\&key=.*$/,
+                                       "&key=" + new_link);
 
-                                                       new Effect.Highlight(e);
+                               new Effect.Highlight(e);
 
-                                                       notify('');
+                               notify('');
 
-                                               } else {
-                                                       notify_error("Could not change feed URL.");
-                                               }
-                               } });
-               }
-       } catch (e) {
-               exception_error("genUrlChangeKey", e);
+                       } else {
+                               notify_error("Could not change feed URL.");
+                       }
+               });
        }
        return false;
 }
 
-function labelSelectOnChange(elem) {
-       try {
-/*             var value = elem[elem.selectedIndex].value;
-               var def = elem.getAttribute('default');
-
-               if (value == "ADD_LABEL") {
-
-                       if (def)
-                               dropboxSelect(elem, def);
-                       else
-                               elem.selectedIndex = 0;
-
-                       addLabel(elem, function(transport) {
-
-                                       try {
-
-                                               var response = transport.responseXML;
-                                               var select = response.getElementsByTagName("select")[0];
-                                               var options = select.getElementsByTagName("option");
-
-                                               dropbox_replace_options(elem, options);
-
-                                               notify('');
-                                       } catch (e) {
-                                               exception_error("addLabel", e);
-                                       }
-                       });
-               } */
-
-       } catch (e) {
-               exception_error("labelSelectOnChange", e);
-       }
-}
-
-function dropbox_replace_options(elem, options) {
-
-       try {
-               while (elem.hasChildNodes())
-                       elem.removeChild(elem.firstChild);
-
-               var sel_idx = -1;
-
-               for (var i = 0; i < options.length; i++) {
-                       var text = options[i].firstChild.nodeValue;
-                       var value = options[i].getAttribute("value");
-
-                       if (value == undefined) value = text;
-
-                       var issel = options[i].getAttribute("selected") == "1";
-
-                       var option = new Option(text, value, issel);
-
-                       if (options[i].getAttribute("disabled"))
-                               option.setAttribute("disabled", true);
-
-                       elem.insert(option);
-
-                       if (issel) sel_idx = i;
-               }
-
-               // Chrome doesn't seem to just select stuff when you pass new Option(x, y, true)
-               if (sel_idx >= 0) elem.selectedIndex = sel_idx;
-
-       } catch (e) {
-               exception_error("dropbox_replace_options", e);
-       }
-}
-
 // mode = all, none, invert
 function selectTableRows(id, mode) {
-       try {
-               var rows = $(id).rows;
+       const rows = $(id).rows;
 
-               for (var i = 0; i < rows.length; i++) {
-                       var row = rows[i];
-                       var cb = false;
-                       var dcb = false;
+       for (let i = 0; i < rows.length; i++) {
+               const row = rows[i];
+               let cb = false;
+               let dcb = false;
 
-                       if (row.id && row.className) {
-                               var bare_id = row.id.replace(/^[A-Z]*?-/, "");
-                               var inputs = rows[i].getElementsByTagName("input");
+               if (row.id && row.className) {
+                       const bare_id = row.id.replace(/^[A-Z]*?-/, "");
+                       const inputs = rows[i].getElementsByTagName("input");
 
-                               for (var j = 0; j < inputs.length; j++) {
-                                       var input = inputs[j];
+                       for (let j = 0; j < inputs.length; j++) {
+                               const input = inputs[j];
 
-                                       if (input.getAttribute("type") == "checkbox" &&
-                                                       input.id.match(bare_id)) {
+                               if (input.getAttribute("type") == "checkbox" &&
+                                               input.id.match(bare_id)) {
 
-                                               cb = input;
-                                               dcb = dijit.getEnclosingWidget(cb);
-                                               break;
-                                       }
+                                       cb = input;
+                                       dcb = dijit.getEnclosingWidget(cb);
+                                       break;
                                }
+                       }
 
-                               if (cb || dcb) {
-                                       var issel = row.hasClassName("Selected");
+                       if (cb || dcb) {
+                               const issel = row.hasClassName("Selected");
 
-                                       if (mode == "all" && !issel) {
-                                               row.addClassName("Selected");
-                                               cb.checked = true;
-                                               if (dcb) dcb.set("checked", true);
-                                       } else if (mode == "none" && issel) {
+                               if (mode == "all" && !issel) {
+                                       row.addClassName("Selected");
+                                       cb.checked = true;
+                                       if (dcb) dcb.set("checked", true);
+                               } else if (mode == "none" && issel) {
+                                       row.removeClassName("Selected");
+                                       cb.checked = false;
+                                       if (dcb) dcb.set("checked", false);
+
+                               } else if (mode == "invert") {
+
+                                       if (issel) {
                                                row.removeClassName("Selected");
                                                cb.checked = false;
                                                if (dcb) dcb.set("checked", false);
-
-                                       } else if (mode == "invert") {
-
-                                               if (issel) {
-                                                       row.removeClassName("Selected");
-                                                       cb.checked = false;
-                                                       if (dcb) dcb.set("checked", false);
-                                               } else {
-                                                       row.addClassName("Selected");
-                                                       cb.checked = true;
-                                                       if (dcb) dcb.set("checked", true);
-                                               }
+                                       } else {
+                                               row.addClassName("Selected");
+                                               cb.checked = true;
+                                               if (dcb) dcb.set("checked", true);
                                        }
                                }
                        }
                }
-
-       } catch (e) {
-               exception_error("selectTableRows", e);
-
        }
+
 }
 
 function getSelectedTableRowIds(id) {
-       var rows = [];
+       const rows = [];
 
-       try {
-               var elem_rows = $(id).rows;
+       const elem_rows = $(id).rows;
 
-               for (var i = 0; i < elem_rows.length; i++) {
-                       if (elem_rows[i].hasClassName("Selected")) {
-                               var bare_id = elem_rows[i].id.replace(/^[A-Z]*?-/, "");
-                               rows.push(bare_id);
-                       }
+       for (let i = 0; i < elem_rows.length; i++) {
+               if (elem_rows[i].hasClassName("Selected")) {
+                       const bare_id = elem_rows[i].id.replace(/^[A-Z]*?-/, "");
+                       rows.push(bare_id);
                }
-
-       } catch (e) {
-               exception_error("getSelectedTableRowIds", e);
        }
 
        return rows;
 }
 
-function editFeed(feed, event) {
-       try {
-               if (feed <= 0)
-                       return alert(__("You can't edit this kind of feed."));
+function editFeed(feed) {
+       if (feed <= 0)
+               return alert(__("You can't edit this kind of feed."));
 
-               var query = "backend.php?op=pref-feeds&method=editfeed&id=" +
-                       param_escape(feed);
+       const query = "backend.php?op=pref-feeds&method=editfeed&id=" +
+               param_escape(feed);
 
-               console.log(query);
+       console.log(query);
 
-               if (dijit.byId("filterEditDlg"))
-                       dijit.byId("filterEditDlg").destroyRecursive();
+       if (dijit.byId("filterEditDlg"))
+               dijit.byId("filterEditDlg").destroyRecursive();
 
-               if (dijit.byId("feedEditDlg"))
-                       dijit.byId("feedEditDlg").destroyRecursive();
+       if (dijit.byId("feedEditDlg"))
+               dijit.byId("feedEditDlg").destroyRecursive();
 
-               dialog = new dijit.Dialog({
-                       id: "feedEditDlg",
-                       title: __("Edit Feed"),
-                       style: "width: 600px",
-                       execute: function() {
-                               if (this.validate()) {
-//                                     console.log(dojo.objectToQuery(this.attr('value')));
-
-                                       notify_progress("Saving data...", true);
-
-                                       new Ajax.Request("backend.php", {
-                                               parameters: dojo.objectToQuery(dialog.attr('value')),
-                                               onComplete: function(transport) {
-                                                       dialog.hide();
-                                                       notify('');
-                                                       updateFeedList();
-                                       }});
-                               }
-                       },
-                       href: query});
+       const dialog = new dijit.Dialog({
+               id: "feedEditDlg",
+               title: __("Edit Feed"),
+               style: "width: 600px",
+               execute: function() {
+                       if (this.validate()) {
+                               notify_progress("Saving data...", true);
 
-               dialog.show();
+                               xhrPost("backend.php", dialog.attr('value'), (transport) => {
+                                       dialog.hide();
+                                       notify('');
+                                       updateFeedList();
+                               });
+                       }
+               },
+               href: query});
 
-       } catch (e) {
-               exception_error("editFeed", e);
-       }
+       dialog.show();
 }
 
 function feedBrowser() {
-       try {
-               var query = "backend.php?op=feeds&method=feedBrowser";
-
-               if (dijit.byId("feedAddDlg"))
-                       dijit.byId("feedAddDlg").hide();
+       const query = "backend.php?op=feeds&method=feedBrowser";
 
-               if (dijit.byId("feedBrowserDlg"))
-                       dijit.byId("feedBrowserDlg").destroyRecursive();
+       if (dijit.byId("feedAddDlg"))
+               dijit.byId("feedAddDlg").hide();
 
-               var dialog = new dijit.Dialog({
-                       id: "feedBrowserDlg",
-                       title: __("More Feeds"),
-                       style: "width: 600px",
-                       getSelectedFeedIds: function() {
-                               var list = $$("#browseFeedList li[id*=FBROW]");
-                               var selected = new Array();
+       if (dijit.byId("feedBrowserDlg"))
+               dijit.byId("feedBrowserDlg").destroyRecursive();
 
-                               list.each(function(child) {
-                                       var id = child.id.replace("FBROW-", "");
-
-                                       if (child.hasClassName('Selected')) {
-                                               selected.push(id);
-                                       }
-                               });
-
-                               return selected;
-                       },
-                       getSelectedFeeds: function() {
-                               var list = $$("#browseFeedList li.Selected");
-                               var selected = new Array();
-
-                               list.each(function(child) {
-                                       var title = child.getElementsBySelector("span.fb_feedTitle")[0].innerHTML;
-                                       var url = child.getElementsBySelector("a.fb_feedUrl")[0].href;
-
-                                       selected.push([title,url]);
-
-                               });
-
-                               return selected;
-                       },
+       const dialog = new dijit.Dialog({
+               id: "feedBrowserDlg",
+               title: __("More Feeds"),
+               style: "width: 600px",
+               getSelectedFeedIds: function () {
+                       const list = $$("#browseFeedList li[id*=FBROW]");
+                       const selected = [];
 
-                       subscribe: function() {
-                               var mode = this.attr('value').mode;
-                               var selected = [];
+                       list.each(function (child) {
+                               const id = child.id.replace("FBROW-", "");
 
-                               if (mode == "1")
-                                       selected = this.getSelectedFeeds();
-                               else
-                                       selected = this.getSelectedFeedIds();
-
-                               if (selected.length > 0) {
-                                       dijit.byId("feedBrowserDlg").hide();
-
-                                       notify_progress("Loading, please wait...", true);
-
-                                       // we use dojo.toJson instead of JSON.stringify because
-                                       // it somehow escapes everything TWICE, at least in Chrome 9
-
-                                       var query = "?op=rpc&method=massSubscribe&payload="+
-                                               param_escape(dojo.toJson(selected)) + "&mode=" + param_escape(mode);
-
-                                       console.log(query);
-
-                                       new Ajax.Request("backend.php", {
-                                               parameters: query,
-                                               onComplete: function(transport) {
-                                                       notify('');
-                                                       updateFeedList();
-                                               } });
-
-                               } else {
-                                       alert(__("No feeds are selected."));
+                               if (child.hasClassName('Selected')) {
+                                       selected.push(id);
                                }
+                       });
 
-                       },
-                       update: function() {
-                               var query = dojo.objectToQuery(dialog.attr('value'));
-
-                               Element.show('feed_browser_spinner');
-
-                               new Ajax.Request("backend.php", {
-                                       parameters: query,
-                                       onComplete: function(transport) {
-                                               notify('');
-
-                                               Element.hide('feed_browser_spinner');
-
-                                               var c = $("browseFeedList");
-
-                                               var reply = JSON.parse(transport.responseText);
-
-                                               var r = reply['content'];
-                                               var mode = reply['mode'];
-
-                                               if (c && r) {
-                                                       c.innerHTML = r;
-                                               }
-
-                                               dojo.parser.parse("browseFeedList");
+                       return selected;
+               },
+               getSelectedFeeds: function () {
+                       const list = $$("#browseFeedList li.Selected");
+                       const selected = [];
 
-                                               if (mode == 2) {
-                                                       Element.show(dijit.byId('feed_archive_remove').domNode);
-                                               } else {
-                                                       Element.hide(dijit.byId('feed_archive_remove').domNode);
-                                               }
+                       list.each(function (child) {
+                               const title = child.getElementsBySelector("span.fb_feedTitle")[0].innerHTML;
+                               const url = child.getElementsBySelector("a.fb_feedUrl")[0].href;
 
-                                       } });
-                       },
-                       removeFromArchive: function() {
-                               var selected = this.getSelectedFeedIds();
+                               selected.push([title, url]);
 
-                               if (selected.length > 0) {
+                       });
 
-                                       var pr = __("Remove selected feeds from the archive? Feeds with stored articles will not be removed.");
+                       return selected;
+               },
 
-                                       if (confirm(pr)) {
-                                               Element.show('feed_browser_spinner');
+               subscribe: function () {
+                       const mode = this.attr('value').mode;
+                       let selected = [];
 
-                                               var query = "?op=rpc&method=remarchive&ids=" +
-                                                       param_escape(selected.toString());;
+                       if (mode == "1")
+                               selected = this.getSelectedFeeds();
+                       else
+                               selected = this.getSelectedFeedIds();
 
-                                               new Ajax.Request("backend.php", {
-                                                       parameters: query,
-                                                       onComplete: function(transport) {
-                                                               dialog.update();
-                                                       } });
-                                       }
-                               }
-                       },
-                       execute: function() {
-                               if (this.validate()) {
-                                       this.subscribe();
-                               }
-                       },
-                       href: query});
+                       if (selected.length > 0) {
+                               dijit.byId("feedBrowserDlg").hide();
 
-               dialog.show();
+                               notify_progress("Loading, please wait...", true);
 
-       } catch (e) {
-               exception_error("editFeed", e);
-       }
-}
+                               const query = { op: "rpc", method: "massSubscribe",
+                                       payload: JSON.stringify(selected), mode: mode };
 
-function showFeedsWithErrors() {
-       try {
-               var query = "backend.php?op=pref-feeds&method=feedsWithErrors";
+                               xhrPost("backend.php", query, () => {
+                                       notify('');
+                                       updateFeedList();
+                               });
 
-               if (dijit.byId("errorFeedsDlg"))
-                       dijit.byId("errorFeedsDlg").destroyRecursive();
+                       } else {
+                               alert(__("No feeds are selected."));
+                       }
 
-               dialog = new dijit.Dialog({
-                       id: "errorFeedsDlg",
-                       title: __("Feeds with update errors"),
-                       style: "width: 600px",
-                       getSelectedFeeds: function() {
-                               return getSelectedTableRowIds("prefErrorFeedList");
-                       },
-                       removeSelected: function() {
-                               var sel_rows = this.getSelectedFeeds();
+               },
+               update: function () {
+                       Element.show('feed_browser_spinner');
 
-                               console.log(sel_rows);
+                       xhrPost("backend.php", dialog.attr("value"), (transport) => {
+                               notify('');
 
-                               if (sel_rows.length > 0) {
-                                       var ok = confirm(__("Remove selected feeds?"));
+                               Element.hide('feed_browser_spinner');
 
-                                       if (ok) {
-                                               notify_progress("Removing selected feeds...", true);
+                               const reply = JSON.parse(transport.responseText);
+                               const mode = reply['mode'];
 
-                                               var query = "?op=pref-feeds&method=remove&ids="+
-                                                       param_escape(sel_rows.toString());
+                               if ($("browseFeedList") && reply['content']) {
+                                       $("browseFeedList").innerHTML = reply['content'];
+                               }
 
-                                               new Ajax.Request("backend.php", {
-                                                       parameters: query,
-                                                       onComplete: function(transport) {
-                                                               notify('');
-                                                               dialog.hide();
-                                                               updateFeedList();
-                                                       } });
-                                       }
+                               dojo.parser.parse("browseFeedList");
 
+                               if (mode == 2) {
+                                       Element.show(dijit.byId('feed_archive_remove').domNode);
                                } else {
-                                       alert(__("No feeds are selected."));
-                               }
-                       },
-                       execute: function() {
-                               if (this.validate()) {
+                                       Element.hide(dijit.byId('feed_archive_remove').domNode);
                                }
-                       },
-                       href: query});
-
-               dialog.show();
-
-       } catch (e) {
-               exception_error("showFeedsWithErrors", e);
-       }
+                       });
+               },
+               removeFromArchive: function () {
+                       const selected = this.getSelectedFeedIds();
 
-}
+                       if (selected.length > 0) {
 
-/* new support functions for SelectByTag */
+                               const pr = __("Remove selected feeds from the archive? Feeds with stored articles will not be removed.");
 
-function get_all_tags(selObj){
-       try {
-               if( !selObj ) return "";
+                               if (confirm(pr)) {
+                                       Element.show('feed_browser_spinner');
 
-               var result = "";
-               var len = selObj.options.length;
+                                       const query = { op: "rpc", method: "remarchive", ids: selected.toString() };
 
-               for (var i=0; i < len; i++){
-                       if (selObj.options[i].selected) {
-                               result += selObj[i].value + "%2C";   // is really a comma
+                                       xhrPost("backend.php", query, () => {
+                                               dialog.update();
+                                       });
+                               }
                        }
-               }
-
-               if (result.length > 0){
-                       result = result.substr(0, result.length-3);  // remove trailing %2C
-               }
-
-               return(result);
+               },
+               execute: function () {
+                       if (this.validate()) {
+                               this.subscribe();
+                       }
+               },
+               href: query
+       });
 
-       } catch (e) {
-               exception_error("get_all_tags", e);
-       }
+       dialog.show();
 }
 
-function get_radio_checked(radioObj) {
-       try {
-               if (!radioObj) return "";
-
-               var len = radioObj.length;
+function showFeedsWithErrors() {
+       const query = "backend.php?op=pref-feeds&method=feedsWithErrors";
+
+       if (dijit.byId("errorFeedsDlg"))
+               dijit.byId("errorFeedsDlg").destroyRecursive();
+
+       const dialog = new dijit.Dialog({
+               id: "errorFeedsDlg",
+               title: __("Feeds with update errors"),
+               style: "width: 600px",
+               getSelectedFeeds: function() {
+                       return getSelectedTableRowIds("prefErrorFeedList");
+               },
+               removeSelected: function() {
+                       const sel_rows = this.getSelectedFeeds();
+
+                       if (sel_rows.length > 0) {
+                               if (confirm(__("Remove selected feeds?"))) {
+                                       notify_progress("Removing selected feeds...", true);
+
+                                       const query = { op: "pref-feeds", method: "remove",
+                                               ids: sel_rows.toString() };
+
+                                       xhrPost("backend.php",  query, () => {
+                        notify('');
+                        dialog.hide();
+                        updateFeedList();
+                    });
+                               }
 
-               if (len == undefined){
-                       if(radioObj.checked){
-                               return(radioObj.value);
                        } else {
-                               return("");
+                               alert(__("No feeds are selected."));
                        }
-               }
-
-               for( var i=0; i < len; i++ ){
-                       if( radioObj[i].checked ){
-                               return( radioObj[i].value);
+               },
+               execute: function() {
+                       if (this.validate()) {
+                               //
                        }
-               }
+               },
+               href: query});
 
-       } catch (e) {
-               exception_error("get_radio_checked", e);
-       }
-       return("");
+       dialog.show();
 }
 
 function get_timestamp() {
-       var date = new Date();
+       const date = new Date();
        return Math.round(date.getTime() / 1000);
 }
 
 function helpDialog(topic) {
-       try {
-               var query = "backend.php?op=backend&method=help&topic=" + param_escape(topic);
-
-               if (dijit.byId("helpDlg"))
-                       dijit.byId("helpDlg").destroyRecursive();
+       const query = "backend.php?op=backend&method=help&topic=" + param_escape(topic);
 
-               dialog = new dijit.Dialog({
-                       id: "helpDlg",
-                       title: __("Help"),
-                       style: "width: 600px",
-                       href: query,
-               });
+       if (dijit.byId("helpDlg"))
+               dijit.byId("helpDlg").destroyRecursive();
 
-               dialog.show();
+       const dialog = new dijit.Dialog({
+               id: "helpDlg",
+               title: __("Help"),
+               style: "width: 600px",
+               href: query,
+       });
 
-       } catch (e) {
-               exception_error("helpDialog", e);
-       }
+       dialog.show();
 }
 
 function htmlspecialchars_decode (string, quote_style) {
@@ -1882,14 +1459,14 @@ function htmlspecialchars_decode (string, quote_style) {
   // *     returns 1: '<p>this -> &quot;</p>'
   // *     example 2: htmlspecialchars_decode("&amp;quot;");
   // *     returns 2: '&quot;'
-  var optTemp = 0,
+  let optTemp = 0,
     i = 0,
     noquotes = false;
   if (typeof quote_style === 'undefined') {
     quote_style = 2;
   }
   string = string.toString().replace(/&lt;/g, '<').replace(/&gt;/g, '>');
-  var OPTS = {
+  const OPTS = {
     'ENT_NOQUOTES': 0,
     'ENT_HTML_QUOTE_SINGLE': 1,
     'ENT_HTML_QUOTE_DOUBLE': 2,
@@ -1934,3 +1511,95 @@ function feed_to_label_id(feed) {
        return _label_base_index - 1 + Math.abs(feed);
 }
 
+// http://stackoverflow.com/questions/6251937/how-to-get-selecteduser-highlighted-text-in-contenteditable-element-and-replac
+
+function getSelectionText() {
+       let text = "";
+
+       if (typeof window.getSelection != "undefined") {
+               const sel = window.getSelection();
+               if (sel.rangeCount) {
+                       const container = document.createElement("div");
+                       for (let i = 0, len = sel.rangeCount; i < len; ++i) {
+                               container.appendChild(sel.getRangeAt(i).cloneContents());
+                       }
+                       text = container.innerHTML;
+               }
+       } else if (typeof document.selection != "undefined") {
+               if (document.selection.type == "Text") {
+                       text = document.selection.createRange().textText;
+               }
+       }
+
+       return text.stripTags();
+}
+
+function openUrlPopup(url) {
+       const w = window.open("");
+
+       w.opener = null;
+       w.location = url;
+}
+function openArticlePopup(id) {
+       const w = window.open("",
+               "ttrss_article_popup",
+               "height=900,width=900,resizable=yes,status=no,location=no,menubar=no,directories=no,scrollbars=yes,toolbar=no");
+
+       w.opener = null;
+       w.location = "backend.php?op=article&method=view&mode=raw&html=1&zoom=1&id=" + id + "&csrf_token=" + getInitParam("csrf_token");
+}
+
+function keyevent_to_action(e) {
+
+    const hotkeys_map = getInitParam("hotkeys");
+    const keycode = e.which;
+    const keychar = String.fromCharCode(keycode).toLowerCase();
+
+    if (keycode == 27) { // escape and drop prefix
+        hotkey_prefix = false;
+    }
+
+    if (keycode == 16 || keycode == 17) return; // ignore lone shift / ctrl
+
+    if (!hotkey_prefix && hotkeys_map[0].indexOf(keychar) != -1) {
+
+        const date = new Date();
+        const ts = Math.round(date.getTime() / 1000);
+
+        hotkey_prefix = keychar;
+        hotkey_prefix_pressed = ts;
+
+        $("cmdline").innerHTML = keychar;
+        Element.show("cmdline");
+
+        e.stopPropagation();
+
+        return false;
+    }
+
+    Element.hide("cmdline");
+
+    let hotkey_name = keychar.search(/[a-zA-Z0-9]/) != -1 ? keychar : "(" + keycode + ")";
+
+    // ensure ^*char notation
+    if (e.shiftKey) hotkey_name = "*" + hotkey_name;
+    if (e.ctrlKey) hotkey_name = "^" + hotkey_name;
+    if (e.altKey) hotkey_name = "+" + hotkey_name;
+    if (e.metaKey) hotkey_name = "%" + hotkey_name;
+
+    const hotkey_full = hotkey_prefix ? hotkey_prefix + " " + hotkey_name : hotkey_name;
+    hotkey_prefix = false;
+
+    let action_name = false;
+
+    for (const sequence in hotkeys_map[1]) {
+        if (sequence == hotkey_full) {
+            action_name = hotkeys_map[1][sequence];
+            break;
+        }
+    }
+
+    console.log('keyevent_to_action', hotkey_full, '=>', action_name);
+
+    return action_name;
+}
\ No newline at end of file