diff options
Diffstat (limited to 'js')
-rw-r--r-- | js/App.js | 592 | ||||
-rw-r--r-- | js/Article.js | 182 | ||||
-rw-r--r-- | js/CommonDialogs.js | 633 | ||||
-rw-r--r-- | js/CommonFilters.js | 795 | ||||
-rwxr-xr-x | js/FeedTree.js | 6 | ||||
-rw-r--r-- | js/Feeds.js | 150 | ||||
-rwxr-xr-x | js/Headlines.js | 363 | ||||
-rw-r--r-- | js/PrefFeedTree.js | 310 | ||||
-rw-r--r-- | js/PrefFilterTree.js | 33 | ||||
-rw-r--r-- | js/PrefHelpers.js | 425 | ||||
-rw-r--r-- | js/PrefLabelTree.js | 185 | ||||
-rw-r--r-- | js/PrefUsers.js | 115 | ||||
-rw-r--r-- | js/SingleUseDialog.js | 11 | ||||
-rwxr-xr-x | js/common.js | 273 | ||||
-rw-r--r-- | js/form/ValidationMultiSelect.js | 20 | ||||
-rwxr-xr-x | js/prefs.js | 2 | ||||
-rw-r--r-- | js/tt-rss.js | 13 | ||||
-rw-r--r-- | js/utility.js | 10 |
18 files changed, 2646 insertions, 1472 deletions
@@ -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 = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''' - }; - - return text.replace(/[&<>"']/g, function(m) { return map[m]; }); + escapeHtml: function(p) { + if (typeof p == "string") { + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + + 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 += " "; // 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)} <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(/&key=.*$/, - "&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(/&key=.*$/, + "&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'> + <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'> - <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"); |