mirror of
https://tt-rss.org/git/tt-rss.git
synced 2024-06-29 12:20:51 +02:00
e74f7bde22
keydown returns the "raw" key in event.which. Depending on the keyboard layout, this may not be what is wanted. For example, on a German keyboard, Shift+7 has to be pressed to get a slash. However, event.which will be 55, which corresponds to "7". In the keypress event, however, event.which will be 47, which corresponds to "/". Sadly, several important keys (such as escape and the arrow keys) do not trigger a keypress event. Therefore, they have to be handled using a keydown event. This change refactors the hotkey support to make use of keypress events whenever possible. This will make hotkeys work regardless of the user's keyboard layout. Escape and arrow keys are still handled via keydown events. There should be only one change in behavior: I could not make Ctrl+/ work and therefore rebound the help dialog to "?".
474 lines
12 KiB
JavaScript
474 lines
12 KiB
JavaScript
'use strict'
|
|
/* global __, ngettext */
|
|
define(["dojo/_base/declare"], function (declare) {
|
|
return declare("fox.AppBase", null, {
|
|
_initParams: [],
|
|
_rpc_seq: 0,
|
|
hotkey_prefix: 0,
|
|
hotkey_prefix_pressed: false,
|
|
hotkey_prefix_timeout: 0,
|
|
constructor: function() {
|
|
window.onerror = this.Error.onWindowError;
|
|
},
|
|
getInitParam: function(k) {
|
|
return this._initParams[k];
|
|
},
|
|
setInitParam: function(k, v) {
|
|
this._initParams[k] = v;
|
|
},
|
|
enableCsrfSupport: function() {
|
|
Ajax.Base.prototype.initialize = Ajax.Base.prototype.initialize.wrap(
|
|
function (callOriginal, options) {
|
|
|
|
if (App.getInitParam("csrf_token") != undefined) {
|
|
Object.extend(options, options || { });
|
|
|
|
if (Object.isString(options.parameters))
|
|
options.parameters = options.parameters.toQueryParams();
|
|
else if (Object.isHash(options.parameters))
|
|
options.parameters = options.parameters.toObject();
|
|
|
|
options.parameters["csrf_token"] = App.getInitParam("csrf_token");
|
|
}
|
|
|
|
return callOriginal(options);
|
|
}
|
|
);
|
|
},
|
|
urlParam: function(param) {
|
|
return String(window.location.href).parseQuery()[param];
|
|
},
|
|
next_seq: function() {
|
|
this._rpc_seq += 1;
|
|
return this._rpc_seq;
|
|
},
|
|
get_seq: function() {
|
|
return this._rpc_seq;
|
|
},
|
|
setLoadingProgress: function(p) {
|
|
loading_progress += p;
|
|
|
|
if (dijit.byId("loading_bar"))
|
|
dijit.byId("loading_bar").update({progress: loading_progress});
|
|
|
|
if (loading_progress >= 90) {
|
|
$("overlay").hide();
|
|
}
|
|
|
|
},
|
|
keyeventToAction: function(event) {
|
|
|
|
const hotkeys_map = App.getInitParam("hotkeys");
|
|
const keycode = event.which;
|
|
const keychar = String.fromCharCode(keycode);
|
|
|
|
if (keycode == 27) { // escape and drop prefix
|
|
this.hotkey_prefix = false;
|
|
}
|
|
|
|
if (!this.hotkey_prefix && hotkeys_map[0].indexOf(keychar) != -1) {
|
|
|
|
this.hotkey_prefix = keychar;
|
|
$("cmdline").innerHTML = keychar;
|
|
Element.show("cmdline");
|
|
|
|
window.clearTimeout(this.hotkey_prefix_timeout);
|
|
this.hotkey_prefix_timeout = window.setTimeout(() => {
|
|
this.hotkey_prefix = false;
|
|
Element.hide("cmdline");
|
|
}, 3 * 1000);
|
|
|
|
event.stopPropagation();
|
|
|
|
return false;
|
|
}
|
|
|
|
Element.hide("cmdline");
|
|
|
|
let hotkey_name = "";
|
|
|
|
if (event.type == "keydown") {
|
|
hotkey_name = "(" + keycode + ")";
|
|
|
|
// ensure ^*char notation
|
|
if (event.shiftKey) hotkey_name = "*" + hotkey_name;
|
|
if (event.ctrlKey) hotkey_name = "^" + hotkey_name;
|
|
if (event.altKey) hotkey_name = "+" + hotkey_name;
|
|
if (event.metaKey) hotkey_name = "%" + hotkey_name;
|
|
} else {
|
|
hotkey_name = keychar ? keychar : "(" + keycode + ")";
|
|
}
|
|
|
|
const hotkey_full = this.hotkey_prefix ? this.hotkey_prefix + " " + hotkey_name : hotkey_name;
|
|
this.hotkey_prefix = false;
|
|
|
|
let action_name = false;
|
|
|
|
for (const sequence in hotkeys_map[1]) {
|
|
if (hotkeys_map[1].hasOwnProperty(sequence)) {
|
|
if (sequence == hotkey_full) {
|
|
action_name = hotkeys_map[1][sequence];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('keyeventToAction', hotkey_full, '=>', action_name);
|
|
|
|
return action_name;
|
|
},
|
|
cleanupMemory: function(root) {
|
|
const dijits = dojo.query("[widgetid]", dijit.byId(root).domNode).map(dijit.byNode);
|
|
|
|
dijits.each(function (d) {
|
|
dojo.destroy(d.domNode);
|
|
});
|
|
|
|
$$("#" + root + " *").each(function (i) {
|
|
i.parentNode ? i.parentNode.removeChild(i) : true;
|
|
});
|
|
},
|
|
helpDialog: function(topic) {
|
|
const query = "backend.php?op=backend&method=help&topic=" + encodeURIComponent(topic);
|
|
|
|
if (dijit.byId("helpDlg"))
|
|
dijit.byId("helpDlg").destroyRecursive();
|
|
|
|
const dialog = new dijit.Dialog({
|
|
id: "helpDlg",
|
|
title: __("Help"),
|
|
style: "width: 600px",
|
|
href: query,
|
|
});
|
|
|
|
dialog.show();
|
|
},
|
|
displayDlg: function(title, id, param, callback) {
|
|
Notify.progress("Loading, please wait...", true);
|
|
|
|
const query = {op: "dlg", method: id, param: param};
|
|
|
|
xhrPost("backend.php", query, (transport) => {
|
|
try {
|
|
const content = transport.responseText;
|
|
|
|
let dialog = dijit.byId("infoBox");
|
|
|
|
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;
|
|
},
|
|
content: content
|
|
});
|
|
} else {
|
|
dialog.attr('title', title);
|
|
dialog.attr('content', content);
|
|
}
|
|
|
|
dialog.show();
|
|
|
|
Notify.close();
|
|
|
|
if (callback) callback(transport);
|
|
} catch (e) {
|
|
this.Error.report(e);
|
|
}
|
|
});
|
|
|
|
return false;
|
|
},
|
|
handleRpcJson: function(transport) {
|
|
|
|
const netalert = $$("#toolbar .net-alert")[0];
|
|
|
|
try {
|
|
const reply = JSON.parse(transport.responseText);
|
|
|
|
if (reply) {
|
|
const error = reply['error'];
|
|
|
|
if (error) {
|
|
const code = error['code'];
|
|
const msg = error['message'];
|
|
|
|
console.warn("[handleRpcJson] received fatal error ", code, msg);
|
|
|
|
if (code != 0) {
|
|
/* global ERRORS */
|
|
this.Error.fatal(ERRORS[code], {info: msg, code: code});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const seq = reply['seq'];
|
|
|
|
if (seq && this.get_seq() != seq) {
|
|
console.log("[handleRpcJson] sequence mismatch: ", seq, '!=', this.get_seq());
|
|
return true;
|
|
}
|
|
|
|
const message = reply['message'];
|
|
|
|
if (message == "UPDATE_COUNTERS") {
|
|
console.log("need to refresh counters...");
|
|
Feeds.requestCounters(true);
|
|
}
|
|
|
|
const counters = reply['counters'];
|
|
|
|
if (counters)
|
|
Feeds.parseCounters(counters);
|
|
|
|
const runtime_info = reply['runtime-info'];
|
|
|
|
if (runtime_info)
|
|
App.parseRuntimeInfo(runtime_info);
|
|
|
|
if (netalert) netalert.hide();
|
|
|
|
return reply;
|
|
|
|
} else {
|
|
if (netalert) netalert.show();
|
|
|
|
Notify.error("Communication problem with server.");
|
|
}
|
|
|
|
} catch (e) {
|
|
if (netalert) netalert.show();
|
|
|
|
Notify.error("Communication problem with server.");
|
|
|
|
console.error(e);
|
|
}
|
|
|
|
return false;
|
|
},
|
|
parseRuntimeInfo: function(data) {
|
|
for (const k in data) {
|
|
if (data.hasOwnProperty(k)) {
|
|
const v = data[k];
|
|
|
|
console.log("RI:", k, "=>", v);
|
|
|
|
if (k == "daemon_is_running" && v != 1) {
|
|
Notify.error("<span onclick=\"App.explainError(1)\">Update daemon is not running.</span>", true);
|
|
return;
|
|
}
|
|
|
|
if (k == "recent_log_events") {
|
|
const alert = $$(".log-alert")[0];
|
|
|
|
if (alert) {
|
|
v > 0 ? alert.show() : alert.hide();
|
|
}
|
|
}
|
|
|
|
if (k == "daemon_stamp_ok" && v != 1) {
|
|
Notify.error("<span onclick=\"App.explainError(3)\">Update daemon is not updating feeds.</span>", true);
|
|
return;
|
|
}
|
|
|
|
if (k == "max_feed_id" || k == "num_feeds") {
|
|
if (App.getInitParam(k) != v) {
|
|
console.log("feed count changed, need to reload feedlist.");
|
|
Feeds.reload();
|
|
}
|
|
}
|
|
|
|
this.setInitParam(k, v);
|
|
}
|
|
}
|
|
|
|
PluginHost.run(PluginHost.HOOK_RUNTIME_INFO_LOADED, data);
|
|
},
|
|
backendSanityCallback: function (transport) {
|
|
const reply = JSON.parse(transport.responseText);
|
|
|
|
/* global ERRORS */
|
|
|
|
if (!reply) {
|
|
this.Error.fatal(ERRORS[3], {info: transport.responseText});
|
|
return;
|
|
}
|
|
|
|
if (reply['error']) {
|
|
const code = reply['error']['code'];
|
|
|
|
if (code && code != 0) {
|
|
return this.Error.fatal(ERRORS[code],
|
|
{code: code, info: reply['error']['message']});
|
|
}
|
|
}
|
|
|
|
console.log("sanity check ok");
|
|
|
|
const params = reply['init-params'];
|
|
|
|
if (params) {
|
|
console.log('reading init-params...');
|
|
|
|
for (const k in params) {
|
|
if (params.hasOwnProperty(k)) {
|
|
switch (k) {
|
|
case "label_base_index":
|
|
_label_base_index = parseInt(params[k]);
|
|
break;
|
|
case "cdm_auto_catchup":
|
|
if (params[k] == 1) {
|
|
const hl = $("headlines-frame");
|
|
if (hl) hl.addClassName("auto_catchup");
|
|
}
|
|
break;
|
|
case "hotkeys":
|
|
// filter mnemonic definitions (used for help panel) from hotkeys map
|
|
// i.e. *(191)|Ctrl-/ -> *(191)
|
|
|
|
const tmp = [];
|
|
for (const sequence in params[k][1]) {
|
|
if (params[k][1].hasOwnProperty(sequence)) {
|
|
const filtered = sequence.replace(/\|.*$/, "");
|
|
tmp[filtered] = params[k][1][sequence];
|
|
}
|
|
}
|
|
|
|
params[k][1] = tmp;
|
|
break;
|
|
}
|
|
|
|
console.log("IP:", k, "=>", params[k]);
|
|
this.setInitParam(k, params[k]);
|
|
}
|
|
}
|
|
|
|
// PluginHost might not be available on non-index pages
|
|
if (typeof PluginHost !== 'undefined')
|
|
PluginHost.run(PluginHost.HOOK_PARAMS_LOADED, App._initParams);
|
|
}
|
|
|
|
this.initSecondStage();
|
|
},
|
|
toggleNightMode: function() {
|
|
const link = $("theme_css");
|
|
|
|
if (link) {
|
|
|
|
let user_theme = "";
|
|
let user_css = "";
|
|
|
|
if (link.getAttribute("href").indexOf("themes/night.css") == -1) {
|
|
user_css = "themes/night.css?" + Date.now();
|
|
user_theme = "night.css";
|
|
} else {
|
|
user_theme = "default.php";
|
|
user_css = "css/default.css?" + Date.now();
|
|
}
|
|
|
|
$("main").fade({duration: 0.5, afterFinish: () => {
|
|
link.setAttribute("href", user_css);
|
|
$("main").appear({duration: 0.5});
|
|
xhrPost("backend.php", {op: "rpc", method: "setpref", key: "USER_CSS_THEME", value: user_theme});
|
|
}});
|
|
|
|
}
|
|
},
|
|
explainError: function(code) {
|
|
return this.displayDlg(__("Error explained"), "explainError", code);
|
|
},
|
|
Error: {
|
|
fatal: function (error, params) {
|
|
params = params || {};
|
|
|
|
if (params.code) {
|
|
if (params.code == 6) {
|
|
window.location.href = "index.php";
|
|
return;
|
|
} else if (params.code == 5) {
|
|
window.location.href = "public.php?op=dbupdate";
|
|
return;
|
|
}
|
|
}
|
|
|
|
return this.report(error,
|
|
Object.extend({title: __("Fatal error")}, params));
|
|
},
|
|
report: function(error, params) {
|
|
params = params || {};
|
|
|
|
if (!error) return;
|
|
|
|
console.error("[Error.report]", error, params);
|
|
|
|
const message = params.message ? params.message : error.toString();
|
|
|
|
try {
|
|
xhrPost("backend.php",
|
|
{op: "rpc", method: "log",
|
|
file: params.filename ? params.filename : error.fileName,
|
|
line: params.lineno ? params.lineno : error.lineNumber,
|
|
msg: message,
|
|
context: error.stack},
|
|
(transport) => {
|
|
console.warn("[Error.report] log response", transport.responseText);
|
|
});
|
|
} catch (re) {
|
|
console.error("[Error.report] exception while saving logging error on server", re);
|
|
}
|
|
|
|
try {
|
|
if (dijit.byId("exceptionDlg"))
|
|
dijit.byId("exceptionDlg").destroyRecursive();
|
|
|
|
let stack_msg = "";
|
|
|
|
if (error.stack)
|
|
stack_msg += `<div><b>Stack trace:</b></div>
|
|
<textarea name="stack" readonly="1">${error.stack}</textarea>`;
|
|
|
|
if (params.info)
|
|
stack_msg += `<div><b>Additional information:</b></div>
|
|
<textarea name="stack" readonly="1">${params.info}</textarea>`;
|
|
|
|
let content = `<div class="error-contents">
|
|
<p class="message">${message}</p>
|
|
${stack_msg}
|
|
<div class="dlgButtons">
|
|
<button dojoType="dijit.form.Button"
|
|
onclick=\"dijit.byId('exceptionDlg').hide()">${__('Close this window')}</button>
|
|
</div>
|
|
</div>`;
|
|
|
|
const dialog = new dijit.Dialog({
|
|
id: "exceptionDlg",
|
|
title: params.title || __("Unhandled exception"),
|
|
style: "width: 600px",
|
|
content: content
|
|
});
|
|
|
|
dialog.show();
|
|
} catch (de) {
|
|
console.error("[Error.report] exception while showing error dialog", de);
|
|
|
|
alert(error.stack ? error.stack : message);
|
|
}
|
|
|
|
},
|
|
onWindowError: function (message, filename, lineno, colno, error) {
|
|
// called without context (this) from window.onerror
|
|
App.Error.report(error,
|
|
{message: message, filename: filename, lineno: lineno, colno: colno});
|
|
},
|
|
}
|
|
});
|
|
});
|