diff options
Diffstat (limited to 'js')
| -rw-r--r-- | js/App.js | 81 | ||||
| -rw-r--r-- | js/Article.js | 17 | ||||
| -rw-r--r-- | js/CommonDialogs.js | 55 | ||||
| -rw-r--r-- | js/CommonFilters.js | 20 | ||||
| -rw-r--r-- | js/Feeds.js | 36 | ||||
| -rwxr-xr-x | js/Headlines.js | 18 | ||||
| -rw-r--r-- | js/PrefFeedTree.js | 4 | ||||
| -rw-r--r-- | js/PrefHelpers.js | 589 | ||||
| -rw-r--r-- | js/PrefLabelTree.js | 9 | ||||
| -rw-r--r-- | js/PrefUsers.js | 46 | ||||
| -rwxr-xr-x | js/common.js | 83 | ||||
| -rw-r--r-- | js/tt-rss.js | 12 |
12 files changed, 758 insertions, 212 deletions
@@ -18,6 +18,15 @@ const App = { is_prefs: false, LABEL_BASE_INDEX: -1024, _translations: {}, + Hash: { + get: function() { + return dojo.queryToObject(window.location.hash.substring(1)); + }, + set: function(params) { + const obj = dojo.queryToObject(window.location.hash.substring(1)); + window.location.hash = dojo.objectToQuery({...obj, ...params}); + } + }, l10n: { ngettext: function(msg1, msg2, n) { return self.__((parseInt(n) > 1) ? msg2 : msg1); @@ -52,8 +61,9 @@ const App = { return this.button_tag(value, "", {...{onclick: "App.dialogOf(this).hide()"}, ...attributes}); }, checkbox_tag: function(name, checked = false, value = "", attributes = {}, id = "") { + // checked !== '0' prevents mysql "boolean" false to be implicitly cast as true return `<input dojoType="dijit.form.CheckBox" type="checkbox" name="${App.escapeHtml(name)}" - ${checked ? "checked" : ""} + ${checked !== '0' && checked ? "checked" : ""} ${value ? `value="${App.escapeHtml(value)}"` : ""} ${this.attributes_to_string(attributes)} id="${App.escapeHtml(id)}">` }, @@ -418,7 +428,7 @@ const App = { if (error && error.code && error.code != App.Error.E_SUCCESS) { console.warn("handleRpcJson: fatal error", error); - this.Error.fatal(error.code); + this.Error.fatal(error.code, error.params); return false; } @@ -547,6 +557,7 @@ const App = { E_SUCCESS: "E_SUCCESS", E_UNAUTHORIZED: "E_UNAUTHORIZED", E_SCHEMA_MISMATCH: "E_SCHEMA_MISMATCH", + E_URL_SCHEME_MISMATCH: "E_URL_SCHEME_MISMATCH", fatal: function (error, params = {}) { if (error == App.Error.E_UNAUTHORIZED) { window.location.href = "index.php"; @@ -554,9 +565,14 @@ const App = { } else if (error == App.Error.E_SCHEMA_MISMATCH) { window.location.href = "public.php?op=dbupdate"; return; + } else if (error == App.Error.E_URL_SCHEME_MISMATCH) { + params.description = __("URL scheme reported by your browser (%a) doesn't match server-configured SELF_URL_PATH (%b), check X-Forwarded-Proto.") + .replace("%a", params.client_scheme) + .replace("%b", params.server_scheme); + params.info = `SELF_URL_PATH: ${params.self_url_path}\nCLIENT_LOCATION: ${document.location.href}` } - return this.report(__("Fatal error: %s").replace("%s", error), + return this.report(error, {...{title: __("Fatal error")}, ...params}); }, report: function(error, params = {}) { @@ -587,10 +603,13 @@ const App = { <div class='exception-contents'> <h3>${message}</h3> - <header>${__('Stack trace')}</header> + ${params.description ? `<p>${params.description}</p>` : ''} + + ${error.stack ? + `<header>${__('Stack trace')}</header> <section> <textarea readonly='readonly'>${error.stack}</textarea> - </section> + </section>` : ''} ${params && params.info ? ` @@ -650,7 +669,8 @@ const App = { op: "rpc", method: "sanityCheck", clientTzOffset: new Date().getTimezoneOffset() * 60, - hasSandbox: "sandbox" in document.createElement("iframe") + hasSandbox: "sandbox" in document.createElement("iframe"), + clientLocation: window.location.href }; xhr.json("backend.php", params, (reply) => { @@ -757,18 +777,15 @@ const App = { } }); - const toolbar = document.forms["toolbar-main"]; - - dijit.getEnclosingWidget(toolbar.view_mode).attr('value', - this.getInitParam("default_view_mode")); - - dijit.getEnclosingWidget(toolbar.order_by).attr('value', - this.getInitParam("default_view_order_by")); + dijit.byId('toolbar-main').setValues({ + view_mode: this.getInitParam("default_view_mode"), + order_by: this.getInitParam("default_view_order_by") + }); this.setLoadingProgress(50); this._widescreen_mode = this.getInitParam("widescreen"); - this.switchPanelMode(this._widescreen_mode); + this.setWidescreen(this._widescreen_mode); Headlines.initScrollHandler(); @@ -801,10 +818,23 @@ const App = { .then((reply) => { console.log('update reply', reply); - if (reply.id) { - App.byId("updates-available").show(); + const icon = App.byId("updates-available"); + + if (reply.changeset.id || reply.plugins.length > 0) { + icon.show(); + + const tips = []; + + if (reply.changeset.id) + tips.push(__("Updates for Tiny Tiny RSS are available.")); + + if (reply.plugins.length > 0) + tips.push(__("Updates for some local plugins are available.")); + + icon.setAttribute("title", tips.join("\n")); + } else { - App.byId("updates-available").hide(); + icon.hide(); } }); }, @@ -817,13 +847,6 @@ const App = { document.title = tmp; }, - onViewModeChanged: function() { - const view_mode = document.forms["toolbar-main"].view_mode.value; - - App.findAll("body")[0].setAttribute("view-mode", view_mode); - - return Feeds.reloadCurrent(''); - }, hotkeyHandler: function(event) { if (event.target.nodeName == "INPUT" || event.target.nodeName == "TEXTAREA") return; @@ -843,7 +866,7 @@ const App = { } } }, - switchPanelMode: function(wide) { + setWidescreen: function(wide) { const article_id = Article.getActive(); if (wide) { @@ -884,7 +907,7 @@ const App = { if (article_id) Article.view(article_id); - xhr.post("backend.php", {op: "rpc", method: "setpanelmode", wide: wide ? 1 : 0}); + xhr.post("backend.php", {op: "rpc", method: "setWidescreen", wide: wide ? 1 : 0}); }, initHotkeyActions: function() { if (this.is_prefs) { @@ -1144,7 +1167,7 @@ const App = { Cookie.set("ttrss_ci_width", 0); Cookie.set("ttrss_ci_height", 0); - this.switchPanelMode(this._widescreen_mode); + this.setWidescreen(this._widescreen_mode); } else { alert(__("Widescreen is not available in combined mode.")); } @@ -1234,7 +1257,7 @@ const App = { Cookie.set("ttrss_ci_width", 0); Cookie.set("ttrss_ci_height", 0); - this.switchPanelMode(this._widescreen_mode); + this.setWidescreen(this._widescreen_mode); } else { alert(__("Widescreen is not available in combined mode.")); } @@ -1245,6 +1268,6 @@ const App = { default: console.log("quickMenuGo: unknown action: " + opid); } - } + }, } diff --git a/js/Article.js b/js/Article.js index 5f695561c..ed74051a6 100644 --- a/js/Article.js +++ b/js/Article.js @@ -144,10 +144,15 @@ const Article = { ).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>`; + return `<span class="labels" data-labels-for="${id}"> + ${labels.map((label) => ` + <a href="#" class="label" data-label-id="${label[0]}" + style="color : ${label[2]}; background-color : ${label[3]}" + onclick="event.stopPropagation(); Feeds.open({feed:'${label[0]}'})"> + ${App.escapeHtml(label[1])} + </a>` + ).join("")} + </span>`; }, renderEnclosures: function (enclosures) { return ` @@ -317,7 +322,7 @@ const Article = { }, editTags: function (id) { const dialog = new fox.SingleUseDialog({ - title: __("Edit article Tags"), + title: __("Article tags"), content: ` ${App.FormFields.hidden_tag("id", id.toString())} ${App.FormFields.hidden_tag("op", "article")} @@ -329,7 +334,7 @@ const Article = { <section> <textarea dojoType='dijit.form.SimpleTextarea' rows='4' disabled='true' - id='tags_str' name='tags_str'></textarea> + id='tags_str' name='tags_str'>${__("Loading, please wait...")}</textarea> <div class='autocomplete' id='tags_choices' style='display:none'></div> </section> diff --git a/js/CommonDialogs.js b/js/CommonDialogs.js index 321ddf6d3..ab8441cac 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, xhr, Tables, fox */ +/* global __, dojo, dijit, Notify, App, Feeds, xhr, Tables, fox */ /* exported CommonDialogs */ const CommonDialogs = { @@ -16,7 +16,7 @@ const CommonDialogs = { {op: "feeds", method: "subscribeToFeed"}, (reply) => { const dialog = new fox.SingleUseDialog({ - title: __("Subscribe to Feed"), + title: __("Subscribe to feed"), content: ` <form onsubmit='return false'> @@ -181,7 +181,7 @@ const CommonDialogs = { } } catch (e) { - console.error(transport.responseText); + console.error(reply); App.Error.report(e); } }); @@ -248,7 +248,7 @@ const CommonDialogs = { ${reply.map((row) => ` <tr data-row-id='${row.id}'> - <td width='5%' align='center'> + <td class='checkbox'> <input onclick='Tables.onRowChecked(this)' dojoType="dijit.form.CheckBox" type="checkbox"> </td> @@ -333,8 +333,12 @@ const CommonDialogs = { const dialog = new fox.SingleUseDialog({ id: "feedEditDlg", - title: __("Edit Feed"), + title: __("Edit feed"), feed_title: "", + E_ICON_FILE_TOO_LARGE: 'E_ICON_FILE_TOO_LARGE', + E_ICON_RENAME_FAILED: 'E_ICON_RENAME_FAILED', + E_ICON_UPLOAD_FAILED: 'E_ICON_UPLOAD_FAILED', + E_ICON_UPLOAD_SUCCESS: 'E_ICON_UPLOAD_SUCCESS', unsubscribe: function() { if (confirm(__("Unsubscribe from %s?").replace("%s", this.feed_title))) { dialog.hide(); @@ -361,20 +365,18 @@ const CommonDialogs = { xhr.open( 'POST', 'backend.php', true ); xhr.onload = function () { - console.log(this.responseText); + const ret = JSON.parse(this.responseText); // TODO: make a notice box within panel content - switch (parseInt(this.responseText)) { - case 1: - Notify.error("Upload failed: icon is too big."); + switch (ret.rc) { + case dialog.E_ICON_FILE_TOO_LARGE: + alert(__("Icon file is too large.")); break; - case 2: - Notify.error("Upload failed."); + case dialog.E_ICON_UPLOAD_FAILED: + alert(__("Upload failed.")); break; - default: + case dialog.E_ICON_UPLOAD_SUCCESS: { - Notify.info("Upload complete."); - if (App.isPrefs()) dijit.byId("feedTree").reload(); else @@ -383,12 +385,16 @@ const CommonDialogs = { const icon = dialog.domNode.querySelector(".feedIcon"); if (icon) { - icon.src = this.responseText; + icon.src = ret.icon_url; icon.show(); } input.value = ""; } + break; + default: + alert(this.responseText); + break; } }; @@ -400,9 +406,7 @@ const CommonDialogs = { 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, () => { + xhr.post("backend.php", {op: "pref-feeds", method: "removeicon", feed_id: id}, () => { Notify.info("Feed icon removed."); if (App.isPrefs()) @@ -473,8 +477,8 @@ const CommonDialogs = { <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)}"> + placeHolder="${__("Feed title")}" + style='font-size : 16px; width: 530px' name='title' value="${App.escapeHtml(feed.title)}"> </fieldset> <fieldset> @@ -565,19 +569,21 @@ const CommonDialogs = { <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...")} + <label class="dijitButton"> + ${App.FormFields.icon("file_upload")} + ${__("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+")"})} + ${App.FormFields.submit_tag(App.FormFields.icon("delete") + " " + __("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.button_tag(App.FormFields.icon("delete") + " " + __("Unsubscribe"), "", {class: "pull-left alt-danger", onclick: "App.dialogOf(this).unsubscribe()"})} + ${App.FormFields.submit_tag(App.FormFields.icon("save") + " " + __("Save"), {onclick: "App.dialogOf(this).execute()"})} ${App.FormFields.cancel_dialog_tag(__("Cancel"))} </footer> </form> @@ -634,6 +640,7 @@ const CommonDialogs = { 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 App.dialogOf(this).regenFeedKey('${feed}', '${is_cat}')"> + ${App.FormFields.icon("refresh")} ${__('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 0c138760d..1450458f8 100644 --- a/js/CommonFilters.js +++ b/js/CommonFilters.js @@ -11,7 +11,7 @@ const Filters = { const dialog = new fox.SingleUseDialog({ id: "filterEditDlg", - title: filter_id ? __("Edit Filter") : __("Create Filter"), + title: filter_id ? __("Edit filter") : __("Create new filter"), ACTION_TAG: 4, ACTION_SCORE: 6, ACTION_LABEL: 7, @@ -115,7 +115,7 @@ const Filters = { const li = document.createElement('li'); li.addClassName("rule"); - li.innerHTML = `${App.FormFields.checkbox_tag("", false, {onclick: 'Lists.onRowChecked(this)'})} + 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>`; @@ -147,7 +147,7 @@ const Filters = { const li = document.createElement('li'); li.addClassName("action"); - li.innerHTML = `${App.FormFields.checkbox_tag("", false, {onclick: 'Lists.onRowChecked(this)'})} + 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>`; @@ -229,7 +229,7 @@ const Filters = { <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.submit_tag(App.FormFields.icon("save") + " " + __("Save"), {onclick: "App.dialogOf(this).execute()"})} ${App.FormFields.cancel_dialog_tag(__("Cancel"))} </footer> @@ -313,7 +313,7 @@ const Filters = { "filterDlg_actionParamPlugin")} </section> <footer> - ${App.FormFields.submit_tag(__("Save action"), {onclick: "App.dialogOf(this).execute()"})} + ${App.FormFields.submit_tag(App.FormFields.icon("save") + " " + __("Save"), {onclick: "App.dialogOf(this).execute()"})} ${App.FormFields.cancel_dialog_tag(__("Cancel"))} </footer> </form> @@ -511,13 +511,13 @@ const Filters = { <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.button_tag(App.FormFields.icon("delete") + " " + __("Remove"), "", {class: "pull-left alt-danger", onclick: "App.dialogOf(this).removeFilter()"})} + ${App.FormFields.button_tag(App.FormFields.icon("check_circle") + " " + __("Test"), "", {class: "alt-info", onclick: "App.dialogOf(this).test()"})} + ${App.FormFields.submit_tag(App.FormFields.icon("save") + " " + __("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.button_tag(App.FormFields.icon("check_circle") + " " + __("Test"), "", {class: "alt-info", onclick: "App.dialogOf(this).test()"})} + ${App.FormFields.submit_tag(App.FormFields.icon("add") + " " + __("Create"), {onclick: "App.dialogOf(this).execute()"})} ${App.FormFields.cancel_dialog_tag(__("Cancel"))} `} </footer> diff --git a/js/Feeds.js b/js/Feeds.js index 5a2dee5cf..33a1fa3dc 100644 --- a/js/Feeds.js +++ b/js/Feeds.js @@ -117,15 +117,21 @@ const Feeds = { }, reloadCurrent: function(method) { if (this.getActive() != undefined) { - console.log("reloadCurrent: " + method); + console.log("reloadCurrent", this.getActive(), this.activeIsCat(), method); this.open({feed: this.getActive(), is_cat: this.activeIsCat(), method: method}); } - return false; // block unneeded form submits }, openDefaultFeed: function() { this.open({feed: this._default_feed_id}); }, + onViewModeChanged: function() { + // TODO: is this still needed? + App.find("body").setAttribute("view-mode", + dijit.byId("toolbar-main").getValues().view_mode); + + return Feeds.reloadCurrent(''); + }, openNextUnread: function() { const is_cat = this.activeIsCat(); const nuf = this.getNextUnread(this.getActive(), is_cat); @@ -236,12 +242,12 @@ const Feeds = { //document.onkeypress = (event) => { return App.hotkeyHandler(event) }; window.onresize = () => { Headlines.scrollHandler(); } - /* global hash_get */ - const hash_feed_id = hash_get('f'); - const hash_feed_is_cat = hash_get('c') == "1"; + const hash = App.Hash.get(); + + console.log('got hash', hash); - if (hash_feed_id != undefined) { - this.open({feed: hash_feed_id, is_cat: hash_feed_is_cat}); + if (hash.f != undefined) { + this.open({feed: parseInt(hash.f), is_cat: parseInt(hash.c)}); } else { this.openDefaultFeed(); } @@ -305,9 +311,12 @@ const Feeds = { setActive: function(id, is_cat) { console.log('setActive', id, is_cat); - /* global hash_set */ - hash_set('f', id); - hash_set('c', is_cat ? 1 : 0); + if ('requestIdleCallback' in window) + window.requestIdleCallback(() => { + App.Hash.set({f: id, c: is_cat ? 1 : 0}); + }); + else + App.Hash.set({f: id, c: is_cat ? 1 : 0}); this._active_feed_id = id; this._active_feed_is_cat = is_cat; @@ -366,10 +375,7 @@ const Feeds = { }, 10 * 1000); } - //Form.enable("toolbar-main"); - - let query = Object.assign({op: "feeds", method: "view", feed: feed}, - dojo.formToObject("toolbar-main")); + let query = {...{op: "feeds", method: "view", feed: feed}, ...dojo.formToObject("toolbar-main")}; if (method) query.m = method; @@ -612,7 +618,7 @@ const Feeds = { {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.submit_tag(App.FormFields.icon("search") + " " + __('Search'), {onclick: "App.dialogOf(this).execute()"})} ${App.FormFields.cancel_dialog_tag(__('Cancel'))} </footer> </form> diff --git a/js/Headlines.js b/js/Headlines.js index fd9bc6661..28e43be1f 100755 --- a/js/Headlines.js +++ b/js/Headlines.js @@ -278,7 +278,7 @@ const Headlines = { } }, loadMore: function () { - const view_mode = document.forms["toolbar-main"].view_mode.value; + const view_mode = dijit.byId("toolbar-main").getValues().view_mode; 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()); @@ -819,19 +819,15 @@ const Headlines = { Notify.close(); }, reverse: function () { - const toolbar = document.forms["toolbar-main"]; - const order_by = dijit.getEnclosingWidget(toolbar.order_by); + const toolbar = dijit.byId("toolbar-main"); + let order_by = toolbar.getValues().order_by; - let value = order_by.attr('value'); - - if (value != "date_reverse") - value = "date_reverse"; + if (order_by != "date_reverse") + order_by = "date_reverse"; else - value = "default"; - - order_by.attr('value', value); + order_by = App.getInitParam("default_view_order_by"); - Feeds.reloadCurrent(); + toolbar.setValues({order_by: order_by}); }, selectionToggleUnread: function (params = {}) { const cmode = params.cmode != undefined ? params.cmode : 2; diff --git a/js/PrefFeedTree.js b/js/PrefFeedTree.js index bb5d25e67..013c01262 100644 --- a/js/PrefFeedTree.js +++ b/js/PrefFeedTree.js @@ -300,7 +300,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b try { const dialog = new fox.SingleUseDialog({ - title: __("Edit Multiple Feeds"), + title: __("Edit multiple feeds"), /*getChildByName: function (name) { let rv = null; this.getChildren().forEach( @@ -513,7 +513,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dojo/_b <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'> + <td class='checkbox'> <input onclick='Tables.onRowChecked(this)' dojoType='dijit.form.CheckBox' type='checkbox'> </td> <td> diff --git a/js/PrefHelpers.js b/js/PrefHelpers.js index 62f6d91b1..3f738aa95 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, xhr, App, fox */ +/* global __, dijit, dojo, Tables, Notify, xhr, App, fox */ const Helpers = { AppPasswords: { @@ -19,7 +19,7 @@ const Helpers = { alert("No passwords selected."); } else if (confirm(__("Remove selected app passwords?"))) { - xhr.post("backend.php", {op: "pref-prefs", method: "deleteAppPassword", ids: rows.toString()}, (reply) => { + xhr.post("backend.php", {op: "pref-prefs", method: "deleteAppPasswords", "ids[]": rows}, (reply) => { this.updateContent(reply); Notify.close(); }); @@ -53,6 +53,33 @@ const Helpers = { return false; }, }, + Digest: { + preview: function() { + const dialog = new fox.SingleUseDialog({ + title: __("Digest preview"), + content: ` + <div class='panel panel-scrollable digest-preview'> + <div class='text-center'>${__("Loading, please wait...")}</div> + </div> + + <footer class='text-center'> + ${App.FormFields.submit_tag(__('Close this window'))} + </footer> + ` + }); + + const tmph = dojo.connect(dialog, 'onShow', function () { + dojo.disconnect(tmph); + + xhr.json("backend.php", {op: "pref-prefs", method: "previewDigest"}, (reply) => { + dialog.domNode.querySelector('.digest-preview').innerHTML = reply[0]; + }); + }); + + dialog.show(); + + } + }, System: { // }, @@ -97,7 +124,7 @@ const Helpers = { edit: function() { const dialog = new fox.SingleUseDialog({ id: "profileEditDlg", - title: __("Settings Profiles"), + title: __("Manage profiles"), getSelectedProfiles: function () { return Tables.getSelected("pref-profiles-list"); }, @@ -108,12 +135,7 @@ const Helpers = { 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, () => { + xhr.post("backend.php", {op: "pref-prefs", method: "remprofiles", "ids[]": sel_rows}, () => { Notify.close(); dialog.refresh(); }); @@ -161,7 +183,7 @@ const Helpers = { <table width='100%' id='pref-profiles-list'> ${reply.map((profile) => ` <tr data-row-id="${profile.id}"> - <td width='5%'> + <td class='checkbox'> ${App.FormFields.checkbox_tag("", false, "", {onclick: 'Tables.onRowChecked(this)'})} </td> <td> @@ -183,9 +205,9 @@ const Helpers = { </div> <footer> - ${App.FormFields.button_tag(__('Remove selected profiles'), "", + ${App.FormFields.button_tag(App.FormFields.icon("delete") + " " +__('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.submit_tag(App.FormFields.icon("check") + " " + __('Activate profile'), {onclick: 'App.dialogOf(this).execute()'})} ${App.FormFields.cancel_dialog_tag(__('Cancel'))} </footer> </form> @@ -217,58 +239,70 @@ const Helpers = { }, 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() { + 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); - 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.")} + 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> + + ${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> + + <textarea class='panel user-css-editor' disabled='true' dojoType='dijit.form.SimpleTextarea' + style='font-size : 12px;' name='value'>${__("Loading, please wait...")}</textarea> + + <footer> + <button dojoType='dijit.form.Button' class='alt-success' onclick="App.dialogOf(this).apply()"> + ${App.FormFields.icon("check")} + ${__('Apply')} + </button> + <button dojoType='dijit.form.Button' class='alt-primary' type='submit'> + ${App.FormFields.icon("refresh")} + ${__('Save and reload')} + </button> + <button dojoType='dijit.form.Button' onclick="App.dialogOf(this).hide()"> + ${__('Cancel')} + </button> + </footer> + ` + }); - ${App.FormFields.hidden_tag('op', 'rpc')} - ${App.FormFields.hidden_tag('method', 'setpref')} - ${App.FormFields.hidden_tag('key', 'USER_STYLESHEET')} + const tmph = dojo.connect(dialog, 'onShow', function () { + dojo.disconnect(tmph); - <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> + xhr.json("backend.php", {op: "pref-prefs", method: "customizeCSS"}, (reply) => { - <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> - ` - }); + const editor = dijit.getEnclosingWidget(dialog.domNode.querySelector(".user-css-editor")); - dialog.show(); + editor.attr('value', reply.value); + editor.attr('disabled', false); + }); }); + + dialog.show(); + }, confirmReset: function() { if (confirm(__("Reset to defaults?"))) { @@ -278,20 +312,448 @@ const Helpers = { }); } }, - clearPluginData: function(name) { - if (confirm(__("Clear stored data for this plugin?"))) { + refresh: function() { + xhr.post("backend.php", { op: "pref-prefs" }, (reply) => { + dijit.byId('prefsTab').attr('content', reply); + Notify.close(); + }); + }, + }, + Plugins: { + _list_of_plugins: [], + _search_query: "", + enableSelected: function() { + const form = dijit.byId("changePluginsForm"); + + if (form.validate()) { + xhr.post("backend.php", form.getValues(), () => { + Notify.close(); + if (confirm(__('Selected plugins have been enabled. Reload?'))) { + window.location.reload(); + } + }) + } + }, + search: function() { + this._search_query = dijit.byId("changePluginsForm").getValues().search; + this.render_contents(); + }, + reload: function() { + xhr.json("backend.php", {op: "pref-prefs", method: "getPluginsList"}, (reply) => { + this._list_of_plugins = reply; + this.render_contents(); + }); + }, + render_contents: function() { + const container = document.querySelector(".prefs-plugin-list"); + + container.innerHTML = ""; + let results_rendered = 0; + + const is_admin = this._list_of_plugins.is_admin; + + const search_tokens = this._search_query + .split(/ {1,}/) + .filter((stoken) => (stoken.length > 0 ? stoken : null)); + + this._list_of_plugins.plugins.forEach((plugin) => { + + if (search_tokens.length == 0 || + Object.values(plugin).filter((pval) => + search_tokens.filter((stoken) => + (pval.toString().indexOf(stoken) != -1 ? stoken : null) + ).length == search_tokens.length).length > 0) { + + ++results_rendered; + + // only user-enabled actually counts in the checkbox when saving because system plugin checkboxes are disabled (see below) + container.innerHTML += ` + <li data-row-value="${App.escapeHtml(plugin.name)}" data-plugin-local="${plugin.is_local}" data-plugin-name="${App.escapeHtml(plugin.name)}" title="${plugin.is_system ? __("System plugins are enabled using global configuration.") : ""}"> + <label class="checkbox ${plugin.is_system ? "system text-info" : ""}"> + ${App.FormFields.checkbox_tag("plugins[]", plugin.user_enabled || plugin.system_enabled, plugin.name, + {disabled: plugin.is_system})}</div> + <span class='name'>${plugin.name}:</span> + </label> + <div class="description ${plugin.is_system ? "text-info" : ""}"> + ${plugin.description} + </div> + <div class='actions'> + ${plugin.is_system ? + App.FormFields.button_tag(App.FormFields.icon("security"), "", + {disabled: true}) : ''} + ${plugin.more_info ? + App.FormFields.button_tag(App.FormFields.icon("help"), "", + {class: 'alt-info', onclick: `window.open("${App.escapeHtml(plugin.more_info)}")`}) : ''} + ${is_admin && plugin.is_local ? + App.FormFields.button_tag(App.FormFields.icon("update"), "", + {title: __("Update"), class: 'alt-warning', "data-update-btn-for-plugin": plugin.name, style: 'display : none', + onclick: `Helpers.Plugins.update("${App.escapeHtml(plugin.name)}")`}) : ''} + ${is_admin && plugin.has_data ? + App.FormFields.button_tag(App.FormFields.icon("clear"), "", + {title: __("Clear data"), onclick: `Helpers.Plugins.clearData("${App.escapeHtml(plugin.name)}")`}) : ''} + ${is_admin && plugin.is_local ? + App.FormFields.button_tag(App.FormFields.icon("delete"), "", + {title: __("Uninstall"), onclick: `Helpers.Plugins.uninstall("${App.escapeHtml(plugin.name)}")`}) : ''} + </div> + <div class='version text-muted'>${plugin.version}</div> + </li> + `; + } else { + // if plugin is outside of search scope, keep current value in case of saving (only user-enabled is needed) + container.innerHTML += App.FormFields.checkbox_tag("plugins[]", plugin.user_enabled, plugin.name, {style: 'display : none'}); + } + }); + + if (results_rendered == 0) { + container.innerHTML += `<li class='text-center text-info'>${__("Could not find any plugins for this search query.")}</li>`; + } + + dojo.parser.parse(container); + + }, + clearData: function(name) { + if (confirm(__("Clear stored data for %s?").replace("%s", name))) { Notify.progress("Loading, please wait..."); - xhr.post("backend.php", {op: "pref-prefs", method: "clearplugindata", name: name}, () => { + 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(); + uninstall: function(plugin) { + const msg = __("Uninstall plugin %s?").replace("%s", plugin); + + if (confirm(msg)) { + Notify.progress("Loading, please wait..."); + + xhr.json("backend.php", {op: "pref-prefs", method: "uninstallPlugin", plugin: plugin}, (reply) => { + if (reply && reply.status == 1) + Helpers.Prefs.refresh(); + else { + Notify.error("Plugin uninstallation failed."); + } + }); + + } + }, + install: function() { + const dialog = new fox.SingleUseDialog({ + PI_RES_ALREADY_INSTALLED: "PI_RES_ALREADY_INSTALLED", + PI_RES_SUCCESS: "PI_RES_SUCCESS", + PI_ERR_NO_CLASS: "PI_ERR_NO_CLASS", + PI_ERR_NO_INIT_PHP: "PI_ERR_NO_INIT_PHP", + PI_ERR_EXEC_FAILED: "PI_ERR_EXEC_FAILED", + PI_ERR_NO_TEMPDIR: "PI_ERR_NO_TEMPDIR", + PI_ERR_PLUGIN_NOT_FOUND: "PI_ERR_PLUGIN_NOT_FOUND", + PI_ERR_NO_WORKDIR: "PI_ERR_NO_WORKDIR", + title: __("Available plugins"), + need_refresh: false, + entries: false, + search_query: "", + installed_plugins: [], + onHide: function() { + if (this.need_refresh) { + Helpers.Prefs.refresh(); + } + }, + performInstall: function(plugin) { + + const install_dialog = new fox.SingleUseDialog({ + title: __("Plugin installer"), + content: ` + <ul class="panel panel-scrollable contents"> + <li class='text-center'>${__("Installing %s, please wait...").replace("%s", plugin)}</li> + </ul> + + <footer class='text-center'> + ${App.FormFields.submit_tag(__("Close this window"))} + </footer>` + }); + + const tmph = dojo.connect(install_dialog, 'onShow', function () { + dojo.disconnect(tmph); + + const container = install_dialog.domNode.querySelector(".contents"); + + xhr.json("backend.php", {op: "pref-prefs", method: "installPlugin", plugin: plugin}, (reply) => { + if (!reply) { + container.innerHTML = `<li class='text-center text-error'>${__("Operation failed: check event log.")}</li>`; + } else { + switch (reply.result) { + case dialog.PI_RES_SUCCESS: + container.innerHTML = `<li class='text-success text-center'>${__("Plugin has been installed.")}</li>` + dialog.need_refresh = true; + break; + case dialog.PI_RES_ALREADY_INSTALLED: + container.innerHTML = `<li class='text-success text-center'>${__("Plugin is already installed.")}</li>` + break; + default: + container.innerHTML = ` + <li> + <h3 style="margin-top: 0">${plugin}</h3> + <div class='text-error'>${reply.result}</div> + ${reply.stderr ? `<pre class="small text-error pre-wrap">${reply.stderr}</pre>` : ''} + ${reply.stdour ? `<pre class="small text-success pre-wrap">${reply.stdout}</pre>` : ''} + <p class="small"> + ${App.FormFields.icon("error_outline") + " " + __("Exited with RC: %d").replace("%d", reply.git_status)} + </p> + </li> + `; + } + } + }); + }); + + install_dialog.show(); + + }, + search: function() { + this.search_query = this.attr('value').search.toLowerCase(); + + if ('requestIdleCallback' in window) + window.requestIdleCallback(() => { + this.render_contents(); + }); + else + this.render_contents(); + }, + render_contents: function() { + const container = dialog.domNode.querySelector(".contents"); + + if (!dialog.entries) { + container.innerHTML = `<li class='text-center text-error'>${__("Operation failed: check event log.")}</li>`; + } else { + container.innerHTML = ""; + + let results_rendered = 0; + + const search_tokens = dialog.search_query + .split(/ {1,}/) + .filter((stoken) => (stoken.length > 0 ? stoken : null)); + + dialog.entries.forEach((plugin) => { + const is_installed = (dialog.installed_plugins + .filter((p) => plugin.topics.map((t) => t.replace(/-/g, "_")).includes(p))).length > 0; + + if (search_tokens.length == 0 || + Object.values(plugin).filter((pval) => + search_tokens.filter((stoken) => + (pval.indexOf(stoken) != -1 ? stoken : null) + ).length == search_tokens.length).length > 0) { + + ++results_rendered; + + container.innerHTML += ` + <li data-row-value="${App.escapeHtml(plugin.name)}" class="${is_installed ? "plugin-installed" : ""}"> + ${App.FormFields.button_tag((is_installed ? + App.FormFields.icon("check") + " " +__("Already installed") : + App.FormFields.icon("file_download") + " " +__('Install')), "", {class: 'alt-primary pull-right', + disabled: is_installed, + onclick: `App.dialogOf(this).performInstall("${App.escapeHtml(plugin.name)}")`})} + + <h3>${plugin.name} + <a target="_blank" href="${App.escapeHtml(plugin.html_url)}"> + ${App.FormFields.icon("open_in_new_window")} + </a> + </h3> + + <div class='small text-muted'>${__("Updated: %s").replace("%s", plugin.last_update)}</div> + + <div class='description'>${plugin.description}</div> + </li> + ` + } + }); + + if (results_rendered == 0) { + container.innerHTML = `<li class='text-center text-info'>${__("Could not find any plugins for this search query.")}</li>`; + } + + dojo.parser.parse(container); + } + }, + reload: function() { + const container = dialog.domNode.querySelector(".contents"); + container.innerHTML = `<li class='text-center'>${__("Looking for plugins...")}</li>`; + + xhr.json("backend.php", {op: "pref-prefs", method: "getAvailablePlugins"}, (reply) => { + dialog.entries = reply; + dialog.render_contents(); + }); + }, + content: ` + <div dojoType='fox.Toolbar'> + <div class='pull-right'> + <input name="search" placeholder="${__("Search...")}" type="search" dojoType="dijit.form.TextBox" onkeyup="App.dialogOf(this).search()"> + </div> + <div style='height : 16px'> </div> <!-- disgusting --> + </div> + + <ul style='clear : both' class="panel panel-scrollable-400px contents plugin-installer-list"> </ul> + + <footer> + ${App.FormFields.button_tag(App.FormFields.icon("refresh") + " " +__("Refresh"), "", {class: 'alt-primary', onclick: 'App.dialogOf(this).reload()'})} + ${App.FormFields.cancel_dialog_tag(__("Close"))} + </footer> + `, + }); + + const tmph = dojo.connect(dialog, 'onShow', function () { + dojo.disconnect(tmph); + + dialog.installed_plugins = [...document.querySelectorAll('*[data-plugin-name]')].map((p) => p.getAttribute('data-plugin-name')); + + dialog.reload(); + }); + + dialog.show(); + }, + update: function(name = null) { + + const dialog = new fox.SingleUseDialog({ + title: __("Update plugins"), + need_refresh: false, + plugins_to_update: [], + plugins_to_check: [], + onHide: function() { + if (this.need_refresh) { + Helpers.Prefs.refresh(); + } + }, + performUpdate: function() { + const container = dialog.domNode.querySelector(".update-results"); + + console.log('updating', dialog.plugins_to_update); + dialog.attr('title', __('Updating...')); + + container.innerHTML = `<li class='text-center'>${__("Updating, please wait...")}</li>`; + let enable_update_btn = false; + + xhr.json("backend.php", {op: "pref-prefs", method: "updateLocalPlugins", plugins: dialog.plugins_to_update.join(",")}, (reply) => { + + if (!reply) { + container.innerHTML = `<li class='text-center text-error'>${__("Operation failed: check event log.")}</li>`; + } else { + container.innerHTML = ""; + + reply.forEach((p) => { + if (p.rv.git_status == 0) + dialog.need_refresh = true; + else + enable_update_btn = true; + + container.innerHTML += + ` + <li> + <h3>${p.plugin}</h3> + ${p.rv.stderr ? `<pre class="small text-error pre-wrap">${p.rv.stderr}</pre>` : ''} + ${p.rv.stdout ? `<pre class="small text-success pre-wrap">${p.rv.stdout}</pre>` : ''} + <div class="small"> + ${p.rv.git_status ? App.FormFields.icon("error_outline") + " " + __("Exited with RC: %d").replace("%d", p.rv.git_status) : + App.FormFields.icon("check") + " " + __("Update done.")} + </div> + </li> + ` + }); + } + + dialog.attr('title', __('Updates complete')); + dijit.getEnclosingWidget(dialog.domNode.querySelector(".update-btn")).attr('disabled', !enable_update_btn); + }); + }, + checkNextPlugin: function() { + const name = dialog.plugins_to_check.shift(); + + if (name) { + this.checkUpdates(name); + } else { + const num_updated = dialog.plugins_to_update.length; + + if (num_updated > 0) + dialog.attr('title', + App.l10n.ngettext('Updates pending for %d plugin', 'Updates pending for %d plugins', num_updated) + .replace("%d", num_updated)); + else + dialog.attr('title', __("No updates available")); + + dijit.getEnclosingWidget(dialog.domNode.querySelector(".update-btn")) + .attr('disabled', num_updated == 0); + + } + }, + checkUpdates: function(name) { + console.log('checkUpdates', name); + + const container = dialog.domNode.querySelector(".update-results"); + + dialog.attr('title', __("Checking: %s").replace("%s", name)); + + //container.innerHTML = `<li class='text-center'>${__("Checking: %s...").replace("%s", name)}</li>`; + + xhr.json("backend.php", {op: "pref-prefs", method: "checkForPluginUpdates", name: name}, (reply) => { + + if (!reply) { + container.innerHTML += `<li class='text-error'>${__("%s: Operation failed: check event log.").replace("%s", name)}</li>`; + } else { + + reply.forEach((p) => { + if (p.rv) { + if (p.rv.need_update) { + dialog.plugins_to_update.push(p.plugin); + + const update_button = dijit.getEnclosingWidget( + App.find(`*[data-update-btn-for-plugin="${p.plugin}"]`)); + + if (update_button) + update_button.domNode.show(); + } + + if (p.rv.need_update || p.rv.git_status != 0) { + container.innerHTML += + ` + <li><h3>${p.plugin}</h3> + ${p.rv.stderr ? `<pre class="small text-error pre-wrap">${p.rv.stderr}</pre>` : ''} + ${p.rv.stdout ? `<pre class="small text-success pre-wrap">${p.rv.stdout}</pre>` : ''} + <div class="small"> + ${p.rv.git_status ? App.FormFields.icon("error_outline") + " " + __("Exited with RC: %d").replace("%d", p.rv.git_status) : + App.FormFields.icon("check") + " " + __("Ready to update")} + </div> + </li> + ` + } + } + dialog.checkNextPlugin(); + }); + } + + }); + + }, + content: ` + <ul class="panel panel-scrollable plugin-updater-list update-results"> + </ul> + + <footer> + ${App.FormFields.button_tag(App.FormFields.icon("update") + " " + __("Update"), "", {disabled: true, class: "update-btn alt-primary", onclick: "App.dialogOf(this).performUpdate()"})} + ${App.FormFields.cancel_dialog_tag(__("Close"))} + </footer> + `, }); + + const tmph = dojo.connect(dialog, 'onShow', function () { + dojo.disconnect(tmph); + + dialog.plugins_to_update = []; + + if (name) { + dialog.checkUpdates(name); + } else { + dialog.plugins_to_check = [...document.querySelectorAll('*[data-plugin-name][data-plugin-local=true]')].map((p) => p.getAttribute('data-plugin-name')); + dialog.checkNextPlugin(); + } + }); + + dialog.show(); }, }, OPML: { @@ -386,6 +848,7 @@ const Helpers = { </section> <footer class='text-center'> <button dojoType='dijit.form.Button' onclick="return App.dialogOf(this).regenOPMLKey()"> + ${App.FormFields.icon("refresh")} ${__('Generate new URL')} </button> <button dojoType='dijit.form.Button' type='submit' class='alt-primary'> diff --git a/js/PrefLabelTree.js b/js/PrefLabelTree.js index 2b78927c2..39e3f8315 100644 --- a/js/PrefLabelTree.js +++ b/js/PrefLabelTree.js @@ -68,8 +68,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f const dialog = new fox.SingleUseDialog({ id: "labelEditDlg", - title: __("Label Editor"), - style: "width: 650px", + title: __("Edit label"), setLabelColor: function (id, fg, bg) { let kind = ''; @@ -121,10 +120,10 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f 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' + <input style='font-size : 16px; width : 550px; color : ${fg_color}; background : ${bg_color}; transition : background 0.1s linear' id='labelEdit_caption' + placeholder="${__("Caption")}" name='caption' dojoType='dijit.form.ValidationTextBox' required='true' @@ -138,7 +137,6 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f ${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> @@ -168,6 +166,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree", "dijit/f <footer> <button dojoType='dijit.form.Button' type='submit' class='alt-primary' onclick='App.dialogOf(this).execute()'> + ${App.FormFields.icon("save")} ${__('Save')} </button> <button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'> diff --git a/js/PrefUsers.js b/js/PrefUsers.js index 3eb83b02a..7ce3cae94 100644 --- a/js/PrefUsers.js +++ b/js/PrefUsers.js @@ -1,16 +1,18 @@ 'use strict' -/* global __ */ -/* global xhrPost, xhr, dijit, Notify, Tables, App, fox */ +/* global __, xhr, dijit, Notify, Tables, App, fox */ const Users = { reload: function(sort) { - const user_search = App.byId("user_search"); - const search = user_search ? user_search.value : ""; + return new Promise((resolve, reject) => { + const user_search = App.byId("user_search"); + const search = user_search ? user_search.value : ""; - xhr.post("backend.php", { op: "pref-users", sort: sort, search: search }, (reply) => { - dijit.byId('usersTab').attr('content', reply); - Notify.close(); + xhr.post("backend.php", { op: "pref-users", sort: sort, search: search }, (reply) => { + dijit.byId('usersTab').attr('content', reply); + Notify.close(); + resolve(); + }, (e) => { reject(e) }); }); }, add: function() { @@ -20,8 +22,9 @@ const Users = { Notify.progress("Adding user..."); xhr.post("backend.php", {op: "pref-users", method: "add", login: login}, (reply) => { - alert(reply); - Users.reload(); + Users.reload().then(() => { + Notify.info(reply); + }) }); } @@ -33,14 +36,16 @@ const Users = { const dialog = new fox.SingleUseDialog({ id: "userEditDlg", - title: __("User Editor"), + title: __("Edit user"), execute: function () { if (this.validate()) { Notify.progress("Saving data...", true); - xhr.post("backend.php", this.attr('value'), () => { + xhr.post("backend.php", this.attr('value'), (reply) => { dialog.hide(); - Users.reload(); + Users.reload().then(() => { + Notify.info(reply); + }); }); } }, @@ -54,8 +59,6 @@ const Users = { <div dojoType="dijit.layout.TabContainer" style="height : 400px"> <div dojoType="dijit.layout.ContentPane" title="${__('Edit user')}"> - <header>${__("User")}</header> - <section> <fieldset> <label>${__("Login:")}</label> @@ -66,11 +69,9 @@ const Users = { ${admin_disabled ? App.FormFields.hidden_tag("login", user.login) : ''} </fieldset> - </section> - <header>${__("Authentication")}</header> + <hr/> - <section> <fieldset> <label>${__('Access level: ')}</label> ${App.FormFields.select_hash("access_level", @@ -84,11 +85,15 @@ const Users = { <input dojoType='dijit.form.TextBox' type='password' size='20' placeholder='${__("Change password")}' name='password'> </fieldset> - </section> + <fieldset> + <label></label> + <label class="checkbox"> + ${App.FormFields.checkbox_tag("otp_enabled", user.otp_enabled)} + ${__('OTP enabled')} + </fieldset> - <header>${__("Options")}</header> + <hr/> - <section> <fieldset> <label>${__("E-mail:")}</label> <input dojoType='dijit.form.TextBox' size='30' name='email' @@ -110,6 +115,7 @@ const Users = { <footer> <button dojoType='dijit.form.Button' class='alt-primary' type='submit' onclick='App.dialogOf(this).execute()'> + ${App.FormFields.icon("save")} ${__('Save')} </button> <button dojoType='dijit.form.Button' onclick='App.dialogOf(this).hide()'> diff --git a/js/common.js b/js/common.js index 1544e6d0b..1f8318862 100755 --- a/js/common.js +++ b/js/common.js @@ -100,7 +100,7 @@ Element.prototype.fadeIn = function(display = undefined){ }; Element.prototype.visible = function() { - return this.style.display != "none" && this.offsetHeight != 0 && this.offsetWidth != 0; + return window.getComputedStyle(this).display != "none"; //&& this.offsetHeight != 0 && this.offsetWidth != 0; } Element.visible = function(elem) { @@ -154,7 +154,10 @@ String.prototype.stripTags = function() { /* exported xhr */ const xhr = { - post: function(url, params = {}, complete = undefined) { + _ts: 0, + post: function(url, params = {}, complete = undefined, failed = undefined) { + this._ts = new Date().getTime(); + console.log('xhr.post', '>>>', params); return new Promise((resolve, reject) => { @@ -165,10 +168,13 @@ const xhr = { postData: dojo.objectToQuery(params), handleAs: "text", error: function(error) { + if (failed != undefined) + failed(error); + reject(error); }, load: function(data, ioargs) { - console.log('xhr.post', '<<<', ioargs.xhr); + console.log('xhr.post', '<<<', ioargs.xhr, (new Date().getTime() - xhr._ts) + " ms"); if (complete != undefined) complete(data, ioargs.xhr); @@ -178,7 +184,7 @@ const xhr = { ); }); }, - json: function(url, params = {}, complete = undefined) { + json: function(url, params = {}, complete = undefined, failed = undefined) { return new Promise((resolve, reject) => this.post(url, params).then((data) => { let obj = null; @@ -187,13 +193,21 @@ const xhr = { obj = JSON.parse(data); } catch (e) { console.error("xhr.json", e, xhr); + + if (failed != undefined) + failed(e); + reject(e); } - console.log('xhr.json', '<<<', obj); + console.log('xhr.json', '<<<', obj, (new Date().getTime() - xhr._ts) + " ms"); if (obj && typeof App != "undefined") if (!App.handleRpcJson(obj)) { + + if (failed != undefined) + failed(obj); + reject(obj); return; } @@ -248,8 +262,11 @@ const Lists = { if (row) checked ? row.addClassName("Selected") : row.removeClassName("Selected"); }, - select: function(elemId, selected) { - $(elemId).querySelectorAll("li").forEach((row) => { + select: function(elem, selected) { + if (typeof elem == "string") + elem = document.getElementById(elem); + + elem.querySelectorAll("li").forEach((row) => { const checkNode = row.querySelector(".dijitCheckBox,input[type=checkbox]"); if (checkNode) { const widget = dijit.getEnclosingWidget(checkNode); @@ -264,6 +281,30 @@ const Lists = { } }); }, + getSelected: function(elem) { + const rv = []; + + if (typeof elem == "string") + elem = document.getElementById(elem); + + elem.querySelectorAll("li").forEach((row) => { + if (row.hasClassName("Selected")) { + const rowVal = row.getAttribute("data-row-value"); + + if (rowVal) { + rv.push(rowVal); + } else { + // either older prefix-XXX notation or separate attribute + const rowId = row.getAttribute("data-row-id") || row.id.replace(/^[A-Z]*?-/, ""); + + if (!isNaN(rowId)) + rv.push(parseInt(rowId)); + } + } + }); + + return rv; + } }; /* exported Tables */ @@ -279,8 +320,11 @@ const Tables = { checked ? row.addClassName("Selected") : row.removeClassName("Selected"); }, - select: function(elemId, selected) { - $(elemId).querySelectorAll("tr").forEach((row) => { + select: function(elem, selected) { + if (typeof elem == "string") + elem = document.getElementById(elem); + + elem.querySelectorAll("tr").forEach((row) => { const checkNode = row.querySelector(".dijitCheckBox,input[type=checkbox]"); if (checkNode) { const widget = dijit.getEnclosingWidget(checkNode); @@ -295,16 +339,25 @@ const Tables = { } }); }, - getSelected: function(elemId) { + getSelected: function(elem) { const rv = []; - $(elemId).querySelectorAll("tr").forEach((row) => { + if (typeof elem == "string") + elem = document.getElementById(elem); + + elem.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]*?-/, ""); + const rowVal = row.getAttribute("data-row-value"); - if (!isNaN(rowId)) - rv.push(parseInt(rowId)); + if (rowVal) { + rv.push(rowVal); + } else { + // either older prefix-XXX notation or separate attribute + const rowId = row.getAttribute("data-row-id") || row.id.replace(/^[A-Z]*?-/, ""); + + if (!isNaN(rowId)) + rv.push(parseInt(rowId)); + } } }); diff --git a/js/tt-rss.js b/js/tt-rss.js index 4a7f2e643..10fafc447 100644 --- a/js/tt-rss.js +++ b/js/tt-rss.js @@ -69,15 +69,3 @@ require(["dojo/_base/kernel", }); }); -/* exported hash_get */ -function hash_get(key) { - const obj = dojo.queryToObject(window.location.hash.substring(1)); - return obj[key]; -} - -/* exported hash_set */ -function hash_set(key, value) { - const obj = dojo.queryToObject(window.location.hash.substring(1)); - obj[key] = value; - window.location.hash = dojo.objectToQuery(obj); -} |