summaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
Diffstat (limited to 'js')
-rw-r--r--js/App.js592
-rw-r--r--js/Article.js182
-rw-r--r--js/CommonDialogs.js633
-rw-r--r--js/CommonFilters.js795
-rwxr-xr-xjs/FeedTree.js6
-rw-r--r--js/Feeds.js150
-rwxr-xr-xjs/Headlines.js363
-rw-r--r--js/PrefFeedTree.js310
-rw-r--r--js/PrefFilterTree.js33
-rw-r--r--js/PrefHelpers.js425
-rw-r--r--js/PrefLabelTree.js185
-rw-r--r--js/PrefUsers.js115
-rw-r--r--js/SingleUseDialog.js11
-rwxr-xr-xjs/common.js273
-rw-r--r--js/form/ValidationMultiSelect.js20
-rwxr-xr-xjs/prefs.js2
-rw-r--r--js/tt-rss.js13
-rw-r--r--js/utility.js10
18 files changed, 2646 insertions, 1472 deletions
diff --git a/js/App.js b/js/App.js
index 514a6d4b7..67d932369 100644
--- a/js/App.js
+++ b/js/App.js
@@ -1,9 +1,9 @@
'use strict';
/* eslint-disable new-cap */
-/* global __, Article, Ajax, Headlines, Filters, fox */
-/* global xhrPost, xhrJson, dojo, dijit, PluginHost, Notify, $$, Feeds, Cookie */
-/* global CommonDialogs, Plugins, Effect */
+/* global __, Article, Headlines, Filters, fox */
+/* global xhr, dojo, dijit, PluginHost, Notify, Feeds, Cookie */
+/* global CommonDialogs, Plugins */
const App = {
_initParams: [],
@@ -18,8 +18,53 @@ const App = {
is_prefs: false,
LABEL_BASE_INDEX: -1024,
FormFields: {
- hidden: function(name, value) {
- return `<input dojoType="dijit.form.TextBox" style="display : none" name="${name}" value="${App.escapeHtml(value)}"></input>`
+ attributes_to_string: function(attributes) {
+ return Object.keys(attributes).map((k) =>
+ `${App.escapeHtml(k)}="${App.escapeHtml(attributes[k])}"`)
+ .join(" ");
+ },
+ hidden_tag: function(name, value, attributes = {}, id = "") {
+ return `<input id="${App.escapeHtml(id)}" dojoType="dijit.form.TextBox" ${this.attributes_to_string(attributes)}
+ style="display : none" name="${name}" value="${App.escapeHtml(value)}"></input>`
+ },
+ // allow html inside because of icons
+ button_tag: function(value, type, attributes = {}) {
+ return `<button dojoType="dijit.form.Button" ${this.attributes_to_string(attributes)}
+ type="${type}">${value}</button>`
+
+ },
+ icon: function(icon, attributes = {}) {
+ return `<i class="material-icons" ${this.attributes_to_string(attributes)}>${icon}</i>`;
+ },
+ submit_tag: function(value, attributes = {}) {
+ return this.button_tag(value, "submit", {...{class: "alt-primary"}, ...attributes});
+ },
+ cancel_dialog_tag: function(value, attributes = {}) {
+ return this.button_tag(value, "", {...{onclick: "App.dialogOf(this).hide()"}, ...attributes});
+ },
+ checkbox_tag: function(name, checked = false, value = "", attributes = {}, id = "") {
+ return `<input dojoType="dijit.form.CheckBox" type="checkbox" name="${App.escapeHtml(name)}"
+ ${checked ? "checked" : ""}
+ ${value ? `value="${App.escapeHtml(value)}"` : ""}
+ ${this.attributes_to_string(attributes)} id="${App.escapeHtml(id)}">`
+ },
+ select_tag: function(name, value, values = [], attributes = {}, id = "") {
+ return `
+ <select name="${name}" dojoType="fox.form.Select" id="${App.escapeHtml(id)}" ${this.attributes_to_string(attributes)}>
+ ${values.map((v) =>
+ `<option ${v == value ? 'selected="selected"' : ''} value="${App.escapeHtml(v)}">${App.escapeHtml(v)}</option>`
+ ).join("")}
+ </select>
+ `
+ },
+ select_hash: function(name, value, values = {}, attributes = {}, id = "") {
+ return `
+ <select name="${name}" dojoType="fox.form.Select" id="${App.escapeHtml(id)}" ${this.attributes_to_string(attributes)}>
+ ${Object.keys(values).map((vk) =>
+ `<option ${vk == value ? 'selected="selected"' : ''} value="${App.escapeHtml(vk)}">${App.escapeHtml(values[vk])}</option>`
+ ).join("")}
+ </select>
+ `
}
},
Scrollable: {
@@ -53,7 +98,25 @@ const App = {
return elem.offsetTop + elem.offsetHeight <= ctr.scrollTop + ctr.offsetHeight &&
elem.offsetTop >= ctr.scrollTop;
- }
+ },
+ scrollTo: function (elem, ctr, params = {}) {
+ const force_to_top = params.force_to_top || false;
+
+ if (!elem || !ctr) return;
+
+ if (force_to_top || !App.Scrollable.fitsInContainer(elem, ctr)) {
+ ctr.scrollTop = elem.offsetTop;
+ }
+ }
+ },
+ byId: function(id) {
+ return document.getElementById(id);
+ },
+ find: function(query) {
+ return document.querySelector(query)
+ },
+ findAll: function(query) {
+ return document.querySelectorAll(query);
},
dialogOf: function (elem) {
@@ -62,6 +125,9 @@ const App = {
return dijit.getEnclosingWidget(elem.closest('.dijitDialog'));
},
+ getPhArgs(plugin, method, args = {}) {
+ return {...{op: "pluginhandler", plugin: plugin, method: method}, ...args};
+ },
label_to_feed_id: function(label) {
return this.LABEL_BASE_INDEX - 1 - Math.abs(label);
},
@@ -83,21 +149,20 @@ const App = {
}
},
setupNightModeDetection: function(callback) {
- if (!$("theme_css")) {
+ if (!App.byId("theme_css")) {
const mql = window.matchMedia('(prefers-color-scheme: dark)');
try {
mql.addEventListener("change", () => {
- this.nightModeChanged(mql.matches, $("theme_auto_css"));
+ this.nightModeChanged(mql.matches, App.byId("theme_auto_css"));
});
} catch (e) {
console.warn("exception while trying to set MQL event listener");
}
- const link = new Element("link", {
- rel: "stylesheet",
- id: "theme_auto_css"
- });
+ const link = document.createElement("link");
+ link.rel = "stylesheet";
+ link.id = "theme_auto_css";
if (callback) {
link.onload = function() {
@@ -119,27 +184,6 @@ const App = {
if (callback) callback();
}
},
- enableCsrfSupport: function() {
- const _this = this;
-
- Ajax.Base.prototype.initialize = Ajax.Base.prototype.initialize.wrap(
- function (callOriginal, options) {
-
- if (_this.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"] = _this.getInitParam("csrf_token");
- }
-
- return callOriginal(options);
- }
- );
- },
postCurrentWindow: function(target, params) {
const form = document.createElement("form");
@@ -188,8 +232,13 @@ const App = {
}
},
- urlParam: function(param) {
- return String(window.location.href).parseQuery()[param];
+ urlParam: function(name) {
+ try {
+ const results = new RegExp('[?&]' + name + '=([^&#]*)').exec(window.location.href);
+ return decodeURIComponent(results[1].replace(/\+/g, " ")) || 0;
+ } catch (e) {
+ return 0;
+ }
},
next_seq: function() {
this._rpc_seq += 1;
@@ -205,7 +254,7 @@ const App = {
dijit.byId("loading_bar").update({progress: this._loading_progress});
if (this._loading_progress >= 90) {
- $("overlay").hide();
+ App.byId("overlay").hide();
}
},
@@ -236,7 +285,7 @@ const App = {
if (!this.hotkey_prefix && hotkeys_map[0].indexOf(keychar) != -1) {
this.hotkey_prefix = keychar;
- $("cmdline").innerHTML = keychar;
+ App.byId("cmdline").innerHTML = keychar;
Element.show("cmdline");
window.clearTimeout(this.hotkey_prefix_timeout);
@@ -285,165 +334,154 @@ const App = {
cleanupMemory: function(root) {
const dijits = dojo.query("[widgetid]", dijit.byId(root).domNode).map(dijit.byNode);
- dijits.each(function (d) {
+ dijits.forEach(function (d) {
dojo.destroy(d.domNode);
});
- $$("#" + root + " *").each(function (i) {
+ App.findAll("#" + root + " *").forEach(function (i) {
i.parentNode ? i.parentNode.removeChild(i) : true;
});
},
// htmlspecialchars()-alike for headlines data-content attribute
- escapeHtml: function(text) {
- const map = {
- '&': '&amp;',
- '<': '&lt;',
- '>': '&gt;',
- '"': '&quot;',
- "'": '&#039;'
- };
-
- return text.replace(/[&<>"']/g, function(m) { return map[m]; });
+ escapeHtml: function(p) {
+ if (typeof p == "string") {
+ const map = {
+ '&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ '"': '&quot;',
+ "'": '&#039;'
+ };
+
+ return p.replace(/[&<>"']/g, function(m) { return map[m]; });
+ } else {
+ return p;
+ }
+ },
+ // http://stackoverflow.com/questions/6251937/how-to-get-selecteduser-highlighted-text-in-contenteditable-element-and-replac
+ getSelectedText: function() {
+ 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();
},
displayIfChecked: function(checkbox, elemId) {
if (checkbox.checked) {
- Effect.Appear(elemId, {duration : 0.5});
+ Element.show(elemId);
} else {
- Effect.Fade(elemId, {duration : 0.5});
+ Element.hide(elemId);
}
},
- helpDialog: function(topic) {
- xhrPost("backend.php", {op: "backend", method: "help", topic: topic}, (transport) => {
+ hotkeyHelp: function() {
+ xhr.post("backend.php", {op: "rpc", method: "hotkeyHelp"}, (reply) => {
const dialog = new fox.SingleUseDialog({
- title: __("Help"),
- content: transport.responseText,
+ title: __("Keyboard shortcuts"),
+ content: reply,
});
dialog.show();
});
},
- 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);
- }
+ handleRpcJson: function(reply) {
- const counters = reply['counters'];
+ const netalert = App.find(".net-alert");
- if (counters)
- Feeds.parseCounters(counters);
+ if (reply) {
+ const error = reply['error'];
+ const seq = reply['seq'];
+ const message = reply['message'];
+ const counters = reply['counters'];
+ const runtime_info = reply['runtime-info'];
- const runtime_info = reply['runtime-info'];
+ if (error && error.code && error.code != App.Error.E_SUCCESS) {
+ console.warn("handleRpcJson: fatal error", error);
+ this.Error.fatal(error.code);
+ return false;
+ }
- if (runtime_info)
- this.parseRuntimeInfo(runtime_info);
+ if (seq && this.get_seq() != seq) {
+ console.warn("handleRpcJson: sequence mismatch: ", seq, '!=', this.get_seq());
+ return false;
+ }
- if (netalert) netalert.hide();
+ // not in preferences
+ if (typeof Feeds != "undefined") {
+ if (message == "UPDATE_COUNTERS") {
+ console.log("need to refresh counters for", reply.feeds);
+ Feeds.requestCounters(reply.feeds);
+ }
- return reply;
+ if (counters)
+ Feeds.parseCounters(counters);
+ }
- } else {
- if (netalert) netalert.show();
+ if (runtime_info)
+ this.parseRuntimeInfo(runtime_info);
- Notify.error("Communication problem with server.");
- }
+ if (netalert) netalert.hide();
- } catch (e) {
- if (netalert) netalert.show();
+ return true;
+ } else {
+ if (netalert) netalert.show();
- Notify.error("Communication problem with server.");
+ Notify.error("Communication problem with server.");
- console.error(e);
+ return false;
}
-
- return false;
},
parseRuntimeInfo: function(data) {
- for (const k in data) {
- if (data.hasOwnProperty(k)) {
- const v = data[k];
+ Object.keys(data).forEach((k) => {
+ const v = data[k];
- console.log("RI:", k, "=>", v);
+ console.log("RI:", k, "=>", v);
- if (k == "daemon_is_running" && v != 1) {
- Notify.error("Update daemon is not running.", true);
- return;
- }
+ if (k == "daemon_is_running" && v != 1) {
+ Notify.error("Update daemon is not running.", true);
+ return;
+ }
- if (k == "recent_log_events") {
- const alert = $$(".log-alert")[0];
+ if (k == "recent_log_events") {
+ const alert = App.find(".log-alert");
- if (alert) {
- v > 0 ? alert.show() : alert.hide();
- }
- }
+ if (alert) {
+ v > 0 ? alert.show() : alert.hide();
+ }
+ }
- if (k == "daemon_stamp_ok" && v != 1) {
- Notify.error("Update daemon is not updating feeds.", true);
- return;
- }
+ if (k == "daemon_stamp_ok" && v != 1) {
+ Notify.error("Update daemon is not updating feeds.", true);
+ return;
+ }
- if (k == "max_feed_id" || k == "num_feeds") {
- if (this.getInitParam(k) != v) {
- console.log("feed count changed, need to reload feedlist.");
- Feeds.reload();
- }
- }
+ if (typeof Feeds != "undefined") {
+ if (k == "max_feed_id" || k == "num_feeds") {
+ if (this.getInitParam(k) && this.getInitParam(k) != v) {
+ console.log("feed count changed, need to reload feedlist:", this.getInitParam(k), v);
+ Feeds.reload();
+ }
+ }
+ }
- this.setInitParam(k, v);
- }
- }
+ this.setInitParam(k, v);
+ });
PluginHost.run(PluginHost.HOOK_RUNTIME_INFO_LOADED, data);
},
- backendSanityCallback: function(transport) {
- const reply = JSON.parse(transport.responseText);
-
- 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']});
- }
- }
-
+ backendSanityCallback: function(reply) {
console.log("sanity check ok");
const params = reply['init-params'];
@@ -451,39 +489,36 @@ const App = {
if (params) {
console.log('reading init-params...');
- for (const k in params) {
- if (params.hasOwnProperty(k)) {
- switch (k) {
- case "label_base_index":
- this.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;
- }
+ Object.keys(params).forEach((k) => {
+ switch (k) {
+ case "label_base_index":
+ this.LABEL_BASE_INDEX = parseInt(params[k]);
+ break;
+ case "cdm_auto_catchup":
+ if (params[k] == 1) {
+ const hl = App.byId("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 = [];
+
+ Object.keys(params[k][1]).forEach((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]);
- }
- }
+ console.log("IP:", k, "=>", params[k]);
+ this.setInitParam(k, params[k]);
+ });
// PluginHost might not be available on non-index pages
if (typeof PluginHost !== 'undefined')
@@ -493,69 +528,68 @@ const App = {
this.initSecondStage();
},
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;
- }
- }
+ E_SUCCESS: "E_SUCCESS",
+ E_UNAUTHORIZED: "E_UNAUTHORIZED",
+ E_SCHEMA_MISMATCH: "E_SCHEMA_MISMATCH",
+ fatal: function (error, params = {}) {
+ if (error == App.Error.E_UNAUTHORIZED) {
+ window.location.href = "index.php";
+ return;
+ } else if (error == App.Error.E_SCHEMA_MISMATCH) {
+ window.location.href = "public.php?op=dbupdate";
+ return;
+ }
- return this.report(error,
- Object.extend({title: __("Fatal error")}, params));
+ return this.report(__("Fatal error: %s").replace("%s", error),
+ {...{title: __("Fatal error")}, ...params});
},
- report: function(error, params) {
- params = params || {};
-
+ report: function(error, params = {}) {
if (!error) return;
- console.error("[Error.report]", error, params);
+ console.error("error.report:", error, params);
const message = params.message ? params.message : error.toString();
try {
- xhrPost("backend.php",
+ xhr.post("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);
+ (reply) => {
+ console.warn("[Error.report] log response", reply);
});
} catch (re) {
console.error("[Error.report] exception while saving logging error on server", re);
}
try {
- 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>`;
-
- const 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 fox.SingleUseDialog({
- id: "exceptionDlg",
title: params.title || __("Unhandled exception"),
- content: content
+ content: `
+ <div class='exception-contents'>
+ <h3>${message}</h3>
+
+ <header>${__('Stack trace')}</header>
+ <section>
+ <textarea readonly='readonly'>${error.stack}</textarea>
+ </section>
+
+ ${params && params.info ?
+ `
+ <header>${__('Additional information')}</header>
+ <section>
+ <textarea readonly='readonly'>${params.info}</textarea>
+ </section>
+ ` : ''}
+ </div>
+ <footer class='text-center'>
+ <button dojoType="dijit.form.Button" class='alt-primary' type='submit'>
+ ${__('Close this window')}
+ </button>
+ </footer>
+ </div>`
});
dialog.show();
@@ -575,6 +609,10 @@ const App = {
isPrefs() {
return this.is_prefs;
},
+ audioCanPlay: function(ctype) {
+ const a = document.createElement('audio');
+ return a.canPlayType(ctype);
+ },
init: function(parser, is_prefs) {
this.is_prefs = is_prefs;
window.onerror = this.Error.onWindowError;
@@ -591,24 +629,17 @@ const App = {
this.setLoadingProgress(30);
this.initHotkeyActions();
- this.enableCsrfSupport();
-
- const a = document.createElement('audio');
- const hasAudio = !!a.canPlayType;
- const hasSandbox = "sandbox" in document.createElement("iframe");
- const hasMp3 = !!(a.canPlayType && a.canPlayType('audio/mpeg;').replace(/no/, ''));
- const clientTzOffset = new Date().getTimezoneOffset() * 60;
const params = {
- op: "rpc", method: "sanityCheck", hasAudio: hasAudio,
- hasMp3: hasMp3,
- clientTzOffset: clientTzOffset,
- hasSandbox: hasSandbox
+ op: "rpc",
+ method: "sanityCheck",
+ clientTzOffset: new Date().getTimezoneOffset() * 60,
+ hasSandbox: "sandbox" in document.createElement("iframe")
};
- xhrPost("backend.php", params, (transport) => {
+ xhr.json("backend.php", params, (reply) => {
try {
- this.backendSanityCallback(transport);
+ this.backendSanityCallback(reply);
} catch (e) {
this.Error.report(e);
}
@@ -618,7 +649,7 @@ const App = {
checkBrowserFeatures: function() {
let errorMsg = "";
- ['MutationObserver'].each(function(wf) {
+ ['MutationObserver'].forEach(function(wf) {
if (!(wf in window)) {
errorMsg = `Browser feature check failed: <code>window.${wf}</code> not found.`;
throw new Error(errorMsg);
@@ -631,6 +662,11 @@ const App = {
return errorMsg == "";
},
+ updateRuntimeInfo: function() {
+ xhr.json("backend.php", {op: "rpc", method: "getruntimeinfo"}, () => {
+ // handled by xhr.json()
+ });
+ },
initSecondStage: function() {
document.onkeydown = (event) => this.hotkeyHandler(event);
@@ -648,14 +684,18 @@ const App = {
if (tab) {
dijit.byId("pref-tabs").selectChild(tab);
- switch (this.urlParam('method')) {
- case "editfeed":
- window.setTimeout(() => {
- CommonDialogs.editFeed(this.urlParam('methodparam'))
- }, 100);
- break;
- default:
- console.warn("initSecondStage, unknown method:", this.urlParam("method"));
+ const method = this.urlParam("method");
+
+ if (method) {
+ switch (method) {
+ case "editfeed":
+ window.setTimeout(() => {
+ CommonDialogs.editFeed(this.urlParam('methodparam'))
+ }, 100);
+ break;
+ default:
+ console.warn("initSecondStage, unknown method:", method);
+ }
}
}
} else {
@@ -671,6 +711,7 @@ const App = {
dojo.connect(dijit.byId("pref-tabs"), "selectChild", function (elem) {
localStorage.setItem("ttrss:prefs-tab", elem.id);
+ App.updateRuntimeInfo();
});
} else {
@@ -726,24 +767,28 @@ const App = {
}, 3600 * 1000);
}
- console.log("second stage ok");
-
PluginHost.run(PluginHost.HOOK_INIT_COMPLETE, null);
-
}
+ if (!this.getInitParam("bw_limit"))
+ window.setInterval(() => {
+ App.updateRuntimeInfo();
+ }, 60 * 1000)
+
+ console.log("second stage ok");
+
},
checkForUpdates: function() {
console.log('checking for updates...');
- xhrJson("backend.php", {op: 'rpc', method: 'checkforupdates'})
+ xhr.json("backend.php", {op: 'rpc', method: 'checkforupdates'})
.then((reply) => {
console.log('update reply', reply);
if (reply.id) {
- $("updates-available").show();
+ App.byId("updates-available").show();
} else {
- $("updates-available").hide();
+ App.byId("updates-available").hide();
}
});
},
@@ -759,7 +804,7 @@ const App = {
onViewModeChanged: function() {
const view_mode = document.forms["toolbar-main"].view_mode.value;
- $$("body")[0].setAttribute("view-mode", view_mode);
+ App.findAll("body")[0].setAttribute("view-mode", view_mode);
return Feeds.reloadCurrent('');
},
@@ -798,8 +843,8 @@ const App = {
{width: Cookie.get("ttrss_ci_width") + "px" });
}
- $("headlines-frame").setStyle({ borderBottomWidth: '0px' });
- $("headlines-frame").addClassName("wide");
+ App.byId("headlines-frame").setStyle({ borderBottomWidth: '0px' });
+ App.byId("headlines-frame").addClassName("wide");
} else {
@@ -814,8 +859,8 @@ const App = {
{height: Cookie.get("ttrss_ci_height") + "px" });
}
- $("headlines-frame").setStyle({ borderBottomWidth: '1px' });
- $("headlines-frame").removeClassName("wide");
+ App.byId("headlines-frame").setStyle({ borderBottomWidth: '1px' });
+ App.byId("headlines-frame").removeClassName("wide");
}
@@ -823,13 +868,13 @@ const App = {
if (article_id) Article.view(article_id);
- xhrPost("backend.php", {op: "rpc", method: "setpanelmode", wide: wide ? 1 : 0});
+ xhr.post("backend.php", {op: "rpc", method: "setpanelmode", wide: wide ? 1 : 0});
},
initHotkeyActions: function() {
if (this.is_prefs) {
this.hotkey_actions["feed_subscribe"] = () => {
- CommonDialogs.quickAddFeed();
+ CommonDialogs.subscribeToFeed();
};
this.hotkey_actions["create_label"] = () => {
@@ -841,7 +886,7 @@ const App = {
};
this.hotkey_actions["help_dialog"] = () => {
- this.helpDialog("main");
+ this.hotkeyHelp();
};
} else {
@@ -985,14 +1030,13 @@ const App = {
Feeds.toggleUnread();
};
this.hotkey_actions["feed_subscribe"] = () => {
- CommonDialogs.quickAddFeed();
+ CommonDialogs.subscribeToFeed();
};
this.hotkey_actions["feed_debug_update"] = () => {
if (!Feeds.activeIsCat() && parseInt(Feeds.getActive()) > 0) {
- //window.open("backend.php?op=feeds&method=update_debugger&feed_id=" + Feeds.getActive());
/* global __csrf_token */
- App.postOpenWindow("backend.php", {op: "feeds", method: "update_debugger",
+ App.postOpenWindow("backend.php", {op: "feeds", method: "updatedebugger",
feed_id: Feeds.getActive(), csrf_token: __csrf_token});
} else {
@@ -1001,8 +1045,6 @@ const App = {
};
this.hotkey_actions["feed_debug_viewfeed"] = () => {
- //Feeds.open({feed: Feeds.getActive(), is_cat: Feeds.activeIsCat(), viewfeed_debug: true});
-
App.postOpenWindow("backend.php", {op: "feeds", method: "view",
feed: Feeds.getActive(), timestamps: 1, debug: 1, cat: Feeds.activeIsCat(), csrf_token: __csrf_token});
};
@@ -1022,7 +1064,7 @@ const App = {
Headlines.reverse();
};
this.hotkey_actions["feed_toggle_vgroup"] = () => {
- xhrPost("backend.php", {op: "rpc", method: "togglepref", key: "VFEED_GROUP_BY_FEED"}, () => {
+ xhr.post("backend.php", {op: "rpc", method: "togglepref", key: "VFEED_GROUP_BY_FEED"}, () => {
Feeds.reloadCurrent();
})
};
@@ -1055,7 +1097,7 @@ const App = {
this.hotkey_actions["select_article_cursor"] = () => {
const id = Article.getUnderPointer();
if (id) {
- const row = $("RROW-" + id);
+ const row = App.byId(`RROW-${id}`);
if (row)
row.toggleClassName("Selected");
@@ -1092,12 +1134,12 @@ const App = {
}
};
this.hotkey_actions["help_dialog"] = () => {
- this.helpDialog("main");
+ this.hotkeyHelp();
};
this.hotkey_actions["toggle_combined_mode"] = () => {
const value = this.isCombinedMode() ? "false" : "true";
- xhrPost("backend.php", {op: "rpc", method: "setpref", key: "COMBINED_DISPLAY_MODE", value: value}, () => {
+ xhr.post("backend.php", {op: "rpc", method: "setpref", key: "COMBINED_DISPLAY_MODE", value: value}, () => {
this.setInitParam("combined_display_mode",
!this.getInitParam("combined_display_mode"));
@@ -1108,7 +1150,7 @@ const App = {
this.hotkey_actions["toggle_cdm_expanded"] = () => {
const value = this.getInitParam("cdm_expanded") ? "false" : "true";
- xhrPost("backend.php", {op: "rpc", method: "setpref", key: "CDM_EXPANDED", value: value}, () => {
+ xhr.post("backend.php", {op: "rpc", method: "setpref", key: "CDM_EXPANDED", value: value}, () => {
this.setInitParam("cdm_expanded", !this.getInitParam("cdm_expanded"));
Headlines.renderAgain();
});
@@ -1130,7 +1172,7 @@ const App = {
Feeds.search();
break;
case "qmcAddFeed":
- CommonDialogs.quickAddFeed();
+ CommonDialogs.subscribeToFeed();
break;
case "qmcDigest":
window.location.href = "backend.php?op=digest";
@@ -1182,7 +1224,7 @@ const App = {
}
break;
case "qmcHKhelp":
- this.helpDialog("main");
+ this.hotkeyHelp()
break;
default:
console.log("quickMenuGo: unknown action: " + opid);
diff --git a/js/Article.js b/js/Article.js
index 61368dfed..5f695561c 100644
--- a/js/Article.js
+++ b/js/Article.js
@@ -1,7 +1,7 @@
'use strict'
/* eslint-disable no-new */
-/* global __, ngettext, App, Headlines, xhrPost, xhrJson, dojo, dijit, PluginHost, Notify, $$, Ajax, fox */
+/* global __, ngettext, App, Headlines, xhr, dojo, dijit, PluginHost, Notify, fox */
const Article = {
_scroll_reset_timeout: false,
@@ -36,19 +36,19 @@ const Article = {
const score = prompt(__("Please enter new score for selected articles:"));
if (!isNaN(parseInt(score))) {
- ids.each((id) => {
- const row = $("RROW-" + id);
+ ids.forEach((id) => {
+ const row = App.byId(`RROW-${id}`);
if (row) {
row.setAttribute("data-score", score);
- const pic = row.select(".icon-score")[0];
+ const pic = row.querySelector(".icon-score");
pic.innerHTML = Article.getScorePic(score);
pic.setAttribute("title", score);
["score-low", "score-high", "score-half-low", "score-half-high", "score-neutral"]
- .each(function(scl) {
+ .forEach(function(scl) {
if (row.hasClassName(scl))
row.removeClassName(scl);
});
@@ -63,7 +63,7 @@ const Article = {
}
},
setScore: function (id, pic) {
- const row = pic.up("div[id*=RROW]");
+ const row = pic.closest("div[id*=RROW]");
if (row) {
const score_old = row.getAttribute("data-score");
@@ -72,13 +72,13 @@ const Article = {
if (!isNaN(parseInt(score))) {
row.setAttribute("data-score", score);
- const pic = row.select(".icon-score")[0];
+ const pic = row.querySelector(".icon-score");
pic.innerHTML = Article.getScorePic(score);
pic.setAttribute("title", score);
["score-low", "score-high", "score-half-low", "score-half-high", "score-neutral"]
- .each(function(scl) {
+ .forEach(function(scl) {
if (row.hasClassName(scl))
row.removeClassName(scl);
});
@@ -93,18 +93,8 @@ const Article = {
w.opener = null;
w.location = url;
},
- /* popupOpenArticle: function(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");
-
- if (w) {
- w.opener = null;
- w.location = "backend.php?op=article&method=view&mode=raw&html=1&zoom=1&id=" + id + "&csrf_token=" + App.getInitParam("csrf_token");
- }
- }, */
cdmUnsetActive: function (event) {
- const row = $("RROW-" + Article.getActive());
+ const row = App.byId(`RROW-${Article.getActive()}`);
if (row) {
row.removeClassName("active");
@@ -123,11 +113,13 @@ const Article = {
Article.setActive(0);
},
displayUrl: function (id) {
- const query = {op: "rpc", method: "getlinktitlebyid", id: id};
+ const query = {op: "article", method: "getmetadatabyid", id: id};
- xhrJson("backend.php", query, (reply) => {
+ xhr.json("backend.php", query, (reply) => {
if (reply && reply.link) {
prompt(__("Article URL:"), reply.link);
+ } else {
+ alert(__("No URL could be displayed for this article."));
}
});
},
@@ -138,6 +130,77 @@ const Article = {
Headlines.toggleUnread(id, 0);
},
+ renderNote: function (id, note) {
+ return `<div class="article-note" data-note-for="${id}" style="display : ${note ? "" : "none"}">
+ ${App.FormFields.icon('note')} <div onclick class='body'>${note ? App.escapeHtml(note) : ""}</div>
+ </div>`;
+ },
+ renderTags: function (id, tags) {
+ const tags_short = tags.length > 5 ? tags.slice(0, 5) : tags;
+
+ return `<span class="tags" title="${tags.join(", ")}" data-tags-for="${id}">
+ ${tags_short.length > 0 ? tags_short.map((tag) => `
+ <a href="#" onclick="Feeds.open({feed: '${tag.trim()}'})" class="tag">${tag}</a>`
+ ).join(", ") : `${__("no tags")}`}</span>`;
+ },
+ renderLabels: function(id, labels) {
+ return `<span class="labels" data-labels-for="${id}">${labels.map((label) => `
+ <span class="label" data-label-id="${label[0]}"
+ style="color : ${label[2]}; background-color : ${label[3]}">${App.escapeHtml(label[1])}</span>`
+ ).join("")}</span>`;
+ },
+ renderEnclosures: function (enclosures) {
+ return `
+ ${enclosures.formatted}
+ ${enclosures.can_inline ?
+ `<div class='attachments-inline'>
+ ${enclosures.entries.map((enc) => {
+ if (!enclosures.inline_text_only) {
+ if (enc.content_type && enc.content_type.indexOf("image/") != -1) {
+ return `<p>
+ <img loading="lazy"
+ width="${enc.width ? enc.width : ''}"
+ height="${enc.height ? enc.height : ''}"
+ src="${App.escapeHtml(enc.content_url)}"
+ title="${App.escapeHtml(enc.title ? enc.title : enc.content_url)}"/>
+ </p>`
+ } else if (enc.content_type && enc.content_type.indexOf("audio/") != -1 && App.audioCanPlay(enc.content_type)) {
+ return `<p class='inline-player' title="${App.escapeHtml(enc.content_url)}">
+ <audio preload="none" controls="controls">
+ <source type="${App.escapeHtml(enc.content_type)}" src="${App.escapeHtml(enc.content_url)}"/>
+ </audio>
+ </p>
+ `;
+ } else {
+ return `<p>
+ <a target="_blank" href="${App.escapeHtml(enc.content_url)}"
+ title="${App.escapeHtml(enc.title ? enc.title : enc.content_url)}"
+ rel="noopener noreferrer">${App.escapeHtml(enc.content_url)}</a>
+ </p>`
+ }
+ } else {
+ return `<p>
+ <a target="_blank" href="${App.escapeHtml(enc.content_url)}"
+ title="${App.escapeHtml(enc.title ? enc.title : enc.content_url)}"
+ rel="noopener noreferrer">${App.escapeHtml(enc.content_url)}</a>
+ </p>`
+ }
+ }).join("")}
+ </div>` : ''}
+ ${enclosures.entries.length > 0 ?
+ `<div class="attachments" dojoType="fox.form.DropDownButton">
+ <span>${__('Attachments')}</span>
+ <div dojoType="dijit.Menu" style="display: none">
+ ${enclosures.entries.map((enc) => `
+ <div onclick='Article.popupOpenUrl("${App.escapeHtml(enc.content_url)}")'
+ title="${App.escapeHtml(enc.title ? enc.title : enc.content_url)}" dojoType="dijit.MenuItem">
+ ${enc.title ? enc.title : enc.filename}
+ </div>
+ `).join("")}
+ </div>
+ </div>` : ''}
+ `
+ },
render: function (article) {
App.cleanupMemory("content-insert");
@@ -184,12 +247,14 @@ const Article = {
container.innerHTML = row.getAttribute("data-content").trim();
+ dojo.parser.parse(container);
+
// blank content element might screw up onclick selection and keyboard moving
if (container.textContent.length == 0)
container.innerHTML += "&nbsp;";
// in expandable mode, save content for later, so that we can pack unfocused rows back
- if (App.isCombinedMode() && $("main").hasClassName("expandable"))
+ if (App.isCombinedMode() && App.byId("main").hasClassName("expandable"))
row.setAttribute("data-content-original", row.getAttribute("data-content"));
row.removeAttribute("data-content");
@@ -230,16 +295,16 @@ const Article = {
<div class="comments">${comments}</div>
<div class="author">${hl.author}</div>
<i class="material-icons">label_outline</i>
- <span id="ATSTR-${hl.id}">${hl.tags_str}</span>
+ ${Article.renderTags(hl.id, hl.tags)}
&nbsp;<a title="${__("Edit tags for this article")}" href="#"
onclick="Article.editTags(${hl.id})">(+)</a>
<div class="buttons right">${hl.buttons}</div>
</div>
</div>
- <div id="POSTNOTE-${hl.id}">${hl.note}</div>
+ ${Article.renderNote(hl.id, hl.note)}
<div class="content" lang="${hl.lang ? hl.lang : 'en'}">
${hl.content}
- ${hl.enclosures}
+ ${Article.renderEnclosures(hl.enclosures)}
</div>
</div>`;
@@ -252,29 +317,41 @@ const Article = {
},
editTags: function (id) {
const dialog = new fox.SingleUseDialog({
- id: "editTagsDlg",
title: __("Edit article Tags"),
- content: __("Loading, please wait..."),
+ content: `
+ ${App.FormFields.hidden_tag("id", id.toString())}
+ ${App.FormFields.hidden_tag("op", "article")}
+ ${App.FormFields.hidden_tag("method", "setArticleTags")}
+
+ <header class='horizontal'>
+ ${__("Tags for this article (separated by commas):")}
+ </header>
+
+ <section>
+ <textarea dojoType='dijit.form.SimpleTextarea' rows='4' disabled='true'
+ id='tags_str' name='tags_str'></textarea>
+ <div class='autocomplete' id='tags_choices' style='display:none'></div>
+ </section>
+
+ <footer>
+ <button dojoType='dijit.form.Button' type='submit' class='alt-primary'>
+ ${__('Save')}
+ </button>
+ <button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'>
+ ${__('Cancel')}
+ </button>
+ </footer>
+ `,
execute: function () {
if (this.validate()) {
Notify.progress("Saving article tags...", true);
- xhrPost("backend.php", this.attr('value'), (transport) => {
+ xhr.json("backend.php", this.attr('value'), (data) => {
try {
Notify.close();
dialog.hide();
- const data = JSON.parse(transport.responseText);
-
- if (data) {
- const id = data.id;
-
- const tags = $("ATSTR-" + id);
- const tooltip = dijit.byId("ATSTRTIP-" + id);
-
- if (tags) tags.innerHTML = data.content;
- if (tooltip) tooltip.attr('label', data.content_full);
- }
+ Headlines.onTagsUpdated(data);
} catch (e) {
App.Error.report(e);
}
@@ -286,25 +363,26 @@ const Article = {
const tmph = dojo.connect(dialog, 'onShow', function () {
dojo.disconnect(tmph);
- xhrPost("backend.php", {op: "article", method: "editarticletags", param: id}, (transport) => {
- dialog.attr('content', transport.responseText);
+ xhr.json("backend.php", {op: "article", method: "printArticleTags", id: id}, (reply) => {
+
+ dijit.getEnclosingWidget(App.byId("tags_str"))
+ .attr('value', reply.tags.join(", "))
+ .attr('disabled', false);
- new Ajax.Autocompleter('tags_str', 'tags_choices',
+ /* new Ajax.Autocompleter("tags_str", "tags_choices",
"backend.php?op=article&method=completeTags",
- {tokens: ',', paramName: "search"});
+ {tokens: ',', paramName: "search"}); */
});
});
dialog.show();
},
- cdmMoveToId: function (id, params) {
- params = params || {};
-
+ cdmMoveToId: function (id, params = {}) {
const force_to_top = params.force_to_top || false;
- const ctr = $("headlines-frame");
- const row = $("RROW-" + id);
+ const ctr = App.byId("headlines-frame");
+ const row = App.byId(`RROW-${id}`);
if (!row || !ctr) return;
@@ -316,12 +394,12 @@ const Article = {
if (id != Article.getActive()) {
console.log("setActive", id, "was", Article.getActive());
- $$("div[id*=RROW][class*=active]").each((row) => {
+ App.findAll("div[id*=RROW][class*=active]").forEach((row) => {
row.removeClassName("active");
Article.pack(row);
});
- const row = $("RROW-" + id);
+ const row = App.byId(`RROW-${id}`);
if (row) {
Article.unpack(row);
@@ -342,10 +420,10 @@ const Article = {
return 0;
},
scrollByPages: function (page_offset) {
- App.Scrollable.scrollByPages($("content-insert"), page_offset);
+ App.Scrollable.scrollByPages(App.byId("content-insert"), page_offset);
},
scroll: function (offset) {
- App.Scrollable.scroll($("content-insert"), offset);
+ App.Scrollable.scroll(App.byId("content-insert"), offset);
},
mouseIn: function (id) {
this.post_under_pointer = id;
diff --git a/js/CommonDialogs.js b/js/CommonDialogs.js
index 70596539b..321ddf6d3 100644
--- a/js/CommonDialogs.js
+++ b/js/CommonDialogs.js
@@ -3,7 +3,7 @@
/* eslint-disable new-cap */
/* eslint-disable no-new */
-/* global __, dojo, dijit, Notify, App, Feeds, $$, xhrPost, xhrJson, Tables, Effect, fox */
+/* global __, dojo, dijit, Notify, App, Feeds, xhrPost, xhr, Tables, fox */
/* exported CommonDialogs */
const CommonDialogs = {
@@ -11,89 +11,99 @@ const CommonDialogs = {
const dialog = dijit.byId("infoBox");
if (dialog) dialog.hide();
},
- removeFeedIcon: function(id) {
- if (confirm(__("Remove stored feed icon?"))) {
- Notify.progress("Removing feed icon...", true);
-
- const query = {op: "pref-feeds", method: "removeicon", feed_id: id};
-
- xhrPost("backend.php", query, () => {
- Notify.info("Feed icon removed.");
-
- if (App.isPrefs())
- dijit.byId("feedTree").reload();
- else
- Feeds.reload();
-
- const icon = $$(".feed-editor-icon")[0];
-
- if (icon)
- icon.src = icon.src.replace(/\?[0-9]+$/, "?" + new Date().getTime());
-
- });
- }
-
- return false;
- },
- uploadFeedIcon: function() {
- 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);
-
- const xhr = new XMLHttpRequest();
-
- xhr.open( 'POST', 'backend.php', true );
- xhr.onload = function () {
- switch (parseInt(this.responseText)) {
- case 0:
- {
- Notify.info("Upload complete.");
-
- if (App.isPrefs())
- dijit.byId("feedTree").reload();
- else
- Feeds.reload();
-
- const icon = $$(".feed-editor-icon")[0];
-
- if (icon)
- icon.src = icon.src.replace(/\?[0-9]+$/, "?" + new Date().getTime());
-
- }
- break;
- case 1:
- Notify.error("Upload failed: icon is too big.");
- break;
- case 2:
- Notify.error("Upload failed.");
- break;
- }
- };
- xhr.send(new FormData($("feed_icon_upload_form")));
- }
-
- return false;
- },
- quickAddFeed: function() {
- xhrPost("backend.php",
- {op: "feeds", method: "quickAddFeed"},
- (transport) => {
-
+ subscribeToFeed: function() {
+ xhr.json("backend.php",
+ {op: "feeds", method: "subscribeToFeed"},
+ (reply) => {
const dialog = new fox.SingleUseDialog({
- id: "feedAddDlg",
title: __("Subscribe to Feed"),
- content: transport.responseText,
+ content: `
+ <form onsubmit='return false'>
+
+ ${App.FormFields.hidden_tag("op", "feeds")}
+ ${App.FormFields.hidden_tag("method", "add")}
+
+ <div id='fadd_error_message' style='display : none' class='alert alert-danger'></div>
+
+ <div id='fadd_multiple_notify' style='display : none'>
+ <div class='alert alert-info'>
+ ${__("Provided URL is a HTML page referencing multiple feeds, please select required feed from the dropdown menu below.")}
+ </div>
+ </div>
+
+ <section>
+ <fieldset>
+ <div style='float : right'><img style='display : none' id='feed_add_spinner' src='images/indicator_white.gif'></div>
+ <input style='font-size : 16px; width : 500px;'
+ placeHolder="${__("Feed or site URL")}"
+ dojoType='dijit.form.ValidationTextBox'
+ required='1' name='feed' id='feedDlg_feedUrl'>
+ </fieldset>
+
+ ${App.getInitParam('enable_feed_cats') ?
+ `
+ <fieldset>
+ <label class='inline'>${__('Place in category:')}</label>
+ ${reply.cat_select}
+ </fieldset>
+ ` : ''}
+ </section>
+
+ <div id="feedDlg_feedsContainer" style="display : none">
+ <header>${__('Available feeds')}</header>
+ <section>
+ <fieldset>
+ <select id="feedDlg_feedContainerSelect"
+ dojoType="fox.form.Select" size="3">
+ <script type="dojo/method" event="onChange" args="value">
+ dijit.byId("feedDlg_feedUrl").attr("value", value);
+ </script>
+ </select>
+ </fieldset>
+ </section>
+ </div>
+
+ <div id='feedDlg_loginContainer' style='display : none'>
+ <section>
+ <fieldset>
+ <input dojoType="dijit.form.TextBox" name='login'"
+ placeHolder="${__("Login")}"
+ autocomplete="new-password"
+ style="width : 10em;">
+ <input
+ placeHolder="${__("Password")}"
+ dojoType="dijit.form.TextBox" type='password'
+ autocomplete="new-password"
+ style="width : 10em;" name='pass'">
+ </fieldset>
+ </section>
+ </div>
+
+ <section>
+ <label class='checkbox'>
+ <input type='checkbox' name='need_auth' dojoType='dijit.form.CheckBox' id='feedDlg_loginCheck'
+ onclick='App.displayIfChecked(this, "feedDlg_loginContainer")'>
+ ${__('This feed requires authentication.')}
+ </label>
+ </section>
+
+ <footer>
+ <button dojoType='dijit.form.Button' class='alt-primary' type='submit'
+ onclick='App.dialogOf(this).execute()'>
+ ${__('Subscribe')}
+ </button>
+ <button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'>
+ ${__('Cancel')}
+ </button>
+ </footer>
+ </form>
+ `,
show_error: function (msg) {
- const elem = $("fadd_error_message");
+ const elem = App.byId("fadd_error_message");
elem.innerHTML = msg;
- if (!Element.visible(elem))
- new Effect.Appear(elem);
-
+ Element.show(elem);
},
execute: function () {
if (this.validate()) {
@@ -104,17 +114,12 @@ const CommonDialogs = {
Element.show("feed_add_spinner");
Element.hide("fadd_error_message");
- xhrPost("backend.php", this.attr('value'), (transport) => {
+ xhr.json("backend.php", this.attr('value'), (reply) => {
try {
- let reply;
-
- try {
- reply = JSON.parse(transport.responseText);
- } catch (e) {
+ if (!reply) {
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;
}
@@ -161,7 +166,7 @@ const CommonDialogs = {
}
}
- Effect.Appear('feedDlg_feedsContainer', {duration: 0.5});
+ Element.show('feedDlg_feedsContainer');
}
break;
case 5:
@@ -188,69 +193,103 @@ const CommonDialogs = {
});
},
showFeedsWithErrors: function() {
- const dialog = new fox.SingleUseDialog({
- id: "errorFeedsDlg",
- title: __("Feeds with update errors"),
- getSelectedFeeds: function () {
- return Tables.getSelected("error-feeds-list");
- },
- removeSelected: function () {
- const sel_rows = this.getSelectedFeeds();
- if (sel_rows.length > 0) {
- if (confirm(__("Remove selected feeds?"))) {
- Notify.progress("Removing selected feeds...", true);
+ xhr.json("backend.php", {op: "pref-feeds", method: "feedsWithErrors"}, (reply) => {
- const query = {
- op: "pref-feeds", method: "remove",
- ids: sel_rows.toString()
- };
+ const dialog = new fox.SingleUseDialog({
+ id: "errorFeedsDlg",
+ title: __("Feeds with update errors"),
+ getSelectedFeeds: function () {
+ return Tables.getSelected("error-feeds-list");
+ },
+ removeSelected: function () {
+ const sel_rows = this.getSelectedFeeds();
- xhrPost("backend.php", query, () => {
- Notify.close();
- dialog.hide();
+ if (sel_rows.length > 0) {
+ if (confirm(__("Remove selected feeds?"))) {
+ Notify.progress("Removing selected feeds...", true);
- if (App.isPrefs())
- dijit.byId("feedTree").reload();
- else
- Feeds.reload();
+ const query = {
+ op: "pref-feeds", method: "remove",
+ ids: sel_rows.toString()
+ };
- });
- }
+ xhr.post("backend.php", query, () => {
+ Notify.close();
+ dialog.hide();
- } else {
- alert(__("No feeds selected."));
- }
- },
- content: __("Loading, please wait...")
- });
+ if (App.isPrefs())
+ dijit.byId("feedTree").reload();
+ else
+ Feeds.reload();
- const tmph = dojo.connect(dialog, 'onShow', function () {
- dojo.disconnect(tmph);
+ });
+ }
- xhrPost("backend.php", {op: "pref-feeds", method: "feedsWithErrors"}, (transport) => {
- dialog.attr('content', transport.responseText);
- })
- });
+ } else {
+ alert(__("No feeds selected."));
+ }
+ },
+ content: `
+ <div dojoType="fox.Toolbar">
+ <div dojoType="fox.form.DropDownButton">
+ <span>${__('Select')}</span>
+ <div dojoType="dijit.Menu" style="display: none">
+ <div onclick="Tables.select('error-feeds-list', true)"
+ dojoType="dijit.MenuItem">${__('All')}</div>
+ <div onclick="Tables.select('error-feeds-list', false)"
+ dojoType="dijit.MenuItem">${__('None')}</div>
+ </div>
+ </div>
+ </div>
+
+ <div class='panel panel-scrollable'>
+ <table width='100%' id='error-feeds-list'>
+
+ ${reply.map((row) => `
+ <tr data-row-id='${row.id}'>
+ <td width='5%' align='center'>
+ <input onclick='Tables.onRowChecked(this)' dojoType="dijit.form.CheckBox"
+ type="checkbox">
+ </td>
+ <td>
+ <a href="#" title="${__("Click to edit feed")}" onclick="CommonDialogs.editFeed(${row.id})">
+ ${App.escapeHtml(row.title)}
+ </a>
+ </td>
+ <td class='text-muted small' align='right' width='50%'>
+ ${App.escapeHtml(row.last_error)}
+ </td>
+ </tr>
+ `).join("")}
+ </table>
+ </div>
+
+ <footer>
+ <button style='float : left' class='alt-danger' dojoType='dijit.form.Button' onclick='App.dialogOf(this).removeSelected()'>
+ ${__('Unsubscribe from selected feeds')}
+ </button>
+ <button dojoType='dijit.form.Button' class='alt-primary' type='submit'>
+ ${__('Close this window')}
+ </button>
+ </footer>
+ `
+ });
- dialog.show();
+ dialog.show();
+ })
},
- addLabel: function(select, callback) {
+ addLabel: function() {
const caption = prompt(__("Please enter label caption:"), "");
if (caption != undefined && caption.trim().length > 0) {
const query = {op: "pref-labels", method: "add", caption: caption.trim()};
- if (select)
- Object.extend(query, {output: "select"});
-
Notify.progress("Loading, please wait...", true);
- xhrPost("backend.php", query, (transport) => {
- if (callback) {
- callback(transport);
- } else if (App.isPrefs()) {
+ xhr.post("backend.php", query, () => {
+ if (dijit.byId("labelTree")) {
dijit.byId("labelTree").reload();
} else {
Feeds.reload();
@@ -267,13 +306,13 @@ const CommonDialogs = {
const query = {op: "pref-feeds", quiet: 1, method: "remove", ids: feed_id};
- xhrPost("backend.php", query, () => {
+ xhr.post("backend.php", query, () => {
if (App.isPrefs()) {
dijit.byId("feedTree").reload();
} else {
if (feed_id == Feeds.getActive())
setTimeout(() => {
- Feeds.open({feed: -5})
+ Feeds.openDefaultFeed();
},
100);
@@ -284,28 +323,109 @@ const CommonDialogs = {
return false;
},
- editFeed: function (feed) {
- if (feed <= 0)
+ editFeed: function (feed_id) {
+ if (feed_id <= 0)
return alert(__("You can't edit this kind of feed."));
- const query = {op: "pref-feeds", method: "editfeed", id: feed};
+ const query = {op: "pref-feeds", method: "editfeed", id: feed_id};
console.log("editFeed", query);
const dialog = new fox.SingleUseDialog({
id: "feedEditDlg",
title: __("Edit Feed"),
- unsubscribeFeed: function(feed_id, title) {
- if (confirm(__("Unsubscribe from %s?").replace("%s", title))) {
+ feed_title: "",
+ unsubscribe: function() {
+ if (confirm(__("Unsubscribe from %s?").replace("%s", this.feed_title))) {
dialog.hide();
CommonDialogs.unsubscribeFeed(feed_id);
}
},
+ uploadIcon: function(input) {
+ if (input.files.length != 0) {
+ const icon_file = input.files[0];
+
+ if (icon_file.type.indexOf("image/") == -1) {
+ alert(__("Please select an image file."));
+ return;
+ }
+
+ const fd = new FormData();
+ fd.append('icon_file', icon_file)
+ fd.append('feed_id', feed_id);
+ fd.append('op', 'pref-feeds');
+ fd.append('method', 'uploadIcon');
+ fd.append('csrf_token', App.getInitParam("csrf_token"));
+
+ const xhr = new XMLHttpRequest();
+
+ xhr.open( 'POST', 'backend.php', true );
+ xhr.onload = function () {
+ console.log(this.responseText);
+
+ // TODO: make a notice box within panel content
+ switch (parseInt(this.responseText)) {
+ case 1:
+ Notify.error("Upload failed: icon is too big.");
+ break;
+ case 2:
+ Notify.error("Upload failed.");
+ break;
+ default:
+ {
+ Notify.info("Upload complete.");
+
+ if (App.isPrefs())
+ dijit.byId("feedTree").reload();
+ else
+ Feeds.reload();
+
+ const icon = dialog.domNode.querySelector(".feedIcon");
+
+ if (icon) {
+ icon.src = this.responseText;
+ icon.show();
+ }
+
+ input.value = "";
+ }
+ }
+ };
+
+ xhr.send(fd);
+
+ }
+ },
+ removeIcon: function(id) {
+ if (confirm(__("Remove stored feed icon?"))) {
+ Notify.progress("Removing feed icon...", true);
+
+ const query = {op: "pref-feeds", method: "removeicon", feed_id: id};
+
+ xhr.post("backend.php", query, () => {
+ Notify.info("Feed icon removed.");
+
+ if (App.isPrefs())
+ dijit.byId("feedTree").reload();
+ else
+ Feeds.reload();
+
+ const icon = dialog.domNode.querySelector(".feedIcon");
+
+ if (icon) {
+ icon.src = "";
+ icon.hide();
+ }
+ });
+ }
+
+ return false;
+ },
execute: function () {
if (this.validate()) {
Notify.progress("Saving data...", true);
- xhrPost("backend.php", dialog.attr('value'), () => {
+ xhr.post("backend.php", dialog.attr('value'), () => {
dialog.hide();
Notify.close();
@@ -315,7 +435,9 @@ const CommonDialogs = {
Feeds.reload();
});
+ return true;
}
+ return false;
},
content: __("Loading, please wait...")
});
@@ -323,102 +445,195 @@ const CommonDialogs = {
const tmph = dojo.connect(dialog, 'onShow', function () {
dojo.disconnect(tmph);
- xhrPost("backend.php", {op: "pref-feeds", method: "editfeed", id: feed}, (transport) => {
- dialog.attr('content', transport.responseText);
+ xhr.json("backend.php", {op: "pref-feeds", method: "editfeed", id: feed_id}, (reply) => {
+ const feed = reply.feed;
+
+ // for unsub prompt
+ dialog.feed_title = feed.title;
+
+ // options tab
+ const options = {
+ include_in_digest: [ feed.include_in_digest, __('Include in e-mail digest') ],
+ always_display_enclosures: [ feed.always_display_enclosures, __('Always display image attachments') ],
+ hide_images: [ feed.hide_images, __('Do not embed media') ],
+ cache_images: [ feed.cache_images, __('Cache media') ],
+ mark_unread_on_update: [ feed.mark_unread_on_update, __('Mark updated articles as unread') ]
+ };
+
+ dialog.attr('content',
+ `
+ <form onsubmit="return false">
+ <div dojoType="dijit.layout.TabContainer" style="height : 450px">
+ <div dojoType="dijit.layout.ContentPane" title="${__('General')}">
+
+ ${App.FormFields.hidden_tag("id", feed_id)}
+ ${App.FormFields.hidden_tag("op", "pref-feeds")}
+ ${App.FormFields.hidden_tag("method", "editSave")}
+
+ <section>
+ <fieldset>
+ <input dojoType='dijit.form.ValidationTextBox' required='1'
+ placeHolder="${__("Feed Title")}"
+ style='font-size : 16px; width: 500px' name='title' value="${App.escapeHtml(feed.title)}">
+ </fieldset>
+
+ <fieldset>
+ <label>${__('URL:')}</label>
+ <input dojoType='dijit.form.ValidationTextBox' required='1'
+ placeHolder="${__("Feed URL")}"
+ regExp='^(http|https)://.*' style='width : 300px'
+ name='feed_url' value="${App.escapeHtml(feed.feed_url)}">
+
+ ${feed.last_error ?
+ `<i class="material-icons"
+ title="${App.escapeHtml(feed.last_error)}">error</i>
+ ` : ""}
+ </fieldset>
+
+ ${reply.cats.enabled ?
+ `
+ <fieldset>
+ <label>${__('Place in category:')}</label>
+ ${reply.cats.select}
+ </fieldset>
+ ` : ""}
+
+ <fieldset>
+ <label>${__('Site URL:')}</label>
+ <input dojoType='dijit.form.ValidationTextBox' required='1'
+ placeHolder="${__("Site URL")}"
+ regExp='^(http|https)://.*' style='width : 300px'
+ name='site_url' value="${App.escapeHtml(feed.site_url)}">
+ </fieldset>
+
+ ${reply.lang.enabled ?
+ `
+ <fieldset>
+ <label>${__('Language:')}</label>
+ ${App.FormFields.select_tag("feed_language",
+ feed.feed_language ? feed.feed_language : reply.lang.default,
+ reply.lang.all)}
+ </fieldset>
+ ` : ""}
+
+ <hr/>
+
+ <fieldset>
+ <label>${__("Update interval:")}</label>
+ ${App.FormFields.select_hash("update_interval", feed.update_interval, reply.intervals.update)}
+ </fieldset>
+ <fieldset>
+ <label>${__('Article purging:')}</label>
+
+ ${App.FormFields.select_hash("purge_interval",
+ feed.purge_interval,
+ reply.intervals.purge,
+ reply.force_purge ? {disabled: 1} : {})}
+
+ </fieldset>
+ </section>
+ </div>
+ <div dojoType="dijit.layout.ContentPane" title="${__('Authentication')}">
+ <section>
+ <fieldset>
+ <label>${__("Login:")}</label>
+ <input dojoType='dijit.form.TextBox'
+ autocomplete='new-password'
+ name='auth_login' value="${App.escapeHtml(feed.auth_login)}">
+ </fieldset>
+ <fieldset>
+ <label>${__("Password:")}</label>
+ <input dojoType='dijit.form.TextBox' type='password' name='auth_pass'
+ autocomplete='new-password'
+ value="${App.escapeHtml(feed.auth_pass)}">
+ </fieldset>
+ </section>
+ </div>
+ <div dojoType="dijit.layout.ContentPane" title="${__('Options')}">
+ <section class="narrow">
+ ${Object.keys(options).map((name) =>
+ `
+ <fieldset class='narrow'>
+ <label class="checkbox">
+ ${App.FormFields.checkbox_tag(name, options[name][0])}
+ ${options[name][1]}
+ </label>
+ </fieldset>
+ `).join("")}
+ </section>
+ </div>
+ <div dojoType="dijit.layout.ContentPane" title="${__('Icon')}">
+ <div><img class='feedIcon' style="${feed.icon ? "" : "display : none"}" src="${feed.icon ? App.escapeHtml(feed.icon) : ""}"></div>
+
+ <label class="dijitButton">${__("Upload new icon...")}
+ <input style="display: none" type="file" onchange="App.dialogOf(this).uploadIcon(this)">
+ </label>
+
+ ${App.FormFields.submit_tag(__("Remove"), {class: "alt-danger", onclick: "App.dialogOf(this).removeIcon("+feed_id+")"})}
+ </div>
+ <div dojoType="dijit.layout.ContentPane" title="${__('Plugins')}">
+ ${reply.plugin_data}
+ </div>
+ </div>
+ <footer>
+ ${App.FormFields.button_tag(__("Unsubscribe"), "", {class: "pull-left alt-danger", onclick: "App.dialogOf(this).unsubscribe()"})}
+ ${App.FormFields.submit_tag(__("Save"), {onclick: "App.dialogOf(this).execute()"})}
+ ${App.FormFields.cancel_dialog_tag(__("Cancel"))}
+ </footer>
+ </form>
+ `);
})
});
dialog.show();
},
- genUrlChangeKey: function(feed, is_cat) {
- if (confirm(__("Generate new syndication address for this feed?"))) {
-
- Notify.progress("Trying to change address...", true);
-
- const query = {op: "pref-feeds", method: "regenFeedKey", id: feed, is_cat: is_cat};
-
- xhrJson("backend.php", query, (reply) => {
- const new_link = reply.link;
- const e = $('gen_feed_url');
-
- if (new_link) {
- e.innerHTML = e.innerHTML.replace(/&amp;key=.*$/,
- "&amp;key=" + new_link);
-
- e.href = e.href.replace(/&key=.*$/,
- "&key=" + new_link);
-
- new Effect.Highlight(e);
-
- Notify.close();
-
- } else {
- Notify.error("Could not change feed URL.");
- }
- });
- }
- return false;
- },
- publishedOPML: function() {
+ generatedFeed: function(feed, is_cat, search = "") {
Notify.progress("Loading, please wait...", true);
- xhrJson("backend.php", {op: "pref-feeds", method: "getOPMLKey"}, (reply) => {
+ xhr.json("backend.php", {op: "pref-feeds", method: "getsharedurl", id: feed, is_cat: is_cat, search: search}, (reply) => {
try {
const dialog = new fox.SingleUseDialog({
- title: __("Public OPML URL"),
- content: `
- <header>${__("Your Public OPML URL is:")}</header>
- <section>
- <div class='panel text-center'>
- <a id='pub_opml_url' href="${App.escapeHtml(reply.link)}" target='_blank'>${reply.link}</a>
- </div>
- </section>
- <footer class='text-center'>
- <button dojoType='dijit.form.Button' onclick="return Helpers.OPML.changeKey()">
- ${__('Generate new URL')}
- </button>
- <button dojoType='dijit.form.Button' type='submit' class='alt-primary'>
- ${__('Close this window')}
- </button>
- </footer>
- `
- });
+ title: __("Show as feed"),
+ regenFeedKey: function(feed, is_cat) {
+ if (confirm(__("Generate new syndication address for this feed?"))) {
- dialog.show();
+ Notify.progress("Trying to change address...", true);
- Notify.close();
+ const query = {op: "pref-feeds", method: "regenFeedKey", id: feed, is_cat: is_cat};
- } catch (e) {
- App.Error.report(e);
- }
- });
- },
- generatedFeed: function(feed, is_cat, rss_url, feed_title) {
+ xhr.json("backend.php", query, (reply) => {
+ const new_link = reply.link;
+ const target = this.domNode.querySelector(".generated_url");
- Notify.progress("Loading, please wait...", true);
+ if (new_link && target) {
+ target.innerHTML = target.innerHTML.replace(/&amp;key=.*$/,
+ "&amp;key=" + new_link);
- xhrJson("backend.php", {op: "pref-feeds", method: "getFeedKey", id: feed, is_cat: is_cat}, (reply) => {
- try {
- if (!feed_title && typeof Feeds != "undefined")
- feed_title = Feeds.getName(feed, is_cat);
+ target.href = target.href.replace(/&key=.*$/,
+ "&key=" + new_link);
- const secret_url = rss_url + "&key=" + encodeURIComponent(reply.link);
+ Notify.close();
- const dialog = new fox.SingleUseDialog({
- title: __("Show as feed"),
+ } else {
+ Notify.error("Could not change feed URL.");
+ }
+ });
+ }
+ return false;
+ },
content: `
- <header>${__("%s can be accessed via the following secret URL:").replace("%s", feed_title)}</header>
+ <header>${__("%s can be accessed via the following secret URL:").replace("%s", App.escapeHtml(reply.title))}</header>
<section>
<div class='panel text-center'>
- <a id='gen_feed_url' href="${App.escapeHtml(secret_url)}" target='_blank'>${secret_url}</a>
+ <a class='generated_url' href="${App.escapeHtml(reply.link)}" target='_blank'>${App.escapeHtml(reply.link)}</a>
</div>
</section>
<footer>
<button dojoType='dijit.form.Button' style='float : left' class='alt-info'
onclick='window.open("https://tt-rss.org/wiki/GeneratedFeeds")'>
<i class='material-icons'>help</i> ${__("More info...")}</button>
- <button dojoType='dijit.form.Button' onclick="return CommonDialogs.genUrlChangeKey('${feed}', '${is_cat}')">
+ <button dojoType='dijit.form.Button' onclick="return App.dialogOf(this).regenFeedKey('${feed}', '${is_cat}')">
${__('Generate new URL')}
</button>
<button dojoType='dijit.form.Button' class='alt-primary' type='submit'>
diff --git a/js/CommonFilters.js b/js/CommonFilters.js
index 802cf478d..0c138760d 100644
--- a/js/CommonFilters.js
+++ b/js/CommonFilters.js
@@ -2,365 +2,560 @@
/* eslint-disable no-new */
-/* global __, App, Article, Lists, Effect, fox */
-/* global xhrPost, dojo, dijit, Notify, $$, Feeds */
+/* global __, App, Article, Lists, fox */
+/* global xhr, dojo, dijit, Notify, Feeds */
+/* exported Filters */
const Filters = {
- filterDlgCheckAction: function(sender) {
- const action = sender.value;
-
- const action_param = $("filterDlg_paramBox");
-
- if (!action_param) {
- console.log("filterDlgCheckAction: can't find action param box!");
- return;
- }
-
- // 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});
-
- Element.hide(dijit.byId("filterDlg_actionParam").domNode);
- Element.hide(dijit.byId("filterDlg_actionParamLabel").domNode);
- Element.hide(dijit.byId("filterDlg_actionParamPlugin").domNode);
-
- if (action == 7) {
- Element.show(dijit.byId("filterDlg_actionParamLabel").domNode);
- } else if (action == 9) {
- Element.show(dijit.byId("filterDlg_actionParamPlugin").domNode);
- } else {
- Element.show(dijit.byId("filterDlg_actionParam").domNode);
- }
-
- } else {
- Element.hide(action_param);
- }
- },
- createNewRuleElement: function(parentNode, replaceNode) {
- const rule = dojo.formToJson("filter_new_rule_form");
-
- xhrPost("backend.php", {op: "pref-filters", method: "printrulename", rule: rule}, (transport) => {
- try {
- const li = document.createElement('li');
-
- li.innerHTML = `<input dojoType='dijit.form.CheckBox' type='checkbox' onclick='Lists.onRowChecked(this)'>
- <span onclick='App.dialogOf(this).editRule(this)'>${transport.responseText}</span>
- ${App.FormFields.hidden("rule[]", rule)}`;
+ edit: function(filter_id = null) { // if no id, new filter dialog
- dojo.parser.parse(li);
-
- if (replaceNode) {
- parentNode.replaceChild(li, replaceNode);
- } else {
- parentNode.appendChild(li);
- }
- } catch (e) {
- App.Error.report(e);
- }
- });
- },
- createNewActionElement: function(parentNode, replaceNode) {
- const form = document.forms["filter_new_action_form"];
+ const dialog = new fox.SingleUseDialog({
+ id: "filterEditDlg",
+ title: filter_id ? __("Edit Filter") : __("Create Filter"),
+ ACTION_TAG: 4,
+ ACTION_SCORE: 6,
+ ACTION_LABEL: 7,
+ ACTION_PLUGIN: 9,
+ PARAM_ACTIONS: [4, 6, 7, 9],
+ filter_info: {},
+ test: function() {
+ const test_dialog = new fox.SingleUseDialog({
+ title: "Test Filter",
+ results: 0,
+ limit: 100,
+ max_offset: 10000,
+ getTestResults: function (params, offset) {
+ params.method = 'testFilterDo';
+ params.offset = offset;
+ params.limit = test_dialog.limit;
+
+ console.log("getTestResults:" + offset);
+
+ xhr.json("backend.php", params, (result) => {
+ try {
+ if (result && test_dialog && test_dialog.open) {
+ test_dialog.results += result.length;
+
+ console.log("got results:" + result.length);
+
+ App.byId("prefFilterProgressMsg").innerHTML = __("Looking for articles (%d processed, %f found)...")
+ .replace("%f", test_dialog.results)
+ .replace("%d", offset);
+
+ console.log(offset + " " + test_dialog.max_offset);
+
+ for (let i = 0; i < result.length; i++) {
+ const tmp = dojo.create("table", { innerHTML: result[i]});
+
+ App.byId("prefFilterTestResultList").innerHTML += tmp.innerHTML;
+ }
+
+ if (test_dialog.results < 30 && offset < test_dialog.max_offset) {
+
+ // get the next batch
+ window.setTimeout(function () {
+ test_dialog.getTestResults(params, offset + test_dialog.limit);
+ }, 0);
+
+ } else {
+ // all done
+
+ Element.hide("prefFilterLoadingIndicator");
+
+ if (test_dialog.results == 0) {
+ App.byId("prefFilterTestResultList").innerHTML = `<tr><td align='center'>
+ ${__('No recent articles matching this filter have been found.')}</td></tr>`;
+ App.byId("prefFilterProgressMsg").innerHTML = "Articles matching this filter:";
+ } else {
+ App.byId("prefFilterProgressMsg").innerHTML = __("Found %d articles matching this filter:")
+ .replace("%d", test_dialog.results);
+ }
+
+ }
+
+ } else if (!result) {
+ console.log("getTestResults: can't parse results object");
+ Element.hide("prefFilterLoadingIndicator");
+ Notify.error("Error while trying to get filter test results.");
+ } else {
+ console.log("getTestResults: dialog closed, bailing out.");
+ }
+ } catch (e) {
+ App.Error.report(e);
+ }
+ });
+ },
+ content: `
+ <div>
+ <img id='prefFilterLoadingIndicator' src='images/indicator_tiny.gif'>&nbsp;
+ <span id='prefFilterProgressMsg'>Looking for articles...</span>
+ </div>
+
+ <ul class='panel panel-scrollable list list-unstyled' id='prefFilterTestResultList'></ul>
+
+ <footer class='text-center'>
+ <button dojoType='dijit.form.Button' type='submit' class='alt-primary'>${__('Close this window')}</button>
+ </footer>
+ `
+ });
- 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;
- }
+ const tmph = dojo.connect(test_dialog, "onShow", null, function (/* e */) {
+ dojo.disconnect(tmph);
- const action = dojo.formToJson(form);
+ test_dialog.getTestResults(dialog.attr('value'), 0);
+ });
- xhrPost("backend.php", { op: "pref-filters", method: "printactionname", action: action }, (transport) => {
- try {
- const li = document.createElement('li');
+ test_dialog.show();
+ },
+ insertRule: function(parentNode, replaceNode) {
+ const rule = dojo.formToJson("filter_new_rule_form");
- li.innerHTML = `<input dojoType='dijit.form.CheckBox' type='checkbox' onclick='Lists.onRowChecked(this)'>
- <span onclick='App.dialogOf(this).editAction(this)'>${transport.responseText}</span>
- ${App.FormFields.hidden("action[]", action)}`;
+ xhr.post("backend.php", {op: "pref-filters", method: "printrulename", rule: rule}, (reply) => {
+ try {
+ const li = document.createElement('li');
+ li.addClassName("rule");
- dojo.parser.parse(li);
+ li.innerHTML = `${App.FormFields.checkbox_tag("", false, {onclick: 'Lists.onRowChecked(this)'})}
+ <span class="name" onclick='App.dialogOf(this).onRuleClicked(this)'>${reply}</span>
+ <span class="payload" >${App.FormFields.hidden_tag("rule[]", rule)}</span>`;
- if (replaceNode) {
- parentNode.replaceChild(li, replaceNode);
- } else {
- parentNode.appendChild(li);
- }
+ dojo.parser.parse(li);
- } catch (e) {
- App.Error.report(e);
- }
- });
- },
- addFilterRule: function(replaceNode, ruleStr) {
- const dialog = new fox.SingleUseDialog({
- id: "filterNewRuleDlg",
- title: ruleStr ? __("Edit rule") : __("Add rule"),
- execute: function () {
- if (this.validate()) {
- Filters.createNewRuleElement($("filterDlg_Matches"), replaceNode);
- this.hide();
- }
+ if (replaceNode) {
+ parentNode.replaceChild(li, replaceNode);
+ } else {
+ parentNode.appendChild(li);
+ }
+ } catch (e) {
+ App.Error.report(e);
+ }
+ });
},
- content: __('Loading, please wait...'),
- });
-
- const tmph = dojo.connect(dialog, "onShow", null, function (/* e */) {
- dojo.disconnect(tmph);
-
- xhrPost("backend.php", {op: 'pref-filters', method: 'newrule', rule: ruleStr}, (transport) => {
- dialog.attr('content', transport.responseText);
- });
- });
+ insertAction: function(parentNode, replaceNode) {
+ const form = document.forms["filter_new_action_form"];
- dialog.show();
- },
- addFilterAction: function(replaceNode, actionStr) {
- const dialog = new fox.SingleUseDialog({
- title: actionStr ? __("Edit action") : __("Add action"),
- execute: function () {
- if (this.validate()) {
- Filters.createNewActionElement($("filterDlg_Actions"), replaceNode);
- this.hide();
+ 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;
}
- }
- });
-
- const tmph = dojo.connect(dialog, "onShow", null, function (/* e */) {
- dojo.disconnect(tmph);
- xhrPost("backend.php", {op: 'pref-filters', method: 'newaction', action: actionStr}, (transport) => {
- dialog.attr('content', transport.responseText);
- });
- });
+ const action = dojo.formToJson(form);
- dialog.show();
- },
- test: function(params) {
-
- const dialog = new fox.SingleUseDialog({
- title: "Test Filter",
- results: 0,
- limit: 100,
- max_offset: 10000,
- getTestResults: function (params, offset) {
- params.method = 'testFilterDo';
- params.offset = offset;
- params.limit = dialog.limit;
-
- console.log("getTestResults:" + offset);
-
- xhrPost("backend.php", params, (transport) => {
+ xhr.post("backend.php", { op: "pref-filters", method: "printactionname", action: action }, (reply) => {
try {
- const result = JSON.parse(transport.responseText);
-
- if (result && dialog && dialog.open) {
- dialog.results += result.length;
-
- console.log("got results:" + result.length);
+ const li = document.createElement('li');
+ li.addClassName("action");
- $("prefFilterProgressMsg").innerHTML = __("Looking for articles (%d processed, %f found)...")
- .replace("%f", dialog.results)
- .replace("%d", offset);
+ li.innerHTML = `${App.FormFields.checkbox_tag("", false, {onclick: 'Lists.onRowChecked(this)'})}
+ <span class="name" onclick='App.dialogOf(this).onActionClicked(this)'>${reply}</span>
+ <span class="payload">${App.FormFields.hidden_tag("action[]", action)}</span>`;
- console.log(offset + " " + dialog.max_offset);
+ dojo.parser.parse(li);
- for (let i = 0; i < result.length; i++) {
- const tmp = dojo.create("table", { innerHTML: result[i]});
-
- $("prefFilterTestResultList").innerHTML += tmp.innerHTML;
- }
-
- if (dialog.results < 30 && offset < dialog.max_offset) {
-
- // get the next batch
- window.setTimeout(function () {
- dialog.getTestResults(params, offset + dialog.limit);
- }, 0);
-
- } else {
- // all done
-
- Element.hide("prefFilterLoadingIndicator");
-
- if (dialog.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", dialog.results);
- }
-
- }
-
- } else if (!result) {
- console.log("getTestResults: can't parse results object");
- Element.hide("prefFilterLoadingIndicator");
- Notify.error("Error while trying to get filter test results.");
+ if (replaceNode) {
+ parentNode.replaceChild(li, replaceNode);
} else {
- console.log("getTestResults: dialog closed, bailing out.");
+ parentNode.appendChild(li);
}
+
} catch (e) {
App.Error.report(e);
}
});
},
- content: `
- <div>
- <img id='prefFilterLoadingIndicator' src='images/indicator_tiny.gif'>&nbsp;
- <span id='prefFilterProgressMsg'>Looking for articles...</span>
- </div>
-
- <ul class='panel panel-scrollable list list-unstyled' id='prefFilterTestResultList'></ul>
-
- <footer class='text-center'>
- <button dojoType='dijit.form.Button' type='submit' class='alt-primary'>${__('Close this window')}</button>
- </footer>
- `
- });
-
- dojo.connect(dialog, "onShow", null, function (/* e */) {
- dialog.getTestResults(params, 0);
- });
-
- dialog.show();
- },
- edit: function(id) { // if no id, new filter dialog
- let query;
-
- if (!App.isPrefs()) {
- query = {
- op: "pref-filters", method: "edit",
- feed: Feeds.getActive(), is_cat: Feeds.activeIsCat()
- };
- } else {
- query = {op: "pref-filters", method: "edit", id: id};
- }
-
- console.log('Filters.edit', query);
-
- xhrPost("backend.php", query, function (transport) {
- try {
- const dialog = new fox.SingleUseDialog({
- id: "filterEditDlg",
- title: __("Create Filter"),
- test: function () {
- Filters.test(this.attr('value'));
- },
- selectRules: function (select) {
- Lists.select("filterDlg_Matches", select);
- },
- selectActions: function (select) {
- Lists.select("filterDlg_Actions", select);
- },
- editRule: function (e) {
- const li = e.closest('li');
- const rule = li.querySelector('input[name="rule[]"]').value
-
- Filters.addFilterRule(li, rule);
+ editRule: function(replaceNode, ruleStr = null) {
+ const edit_rule_dialog = new fox.SingleUseDialog({
+ id: "filterNewRuleDlg",
+ title: ruleStr ? __("Edit rule") : __("Add rule"),
+ execute: function () {
+ if (this.validate()) {
+ dialog.insertRule(App.byId("filterDlg_Matches"), replaceNode);
+ this.hide();
+ }
},
- editAction: function (e) {
- const li = e.closest('li');
- const action = li.querySelector('input[name="action[]"]').value
+ content: __('Loading, please wait...'),
+ });
- Filters.addFilterAction(li, action);
- },
- removeFilter: function () {
- const msg = __("Remove filter?");
+ const tmph = dojo.connect(edit_rule_dialog, "onShow", null, function () {
+ dojo.disconnect(tmph);
- if (confirm(msg)) {
- this.hide();
+ let rule;
- Notify.progress("Removing filter...");
+ if (ruleStr) {
+ rule = JSON.parse(ruleStr);
+ } else {
+ rule = {
+ reg_exp: "",
+ filter_type: 1,
+ feed_id: ["0"],
+ inverse: false,
+ };
+ }
- const query = {op: "pref-filters", method: "remove", ids: this.attr('value').id};
+ console.log(rule, dialog.filter_info);
+
+ xhr.json("backend.php", {op: "pref-filters", method: "editrule", ids: rule.feed_id.join(",")}, function (editrule) {
+ edit_rule_dialog.attr('content',
+ `
+ <form name="filter_new_rule_form" id="filter_new_rule_form" onsubmit="return false">
+
+ <section>
+ <textarea dojoType="fox.form.ValidationTextArea"
+ required="true" id="filterDlg_regExp" ValidRegExp="true"
+ rows="4" style="font-size : 14px; width : 530px; word-break: break-all"
+ name="reg_exp">${rule.reg_exp}</textarea>
+
+ <div dojoType="dijit.Tooltip" id="filterDlg_regExp_tip" connectId="filterDlg_regExp" position="below"></div>
+
+ <fieldset>
+ <label class="checkbox">
+ ${App.FormFields.checkbox_tag("inverse", rule.inverse)}
+ ${__("Inverse regular expression matching")}
+ </label>
+ </fieldset>
+ <fieldset>
+ <label style="display : inline">${__("on")}</label>
+ ${App.FormFields.select_hash("filter_type", rule.filter_type, dialog.filter_info.filter_types)}
+ <label style="padding-left : 10px; display : inline">${__("in")}</label>
+ </fieldset>
+ <fieldset>
+ <span id="filterDlg_feeds">
+ ${editrule.multiselect}
+ </span>
+ </fieldset>
+ </section>
+
+ <footer>
+ ${App.FormFields.button_tag(App.FormFields.icon("help") + " " + __("More info"), "", {class: 'pull-left alt-info',
+ onclick: "window.open('https://tt-rss.org/wiki/ContentFilters')"})}
+ ${App.FormFields.submit_tag(__("Save rule"), {onclick: "App.dialogOf(this).execute()"})}
+ ${App.FormFields.cancel_dialog_tag(__("Cancel"))}
+ </footer>
+
+ </form>
+ `);
+ });
- xhrPost("backend.php", query, () => {
- const tree = dijit.byId("filterTree");
+ });
- if (tree) tree.reload();
- });
- }
- },
- addAction: function () {
- Filters.addFilterAction();
- },
- addRule: function () {
- Filters.addFilterRule();
- },
- deleteAction: function () {
- $$("#filterDlg_Actions li[class*=Selected]").each(function (e) {
- e.parentNode.removeChild(e)
- });
+ edit_rule_dialog.show();
+ },
+ editAction: function(replaceNode, actionStr) {
+ const edit_action_dialog = new fox.SingleUseDialog({
+ title: actionStr ? __("Edit action") : __("Add action"),
+ select_labels: function(name, value, labels, attributes = {}, id = "") {
+ const values = Object.values(labels).map((label) => label.caption);
+ return App.FormFields.select_tag(name, value, values, attributes, id);
},
- deleteRule: function () {
- $$("#filterDlg_Matches li[class*=Selected]").each(function (e) {
- e.parentNode.removeChild(e)
- });
+ toggleParam: function(sender) {
+ const action = parseInt(sender.value);
+
+ dijit.byId("filterDlg_actionParam").domNode.hide();
+ dijit.byId("filterDlg_actionParamLabel").domNode.hide();
+ dijit.byId("filterDlg_actionParamPlugin").domNode.hide();
+
+ // if selected action supports parameters, enable params field
+ if (action == dialog.ACTION_LABEL) {
+ dijit.byId("filterDlg_actionParamLabel").domNode.show();
+ } else if (action == dialog.ACTION_PLUGIN) {
+ dijit.byId("filterDlg_actionParamPlugin").domNode.show();
+ } else if (dialog.PARAM_ACTIONS.indexOf(action) != -1) {
+ dijit.byId("filterDlg_actionParam").domNode.show();
+ }
},
execute: function () {
if (this.validate()) {
-
- Notify.progress("Saving data...", true);
-
- xhrPost("backend.php", this.attr('value'), () => {
- dialog.hide();
-
- const tree = dijit.byId("filterTree");
- if (tree) tree.reload();
- });
+ dialog.insertAction(App.byId("filterDlg_Actions"), replaceNode);
+ this.hide();
}
},
- content: transport.responseText
+ content: __("Loading, please wait...")
});
- if (!App.isPrefs()) {
- /* global getSelectionText */
- const selectedText = getSelectionText();
+ const tmph = dojo.connect(edit_action_dialog, "onShow", null, function () {
+ dojo.disconnect(tmph);
- const lh = dojo.connect(dialog, "onShow", function () {
- dojo.disconnect(lh);
+ let action;
- if (selectedText != "") {
+ if (actionStr) {
+ action = JSON.parse(actionStr);
+ } else {
+ action = {
+ action_id: 2,
+ action_param: ""
+ };
+ }
- const feed_id = Feeds.activeIsCat() ? 'CAT:' + parseInt(Feeds.getActive()) :
- Feeds.getActive();
+ console.log(action);
+
+ edit_action_dialog.attr('content',
+ `
+ <form name="filter_new_action_form" id="filter_new_action_form" onsubmit="return false;">
+ <section>
+ ${App.FormFields.select_hash("action_id", -1,
+ dialog.filter_info.action_types,
+ {onchange: "App.dialogOf(this).toggleParam(this)"},
+ "filterDlg_actionSelect")}
+
+ <input dojoType="dijit.form.TextBox"
+ id="filterDlg_actionParam" style="$param_hidden"
+ name="action_param" value="${App.escapeHtml(action.action_param)}">
+
+ ${edit_action_dialog.select_labels("action_param_label", action.action_param,
+ dialog.filter_info.labels,
+ {},
+ "filterDlg_actionParamLabel")}
+
+ ${App.FormFields.select_hash("action_param_plugin", action.action_param,
+ dialog.filter_info.plugin_actions,
+ {},
+ "filterDlg_actionParamPlugin")}
+ </section>
+ <footer>
+ ${App.FormFields.submit_tag(__("Save action"), {onclick: "App.dialogOf(this).execute()"})}
+ ${App.FormFields.cancel_dialog_tag(__("Cancel"))}
+ </footer>
+ </form>
+ `);
+
+ dijit.byId("filterDlg_actionSelect").attr('value', action.action_id);
+
+ /*xhr.post("backend.php", {op: 'pref-filters', method: 'newaction', action: actionStr}, (reply) => {
+ edit_action_dialog.attr('content', reply);
+
+ setTimeout(() => {
+ edit_action_dialog.hideOrShowActionParam(dijit.byId("filterDlg_actionSelect").attr('value'));
+ }, 250);
+ });*/
+ });
- const rule = {reg_exp: selectedText, feed_id: [feed_id], filter_type: 1};
+ edit_action_dialog.show();
+ },
+ selectRules: function (select) {
+ Lists.select("filterDlg_Matches", select);
+ },
+ selectActions: function (select) {
+ Lists.select("filterDlg_Actions", select);
+ },
+ onRuleClicked: function (elem) {
- Filters.addFilterRule(null, dojo.toJson(rule));
+ const li = elem.closest('li');
+ const rule = li.querySelector('input[name="rule[]"]').value;
- } else {
+ this.editRule(li, rule);
+ },
+ onActionClicked: function (elem) {
- const query = {op: "rpc", method: "getlinktitlebyid", id: Article.getActive()};
+ const li = elem.closest('li');
+ const action = li.querySelector('input[name="action[]"]').value;
- xhrPost("backend.php", query, (transport) => {
- const reply = JSON.parse(transport.responseText);
+ this.editAction(li, action);
+ },
+ removeFilter: function () {
+ const msg = __("Remove filter?");
- let title = false;
+ if (confirm(msg)) {
+ this.hide();
+
+ Notify.progress("Removing filter...");
- if (reply && reply.title) title = reply.title;
+ const query = {op: "pref-filters", method: "remove", ids: this.attr('value').id};
- if (title || Feeds.getActive() || Feeds.activeIsCat()) {
+ xhr.post("backend.php", query, () => {
+ const tree = dijit.byId("filterTree");
- console.log(title + " " + Feeds.getActive());
+ if (tree) tree.reload();
+ });
+ }
+ },
+ addAction: function () {
+ this.editAction();
+ },
+ addRule: function () {
+ this.editRule();
+ },
+ deleteAction: function () {
+ App.findAll("#filterDlg_Actions li[class*=Selected]").forEach(function (e) {
+ e.parentNode.removeChild(e)
+ });
+ },
+ deleteRule: function () {
+ App.findAll("#filterDlg_Matches li[class*=Selected]").forEach(function (e) {
+ e.parentNode.removeChild(e)
+ });
+ },
+ execute: function () {
+ if (this.validate()) {
- const feed_id = Feeds.activeIsCat() ? 'CAT:' + parseInt(Feeds.getActive()) :
- Feeds.getActive();
+ Notify.progress("Saving data...", true);
- const rule = {reg_exp: title, feed_id: [feed_id], filter_type: 1};
+ xhr.post("backend.php", this.attr('value'), () => {
+ dialog.hide();
- Filters.addFilterRule(null, dojo.toJson(rule));
- }
- });
- }
+ const tree = dijit.byId("filterTree");
+ if (tree) tree.reload();
});
}
- dialog.show();
+ },
+ content: __("Loading, please wait...")
+ });
- } catch (e) {
- App.Error.report(e);
- }
+ const tmph = dojo.connect(dialog, 'onShow', function () {
+ dojo.disconnect(tmph);
+
+ xhr.json("backend.php", {op: "pref-filters", method: "edit", id: filter_id}, function (filter) {
+
+ dialog.filter_info = filter;
+
+ const options = {
+ enabled: [ filter.enabled, __('Enabled') ],
+ match_any_rule: [ filter.match_any_rule, __('Match any rule') ],
+ inverse: [ filter.inverse, __('Inverse matching') ],
+ };
+
+ dialog.attr('content',
+ `
+ <form onsubmit='return false'>
+
+ ${App.FormFields.hidden_tag("op", "pref-filters")}
+ ${App.FormFields.hidden_tag("id", filter_id)}
+ ${App.FormFields.hidden_tag("method", filter_id ? "editSave" : "add")}
+ ${App.FormFields.hidden_tag("csrf_token", App.getInitParam('csrf_token'))}
+
+ <section class="horizontal">
+ <input required="true" dojoType="dijit.form.ValidationTextBox" style="width : 100%"
+ placeholder="${__("Title")}" name="title" value="${App.escapeHtml(filter.title)}">
+ </section>
+
+ <div dojoType="dijit.layout.TabContainer" style="height : 300px">
+ <div dojoType="dijit.layout.ContentPane" title="${__('Match')}">
+ <div style="padding : 0" dojoType="dijit.layout.BorderContainer" gutters="false">
+ <div dojoType="fox.Toolbar" region="top">
+ <div dojoType="fox.form.DropDownButton">
+ <span>${__("Select")}</span>
+ <div dojoType="dijit.Menu" style="display: none;">
+ <!-- can"t use App.dialogOf() here because DropDownButton is not a child of the Dialog -->
+ <div onclick="dijit.byId('filterEditDlg').selectRules(true)"
+ dojoType="dijit.MenuItem">${__("All")}</div>
+ <div onclick="dijit.byId('filterEditDlg').selectRules(false)"
+ dojoType="dijit.MenuItem">${__("None")}</div>
+ </div>
+ </div>
+ <button dojoType="dijit.form.Button" onclick="App.dialogOf(this).addRule()">
+ ${__("Add")}
+ </button>
+ <button dojoType="dijit.form.Button" onclick="App.dialogOf(this).deleteRule()">
+ ${__("Delete")}
+ </button>
+ </div>
+ <div dojoType="dijit.layout.ContentPane" region="center">
+ <ul id="filterDlg_Matches">
+ ${filter.rules.map((rule) => `
+ <li class='rule'>
+ ${App.FormFields.checkbox_tag("", false, "", {onclick: 'Lists.onRowChecked(this)'})}
+ <span class='name' onclick='App.dialogOf(this).onRuleClicked(this)'>${rule.name}</span>
+ <span class='payload'>${App.FormFields.hidden_tag("rule[]", JSON.stringify(rule))}</span>
+ </li>
+ `).join("")}
+ </ul>
+ </div>
+ </div>
+ </div>
+ <div dojoType="dijit.layout.ContentPane" title="${__('Apply actions')}">
+ <div style="padding : 0" dojoType="dijit.layout.BorderContainer" gutters="false">
+ <div dojoType="fox.Toolbar" region="top">
+ <div dojoType="fox.form.DropDownButton">
+ <span>${__("Select")}</span>
+ <div dojoType="dijit.Menu" style="display: none">
+ <div onclick="dijit.byId('filterEditDlg').selectActions(true)"
+ dojoType="dijit.MenuItem">${__("All")}</div>
+ <div onclick="dijit.byId('filterEditDlg').selectActions(false)"
+ dojoType="dijit.MenuItem">${__("None")}</div>
+ </div>
+ </div>
+ <button dojoType="dijit.form.Button" onclick="App.dialogOf(this).addAction()">
+ ${__("Add")}
+ </button>
+ <button dojoType="dijit.form.Button" onclick="App.dialogOf(this).deleteAction()">
+ ${__("Delete")}
+ </button>
+ </div>
+ <div dojoType="dijit.layout.ContentPane" region="center">
+ <ul id="filterDlg_Actions">
+ ${filter.actions.map((action) => `
+ <li class='rule'>
+ ${App.FormFields.checkbox_tag("", false, "", {onclick: 'Lists.onRowChecked(this)'})}
+ <span class='name' onclick='App.dialogOf(this).onActionClicked(this)'>${App.escapeHtml(action.name)}</span>
+ <span class='payload'>${App.FormFields.hidden_tag("action[]", JSON.stringify(action))}</span>
+ </li>
+ `).join("")}
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <section class="horizontal">
+ ${Object.keys(options).map((name) =>
+ `
+ <fieldset class='narrow'>
+ <label class="checkbox">
+ ${App.FormFields.checkbox_tag(name, options[name][0])}
+ ${options[name][1]}
+ </label>
+ </fieldset>
+ `).join("")}
+ </section>
+
+ <footer>
+ ${filter_id ?
+ `
+ ${App.FormFields.button_tag(__("Remove"), "", {class: "pull-left alt-danger", onclick: "App.dialogOf(this).removeFilter()"})}
+ ${App.FormFields.button_tag(__("Test"), "", {class: "alt-info", onclick: "App.dialogOf(this).test()"})}
+ ${App.FormFields.submit_tag(__("Save"), {onclick: "App.dialogOf(this).execute()"})}
+ ${App.FormFields.cancel_dialog_tag(__("Cancel"))}
+ ` : `
+ ${App.FormFields.button_tag(__("Test"), "", {class: "alt-info", onclick: "App.dialogOf(this).test()"})}
+ ${App.FormFields.submit_tag(__("Create"), {onclick: "App.dialogOf(this).execute()"})}
+ ${App.FormFields.cancel_dialog_tag(__("Cancel"))}
+ `}
+ </footer>
+ </form>
+ `);
+
+ if (!App.isPrefs()) {
+ const selectedText = App.getSelectedText();
+
+ if (selectedText != "") {
+ const feed_id = Feeds.activeIsCat() ? 'CAT:' + parseInt(Feeds.getActive()) :
+ Feeds.getActive();
+ const rule = {reg_exp: selectedText, feed_id: [feed_id], filter_type: 1};
+
+ dialog.editRule(null, dojo.toJson(rule));
+ } else {
+ const query = {op: "article", method: "getmetadatabyid", id: Article.getActive()};
+
+ xhr.json("backend.php", query, (reply) => {
+ let title;
+
+ if (reply && reply.title) title = reply.title;
+
+ if (title || Feeds.getActive() || Feeds.activeIsCat()) {
+ console.log(title + " " + Feeds.getActive());
+
+ const feed_id = Feeds.activeIsCat() ? 'CAT:' + parseInt(Feeds.getActive()) :
+ Feeds.getActive();
+ const rule = {reg_exp: title, feed_id: [feed_id], filter_type: 1};
+
+ dialog.editRule(null, dojo.toJson(rule));
+ }
+ });
+ }
+ }
+ });
});
+
+ dialog.show();
},
};
diff --git a/js/FeedTree.js b/js/FeedTree.js
index 26c1c916c..17cd3deea 100755
--- a/js/FeedTree.js
+++ b/js/FeedTree.js
@@ -102,7 +102,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co
label: __("Debug feed"),
onClick: function() {
/* global __csrf_token */
- App.postOpenWindow("backend.php", {op: "feeds", method: "update_debugger",
+ App.postOpenWindow("backend.php", {op: "feeds", method: "updatedebugger",
feed_id: this.getParent().row_id, csrf_token: __csrf_token});
}}));
}
@@ -286,7 +286,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co
// focus headlines to route key events there
setTimeout(() => {
- $("headlines-frame").focus();
+ App.byId("headlines-frame").focus();
if (treeNode) {
const node = treeNode.rowNode;
@@ -295,7 +295,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co
if (node && tree) {
// scroll tree to selection if needed
if (node.offsetTop < tree.scrollTop || node.offsetTop > tree.scrollTop + tree.clientHeight) {
- $("feedTree").scrollTop = node.offsetTop;
+ App.byId("feedTree").scrollTop = node.offsetTop;
}
}
}
diff --git a/js/Feeds.js b/js/Feeds.js
index 986936285..5a2dee5cf 100644
--- a/js/Feeds.js
+++ b/js/Feeds.js
@@ -1,8 +1,9 @@
'use strict'
-/* global __, App, Headlines, xhrPost, dojo, dijit, Form, fox, PluginHost, Notify, $$, fox */
+/* global __, App, Headlines, xhr, dojo, dijit, fox, PluginHost, Notify, fox */
const Feeds = {
+ _default_feed_id: -3,
counters_last_request: 0,
_active_feed_id: undefined,
_active_feed_is_cat: false,
@@ -12,6 +13,19 @@ const Feeds = {
_search_query: false,
last_search_query: [],
_viewfeed_wait_timeout: false,
+ _feeds_holder_observer: new IntersectionObserver(
+ (entries/*, observer*/) => {
+ entries.forEach((entry) => {
+ //console.log('feeds',entry.target, entry.intersectionRatio);
+
+ if (entry.intersectionRatio == 0)
+ Feeds.onHide(entry);
+ else
+ Feeds.onShow(entry);
+ });
+ },
+ {threshold: [0, 1], root: document.querySelector("body")}
+ ),
_counters_prev: [],
// NOTE: this implementation is incomplete
// for general objects but good enough for counters
@@ -109,6 +123,9 @@ const Feeds = {
}
return false; // block unneeded form submits
},
+ openDefaultFeed: function() {
+ this.open({feed: this._default_feed_id});
+ },
openNextUnread: function() {
const is_cat = this.activeIsCat();
const nuf = this.getNextUnread(this.getActive(), is_cat);
@@ -116,23 +133,20 @@ const Feeds = {
},
toggle: function() {
Element.toggle("feeds-holder");
-
- const splitter = $("feeds-holder_splitter");
-
- Element.visible("feeds-holder") ? splitter.show() : splitter.hide();
-
- dijit.byId("main").resize();
-
- Headlines.updateCurrentUnread();
},
cancelSearch: function() {
this._search_query = "";
this.reloadCurrent();
},
- requestCounters: function() {
- xhrPost("backend.php", {op: "rpc", method: "getAllCounters", seq: App.next_seq()}, (transport) => {
- App.handleRpcJson(transport);
- });
+ // null = get all data, [] would give empty response for specific type
+ requestCounters: function(feed_ids = null, label_ids = null) {
+ xhr.json("backend.php", {op: "rpc",
+ method: "getAllCounters",
+ "feed_ids[]": feed_ids,
+ "feed_id_count": feed_ids ? feed_ids.length : -1,
+ "label_ids[]": label_ids,
+ "label_id_count": label_ids ? label_ids.length : -1,
+ seq: App.next_seq()});
},
reload: function() {
try {
@@ -180,7 +194,7 @@ const Feeds = {
dojo.disconnect(tmph);
});
- $("feeds-holder").appendChild(tree.domNode);
+ App.byId("feeds-holder").appendChild(tree.domNode);
const tmph2 = dojo.connect(tree, 'onLoad', function () {
dojo.disconnect(tmph2);
@@ -199,9 +213,23 @@ const Feeds = {
App.Error.report(e);
}
},
+ onHide: function() {
+ App.byId("feeds-holder_splitter").hide();
+
+ dijit.byId("main").resize();
+ Headlines.updateCurrentUnread();
+ },
+ onShow: function() {
+ App.byId("feeds-holder_splitter").show();
+
+ dijit.byId("main").resize();
+ Headlines.updateCurrentUnread();
+ },
init: function() {
console.log("in feedlist init");
+ this._feeds_holder_observer.observe(App.byId("feeds-holder"));
+
App.setLoadingProgress(50);
//document.onkeydown = (event) => { return App.hotkeyHandler(event) };
@@ -215,7 +243,7 @@ const Feeds = {
if (hash_feed_id != undefined) {
this.open({feed: hash_feed_id, is_cat: hash_feed_is_cat});
} else {
- this.open({feed: -3});
+ this.openDefaultFeed();
}
this.hideOrShowFeeds(App.getInitParam("hide_read_feeds"));
@@ -260,10 +288,10 @@ const Feeds = {
// bw_limit disables timeout() so we request initial counters separately
if (App.getInitParam("bw_limit")) {
- this.requestCounters(true);
+ this.requestCounters();
} else {
setTimeout(() => {
- this.requestCounters(true);
+ this.requestCounters();
setInterval(() => { this.requestCounters(); }, 60 * 1000)
}, 250);
}
@@ -284,8 +312,8 @@ const Feeds = {
this._active_feed_id = id;
this._active_feed_is_cat = is_cat;
- $("headlines-frame").setAttribute("feed-id", id);
- $("headlines-frame").setAttribute("is-cat", is_cat ? 1 : 0);
+ App.byId("headlines-frame").setAttribute("feed-id", id);
+ App.byId("headlines-frame").setAttribute("is-cat", is_cat ? 1 : 0);
this.select(id, is_cat);
@@ -299,7 +327,7 @@ const Feeds = {
toggleUnread: function() {
const hide = !App.getInitParam("hide_read_feeds");
- xhrPost("backend.php", {op: "rpc", method: "setpref", key: "HIDE_READ_FEEDS", value: hide}, () => {
+ xhr.post("backend.php", {op: "rpc", method: "setpref", key: "HIDE_READ_FEEDS", value: hide}, () => {
this.hideOrShowFeeds(hide);
App.setInitParam("hide_read_feeds", hide);
});
@@ -310,14 +338,13 @@ const Feeds = {
if (tree)
return tree.hideRead(hide, App.getInitParam("hide_read_shows_special"));*/
- $$("body")[0].setAttribute("hide-read-feeds", !!hide);
- $$("body")[0].setAttribute("hide-read-shows-special", !!App.getInitParam("hide_read_shows_special"));
+ App.findAll("body")[0].setAttribute("hide-read-feeds", !!hide);
+ App.findAll("body")[0].setAttribute("hide-read-shows-special", !!App.getInitParam("hide_read_shows_special"));
},
open: function(params) {
const feed = params.feed;
const is_cat = !!params.is_cat || false;
const offset = params.offset || 0;
- const viewfeed_debug = params.viewfeed_debug;
const append = params.append || false;
const method = params.method;
// this is used to quickly switch between feeds, sets active but xhr is on a timeout
@@ -339,7 +366,7 @@ const Feeds = {
}, 10 * 1000);
}
- Form.enable("toolbar-main");
+ //Form.enable("toolbar-main");
let query = Object.assign({op: "feeds", method: "view", feed: feed},
dojo.formToObject("toolbar-main"));
@@ -362,8 +389,6 @@ const Feeds = {
query.m = "ForceUpdate";
}
- Form.enable("toolbar-main");
-
if (!delayed)
if (!this.setExpando(feed, is_cat,
(is_cat) ? 'images/indicator_tiny.gif' : 'images/indicator_white.gif'))
@@ -373,20 +398,13 @@ const Feeds = {
this.setActive(feed, is_cat);
- if (viewfeed_debug) {
- window.open("backend.php?" +
- dojo.objectToQuery(
- Object.assign({csrf_token: App.getInitParam("csrf_token")}, query)
- ));
- }
-
window.clearTimeout(this._viewfeed_wait_timeout);
this._viewfeed_wait_timeout = window.setTimeout(() => {
- xhrPost("backend.php", query, (transport) => {
+ xhr.json("backend.php", query, (reply) => {
try {
window.clearTimeout(this._infscroll_timeout);
this.setExpando(feed, is_cat, 'images/blank_icon.gif');
- Headlines.onLoaded(transport, offset, append);
+ Headlines.onLoaded(reply, offset, append);
PluginHost.run(PluginHost.HOOK_FEED_LOADED, [feed, is_cat]);
} catch (e) {
App.Error.report(e);
@@ -401,8 +419,7 @@ const Feeds = {
Notify.progress("Marking all feeds as read...");
- xhrPost("backend.php", {op: "feeds", method: "catchupAll"}, () => {
- this.requestCounters(true);
+ xhr.json("backend.php", {op: "feeds", method: "catchupAll"}, () => {
this.reloadCurrent();
});
@@ -447,9 +464,7 @@ const Feeds = {
Notify.progress("Loading, please wait...", true);
- xhrPost("backend.php", catchup_query, (transport) => {
- App.handleRpcJson(transport);
-
+ xhr.json("backend.php", catchup_query, () => {
const show_next_feed = App.getInitParam("on_catchup_show_next_feed");
// only select next unread feed if catching up entirely (as opposed to last week etc)
@@ -476,9 +491,9 @@ const Feeds = {
if (App.getInitParam("confirm_feed_catchup") != 1 || confirm(str)) {
- const rows = $$("#headlines-frame > div[id*=RROW][class*=Unread][data-orig-feed-id='" + id + "']");
+ const rows = App.findAll("#headlines-frame > div[id*=RROW][class*=Unread][data-orig-feed-id='" + id + "']");
- rows.each((row) => {
+ rows.forEach((row) => {
row.removeClassName("Unread");
})
}
@@ -501,7 +516,7 @@ const Feeds = {
const tree = dijit.byId("feedTree");
if (tree && tree.model)
- return tree.getFeedCategory(feed);
+ return tree._cat_of_feed(feed);
} catch (e) {
//
@@ -566,14 +581,42 @@ const Feeds = {
return tree.model.store.getValue(nuf, 'bare_id');
},
search: function() {
- xhrPost("backend.php",
- {op: "feeds", method: "search",
- param: Feeds.getActive() + ":" + Feeds.activeIsCat()},
- (transport) => {
+ xhr.json("backend.php",
+ {op: "feeds", method: "search"},
+ (reply) => {
try {
const dialog = new fox.SingleUseDialog({
- id: "searchDlg",
- content: transport.responseText,
+ content: `
+ <form onsubmit='return false'>
+ <section>
+ <fieldset>
+ <input dojoType='dijit.form.ValidationTextBox' id='search_query'
+ style='font-size : 16px; width : 540px;'
+ placeHolder="${__("Search %s...").replace("%s", Feeds.getName(Feeds.getActive(), Feeds.activeIsCat()))}"
+ name='query' type='search' value=''>
+ </fieldset>
+
+ ${reply.show_language ?
+ `
+ <fieldset>
+ <label class='inline'>${__("Language:")}</label>
+ ${App.FormFields.select_tag("search_language", reply.default_language, reply.all_languages,
+ {title: __('Used for word stemming')}, "search_language")}
+ </fieldset>
+ ` : ''}
+ </section>
+
+ <footer>
+ ${reply.show_syntax_help ?
+ `${App.FormFields.button_tag(App.FormFields.icon("help") + " " + __("Search syntax"), "",
+ {class: 'alt-info pull-left', onclick: "window.open('https://tt-rss.org/wiki/SearchSyntax')"})}
+ ` : ''}
+
+ ${App.FormFields.submit_tag(__('Search'), {onclick: "App.dialogOf(this).execute()"})}
+ ${App.FormFields.cancel_dialog_tag(__('Cancel'))}
+ </footer>
+ </form>
+ `,
title: __("Search"),
execute: function () {
if (this.validate()) {
@@ -613,8 +656,13 @@ const Feeds = {
updateRandom: function() {
console.log("in update_random_feed");
- xhrPost("backend.php", {op: "rpc", method: "updaterandomfeed"}, (transport) => {
- App.handleRpcJson(transport, true);
+ xhr.json("backend.php", {op: "rpc", method: "updaterandomfeed"}, () => {
+ //
});
},
+ renderIcon: function(feed_id, exists) {
+ return feed_id && exists ?
+ `<img class="icon" src="${App.escapeHtml(App.getInitParam("icons_url"))}/${feed_id}.ico">` :
+ `<i class='icon-no-feed material-icons'>rss_feed</i>`;
+ }
};
diff --git a/js/Headlines.js b/js/Headlines.js
index ea4c81a6a..6dbe24918 100755
--- a/js/Headlines.js
+++ b/js/Headlines.js
@@ -1,13 +1,13 @@
'use strict';
/* global __, ngettext, Article, App */
-/* global xhrPost, dojo, dijit, PluginHost, Notify, $$, Feeds */
+/* global dojo, dijit, PluginHost, Notify, xhr, Feeds */
/* global CommonDialogs */
const Headlines = {
vgroup_last_feed: undefined,
_headlines_scroll_timeout: 0,
- _observer_counters_timeout: 0,
+ //_observer_counters_timeout: 0,
headlines: [],
current_first_id: 0,
_scroll_reset_timeout: false,
@@ -44,7 +44,7 @@ const Headlines = {
row_observer: new MutationObserver((mutations) => {
const modified = [];
- mutations.each((m) => {
+ mutations.forEach((m) => {
if (m.type == 'attributes' && ['class', 'data-score'].indexOf(m.attributeName) != -1) {
const row = m.target;
@@ -54,7 +54,7 @@ const Headlines = {
const hl = Headlines.headlines[id];
if (hl) {
- const hl_old = Object.extend({}, hl);
+ const hl_old = {...{}, ...hl};
hl.unread = row.hasClassName("Unread");
hl.marked = row.hasClassName("marked");
@@ -94,7 +94,7 @@ const Headlines = {
rescore: {},
};
- modified.each(function (m) {
+ modified.forEach(function (m) {
if (m.old.marked != m.new.marked)
ops.tmark.push(m.id);
@@ -118,29 +118,29 @@ const Headlines = {
}
});
- ops.select.each((row) => {
- const cb = dijit.getEnclosingWidget(row.select(".rchk")[0]);
+ ops.select.forEach((row) => {
+ const cb = dijit.getEnclosingWidget(row.querySelector(".rchk"));
if (cb)
cb.attr('checked', true);
});
- ops.deselect.each((row) => {
- const cb = dijit.getEnclosingWidget(row.select(".rchk")[0]);
+ ops.deselect.forEach((row) => {
+ const cb = dijit.getEnclosingWidget(row.querySelector(".rchk"));
if (cb && !row.hasClassName("active"))
cb.attr('checked', false);
});
- ops.activate.each((row) => {
- const cb = dijit.getEnclosingWidget(row.select(".rchk")[0]);
+ ops.activate.forEach((row) => {
+ const cb = dijit.getEnclosingWidget(row.querySelector(".rchk"));
if (cb)
cb.attr('checked', true);
});
- ops.deactivate.each((row) => {
- const cb = dijit.getEnclosingWidget(row.select(".rchk")[0]);
+ ops.deactivate.forEach((row) => {
+ const cb = dijit.getEnclosingWidget(row.querySelector(".rchk"));
if (cb && !row.hasClassName("Selected"))
cb.attr('checked', false);
@@ -149,39 +149,56 @@ const Headlines = {
const promises = [];
if (ops.tmark.length != 0)
- promises.push(xhrPost("backend.php",
- {op: "rpc", method: "markSelected", ids: ops.tmark.toString(), cmode: 2}));
+ promises.push(xhr.post("backend.php",
+ {op: "rpc", method: "markSelected", "ids[]": ops.tmark, cmode: 2}));
if (ops.tpub.length != 0)
- promises.push(xhrPost("backend.php",
- {op: "rpc", method: "publishSelected", ids: ops.tpub.toString(), cmode: 2}));
+ promises.push(xhr.post("backend.php",
+ {op: "rpc", method: "publishSelected", "ids[]": ops.tpub, cmode: 2}));
if (ops.read.length != 0)
- promises.push(xhrPost("backend.php",
- {op: "rpc", method: "catchupSelected", ids: ops.read.toString(), cmode: 0}));
+ promises.push(xhr.post("backend.php",
+ {op: "rpc", method: "catchupSelected", "ids[]": ops.read, cmode: 0}));
if (ops.unread.length != 0)
- promises.push(xhrPost("backend.php",
- {op: "rpc", method: "catchupSelected", ids: ops.unread.toString(), cmode: 1}));
+ promises.push(xhr.post("backend.php",
+ {op: "rpc", method: "catchupSelected", "ids[]": ops.unread, cmode: 1}));
const scores = Object.keys(ops.rescore);
if (scores.length != 0) {
- scores.each((score) => {
- promises.push(xhrPost("backend.php",
- {op: "article", method: "setScore", id: ops.rescore[score].toString(), score: score}));
+ scores.forEach((score) => {
+ promises.push(xhr.post("backend.php",
+ {op: "article", method: "setScore", "ids[]": ops.rescore[score].toString(), score: score}));
});
}
- if (promises.length > 0)
- Promise.all([promises]).then(() => {
- window.clearTimeout(this._observer_counters_timeout);
+ Promise.all(promises).then((results) => {
+ let feeds = [];
+ let labels = [];
- this._observer_counters_timeout = setTimeout(() => {
- Feeds.requestCounters(true);
- }, 1000);
+ results.forEach((res) => {
+ if (res) {
+ try {
+ const obj = JSON.parse(res);
+
+ if (obj.feeds)
+ feeds = feeds.concat(obj.feeds);
+
+ if (obj.labels)
+ labels = labels.concat(obj.labels);
+
+ } catch (e) {
+ console.warn(e, res);
+ }
+ }
});
+ if (feeds.length > 0) {
+ console.log('requesting counters for', feeds, labels);
+ Feeds.requestCounters(feeds, labels);
+ }
+ });
},
click: function (event, id, in_body) {
in_body = in_body || false;
@@ -211,7 +228,7 @@ const Headlines = {
Headlines.select('none');
- const scroll_position_A = $("RROW-" + id).offsetTop - $("headlines-frame").scrollTop;
+ const scroll_position_A = App.byId(`RROW-${id}`).offsetTop - App.byId("headlines-frame").scrollTop;
Article.setActive(id);
@@ -222,10 +239,10 @@ const Headlines = {
Headlines.toggleUnread(id, 0);
} else {
- const scroll_position_B = $("RROW-" + id).offsetTop - $("headlines-frame").scrollTop;
+ const scroll_position_B = App.byId(`RROW-${id}`).offsetTop - App.byId("headlines-frame").scrollTop;
// this would only work if there's enough space
- $("headlines-frame").scrollTop -= scroll_position_A-scroll_position_B;
+ App.byId("headlines-frame").scrollTop -= scroll_position_A-scroll_position_B;
Article.cdmMoveToId(id);
}
@@ -252,7 +269,7 @@ const Headlines = {
return false;
},
initScrollHandler: function () {
- $("headlines-frame").onscroll = (event) => {
+ App.byId("headlines-frame").onscroll = (event) => {
clearTimeout(this._headlines_scroll_timeout);
this._headlines_scroll_timeout = window.setTimeout(function () {
//console.log('done scrolling', event);
@@ -262,8 +279,8 @@ const Headlines = {
},
loadMore: function () {
const view_mode = document.forms["toolbar-main"].view_mode.value;
- const unread_in_buffer = $$("#headlines-frame > div[id*=RROW][class*=Unread]").length;
- const num_all = $$("#headlines-frame > div[id*=RROW]").length;
+ const unread_in_buffer = App.findAll("#headlines-frame > div[id*=RROW][class*=Unread]").length;
+ const num_all = App.findAll("#headlines-frame > div[id*=RROW]").length;
const num_unread = Feeds.getUnread(Feeds.getActive(), Feeds.activeIsCat());
// TODO implement marked & published
@@ -289,10 +306,10 @@ const Headlines = {
Feeds.open({feed: Feeds.getActive(), is_cat: Feeds.activeIsCat(), offset: offset, append: true});
},
isChildVisible: function (elem) {
- return App.Scrollable.isChildVisible(elem, $("headlines-frame"));
+ return App.Scrollable.isChildVisible(elem, App.byId("headlines-frame"));
},
firstVisible: function () {
- const rows = $$("#headlines-frame > div[id*=RROW]");
+ const rows = App.findAll("#headlines-frame > div[id*=RROW]");
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
@@ -303,7 +320,7 @@ const Headlines = {
}
},
unpackVisible: function(container) {
- const rows = $$("#headlines-frame > div[id*=RROW][data-content].cdm");
+ const rows = App.findAll("#headlines-frame > div[id*=RROW][data-content].cdm");
for (let i = 0; i < rows.length; i++) {
if (App.Scrollable.isChildVisible(rows[i], container)) {
@@ -315,8 +332,8 @@ const Headlines = {
scrollHandler: function (/*event*/) {
try {
if (!Feeds.infscroll_disabled && !Feeds.infscroll_in_progress) {
- const hsp = $("headlines-spacer");
- const container = $("headlines-frame");
+ const hsp = App.byId("headlines-spacer");
+ const container = App.byId("headlines-frame");
if (hsp && hsp.previousSibling) {
const last_row = hsp.previousSibling;
@@ -333,7 +350,7 @@ const Headlines = {
}
if (App.isCombinedMode() && App.getInitParam("cdm_expanded")) {
- const container = $("headlines-frame")
+ const container = App.byId("headlines-frame")
/* don't do anything until there was some scrolling */
if (container.scrollTop > 0)
@@ -342,12 +359,12 @@ const Headlines = {
if (App.getInitParam("cdm_auto_catchup")) {
- const rows = $$("#headlines-frame > div[id*=RROW][class*=Unread]");
+ const rows = App.findAll("#headlines-frame > div[id*=RROW][class*=Unread]");
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
- if ($("headlines-frame").scrollTop > (row.offsetTop + row.offsetHeight / 2)) {
+ if (App.byId("headlines-frame").scrollTop > (row.offsetTop + row.offsetHeight / 2)) {
row.removeClassName("Unread");
} else {
break;
@@ -362,23 +379,23 @@ const Headlines = {
return this.headlines[id];
},
setCommonClasses: function () {
- $("headlines-frame").removeClassName("cdm");
- $("headlines-frame").removeClassName("normal");
+ App.byId("headlines-frame").removeClassName("cdm");
+ App.byId("headlines-frame").removeClassName("normal");
- $("headlines-frame").addClassName(App.isCombinedMode() ? "cdm" : "normal");
+ App.byId("headlines-frame").addClassName(App.isCombinedMode() ? "cdm" : "normal");
// for floating title because it's placed outside of headlines-frame
- $("main").removeClassName("expandable");
- $("main").removeClassName("expanded");
+ App.byId("main").removeClassName("expandable");
+ App.byId("main").removeClassName("expanded");
if (App.isCombinedMode())
- $("main").addClassName(App.getInitParam("cdm_expanded") ? " expanded" : " expandable");
+ App.byId("main").addClassName(App.getInitParam("cdm_expanded") ? "expanded" : "expandable");
},
renderAgain: function () {
// TODO: wrap headline elements into a knockoutjs model to prevent all this stuff
Headlines.setCommonClasses();
- $$("#headlines-frame > div[id*=RROW]").each((row) => {
+ App.findAll("#headlines-frame > div[id*=RROW]").forEach((row) => {
const id = row.getAttribute("data-article-id");
const hl = this.headlines[id];
@@ -401,12 +418,12 @@ const Headlines = {
}
});
- $$(".cdm .header-sticky-guard").each((e) => {
+ App.findAll(".cdm .header-sticky-guard").forEach((e) => {
this.sticky_header_observer.observe(e)
});
if (App.getInitParam("cdm_expanded"))
- $$("#headlines-frame > div[id*=RROW].cdm").each((e) => {
+ App.findAll("#headlines-frame > div[id*=RROW].cdm").forEach((e) => {
this.unpack_observer.observe(e)
});
@@ -423,7 +440,7 @@ const Headlines = {
if (headlines.vfeed_group_enabled && hl.feed_title && this.vgroup_last_feed != hl.feed_id) {
const vgrhdr = `<div data-feed-id='${hl.feed_id}' class='feed-title'>
- <div style='float : right'>${hl.feed_icon}</div>
+ <div style='float : right'>${Feeds.renderIcon(hl.feed_id, hl.has_icon)}</div>
<a class="title" href="#" onclick="Feeds.open({feed:${hl.feed_id}})">${hl.feed_title}
<a class="catchup" title="${__('mark feed as read')}" onclick="Feeds.catchupFeedInGroup(${hl.feed_id})" href="#"><i class="icon-done material-icons">done_all</i></a>
</div>`
@@ -431,7 +448,7 @@ const Headlines = {
const tmp = document.createElement("div");
tmp.innerHTML = vgrhdr;
- $("headlines-frame").appendChild(tmp.firstChild);
+ App.byId("headlines-frame").appendChild(tmp.firstChild);
this.vgroup_last_feed = hl.feed_id;
}
@@ -462,7 +479,7 @@ const Headlines = {
<a class="title" title="${App.escapeHtml(hl.title)}" target="_blank" rel="noopener noreferrer" href="${App.escapeHtml(hl.link)}">
${hl.title}</a>
<span class="author">${hl.author}</span>
- ${hl.labels}
+ ${Article.renderLabels(hl.id, hl.labels)}
${hl.cdm_excerpt ? hl.cdm_excerpt : ""}
</span>
@@ -477,25 +494,26 @@ const Headlines = {
<i class="material-icons icon-score" title="${hl.score}" onclick="Article.setScore(${hl.id}, this)">${Article.getScorePic(hl.score)}</i>
<span style="cursor : pointer" title="${App.escapeHtml(hl.feed_title)}" onclick="Feeds.open({feed:${hl.feed_id}})">
- ${hl.feed_icon}</span>
+ ${Feeds.renderIcon(hl.feed_id, hl.has_icon)}
+ </span>
</div>
</div>
<div class="content" onclick="return Headlines.click(event, ${hl.id}, true);">
- <div id="POSTNOTE-${hl.id}">${hl.note}</div>
+ ${Article.renderNote(hl.id, hl.note)}
<div class="content-inner" lang="${hl.lang ? hl.lang : 'en'}">
<img src="${App.getInitParam('icon_indicator_white')}">
</div>
<div class="intermediate">
- ${hl.enclosures}
+ ${Article.renderEnclosures(hl.enclosures)}
</div>
<div class="footer" onclick="event.stopPropagation()">
<div class="left">
${hl.buttons_left}
<i class="material-icons">label_outline</i>
- <span id="ATSTR-${hl.id}">${hl.tags_str}</span>
+ ${Article.renderTags(hl.id, hl.tags)}
<a title="${__("Edit tags for this article")}" href="#"
onclick="Article.editTags(${hl.id})">(+)</a>
${comments}
@@ -527,7 +545,7 @@ const Headlines = {
<span data-article-id="${hl.id}" class="hl-content hlMenuAttach">
<a class="title" href="${App.escapeHtml(hl.link)}">${hl.title} <span class="preview">${hl.content_preview}</span></a>
<span class="author">${hl.author}</span>
- ${hl.labels}
+ ${Article.renderLabels(hl.id, hl.labels)}
</span>
</div>
<span class="feed">
@@ -538,7 +556,7 @@ const Headlines = {
</div>
<div class="right">
<i class="material-icons icon-score" title="${hl.score}" onclick="Article.setScore(${hl.id}, this)">${Article.getScorePic(hl.score)}</i>
- <span onclick="Feeds.open({feed:${hl.feed_id}})" style="cursor : pointer" title="${App.escapeHtml(hl.feed_title)}">${hl.feed_icon}</span>
+ <span onclick="Feeds.open({feed:${hl.feed_id}})" style="cursor : pointer" title="${App.escapeHtml(hl.feed_title)}">${Feeds.renderIcon(hl.feed_id, hl.has_icon)}</span>
</div>
</div>
`;
@@ -555,20 +573,74 @@ const Headlines = {
return tmp.firstChild;
},
updateCurrentUnread: function () {
- if ($("feed_current_unread")) {
+ if (App.byId("feed_current_unread")) {
const feed_unread = Feeds.getUnread(Feeds.getActive(), Feeds.activeIsCat());
if (feed_unread > 0 && !Element.visible("feeds-holder")) {
- $("feed_current_unread").innerText = feed_unread;
+ App.byId("feed_current_unread").innerText = feed_unread;
Element.show("feed_current_unread");
} else {
Element.hide("feed_current_unread");
}
}
},
- onLoaded: function (transport, offset, append) {
- const reply = App.handleRpcJson(transport);
+ renderToolbar: function(headlines) {
+
+ const tb = headlines['toolbar'];
+ const search_query = Feeds._search_query ? Feeds._search_query.query : "";
+ const target = dijit.byId('toolbar-headlines');
+
+ if (tb && typeof tb == 'object') {
+ target.attr('innerHTML',
+ `
+ <span class='left'>
+ <a href="#" title="${__("Show as feed")}"
+ onclick='CommonDialogs.generatedFeed("${headlines.id}", ${headlines.is_cat}, "${App.escapeHtml(search_query)}")'>
+ <i class='icon-syndicate material-icons'>rss_feed</i>
+ </a>
+ ${tb.site_url ?
+ `<a class="feed_title" target="_blank" href="${App.escapeHtml(tb.site_url)}" title="${tb.last_updated}">${tb.title}</a>` :
+ `<span class="feed_title">${tb.title}</span>`}
+ ${search_query ?
+ `
+ <span class='cancel_search'>(<a href='#' onclick='Feeds.cancelSearch()'>${__("Cancel search")}</a>)</span>
+ ` : ''}
+ ${tb.error ? `<i title="${App.escapeHtml(tb.error)}" class='material-icons icon-error'>error</i>` : ''}
+ <span id='feed_current_unread' style='display: none'></span>
+ </span>
+ <span class='right'>
+ <span id='selected_prompt'></span>
+ <div dojoType='fox.form.DropDownButton' title='"${__('Select articles')}'>
+ <span>${__("Select...")}</span>
+ <div dojoType='dijit.Menu' style='display: none;'>
+ <div dojoType='dijit.MenuItem' onclick='Headlines.select("all")'>${__('All')}</div>
+ <div dojoType='dijit.MenuItem' onclick='Headlines.select("unread")'>${__('Unread')}</div>
+ <div dojoType='dijit.MenuItem' onclick='Headlines.select("invert")'>${__('Invert')}</div>
+ <div dojoType='dijit.MenuItem' onclick='Headlines.select("none")'>${__('None')}</div>
+ <div dojoType='dijit.MenuSeparator'></div>
+ <div dojoType='dijit.MenuItem' onclick='Headlines.selectionToggleUnread()'>${__('Toggle unread')}</div>
+ <div dojoType='dijit.MenuItem' onclick='Headlines.selectionToggleMarked()'>${__('Toggle starred')}</div>
+ <div dojoType='dijit.MenuItem' onclick='Headlines.selectionTogglePublished()'>${__('Toggle published')}</div>
+ <div dojoType='dijit.MenuSeparator'></div>
+ <div dojoType='dijit.MenuItem' onclick='Headlines.catchupSelection()'>${__('Mark as read')}</div>
+ <div dojoType='dijit.MenuItem' onclick='Article.selectionSetScore()'>${__('Set score')}</div>
+ ${tb.plugin_menu_items}
+ ${headlines.id === 0 && !headlines.is_cat ?
+ `
+ <div dojoType='dijit.MenuSeparator'></div>
+ <div dojoType='dijit.MenuItem' class='text-error' onclick='Headlines.deleteSelection()'>${__('Delete permanently')}</div>
+ ` : ''}
+ </div>
+ ${tb.plugin_buttons}
+ </span>
+ `);
+ } else {
+ target.attr('innerHTML', '');
+ }
+ dojo.parser.parse(target.domNode);
+ },
+ onLoaded: function (reply, offset, append) {
console.log("Headlines.onLoaded: offset=", offset, "append=", append);
let is_cat = false;
@@ -597,15 +669,15 @@ const Headlines = {
// also called in renderAgain() after view mode switch
Headlines.setCommonClasses();
- $("headlines-frame").setAttribute("is-vfeed",
+ App.byId("headlines-frame").setAttribute("is-vfeed",
reply['headlines']['is_vfeed'] ? 1 : 0);
Article.setActive(0);
try {
- $("headlines-frame").removeClassName("smooth-scroll");
- $("headlines-frame").scrollTop = 0;
- $("headlines-frame").addClassName("smooth-scroll");
+ App.byId("headlines-frame").removeClassName("smooth-scroll");
+ App.byId("headlines-frame").scrollTop = 0;
+ App.byId("headlines-frame").addClassName("smooth-scroll");
} catch (e) {
console.warn(e);
}
@@ -613,25 +685,27 @@ const Headlines = {
this.headlines = [];
this.vgroup_last_feed = undefined;
- dojo.html.set($("toolbar-headlines"),
+ /*dojo.html.set(App.byId("toolbar-headlines"),
reply['headlines']['toolbar'],
- {parseContent: true});
+ {parseContent: true});*/
+
+ Headlines.renderToolbar(reply['headlines']);
if (typeof reply['headlines']['content'] == 'string') {
- $("headlines-frame").innerHTML = reply['headlines']['content'];
+ App.byId("headlines-frame").innerHTML = reply['headlines']['content'];
} else {
- $("headlines-frame").innerHTML = '';
+ App.byId("headlines-frame").innerHTML = '';
for (let i = 0; i < reply['headlines']['content'].length; i++) {
const hl = reply['headlines']['content'][i];
- $("headlines-frame").appendChild(this.render(reply['headlines'], hl));
+ App.byId("headlines-frame").appendChild(this.render(reply['headlines'], hl));
this.headlines[parseInt(hl.id)] = hl;
}
}
- let hsp = $("headlines-spacer");
+ let hsp = App.byId("headlines-spacer");
if (!hsp) {
hsp = document.createElement("div");
@@ -646,18 +720,19 @@ const Headlines = {
hsp.innerHTML = "<a href='#' onclick='Feeds.openNextUnread()'>" +
__("Click to open next unread feed.") + "</a>";
+ /*
if (Feeds._search_query) {
- $("feed_title").innerHTML += "<span id='cancel_search'>" +
+ App.byId("feed_title").innerHTML += "<span id='cancel_search'>" +
" (<a href='#' onclick='Feeds.cancelSearch()'>" + __("Cancel search") + "</a>)" +
"</span>";
- }
+ } */
Headlines.updateCurrentUnread();
} else if (headlines_count > 0 && feed_id == Feeds.getActive() && is_cat == Feeds.activeIsCat()) {
const c = dijit.byId("headlines-frame");
- let hsp = $("headlines-spacer");
+ let hsp = App.byId("headlines-spacer");
if (hsp)
c.domNode.removeChild(hsp);
@@ -665,13 +740,13 @@ const Headlines = {
let headlines_appended = 0;
if (typeof reply['headlines']['content'] == 'string') {
- $("headlines-frame").innerHTML = reply['headlines']['content'];
+ App.byId("headlines-frame").innerHTML = reply['headlines']['content'];
} else {
for (let i = 0; i < reply['headlines']['content'].length; i++) {
const hl = reply['headlines']['content'][i];
if (!this.headlines[parseInt(hl.id)]) {
- $("headlines-frame").appendChild(this.render(reply['headlines'], hl));
+ App.byId("headlines-frame").appendChild(this.render(reply['headlines'], hl));
this.headlines[parseInt(hl.id)] = hl;
++headlines_appended;
@@ -703,7 +778,7 @@ const Headlines = {
console.log("no headlines received, infscroll_disabled=", Feeds.infscroll_disabled, 'first_id_changed=', first_id_changed);
- const hsp = $("headlines-spacer");
+ const hsp = App.byId("headlines-spacer");
if (hsp) {
if (first_id_changed) {
@@ -716,17 +791,16 @@ const Headlines = {
}
}
- $$(".cdm .header-sticky-guard").each((e) => {
+ App.findAll(".cdm .header-sticky-guard").forEach((e) => {
this.sticky_header_observer.observe(e)
});
if (App.getInitParam("cdm_expanded"))
- $$("#headlines-frame > div[id*=RROW].cdm").each((e) => {
+ App.findAll("#headlines-frame > div[id*=RROW].cdm").forEach((e) => {
this.unpack_observer.observe(e)
});
} else {
- console.error("Invalid object received: " + transport.responseText);
dijit.byId("headlines-frame").attr('content', "<div class='whiteBox'>" +
__('Could not update headlines (invalid object received - see error console for details)') +
"</div>");
@@ -755,9 +829,7 @@ const Headlines = {
Feeds.reloadCurrent();
},
- selectionToggleUnread: function (params) {
- params = params || {};
-
+ selectionToggleUnread: function (params = {}) {
const cmode = params.cmode != undefined ? params.cmode : 2;
const no_error = params.no_error || false;
const ids = params.ids || Headlines.getSelected();
@@ -769,8 +841,8 @@ const Headlines = {
return;
}
- ids.each((id) => {
- const row = $("RROW-" + id);
+ ids.forEach((id) => {
+ const row = App.byId(`RROW-${id}`);
if (row) {
switch (cmode) {
@@ -794,7 +866,7 @@ const Headlines = {
return;
}
- ids.each((id) => {
+ ids.forEach((id) => {
this.toggleMark(id);
});
},
@@ -806,26 +878,24 @@ const Headlines = {
return;
}
- ids.each((id) => {
+ ids.forEach((id) => {
this.togglePub(id);
});
},
toggleMark: function (id) {
- const row = $("RROW-" + id);
+ const row = App.byId(`RROW-${id}`);
if (row)
row.toggleClassName("marked");
},
togglePub: function (id) {
- const row = $("RROW-" + id);
+ const row = App.byId(`RROW-${id}`);
if (row)
row.toggleClassName("published");
},
- move: function (mode, params) {
- params = params || {};
-
+ move: function (mode, params = {}) {
const no_expand = params.no_expand || false;
const force_previous = params.force_previous || this.default_force_previous;
const force_to_top = params.force_to_top || this.default_force_to_top;
@@ -834,7 +904,7 @@ const Headlines = {
let next_id = false;
let current_id = Article.getActive();
- if (!Headlines.isChildVisible($("RROW-" + current_id))) {
+ if (!Headlines.isChildVisible(App.byId(`RROW-${current_id}`))) {
console.log('active article is obscured, resetting to first visible...');
current_id = Headlines.firstVisible();
prev_id = current_id;
@@ -871,13 +941,30 @@ const Headlines = {
} else {
Article.view(next_id, no_expand);
}
+ } else if (App.isCombinedMode()) {
+ // try to show hsp if no next article exists, in case there's useful information like first_id_changed etc
+ const row = App.byId(`RROW-${current_id}`);
+ const ctr = App.byId("headlines-frame");
+
+ if (row) {
+ const next = row.nextSibling;
+
+ // hsp has half-screen height in auto catchup mode therefore we use its first child (normally A element)
+ if (next && Element.visible(next) && next.id == "headlines-spacer" && next.firstChild) {
+ const offset = App.byId("headlines-spacer").offsetTop - App.byId("headlines-frame").offsetHeight + next.firstChild.offsetHeight;
+
+ // don't jump back either
+ if (ctr.scrollTop < offset)
+ ctr.scrollTop = offset;
+ }
+ }
}
} else if (mode === "prev") {
if (prev_id || current_id) {
if (App.isCombinedMode()) {
window.requestAnimationFrame(() => {
- const row = $("RROW-" + current_id);
- const ctr = $("headlines-frame");
+ const row = App.byId(`RROW-${current_id}`);
+ const ctr = App.byId("headlines-frame");
const delta_px = Math.round(row.offsetTop) - Math.round(ctr.scrollTop);
console.log('moving back, delta_px', delta_px);
@@ -898,7 +985,7 @@ const Headlines = {
},
updateSelectedPrompt: function () {
const count = Headlines.getSelected().length;
- const elem = $("selected_prompt");
+ const elem = App.byId("selected_prompt");
if (elem) {
elem.innerHTML = ngettext("%d article selected",
@@ -908,7 +995,7 @@ const Headlines = {
}
},
toggleUnread: function (id, cmode) {
- const row = $("RROW-" + id);
+ const row = App.byId(`RROW-${id}`);
if (row) {
if (typeof cmode == "undefined") cmode = 2;
@@ -939,9 +1026,8 @@ const Headlines = {
ids: ids.toString(), lid: id
};
- xhrPost("backend.php", query, (transport) => {
- App.handleRpcJson(transport);
- this.onLabelsUpdated(transport);
+ xhr.json("backend.php", query, (reply) => {
+ this.onLabelsUpdated(reply);
});
},
selectionAssignLabel: function (id, ids) {
@@ -957,9 +1043,8 @@ const Headlines = {
ids: ids.toString(), lid: id
};
- xhrPost("backend.php", query, (transport) => {
- App.handleRpcJson(transport);
- this.onLabelsUpdated(transport);
+ xhr.json("backend.php", query, (reply) => {
+ this.onLabelsUpdated(reply);
});
},
deleteSelection: function () {
@@ -988,15 +1073,14 @@ const Headlines = {
const query = {op: "rpc", method: "delete", ids: rows.toString()};
- xhrPost("backend.php", query, (transport) => {
- App.handleRpcJson(transport);
+ xhr.json("backend.php", query, () => {
Feeds.reloadCurrent();
});
},
getSelected: function () {
const rv = [];
- $$("#headlines-frame > div[id*=RROW][class*=Selected]").each(
+ App.findAll("#headlines-frame > div[id*=RROW][class*=Selected]").forEach(
function (child) {
rv.push(child.getAttribute("data-article-id"));
});
@@ -1010,9 +1094,9 @@ const Headlines = {
getLoaded: function () {
const rv = [];
- const children = $$("#headlines-frame > div[id*=RROW-]");
+ const children = App.findAll("#headlines-frame > div[id*=RROW-]");
- children.each(function (child) {
+ children.forEach(function (child) {
if (Element.visible(child)) {
rv.push(child.getAttribute("data-article-id"));
}
@@ -1021,7 +1105,7 @@ const Headlines = {
return rv;
},
onRowChecked: function (elem) {
- const row = elem.domNode.up("div[id*=RROW]");
+ const row = elem.domNode.closest("div[id*=RROW]");
// do not allow unchecking active article checkbox
if (row.hasClassName("active")) {
@@ -1039,7 +1123,7 @@ const Headlines = {
if (start == stop)
return [start];
- const rows = $$("#headlines-frame > div[id*=RROW]");
+ const rows = App.findAll("#headlines-frame > div[id*=RROW]");
const results = [];
let collecting = false;
@@ -1066,7 +1150,7 @@ const Headlines = {
// mode = all,none,unread,invert,marked,published
let query = "#headlines-frame > div[id*=RROW]";
- if (articleId) query += "[data-article-id=" + articleId + "]";
+ if (articleId) query += `[data-article-id="${articleId}"]`;
switch (mode) {
case "none":
@@ -1086,10 +1170,7 @@ const Headlines = {
console.warn("select: unknown mode", mode);
}
- const rows = $$(query);
-
- for (let i = 0; i < rows.length; i++) {
- const row = rows[i];
+ App.findAll(query).forEach((row) => {
switch (mode) {
case "none":
@@ -1101,7 +1182,7 @@ const Headlines = {
default:
row.addClassName("Selected");
}
- }
+ });
},
catchupSelection: function () {
const rows = Headlines.getSelected();
@@ -1140,7 +1221,7 @@ const Headlines = {
if (!below) {
for (let i = 0; i < visible_ids.length; i++) {
if (visible_ids[i] != id) {
- const e = $("RROW-" + visible_ids[i]);
+ const e = App.byId(`RROW-${visible_ids[i]}`);
if (e && e.hasClassName("Unread")) {
ids_to_mark.push(visible_ids[i]);
@@ -1152,7 +1233,7 @@ const Headlines = {
} else {
for (let i = visible_ids.length - 1; i >= 0; i--) {
if (visible_ids[i] != id) {
- const e = $("RROW-" + visible_ids[i]);
+ const e = App.byId(`RROW-${visible_ids[i]}`);
if (e && e.hasClassName("Unread")) {
ids_to_mark.push(visible_ids[i]);
@@ -1171,26 +1252,40 @@ const Headlines = {
if (App.getInitParam("confirm_feed_catchup") != 1 || confirm(msg)) {
for (let i = 0; i < ids_to_mark.length; i++) {
- const e = $("RROW-" + ids_to_mark[i]);
+ const e = App.byId(`RROW-${ids_to_mark[i]}`);
e.removeClassName("Unread");
}
}
}
},
- onLabelsUpdated: function (transport) {
- const data = JSON.parse(transport.responseText);
+ onTagsUpdated: function (data) {
+ if (data) {
+ if (this.headlines[data.id]) {
+ this.headlines[data.id].tags = data.tags;
+ }
+ App.findAll(`span[data-tags-for="${data.id}"`).forEach((ctr) => {
+ ctr.innerHTML = Article.renderTags(data.id, data.tags);
+ });
+ }
+ },
+ // TODO: maybe this should cause article to be rendered again, although it might cause flicker etc
+ onLabelsUpdated: function (data) {
if (data) {
- data['info-for-headlines'].each(function (elem) {
- $$(".HLLCTR-" + elem.id).each(function (ctr) {
- ctr.innerHTML = elem.labels;
+ data["labels-for"].forEach((row) => {
+ if (this.headlines[row.id]) {
+ this.headlines[row.id].labels = row.labels;
+ }
+
+ App.findAll(`span[data-labels-for="${row.id}"]`).forEach((ctr) => {
+ ctr.innerHTML = Article.renderLabels(row.id, row.labels);
});
});
}
},
scrollToArticleId: function (id) {
- const container = $("headlines-frame");
- const row = $("RROW-" + id);
+ const container = App.byId("headlines-frame");
+ const row = App.byId(`RROW-${id}`);
if (!container || !row) return;
@@ -1289,7 +1384,7 @@ const Headlines = {
const labelAddMenu = new dijit.Menu({ownerMenu: menu});
const labelDelMenu = new dijit.Menu({ownerMenu: menu});
- labels.each(function (label) {
+ labels.forEach(function (label) {
const bare_id = label.id;
const name = label.caption;
@@ -1337,10 +1432,10 @@ const Headlines = {
}
},
scrollByPages: function (page_offset) {
- App.Scrollable.scrollByPages($("headlines-frame"), page_offset);
+ App.Scrollable.scrollByPages(App.byId("headlines-frame"), page_offset);
},
scroll: function (offset) {
- App.Scrollable.scroll($("headlines-frame"), offset);
+ App.Scrollable.scroll(App.byId("headlines-frame"), offset);
},
initHeadlinesMenu: function () {
if (!dijit.byId("headlinesMenu")) {
diff --git a/js/PrefFeedTree.js b/js/PrefFeedTree.js
index 89195e616..bb5d25e67 100644
--- a/js/PrefFeedTree.js
+++ b/js/PrefFeedTree.js
@@ -1,9 +1,44 @@
/* eslint-disable prefer-rest-params */
-/* global __, lib, dijit, define, dojo, CommonDialogs, Notify, Tables, xhrPost, fox, App */
+/* global __, lib, dijit, define, dojo, CommonDialogs, Notify, Tables, xhrPost, xhr, fox, App */
-define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], function (declare, domConstruct) {
+define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_base/array", "dojo/cookie"],
+ function (declare, domConstruct, checkBoxTree, array, cookie) {
return declare("fox.PrefFeedTree", lib.CheckBoxTree, {
+ // save state in localStorage instead of cookies
+ // reference: https://stackoverflow.com/a/27968996
+ _saveExpandedNodes: function(){
+ if (this.persist && this.cookieName){
+ const ary = [];
+ for (const id in this._openedNodes){
+ ary.push(id);
+ }
+ // Was:
+ // cookie(this.cookieName, ary.join(","), {expires: 365});
+ localStorage.setItem(this.cookieName, ary.join(","));
+ }
+ },
+ _initState: function(){
+ this.cookieName = 'prefs:' + this.cookieName;
+ // summary:
+ // Load in which nodes should be opened automatically
+ this._openedNodes = {};
+ if (this.persist && this.cookieName){
+ // Was:
+ // var oreo = cookie(this.cookieName);
+ let oreo = localStorage.getItem(this.cookieName);
+ // migrate old data if nothing in localStorage
+ if (oreo == null || oreo === '') {
+ oreo = cookie(this.cookieName);
+ cookie(this.cookieName, null, { expires: -1 });
+ }
+ if (oreo){
+ array.forEach(oreo.split(','), function(item){
+ this._openedNodes[item] = true;
+ }, this);
+ }
+ }
+ },
_createTreeNode: function(args) {
const tnode = this.inherited(arguments);
@@ -91,11 +126,11 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
return (!item || this.model.store.getValue(item, 'type') == 'category') ? (opened ? "dijitFolderOpened" : "dijitFolderClosed") : "feed-icon";
},
reload: function() {
- const searchElem = $("feed_search");
+ const searchElem = App.byId("feed_search");
const search = (searchElem) ? searchElem.value : "";
- xhrPost("backend.php", { op: "pref-feeds", search: search }, (transport) => {
- dijit.byId('feedsTab').attr('content', transport.responseText);
+ xhr.post("backend.php", { op: "pref-feeds", search: search }, (reply) => {
+ dijit.byId('feedsTab').attr('content', reply);
Notify.close();
});
},
@@ -129,14 +164,14 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
resetFeedOrder: function() {
Notify.progress("Loading, please wait...");
- xhrPost("backend.php", {op: "pref-feeds", method: "feedsortreset"}, () => {
+ xhr.post("backend.php", {op: "pref-feeds", method: "feedsortreset"}, () => {
this.reload();
});
},
resetCatOrder: function() {
Notify.progress("Loading, please wait...");
- xhrPost("backend.php", {op: "pref-feeds", method: "catsortreset"}, () => {
+ xhr.post("backend.php", {op: "pref-feeds", method: "catsortreset"}, () => {
this.reload();
});
},
@@ -144,7 +179,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
if (confirm(__("Remove category %s? Any nested feeds would be placed into Uncategorized.").replace("%s", item.name))) {
Notify.progress("Removing category...");
- xhrPost("backend.php", {op: "pref-feeds", method: "removeCat", ids: id}, () => {
+ xhr.post("backend.php", {op: "pref-feeds", method: "removeCat", ids: id}, () => {
Notify.close();
this.reload();
});
@@ -163,7 +198,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
ids: sel_rows.toString()
};
- xhrPost("backend.php", query, () => {
+ xhr.post("backend.php", query, () => {
this.reload();
});
}
@@ -174,9 +209,16 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
return false;
},
+ checkErrorFeeds: function() {
+ xhr.json("backend.php", {op: "pref-feeds", method: "feedsWithErrors"}, (reply) => {
+ if (reply.length > 0) {
+ Element.show(dijit.byId("pref_feeds_errors_btn").domNode);
+ }
+ });
+ },
checkInactiveFeeds: function() {
- xhrPost("backend.php", {op: "pref-feeds", method: "getinactivefeeds"}, (transport) => {
- if (parseInt(transport.responseText) > 0) {
+ xhr.json("backend.php", {op: "pref-feeds", method: "inactivefeeds"}, (reply) => {
+ if (reply.length > 0) {
Element.show(dijit.byId("pref_feeds_inactive_btn").domNode);
}
});
@@ -186,7 +228,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
const items = tree.model.getCheckedItems();
const rv = [];
- items.each(function (item) {
+ items.forEach(function (item) {
if (item.id[0].match("CAT:"))
rv.push(tree.model.store.getValue(item, 'bare_id'));
});
@@ -205,7 +247,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
ids: sel_rows.toString()
};
- xhrPost("backend.php", query, () => {
+ xhr.post("backend.php", query, () => {
this.reload();
});
}
@@ -220,7 +262,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
const items = tree.model.getCheckedItems();
const rv = [];
- items.each(function (item) {
+ items.forEach(function (item) {
if (item.id[0].match("FEED:"))
rv.push(tree.model.store.getValue(item, 'bare_id'));
});
@@ -253,16 +295,15 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
Notify.progress("Loading, please wait...");
- xhrPost("backend.php", {op: "pref-feeds", method: "editfeeds", ids: rows.toString()}, (transport) => {
+ xhr.post("backend.php", {op: "pref-feeds", method: "editfeeds", ids: rows.toString()}, (reply) => {
Notify.close();
try {
const dialog = new fox.SingleUseDialog({
- id: "feedEditDlg",
title: __("Edit Multiple Feeds"),
- getChildByName: function (name) {
+ /*getChildByName: function (name) {
let rv = null;
- this.getChildren().each(
+ this.getChildren().forEach(
function (child) {
if (child.name == name) {
rv = child;
@@ -270,16 +311,22 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
}
});
return rv;
- },
- toggleField: function (checkbox, elem, label) {
- this.getChildByName(elem).attr('disabled', !checkbox.checked);
+ },*/
+ toggleField: function (checkbox) {
+ const name = checkbox.attr("data-control-for");
+ const target = dijit.getEnclosingWidget(dialog.domNode.querySelector(`input[name="${name}"]`));
- if ($(label))
- if (checkbox.checked)
- $(label).removeClassName('text-muted');
- else
- $(label).addClassName('text-muted');
+ target.attr('disabled', !checkbox.attr('checked'));
+ console.log(target, target.attr('type'));
+
+ if (target.attr('type') == "checkbox") {
+ const label = checkbox.domNode.closest("label");
+ if (checkbox.attr('checked'))
+ label.removeClassName('text-muted');
+ else
+ label.addClassName('text-muted');
+ }
},
execute: function () {
if (this.validate() && confirm(__("Save changes to selected feeds?"))) {
@@ -287,7 +334,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
/* normalize unchecked checkboxes because [] is not serialized */
- Object.keys(query).each((key) => {
+ Object.keys(query).forEach((key) => {
const val = query[key];
if (typeof val == "object" && val.length == 0)
@@ -296,7 +343,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
Notify.progress("Saving data...", true);
- xhrPost("backend.php", query, () => {
+ xhr.post("backend.php", query, () => {
dialog.hide();
const tree = dijit.byId("feedTree");
@@ -305,7 +352,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
});
}
},
- content: transport.responseText
+ content: reply
});
dialog.show();
@@ -325,7 +372,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
Notify.progress("Loading, please wait...");
- xhrPost("backend.php", { op: 'pref-feeds', method: 'renamecat', id: id, title: new_name }, () => {
+ xhr.post("backend.php", { op: 'pref-feeds', method: 'renamecat', id: id, title: new_name }, () => {
this.reload();
});
}
@@ -336,63 +383,22 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
if (title) {
Notify.progress("Creating category...");
- xhrPost("backend.php", {op: "pref-feeds", method: "addCat", cat: title}, () => {
+ xhr.post("backend.php", {op: "pref-feeds", method: "addCat", cat: title}, () => {
Notify.close();
this.reload();
});
}
},
batchSubscribe: function() {
- const dialog = new fox.SingleUseDialog({
- id: "batchSubDlg",
- title: __("Batch subscribe"),
- execute: function () {
- if (this.validate()) {
- Notify.progress(__("Subscribing to feeds..."), true);
-
- xhrPost("backend.php", this.attr('value'), () => {
- Notify.close();
-
- const tree = dijit.byId("feedTree");
- if (tree) tree.reload();
-
- dialog.hide();
- });
- }
- },
- content: __("Loading, please wait...")
- });
-
- const tmph = dojo.connect(dialog, 'onShow', function () {
- dojo.disconnect(tmph);
-
- xhrPost("backend.php", {op: 'pref-feeds', method: 'batchSubscribe'}, (transport) => {
- dialog.attr('content', transport.responseText);
- })
- });
-
- dialog.show();
- },
- showInactiveFeeds: function() {
- const dialog = new fox.SingleUseDialog({
- id: "inactiveFeedsDlg",
- title: __("Feeds without recent updates"),
- getSelectedFeeds: function () {
- return Tables.getSelected("inactive-feeds-list");
- },
- 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, () => {
+ xhr.json("backend.php", {op: 'pref-feeds', method: 'batchSubscribe'}, (reply) => {
+ const dialog = new fox.SingleUseDialog({
+ id: "batchSubDlg",
+ title: __("Batch subscribe"),
+ execute: function () {
+ if (this.validate()) {
+ Notify.progress(__("Subscribing to feeds..."), true);
+
+ xhr.post("backend.php", this.attr('value'), () => {
Notify.close();
const tree = dijit.byId("feedTree");
@@ -401,23 +407,143 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
dialog.hide();
});
}
+ },
+ content: `
+ <form onsubmit='return false'>
+ ${App.FormFields.hidden_tag("op", "pref-feeds")}
+ ${App.FormFields.hidden_tag("method", "batchaddfeeds")}
+
+ <header class='horizontal'>
+ ${__("One valid feed per line (no detection is done)")}
+ </header>
+
+ <section>
+ <textarea style='font-size : 12px; width : 98%; height: 200px;'
+ dojoType='fox.form.ValidationTextArea' required='1' name='feeds'></textarea>
+
+ ${reply.enable_cats ?
+ `<fieldset>
+ <label>${__('Place in category:')}</label>
+ ${reply.cat_select}
+ </fieldset>
+ ` : ''
+ }
+ </section>
+
+ <div id='feedDlg_loginContainer' style='display : none'>
+ <header>${__("Authentication")}</header>
+ <section>
+ <input dojoType='dijit.form.TextBox' name='login' placeHolder="${__("Login")}">
+ <input placeHolder="${__("Password")}" dojoType="dijit.form.TextBox" type='password'
+ autocomplete='new-password' name='pass'></div>
+ </section>
+ </div>
+
+ <fieldset class='narrow'>
+ <label class='checkbox'><input type='checkbox' name='need_auth' dojoType='dijit.form.CheckBox'
+ onclick='App.displayIfChecked(this, "feedDlg_loginContainer")'>
+ ${__('Feeds require authentication.')}
+ </label>
+ </fieldset>
+
+ <footer>
+ <button dojoType='dijit.form.Button' onclick='App.dialogOf(this).execute()' type='submit' class='alt-primary'>
+ ${__('Subscribe')}
+ </button>
+ <button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'>
+ ${__('Cancel')}
+ </button>
+ </footer>
+ </form>
+ `
+ });
+
+ dialog.show();
- } else {
- alert(__("No feeds selected."));
- }
- },
- content: __("Loading, please wait...")
});
+ },
+ showInactiveFeeds: function() {
+ xhr.json("backend.php", {op: 'pref-feeds', method: 'inactivefeeds'}, function (reply) {
+
+ const dialog = new fox.SingleUseDialog({
+ id: "inactiveFeedsDlg",
+ title: __("Feeds without recent updates"),
+ getSelectedFeeds: function () {
+ return Tables.getSelected("inactive-feeds-list");
+ },
+ 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()
+ };
+
+ xhr.post("backend.php", query, () => {
+ Notify.close();
+
+ const tree = dijit.byId("feedTree");
+ if (tree) tree.reload();
+
+ dialog.hide();
+ });
+ }
+
+ } else {
+ alert(__("No feeds selected."));
+ }
+ },
+ content: `
+ <div dojoType='fox.Toolbar'>
+ <div dojoType='fox.form.DropDownButton'>
+ <span>${__('Select')}</span>
+ <div dojoType='dijit.Menu' style='display: none'>
+ <div onclick="Tables.select('inactive-feeds-list', true)"
+ dojoType='dijit.MenuItem'>${__('All')}</div>
+ <div onclick="Tables.select('inactive-feeds-list', false)"
+ dojoType='dijit.MenuItem'>${__('None')}</div>
+ </div>
+ </div>
+ </div>
+
+ <div class='panel panel-scrollable'>
+ <table width='100%' id='inactive-feeds-list'>
+ ${reply.map((row) => `<tr data-row-id='${row.id}'>
+ <td width='5%' align='center'>
+ <input onclick='Tables.onRowChecked(this)' dojoType='dijit.form.CheckBox' type='checkbox'>
+ </td>
+ <td>
+ <a href='#' "title="${__("Click to edit feed")}" onclick="CommonDialogs.editFeed(${row.id})">
+ ${App.escapeHtml(row.title)}
+ </a>
+ </td>
+ <td class='text-muted' align='right'>
+ ${row.last_article}
+ </td>
+ </tr>
+ `).join("")}
+ </table>
+ </div>
+
+ <footer>
+ <button style='float : left' class='alt-danger' dojoType='dijit.form.Button' onclick='App.dialogOf(this).removeSelected()'>
+ ${__('Unsubscribe from selected feeds')}
+ </button>
+ <button dojoType='dijit.form.Button' class='alt-primary' type='submit'>
+ ${__('Close this window')}
+ </button>
+ </footer>
+ `
+ });
- const tmph = dojo.connect(dialog, 'onShow', function () {
- dojo.disconnect(tmph);
+ dialog.show();
- xhrPost("backend.php", {op: "pref-feeds", method: "inactivefeeds"}, (transport) => {
- dialog.attr('content', transport.responseText);
- })
});
- dialog.show();
}
});
});
diff --git a/js/PrefFilterTree.js b/js/PrefFilterTree.js
index abfdbb3b0..fff58ff1a 100644
--- a/js/PrefFilterTree.js
+++ b/js/PrefFilterTree.js
@@ -1,5 +1,5 @@
/* eslint-disable prefer-rest-params */
-/* global __, define, lib, dijit, dojo, xhrPost, Notify */
+/* global __, define, lib, dijit, dojo, xhr, App, Notify */
define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], function (declare, domConstruct) {
@@ -80,26 +80,26 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
const items = tree.model.getCheckedItems();
const rv = [];
- items.each(function (item) {
+ items.forEach(function (item) {
rv.push(tree.model.store.getValue(item, 'bare_id'));
});
return rv;
},
reload: function() {
- const user_search = $("filter_search");
+ const user_search = App.byId("filter_search");
let search = "";
if (user_search) { search = user_search.value; }
- xhrPost("backend.php", { op: "pref-filters", search: search }, (transport) => {
- dijit.byId('filtersTab').attr('content', transport.responseText);
+ xhr.post("backend.php", { op: "pref-filters", search: search }, (reply) => {
+ dijit.byId('filtersTab').attr('content', reply);
Notify.close();
});
},
resetFilterOrder: function() {
Notify.progress("Loading, please wait...");
- xhrPost("backend.php", {op: "pref-filters", method: "filtersortreset"}, () => {
+ xhr.post("backend.php", {op: "pref-filters", method: "filtersortreset"}, () => {
this.reload();
});
},
@@ -114,28 +114,11 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
if (confirm(__("Combine selected filters?"))) {
Notify.progress("Joining filters...");
- xhrPost("backend.php", {op: "pref-filters", method: "join", ids: rows.toString()}, () => {
+ xhr.post("backend.php", {op: "pref-filters", method: "join", ids: rows.toString()}, () => {
this.reload();
});
}
},
- editSelectedFilter: function() {
- const rows = this.getSelectedFilters();
-
- if (rows.length == 0) {
- alert(__("No filters selected."));
- return;
- }
-
- if (rows.length > 1) {
- alert(__("Please select only one filter."));
- return;
- }
-
- Notify.close();
-
- this.editFilter(rows[0]);
- },
removeSelectedFilters: function() {
const sel_rows = this.getSelectedFilters();
@@ -148,7 +131,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
ids: sel_rows.toString()
};
- xhrPost("backend.php", query, () => {
+ xhr.post("backend.php", query, () => {
this.reload();
});
}
diff --git a/js/PrefHelpers.js b/js/PrefHelpers.js
index 5bb76d179..62f6d91b1 100644
--- a/js/PrefHelpers.js
+++ b/js/PrefHelpers.js
@@ -1,7 +1,7 @@
'use strict';
/* eslint-disable no-new */
-/* global __, dijit, dojo, Tables, xhrPost, Notify, xhrJson, App, fox, Effect */
+/* global __, dijit, dojo, Tables, xhrPost, Notify, xhr, App, fox */
const Helpers = {
AppPasswords: {
@@ -9,7 +9,7 @@ const Helpers = {
return Tables.getSelected("app-password-list");
},
updateContent: function(data) {
- $("app_passwords_holder").innerHTML = data;
+ App.byId("app_passwords_holder").innerHTML = data;
dojo.parser.parse("app_passwords_holder");
},
removeSelected: function() {
@@ -19,8 +19,8 @@ const Helpers = {
alert("No passwords selected.");
} else if (confirm(__("Remove selected app passwords?"))) {
- xhrPost("backend.php", {op: "pref-prefs", method: "deleteAppPassword", ids: rows.toString()}, (transport) => {
- this.updateContent(transport.responseText);
+ xhr.post("backend.php", {op: "pref-prefs", method: "deleteAppPassword", ids: rows.toString()}, (reply) => {
+ this.updateContent(reply);
Notify.close();
});
@@ -31,8 +31,8 @@ const Helpers = {
const title = prompt("Password description:")
if (title) {
- xhrPost("backend.php", {op: "pref-prefs", method: "generateAppPassword", title: title}, (transport) => {
- this.updateContent(transport.responseText);
+ xhr.post("backend.php", {op: "pref-prefs", method: "generateAppPassword", title: title}, (reply) => {
+ this.updateContent(reply);
Notify.close();
});
@@ -40,16 +40,21 @@ const Helpers = {
}
},
},
- clearFeedAccessKeys: function() {
- if (confirm(__("This will invalidate all previously generated feed URLs. Continue?"))) {
- Notify.progress("Clearing URLs...");
+ Feeds: {
+ clearFeedAccessKeys: function() {
+ if (confirm(__("This will invalidate all previously generated feed URLs. Continue?"))) {
+ Notify.progress("Clearing URLs...");
- xhrPost("backend.php", {op: "pref-feeds", method: "clearKeys"}, () => {
- Notify.info("Generated URLs cleared.");
- });
- }
+ xhr.post("backend.php", {op: "pref-feeds", method: "clearKeys"}, () => {
+ Notify.info("Generated URLs cleared.");
+ });
+ }
- return false;
+ return false;
+ },
+ },
+ System: {
+ //
},
EventLog: {
log_page: 0,
@@ -58,8 +63,13 @@ const Helpers = {
this.update();
},
update: function() {
- xhrPost("backend.php", { op: "pref-system", severity: dijit.byId("severity").attr('value'), page: Helpers.EventLog.log_page }, (transport) => {
- dijit.byId('systemTab').attr('content', transport.responseText);
+ xhr.post("backend.php", {
+ op: "pref-system",
+ severity: dijit.byId("severity").attr('value'),
+ page: Helpers.EventLog.log_page
+ }, (reply) => {
+
+ dijit.byId('systemTab').attr('content', reply);
Notify.close();
});
},
@@ -77,161 +87,216 @@ const Helpers = {
Notify.progress("Loading, please wait...");
- xhrPost("backend.php", {op: "pref-system", method: "clearLog"}, () => {
+ xhr.post("backend.php", {op: "pref-system", method: "clearLog"}, () => {
Helpers.EventLog.refresh();
});
}
},
},
- editProfiles: function() {
- const dialog = new fox.SingleUseDialog({
- id: "profileEditDlg",
- title: __("Settings Profiles"),
- getSelectedProfiles: function () {
- return Tables.getSelected("pref-profiles-list");
- },
- removeSelected: function () {
- const sel_rows = this.getSelectedProfiles();
-
- if (sel_rows.length > 0) {
- if (confirm(__("Remove selected profiles? Active and default profiles will not be removed."))) {
- Notify.progress("Removing selected profiles...", true);
-
- const query = {
- op: "rpc", method: "remprofiles",
- ids: sel_rows.toString()
- };
-
- xhrPost("backend.php", query, () => {
+ Profiles: {
+ edit: function() {
+ const dialog = new fox.SingleUseDialog({
+ id: "profileEditDlg",
+ title: __("Settings Profiles"),
+ getSelectedProfiles: function () {
+ return Tables.getSelected("pref-profiles-list");
+ },
+ removeSelected: function () {
+ const sel_rows = this.getSelectedProfiles();
+
+ if (sel_rows.length > 0) {
+ if (confirm(__("Remove selected profiles? Active and default profiles will not be removed."))) {
+ Notify.progress("Removing selected profiles...", true);
+
+ const query = {
+ op: "pref-prefs", method: "remprofiles",
+ ids: sel_rows.toString()
+ };
+
+ xhr.post("backend.php", query, () => {
+ Notify.close();
+ dialog.refresh();
+ });
+ }
+
+ } else {
+ alert(__("No profiles selected."));
+ }
+ },
+ addProfile: function () {
+ if (this.validate()) {
+ Notify.progress("Creating profile...", true);
+
+ const query = {op: "pref-prefs", method: "addprofile", title: dialog.attr('value').newprofile};
+
+ xhr.post("backend.php", query, () => {
Notify.close();
dialog.refresh();
});
- }
- } else {
- alert(__("No profiles selected."));
- }
- },
- addProfile: function () {
- if (this.validate()) {
- Notify.progress("Creating profile...", true);
-
- const query = {op: "rpc", method: "addprofile", title: dialog.attr('value').newprofile};
+ }
+ },
+ refresh: function() {
+ xhr.json("backend.php", {op: 'pref-prefs', method: 'getprofiles'}, (reply) => {
+ dialog.attr('content', `
+ <div dojoType='fox.Toolbar'>
+ <div dojoType='fox.form.DropDownButton'>
+ <span>${__('Select')}</span>
+ <div dojoType='dijit.Menu' style='display: none'>
+ <div onclick="Tables.select('pref-profiles-list', true)"
+ dojoType='dijit.MenuItem'>${__('All')}</div>
+ <div onclick="Tables.select('pref-profiles-list', false)"
+ dojoType='dijit.MenuItem'>${__('None')}</div>
+ </div>
+ </div>
+
+ <div class="pull-right">
+ <input name='newprofile' dojoType='dijit.form.ValidationTextBox' required='1'>
+ ${App.FormFields.button_tag(__('Create profile'), "", {onclick: 'App.dialogOf(this).addProfile()'})}
+ </div>
+ </div>
- xhrPost("backend.php", query, () => {
- Notify.close();
- dialog.refresh();
+ <form onsubmit='return false'>
+ <div class='panel panel-scrollable'>
+ <table width='100%' id='pref-profiles-list'>
+ ${reply.map((profile) => `
+ <tr data-row-id="${profile.id}">
+ <td width='5%'>
+ ${App.FormFields.checkbox_tag("", false, "", {onclick: 'Tables.onRowChecked(this)'})}
+ </td>
+ <td>
+ ${profile.id > 0 ?
+ `<span dojoType='dijit.InlineEditBox' width='300px' autoSave='false'
+ profile-id='${profile.id}'>${profile.title}
+ <script type='dojo/method' event='onChange' args='value'>
+ xhr.post("backend.php",
+ {op: 'pref-prefs', method: 'saveprofile', value: value, id: this.attr('profile-id')}, () => {
+ //
+ });
+ </script>
+ </span>` : `${profile.title}`}
+ ${profile.active ? __("(active)") : ""}
+ </td>
+ </tr>
+ `).join("")}
+ </table>
+ </div>
+
+ <footer>
+ ${App.FormFields.button_tag(__('Remove selected profiles'), "",
+ {class: 'pull-left alt-danger', onclick: 'App.dialogOf(this).removeSelected()'})}
+ ${App.FormFields.submit_tag(__('Activate profile'), {onclick: 'App.dialogOf(this).execute()'})}
+ ${App.FormFields.cancel_dialog_tag(__('Cancel'))}
+ </footer>
+ </form>
+ `);
});
+ },
+ execute: function () {
+ const sel_rows = this.getSelectedProfiles();
- }
- },
- refresh: function() {
- xhrPost("backend.php", {op: 'pref-prefs', method: 'editPrefProfiles'}, (transport) => {
- dialog.attr('content', transport.responseText);
- });
- },
- execute: function () {
- const sel_rows = this.getSelectedProfiles();
+ if (sel_rows.length == 1) {
+ if (confirm(__("Activate selected profile?"))) {
+ Notify.progress("Loading, please wait...");
- if (sel_rows.length == 1) {
- if (confirm(__("Activate selected profile?"))) {
- Notify.progress("Loading, please wait...");
+ xhr.post("backend.php", {op: "pref-prefs", method: "activateprofile", id: sel_rows.toString()}, () => {
+ window.location.reload();
+ });
+ }
- xhrPost("backend.php", {op: "rpc", method: "setprofile", id: sel_rows.toString()}, () => {
- window.location.reload();
- });
+ } else {
+ alert(__("Please choose a profile to activate."));
}
+ },
+ content: ""
+ });
- } else {
- alert(__("Please choose a profile to activate."));
- }
- },
- content: ""
- });
-
- dialog.refresh();
- dialog.show();
+ dialog.refresh();
+ dialog.show();
+ },
},
- customizeCSS: function() {
- xhrJson("backend.php", {op: "pref-prefs", method: "customizeCSS"}, (reply) => {
+ Prefs: {
+ customizeCSS: function() {
+ xhr.json("backend.php", {op: "pref-prefs", method: "customizeCSS"}, (reply) => {
+
+ const dialog = new fox.SingleUseDialog({
+ title: __("Customize stylesheet"),
+ apply: function() {
+ xhr.post("backend.php", this.attr('value'), () => {
+ Element.show("css_edit_apply_msg");
+ App.byId("user_css_style").innerText = this.attr('value');
+ });
+ },
+ execute: function () {
+ Notify.progress('Saving data...', true);
- const dialog = new fox.SingleUseDialog({
- title: __("Customize stylesheet"),
- apply: function() {
- xhrPost("backend.php", this.attr('value'), () => {
- new Effect.Appear("css_edit_apply_msg");
- $("user_css_style").innerText = this.attr('value');
- });
- },
- execute: function () {
- Notify.progress('Saving data...', true);
+ xhr.post("backend.php", this.attr('value'), () => {
+ window.location.reload();
+ });
+ },
+ content: `
+ <div class='alert alert-info'>
+ ${__("You can override colors, fonts and layout of your currently selected theme with custom CSS declarations here.")}
+ </div>
- xhrPost("backend.php", this.attr('value'), () => {
- window.location.reload();
- });
- },
- content: `
- <div class='alert alert-info'>
- ${__("You can override colors, fonts and layout of your currently selected theme with custom CSS declarations here.")}
- </div>
-
- ${App.FormFields.hidden('op', 'rpc')}
- ${App.FormFields.hidden('method', 'setpref')}
- ${App.FormFields.hidden('key', 'USER_STYLESHEET')}
-
- <div id='css_edit_apply_msg' style='display : none'>
- <div class='alert alert-warning'>
- ${__("User CSS has been applied, you might need to reload the page to see all changes.")}
+ ${App.FormFields.hidden_tag('op', 'rpc')}
+ ${App.FormFields.hidden_tag('method', 'setpref')}
+ ${App.FormFields.hidden_tag('key', 'USER_STYLESHEET')}
+
+ <div id='css_edit_apply_msg' style='display : none'>
+ <div class='alert alert-warning'>
+ ${__("User CSS has been applied, you might need to reload the page to see all changes.")}
+ </div>
</div>
- </div>
-
- <textarea class='panel user-css-editor' dojoType='dijit.form.SimpleTextarea'
- style='font-size : 12px;' name='value'>${reply.value}</textarea>
-
- <footer>
- <button dojoType='dijit.form.Button' class='alt-success' onclick="App.dialogOf(this).apply()">
- ${__('Apply')}
- </button>
- <button dojoType='dijit.form.Button' class='alt-primary' type='submit'>
- ${__('Save and reload')}
- </button>
- <button dojoType='dijit.form.Button' onclick="App.dialogOf(this).hide()">
- ${__('Cancel')}
- </button>
- </footer>
- `
- });
- dialog.show();
+ <textarea class='panel user-css-editor' dojoType='dijit.form.SimpleTextarea'
+ style='font-size : 12px;' name='value'>${reply.value}</textarea>
+
+ <footer>
+ <button dojoType='dijit.form.Button' class='alt-success' onclick="App.dialogOf(this).apply()">
+ ${__('Apply')}
+ </button>
+ <button dojoType='dijit.form.Button' class='alt-primary' type='submit'>
+ ${__('Save and reload')}
+ </button>
+ <button dojoType='dijit.form.Button' onclick="App.dialogOf(this).hide()">
+ ${__('Cancel')}
+ </button>
+ </footer>
+ `
+ });
+
+ dialog.show();
- });
- },
- confirmReset: function() {
- if (confirm(__("Reset to defaults?"))) {
- xhrPost("backend.php", {op: "pref-prefs", method: "resetconfig"}, (transport) => {
- Helpers.refresh();
- Notify.info(transport.responseText);
});
- }
- },
- clearPluginData: function(name) {
- if (confirm(__("Clear stored data for this plugin?"))) {
- Notify.progress("Loading, please wait...");
+ },
+ confirmReset: function() {
+ if (confirm(__("Reset to defaults?"))) {
+ xhr.post("backend.php", {op: "pref-prefs", method: "resetconfig"}, (reply) => {
+ Helpers.Prefs.refresh();
+ Notify.info(reply);
+ });
+ }
+ },
+ clearPluginData: function(name) {
+ if (confirm(__("Clear stored data for this plugin?"))) {
+ Notify.progress("Loading, please wait...");
- xhrPost("backend.php", {op: "pref-prefs", method: "clearplugindata", name: name}, () => {
- Helpers.refresh();
+ xhr.post("backend.php", {op: "pref-prefs", method: "clearplugindata", name: name}, () => {
+ Helpers.Prefs.refresh();
+ });
+ }
+ },
+ refresh: function() {
+ xhr.post("backend.php", { op: "pref-prefs" }, (reply) => {
+ dijit.byId('prefsTab').attr('content', reply);
+ Notify.close();
});
- }
- },
- refresh: function() {
- xhrPost("backend.php", { op: "pref-prefs" }, (transport) => {
- dijit.byId('prefsTab').attr('content', transport.responseText);
- Notify.close();
- });
+ },
},
OPML: {
import: function() {
- const opml_file = $("opml_file");
+ const opml_file = App.byId("opml_file");
if (opml_file.value.length == 0) {
alert(__("Please choose an OPML file first."));
@@ -273,7 +338,7 @@ const Helpers = {
dialog.show();
};
- xhr.send(new FormData($("opml_import_form")));
+ xhr.send(new FormData(App.byId("opml_import_form")));
return false;
}
@@ -282,30 +347,62 @@ const Helpers = {
console.log("export");
window.open("backend.php?op=opml&method=export&" + dojo.formToQuery("opmlExportForm"));
},
- changeKey: function() {
- if (confirm(__("Replace current OPML publishing address with a new one?"))) {
- Notify.progress("Trying to change address...", true);
-
- xhrJson("backend.php", {op: "pref-feeds", method: "regenOPMLKey"}, (reply) => {
- if (reply) {
- const new_link = reply.link;
- const e = $('pub_opml_url');
+ publish: function() {
+ Notify.progress("Loading, please wait...", true);
- if (new_link) {
- e.href = new_link;
- e.innerHTML = new_link;
+ xhr.json("backend.php", {op: "pref-feeds", method: "getOPMLKey"}, (reply) => {
+ try {
+ const dialog = new fox.SingleUseDialog({
+ title: __("Public OPML URL"),
+ regenOPMLKey: function() {
+ if (confirm(__("Replace current OPML publishing address with a new one?"))) {
+ Notify.progress("Trying to change address...", true);
+
+ xhr.json("backend.php", {op: "pref-feeds", method: "regenOPMLKey"}, (reply) => {
+ if (reply) {
+ const new_link = reply.link;
+ const target = this.domNode.querySelector('.generated_url');
+
+ if (new_link && target) {
+ target.href = new_link;
+ target.innerHTML = new_link;
+
+ Notify.close();
+
+ } else {
+ Notify.error("Could not change feed URL.");
+ }
+ }
+ });
+ }
+ return false;
+ },
+ content: `
+ <header>${__("Your Public OPML URL is:")}</header>
+ <section>
+ <div class='panel text-center'>
+ <a class='generated_url' href="${App.escapeHtml(reply.link)}" target='_blank'>${App.escapeHtml(reply.link)}</a>
+ </div>
+ </section>
+ <footer class='text-center'>
+ <button dojoType='dijit.form.Button' onclick="return App.dialogOf(this).regenOPMLKey()">
+ ${__('Generate new URL')}
+ </button>
+ <button dojoType='dijit.form.Button' type='submit' class='alt-primary'>
+ ${__('Close this window')}
+ </button>
+ </footer>
+ `
+ });
- new Effect.Highlight(e);
+ dialog.show();
- Notify.close();
+ Notify.close();
- } else {
- Notify.error("Could not change feed URL.");
- }
- }
- });
- }
- return false;
+ } catch (e) {
+ App.Error.report(e);
+ }
+ });
},
}
};
diff --git a/js/PrefLabelTree.js b/js/PrefLabelTree.js
index 73f375f2d..2b78927c2 100644
--- a/js/PrefLabelTree.js
+++ b/js/PrefLabelTree.js
@@ -1,5 +1,5 @@
/* eslint-disable prefer-rest-params */
-/* global __, define, lib, dijit, dojo, xhrPost, Notify, fox */
+/* global __, define, lib, dijit, dojo, xhr, Notify, fox, App */
define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/form/DropDownButton"], function (declare, domConstruct) {
@@ -48,83 +48,140 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
const items = tree.model.getCheckedItems();
const rv = [];
- items.each(function(item) {
+ items.forEach(function(item) {
rv.push(tree.model.store.getValue(item, 'bare_id'));
});
return rv;
},
reload: function() {
- xhrPost("backend.php", { op: "pref-labels" }, (transport) => {
- dijit.byId('labelsTab').attr('content', transport.responseText);
+ xhr.post("backend.php", { op: "pref-labels" }, (reply) => {
+ dijit.byId('labelsTab').attr('content', reply);
Notify.close();
});
},
editLabel: function(id) {
- const dialog = new fox.SingleUseDialog({
- id: "labelEditDlg",
- title: __("Label Editor"),
- style: "width: 650px",
- setLabelColor: function (id, fg, bg) {
-
- let kind = '';
- let color = '';
-
- if (fg && bg) {
- kind = 'both';
- } else if (fg) {
- kind = 'fg';
- color = fg;
- } else if (bg) {
- kind = 'bg';
- color = bg;
- }
-
- const e = $("icon-label-" + id);
-
- if (e) {
- if (bg) e.style.color = bg;
- }
-
- const query = {
- op: "pref-labels", method: "colorset", kind: kind,
- ids: id, fg: fg, bg: bg, color: color
- };
-
- xhrPost("backend.php", query, () => {
- const tree = dijit.byId("filterTree");
- if (tree) tree.reload(); // maybe there's labels in there
- });
-
- },
- execute: function () {
- if (this.validate()) {
- const caption = this.attr('value').caption;
- const fg_color = this.attr('value').fg_color;
- const bg_color = this.attr('value').bg_color;
-
- dijit.byId('labelTree').setNameById(id, caption);
- this.setLabelColor(id, fg_color, bg_color);
- this.hide();
-
- xhrPost("backend.php", this.attr('value'), () => {
+ xhr.json("backend.php", {op: "pref-labels", method: "edit", id: id}, (reply) => {
+
+ const fg_color = reply['fg_color'];
+ const bg_color = reply['bg_color'] ? reply['bg_color'] : '#fff7d5';
+
+ const dialog = new fox.SingleUseDialog({
+ id: "labelEditDlg",
+ title: __("Label Editor"),
+ style: "width: 650px",
+ setLabelColor: function (id, fg, bg) {
+
+ let kind = '';
+ let color = '';
+
+ if (fg && bg) {
+ kind = 'both';
+ } else if (fg) {
+ kind = 'fg';
+ color = fg;
+ } else if (bg) {
+ kind = 'bg';
+ color = bg;
+ }
+
+ const e = App.byId(`icon-label-${id}`);
+
+ if (e) {
+ if (bg) e.style.color = bg;
+ }
+
+ const query = {
+ op: "pref-labels", method: "colorset", kind: kind,
+ ids: id, fg: fg, bg: bg, color: color
+ };
+
+ xhr.post("backend.php", query, () => {
const tree = dijit.byId("filterTree");
if (tree) tree.reload(); // maybe there's labels in there
});
- }
- },
- content: __("Loading, please wait...")
- });
- const tmph = dojo.connect(dialog, 'onShow', function () {
- dojo.disconnect(tmph);
+ },
+ execute: function () {
+ if (this.validate()) {
+ const caption = this.attr('value').caption;
+ const fg_color = this.attr('value').fg_color;
+ const bg_color = this.attr('value').bg_color;
+
+ dijit.byId('labelTree').setNameById(id, caption);
+ this.setLabelColor(id, fg_color, bg_color);
+ this.hide();
+
+ xhr.post("backend.php", this.attr('value'), () => {
+ const tree = dijit.byId("filterTree");
+ if (tree) tree.reload(); // maybe there's labels in there
+ });
+ }
+ },
+ content: `
+ <form onsubmit='return false'>
+
+ <header>${__("Caption")}</header>
+ <section>
+ <input style='font-size : 16px; color : ${fg_color}; background : ${bg_color}; transition : background 0.1s linear'
+ id='labelEdit_caption'
+ name='caption'
+ dojoType='dijit.form.ValidationTextBox'
+ required='true'
+ value="${App.escapeHtml(reply.caption)}">
+ </section>
+
+ ${App.FormFields.hidden_tag('id', id)}
+ ${App.FormFields.hidden_tag('op', 'pref-labels')}
+ ${App.FormFields.hidden_tag('method', 'save')}
+
+ ${App.FormFields.hidden_tag('fg_color', fg_color, {}, 'labelEdit_fgColor')}
+ ${App.FormFields.hidden_tag('bg_color', bg_color, {}, 'labelEdit_bgColor')}
+
+ <header>${__("Colors")}</header>
+ <section>
+ <table width='100%'>
+ <tr>
+ <th>${__("Foreground:")}</th>
+ <th>${__("Background:")}</th>
+ </tr>
+ <tr>
+ <td class='text-center'>
+ <div dojoType='dijit.ColorPalette'>
+ <script type='dojo/method' event='onChange' args='fg_color'>
+ dijit.byId('labelEdit_fgColor').attr('value', fg_color);
+ dijit.byId('labelEdit_caption').domNode.setStyle({color: fg_color});
+ </script>
+ </div>
+ </td>
+ <td class='text-center'>
+ <div dojoType='dijit.ColorPalette'>
+ <script type='dojo/method' event='onChange' args='bg_color'>
+ dijit.byId('labelEdit_bgColor').attr('value', bg_color);
+ dijit.byId('labelEdit_caption').domNode.setStyle({backgroundColor: bg_color});
+ </script>
+ </div>
+ </td>
+ </tr>
+ </table>
+ </section>
+
+ <footer>
+ <button dojoType='dijit.form.Button' type='submit' class='alt-primary' onclick='App.dialogOf(this).execute()'>
+ ${__('Save')}
+ </button>
+ <button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'>
+ ${__('Cancel')}
+ </button>
+ </footer>
+
+ </form>
+ `
+ });
- xhrPost("backend.php", {op: "pref-labels", method: "edit", id: id}, (transport) => {
- dialog.attr('content', transport.responseText);
- })
- });
+ dialog.show();
- dialog.show();
+ });
},
resetColors: function() {
const labels = this.getSelectedLabels();
@@ -137,7 +194,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
ids: labels.toString()
};
- xhrPost("backend.php", query, () => {
+ xhr.post("backend.php", query, () => {
this.reload();
});
}
@@ -158,7 +215,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f
ids: sel_rows.toString()
};
- xhrPost("backend.php", query, () => {
+ xhr.post("backend.php", query, () => {
this.reload();
});
}
diff --git a/js/PrefUsers.js b/js/PrefUsers.js
index 0a7e635fe..3eb83b02a 100644
--- a/js/PrefUsers.js
+++ b/js/PrefUsers.js
@@ -1,15 +1,15 @@
'use strict'
/* global __ */
-/* global xhrPost, dojo, dijit, Notify, Tables, fox */
+/* global xhrPost, xhr, dijit, Notify, Tables, App, fox */
const Users = {
reload: function(sort) {
- const user_search = $("user_search");
+ const user_search = App.byId("user_search");
const search = user_search ? user_search.value : "";
- xhrPost("backend.php", { op: "pref-users", sort: sort, search: search }, (transport) => {
- dijit.byId('usersTab').attr('content', transport.responseText);
+ xhr.post("backend.php", { op: "pref-users", sort: sort, search: search }, (reply) => {
+ dijit.byId('usersTab').attr('content', reply);
Notify.close();
});
},
@@ -19,15 +19,18 @@ const Users = {
if (login) {
Notify.progress("Adding user...");
- xhrPost("backend.php", {op: "pref-users", method: "add", login: login}, (transport) => {
- alert(transport.responseText);
+ xhr.post("backend.php", {op: "pref-users", method: "add", login: login}, (reply) => {
+ alert(reply);
Users.reload();
});
}
},
edit: function(id) {
- xhrPost('backend.php', {op: 'pref-users', method: 'edit', id: id}, (transport) => {
+ xhr.json('backend.php', {op: 'pref-users', method: 'edit', id: id}, (reply) => {
+ const user = reply.user;
+ const admin_disabled = (user.id == 1);
+
const dialog = new fox.SingleUseDialog({
id: "userEditDlg",
title: __("User Editor"),
@@ -35,13 +38,86 @@ const Users = {
if (this.validate()) {
Notify.progress("Saving data...", true);
- xhrPost("backend.php", dojo.formToObject("user_edit_form"), (/* transport */) => {
+ xhr.post("backend.php", this.attr('value'), () => {
dialog.hide();
Users.reload();
});
}
},
- content: transport.responseText
+ content: `
+ <form onsubmit='return false'>
+
+ ${App.FormFields.hidden_tag('id', user.id.toString())}
+ ${App.FormFields.hidden_tag('op', 'pref-users')}
+ ${App.FormFields.hidden_tag('method', 'editSave')}
+
+ <div dojoType="dijit.layout.TabContainer" style="height : 400px">
+ <div dojoType="dijit.layout.ContentPane" title="${__('Edit user')}">
+
+ <header>${__("User")}</header>
+
+ <section>
+ <fieldset>
+ <label>${__("Login:")}</label>
+ <input style='font-size : 16px'
+ ${admin_disabled ? "disabled='1'" : ''}
+ dojoType='dijit.form.ValidationTextBox' required='1'
+ name='login' value="${App.escapeHtml(user.login)}">
+
+ ${admin_disabled ? App.FormFields.hidden_tag("login", user.login) : ''}
+ </fieldset>
+ </section>
+
+ <header>${__("Authentication")}</header>
+
+ <section>
+ <fieldset>
+ <label>${__('Access level: ')}</label>
+ ${App.FormFields.select_hash("access_level",
+ user.access_level, reply.access_level_names, {disabled: admin_disabled.toString()})}
+
+ ${admin_disabled ? App.FormFields.hidden_tag("access_level",
+ user.access_level.toString()) : ''}
+ </fieldset>
+ <fieldset>
+ <label>${__("New password:")}</label>
+ <input dojoType='dijit.form.TextBox' type='password' size='20'
+ placeholder='${__("Change password")}' name='password'>
+ </fieldset>
+ </section>
+
+ <header>${__("Options")}</header>
+
+ <section>
+ <fieldset>
+ <label>${__("E-mail:")}</label>
+ <input dojoType='dijit.form.TextBox' size='30' name='email'
+ value="${App.escapeHtml(user.email)}">
+ </fieldset>
+ </section>
+ </div>
+ <div dojoType="dijit.layout.ContentPane" title="${__('User details')}">
+ <script type='dojo/method' event='onShow' args='evt'>
+ if (this.domNode.querySelector('.loading')) {
+ xhr.post("backend.php", {op: 'pref-users', method: 'userdetails', id: ${user.id}}, (reply) => {
+ this.attr('content', reply);
+ });
+ }
+ </script>
+ <span class='loading'>${__("Loading, please wait...")}</span>
+ </div>
+ </div>
+
+ <footer>
+ <button dojoType='dijit.form.Button' class='alt-primary' type='submit' onclick='App.dialogOf(this).execute()'>
+ ${__('Save')}
+ </button>
+ <button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'>
+ ${__('Cancel')}
+ </button>
+ </footer>
+ </form>
+ `
});
dialog.show();
@@ -65,9 +141,9 @@ const Users = {
const id = rows[0];
- xhrPost("backend.php", {op: "pref-users", method: "resetPass", id: id}, (transport) => {
+ xhr.post("backend.php", {op: "pref-users", method: "resetPass", id: id}, (reply) => {
Notify.close();
- Notify.info(transport.responseText, true);
+ Notify.info(reply, true);
});
}
@@ -84,7 +160,7 @@ const Users = {
ids: sel_rows.toString()
};
- xhrPost("backend.php", query, () => {
+ xhr.post("backend.php", query, () => {
this.reload();
});
}
@@ -93,21 +169,6 @@ const Users = {
alert(__("No users selected."));
}
},
- editSelected: function() {
- const rows = this.getSelection();
-
- if (rows.length == 0) {
- alert(__("No users selected."));
- return;
- }
-
- if (rows.length > 1) {
- alert(__("Please select one user."));
- return;
- }
-
- this.edit(rows[0]);
- },
getSelection :function() {
return Tables.getSelected("users-list");
}
diff --git a/js/SingleUseDialog.js b/js/SingleUseDialog.js
index 944f24c6f..2de6f83ff 100644
--- a/js/SingleUseDialog.js
+++ b/js/SingleUseDialog.js
@@ -1,6 +1,17 @@
+/* eslint-disable prefer-rest-params */
/* global dijit, define */
define(["dojo/_base/declare", "dijit/Dialog"], function (declare) {
return declare("fox.SingleUseDialog", dijit.Dialog, {
+ create: function(params) {
+ const extant = dijit.byId(params.id);
+
+ if (extant) {
+ console.warn('SingleUseDialog: destroying existing widget:', params.id, '=', extant)
+ extant.destroyRecursive();
+ }
+
+ return this.inherited(arguments);
+ },
onHide: function() {
this.destroyRecursive();
}
diff --git a/js/common.js b/js/common.js
index fb5cc6531..670ee1b30 100755
--- a/js/common.js
+++ b/js/common.js
@@ -1,60 +1,225 @@
'use strict';
-/* global dijit, __, App, Ajax */
+/* global dijit, __, App, dojo, __csrf_token */
/* eslint-disable no-new */
-/* error reporting shim */
-// TODO: deprecated; remove
-/* function exception_error(e, e_compat, filename, lineno, colno) {
- if (typeof e == "string")
- e = e_compat;
+/* exported $ */
+function $(id) {
+ console.warn("FIXME: please use App.byId() or document.getElementById() instead of $():", id);
+ return document.getElementById(id);
+}
- App.Error.report(e, {filename: filename, lineno: lineno, colno: colno});
-} */
+/* exported $$ */
+function $$(query) {
+ console.warn("FIXME: please use App.findAll() or document.querySelectorAll() instead of $$():", query);
+ return document.querySelectorAll(query);
+}
-/* xhr shorthand helpers */
-/* exported xhrPost */
-function xhrPost(url, params, complete) {
- console.log("xhrPost:", params);
+Element.prototype.hasClassName = function(className) {
+ return this.classList.contains(className);
+};
- return new Promise((resolve, reject) => {
- new Ajax.Request(url, {
- parameters: params,
- onComplete: function(reply) {
- if (complete != undefined) complete(reply);
+Element.prototype.addClassName = function(className) {
+ return this.classList.add(className);
+};
- resolve(reply);
- }
- });
+Element.prototype.removeClassName = function(className) {
+ return this.classList.remove(className);
+};
+
+Element.prototype.toggleClassName = function(className) {
+ if (this.hasClassName(className))
+ return this.removeClassName(className);
+ else
+ return this.addClassName(className);
+};
+
+
+Element.prototype.setStyle = function(args) {
+ Object.keys(args).forEach((k) => {
+ this.style[k] = args[k];
});
+};
+
+Element.prototype.show = function() {
+ this.style.display = "";
+};
+
+Element.prototype.hide = function() {
+ this.style.display = "none";
+};
+
+Element.prototype.toggle = function() {
+ if (this.visible())
+ this.hide();
+ else
+ this.show();
+};
+
+// https://gist.github.com/alirezas/c4f9f43e9fe1abba9a4824dd6fc60a55
+Element.prototype.fadeOut = function() {
+ this.style.opacity = 1;
+ const self = this;
+
+ (function fade() {
+ if ((self.style.opacity -= 0.1) < 0) {
+ self.style.display = "none";
+ } else {
+ requestAnimationFrame(fade);
+ }
+ }());
+};
+
+Element.prototype.fadeIn = function(display = undefined){
+ this.style.opacity = 0;
+ this.style.display = display == undefined ? "block" : display;
+ const self = this;
+
+ (function fade() {
+ let val = parseFloat(self.style.opacity);
+ if (!((val += 0.1) > 1)) {
+ self.style.opacity = val;
+ requestAnimationFrame(fade);
+ }
+ }());
+};
+
+Element.prototype.visible = function() {
+ return this.style.display != "none" && this.offsetHeight != 0 && this.offsetWidth != 0;
}
-/* exported xhrJson */
-function xhrJson(url, params, complete) {
- return new Promise((resolve, reject) =>
- xhrPost(url, params).then((reply) => {
- let obj = null;
-
- try {
- obj = JSON.parse(reply.responseText);
- } catch (e) {
- console.error("xhrJson", e, reply);
- }
+Element.visible = function(elem) {
+ if (typeof elem == "string")
+ elem = document.getElementById(elem);
+
+ return elem.visible();
+}
+
+Element.show = function(elem) {
+ if (typeof elem == "string")
+ elem = document.getElementById(elem);
+
+ return elem.show();
+}
- if (complete != undefined) complete(obj);
+Element.hide = function(elem) {
+ if (typeof elem == "string")
+ elem = document.getElementById(elem);
- resolve(obj);
- }));
+ return elem.hide();
+}
+
+Element.toggle = function(elem) {
+ if (typeof elem == "string")
+ elem = document.getElementById(elem);
+
+ return elem.toggle();
+}
+
+Element.hasClassName = function (elem, className) {
+ if (typeof elem == "string")
+ elem = document.getElementById(elem);
+
+ return elem.hasClassName(className);
}
-/* add method to remove element from array */
Array.prototype.remove = function(s) {
for (let i=0; i < this.length; i++) {
if (s == this[i]) this.splice(i, 1);
}
};
+Array.prototype.uniq = function() {
+ return this.filter((v, i, a) => a.indexOf(v) === i);
+};
+
+String.prototype.stripTags = function() {
+ return this.replace(/<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?(\/)?>|<\/\w+>/gi, '');
+}
+
+/* exported xhr */
+const xhr = {
+ post: function(url, params = {}, complete = undefined) {
+ console.log('xhr.post', '>>>', params);
+
+ return new Promise((resolve, reject) => {
+ if (typeof __csrf_token != "undefined")
+ params = {...params, ...{csrf_token: __csrf_token}};
+
+ dojo.xhrPost({url: url,
+ postData: dojo.objectToQuery(params),
+ handleAs: "text",
+ error: function(error) {
+ reject(error);
+ },
+ load: function(data, ioargs) {
+ console.log('xhr.post', '<<<', ioargs.xhr);
+
+ if (complete != undefined)
+ complete(data, ioargs.xhr);
+
+ resolve(data)
+ }}
+ );
+ });
+ },
+ json: function(url, params = {}, complete = undefined) {
+ return new Promise((resolve, reject) =>
+ this.post(url, params).then((data) => {
+ let obj = null;
+
+ try {
+ obj = JSON.parse(data);
+ } catch (e) {
+ console.error("xhr.json", e, xhr);
+ reject(e);
+ }
+
+ console.log('xhr.json', '<<<', obj);
+
+ if (obj && typeof App != "undefined")
+ if (!App.handleRpcJson(obj)) {
+ reject(obj);
+ return;
+ }
+
+ if (complete != undefined) complete(obj);
+
+ resolve(obj);
+ }
+ ));
+ }
+};
+
+/* exported xhrPost */
+function xhrPost(url, params = {}, complete = undefined) {
+ console.log("xhrPost:", params);
+
+ return new Promise((resolve, reject) => {
+ if (typeof __csrf_token != "undefined")
+ params = {...params, ...{csrf_token: __csrf_token}};
+
+ dojo.xhrPost({url: url,
+ postData: dojo.objectToQuery(params),
+ handleAs: "text",
+ error: function(error) {
+ reject(error);
+ },
+ load: function(data, ioargs) {
+ if (complete != undefined)
+ complete(ioargs.xhr);
+
+ resolve(ioargs.xhr)
+ }});
+ });
+}
+
+/* exported xhrJson */
+function xhrJson(url, params = {}, complete = undefined) {
+ return xhr.json(url, params, complete);
+}
+
/* common helpers not worthy of separate Dojo modules */
/* exported Lists */
@@ -64,14 +229,14 @@ const Lists = {
// account for dojo checkboxes
elem = elem.domNode || elem;
- const row = elem.up("li");
+ const row = elem.closest("li");
if (row)
checked ? row.addClassName("Selected") : row.removeClassName("Selected");
},
select: function(elemId, selected) {
- $(elemId).select("li").each((row) => {
- const checkNode = row.select(".dijitCheckBox,input[type=checkbox]")[0];
+ $(elemId).querySelectorAll("li").forEach((row) => {
+ const checkNode = row.querySelector(".dijitCheckBox,input[type=checkbox]");
if (checkNode) {
const widget = dijit.getEnclosingWidget(checkNode);
@@ -94,15 +259,15 @@ const Tables = {
const checked = elem.domNode ? elem.attr("checked") : elem.checked;
elem = elem.domNode || elem;
- const row = elem.up("tr");
+ const row = elem.closest("tr");
if (row)
checked ? row.addClassName("Selected") : row.removeClassName("Selected");
},
select: function(elemId, selected) {
- $(elemId).select("tr").each((row) => {
- const checkNode = row.select(".dijitCheckBox,input[type=checkbox]")[0];
+ $(elemId).querySelectorAll("tr").forEach((row) => {
+ const checkNode = row.querySelector(".dijitCheckBox,input[type=checkbox]");
if (checkNode) {
const widget = dijit.getEnclosingWidget(checkNode);
@@ -119,7 +284,7 @@ const Tables = {
getSelected: function(elemId) {
const rv = [];
- $(elemId).select("tr").each((row) => {
+ $(elemId).querySelectorAll("tr").forEach((row) => {
if (row.hasClassName("Selected")) {
// either older prefix-XXX notation or separate attribute
const rowId = row.getAttribute("data-row-id") || row.id.replace(/^[A-Z]*?-/, "");
@@ -173,7 +338,7 @@ const Notify = {
kind = kind || this.KIND_GENERIC;
keep = keep || false;
- const notify = $("notify");
+ const notify = App.byId("notify");
window.clearTimeout(this.timeout);
@@ -238,25 +403,3 @@ const Notify = {
}
};
-// http://stackoverflow.com/questions/6251937/how-to-get-selecteduser-highlighted-text-in-contenteditable-element-and-replac
-/* exported getSelectionText */
-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();
-}
diff --git a/js/form/ValidationMultiSelect.js b/js/form/ValidationMultiSelect.js
new file mode 100644
index 000000000..4e7263c61
--- /dev/null
+++ b/js/form/ValidationMultiSelect.js
@@ -0,0 +1,20 @@
+/* global define */
+
+// only supports required for the time being
+// TODO: maybe show dojo native error message? i dunno
+define(["dojo/_base/declare", "dojo/_base/lang", "dijit/form/MultiSelect", ],
+ function(declare, lang, MultiSelect) {
+
+ return declare('fox.form.ValidationMultiSelect', [MultiSelect], {
+ constructor: function(params){
+ this.constraints = {};
+ this.baseClass += ' dijitValidationMultiSelect';
+ },
+ validate: function(/*Boolean*/ isFocused){
+ if (this.required && this.attr('value').length == 0)
+ return false;
+
+ return true;
+ },
+ })
+ });
diff --git a/js/prefs.js b/js/prefs.js
index 803a7edf3..8f4f45700 100755
--- a/js/prefs.js
+++ b/js/prefs.js
@@ -41,6 +41,7 @@ require(["dojo/_base/kernel",
"dojo/data/ItemFileWriteStore",
"lib/CheckBoxStoreModel",
"lib/CheckBoxTree",
+ "fox/PluginHost",
"fox/CommonDialogs",
"fox/CommonFilters",
"fox/PrefUsers",
@@ -52,6 +53,7 @@ require(["dojo/_base/kernel",
"fox/PrefLabelTree",
"fox/Toolbar",
"fox/SingleUseDialog",
+ "fox/form/ValidationMultiSelect",
"fox/form/ValidationTextArea",
"fox/form/Select",
"fox/form/ComboButton",
diff --git a/js/tt-rss.js b/js/tt-rss.js
index 764667a0d..4a7f2e643 100644
--- a/js/tt-rss.js
+++ b/js/tt-rss.js
@@ -1,6 +1,6 @@
'use strict'
-/* global require, App, $H */
+/* global require, App, dojo */
/* exported Plugins */
const Plugins = {};
@@ -51,6 +51,7 @@ require(["dojo/_base/kernel",
"fox/FeedTree",
"fox/Toolbar",
"fox/SingleUseDialog",
+ "fox/form/ValidationMultiSelect",
"fox/form/ValidationTextArea",
"fox/form/Select",
"fox/form/ComboButton",
@@ -70,13 +71,13 @@ require(["dojo/_base/kernel",
/* exported hash_get */
function hash_get(key) {
- const kv = window.location.hash.substring(1).toQueryParams();
- return kv[key];
+ const obj = dojo.queryToObject(window.location.hash.substring(1));
+ return obj[key];
}
/* exported hash_set */
function hash_set(key, value) {
- const kv = window.location.hash.substring(1).toQueryParams();
- kv[key] = value;
- window.location.hash = $H(kv).toQueryString();
+ const obj = dojo.queryToObject(window.location.hash.substring(1));
+ obj[key] = value;
+ window.location.hash = dojo.objectToQuery(obj);
}
diff --git a/js/utility.js b/js/utility.js
index eef1c6b61..43ad5644e 100644
--- a/js/utility.js
+++ b/js/utility.js
@@ -2,7 +2,7 @@
/* TODO: this should probably be something like night_mode.js since it does nothing specific to utility scripts */
-Event.observe(window, "load", function() {
+window.addEventListener("load", function() {
const UtilityJS = {
apply_night_mode: function (is_night, link) {
console.log("night mode changed to", is_night);
@@ -16,10 +16,10 @@ Event.observe(window, "load", function() {
setup_night_mode: function() {
const mql = window.matchMedia('(prefers-color-scheme: dark)');
- const link = new Element("link", {
- rel: "stylesheet",
- id: "theme_auto_css"
- });
+ const link = document.createElement("link");
+
+ link.rel = "stylesheet";
+ link.id = "theme_auto_css";
link.onload = function() {
document.querySelector("body").removeClassName("css_loading");