diff options
Diffstat (limited to 'js')
| -rw-r--r-- | js/App.js | 54 | ||||
| -rw-r--r-- | js/Article.js | 18 | ||||
| -rwxr-xr-x | js/FeedTree.js | 6 | ||||
| -rw-r--r-- | js/Feeds.js | 20 | ||||
| -rwxr-xr-x | js/Headlines.js | 86 | ||||
| -rw-r--r-- | js/PluginHost.js | 6 | ||||
| -rw-r--r-- | js/PrefHelpers.js | 17 |
7 files changed, 144 insertions, 63 deletions
@@ -514,9 +514,12 @@ const App = { 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"); + { + const headlines = App.byId("headlines-frame"); + + // we could be in preferences + if (headlines) + headlines.setAttribute("data-auto-catchup", params[k] ? "true" : "false"); } break; case "hotkeys": @@ -685,15 +688,16 @@ const App = { checkBrowserFeatures: function() { let errorMsg = ""; - ['MutationObserver'].forEach(function(wf) { - if (!(wf in window)) { - errorMsg = `Browser feature check failed: <code>window.${wf}</code> not found.`; + ['MutationObserver', 'requestIdleCallback'].forEach((t) => { + if (!(t in window)) { + errorMsg = `Browser check failed: <code>window.${t}</code> not found.`; throw new Error(errorMsg); } }); - if (errorMsg) { - this.Error.fatal(errorMsg, {info: navigator.userAgent}); + if (typeof Promise.allSettled == "undefined") { + errorMsg = `Browser check failed: <code>Promise.allSettled</code> is not defined.`; + throw new Error(errorMsg); } return errorMsg == ""; @@ -868,41 +872,44 @@ const App = { }, setWidescreen: function(wide) { const article_id = Article.getActive(); + const headlines_frame = App.byId("headlines-frame"); + const content_insert = dijit.byId("content-insert"); + + // TODO: setStyle stuff should probably be handled by CSS if (wide) { dijit.byId("headlines-wrap-inner").attr("design", 'sidebar'); - dijit.byId("content-insert").attr("region", "trailing"); + content_insert.attr("region", "trailing"); - dijit.byId("content-insert").domNode.setStyle({width: '50%', + content_insert.domNode.setStyle({width: '50%', height: 'auto', borderTopWidth: '0px' }); if (parseInt(Cookie.get("ttrss_ci_width")) > 0) { - dijit.byId("content-insert").domNode.setStyle( + content_insert.domNode.setStyle( {width: Cookie.get("ttrss_ci_width") + "px" }); } - App.byId("headlines-frame").setStyle({ borderBottomWidth: '0px' }); - App.byId("headlines-frame").addClassName("wide"); + headlines_frame.setStyle({ borderBottomWidth: '0px' }); } else { - dijit.byId("content-insert").attr("region", "bottom"); + content_insert.attr("region", "bottom"); - dijit.byId("content-insert").domNode.setStyle({width: 'auto', + content_insert.domNode.setStyle({width: 'auto', height: '50%', borderTopWidth: '0px'}); if (parseInt(Cookie.get("ttrss_ci_height")) > 0) { - dijit.byId("content-insert").domNode.setStyle( + content_insert.domNode.setStyle( {height: Cookie.get("ttrss_ci_height") + "px" }); } - App.byId("headlines-frame").setStyle({ borderBottomWidth: '1px' }); - App.byId("headlines-frame").removeClassName("wide"); - + headlines_frame.setStyle({ borderBottomWidth: '1px' }); } + headlines_frame.setAttribute("data-is-wide-screen", wide ? "true" : "false"); + Article.close(); if (article_id) Article.view(article_id); @@ -1102,6 +1109,12 @@ const App = { this.hotkey_actions["feed_reverse"] = () => { Headlines.reverse(); }; + this.hotkey_actions["feed_toggle_grid"] = () => { + xhr.json("backend.php", {op: "rpc", method: "togglepref", key: "CDM_ENABLE_GRID"}, (reply) => { + App.setInitParam("cdm_enable_grid", reply.value); + Headlines.renderAgain(); + }) + }; this.hotkey_actions["feed_toggle_vgroup"] = () => { xhr.post("backend.php", {op: "rpc", method: "togglepref", key: "VFEED_GROUP_BY_FEED"}, () => { Feeds.reloadCurrent(); @@ -1194,6 +1207,9 @@ const App = { Headlines.renderAgain(); }); }; + this.hotkey_actions["article_span_grid"] = () => { + Article.cdmToggleGridSpan(Article.getActive()); + }; } }, openPreferences: function(tab) { diff --git a/js/Article.js b/js/Article.js index ed74051a6..4388b41e6 100644 --- a/js/Article.js +++ b/js/Article.js @@ -93,6 +93,16 @@ const Article = { w.opener = null; w.location = url; }, + cdmToggleGridSpan: function(id) { + const row = App.byId(`RROW-${id}`); + + if (row) { + row.toggleClassName('grid-span-row'); + + this.setActive(id); + this.cdmMoveToId(id); + } + }, cdmUnsetActive: function (event) { const row = App.byId(`RROW-${Article.getActive()}`); @@ -389,10 +399,12 @@ const Article = { const ctr = App.byId("headlines-frame"); const row = App.byId(`RROW-${id}`); - if (!row || !ctr) return; + if (ctr && row) { + const grid_gap = parseInt(window.getComputedStyle(ctr).gridGap) || 0; - if (force_to_top || !App.Scrollable.fitsInContainer(row, ctr)) { - ctr.scrollTop = row.offsetTop; + if (force_to_top || !App.Scrollable.fitsInContainer(row, ctr)) { + ctr.scrollTop = row.offsetTop - grid_gap; + } } }, setActive: function (id) { diff --git a/js/FeedTree.js b/js/FeedTree.js index 17cd3deea..af0f420d6 100755 --- a/js/FeedTree.js +++ b/js/FeedTree.js @@ -82,6 +82,9 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co } if (id.match("FEED:")) { + tnode.rowNode.setAttribute('data-feed-id', bare_id); + tnode.rowNode.setAttribute('data-is-cat', "false"); + const menu = new dijit.Menu(); menu.row_id = bare_id; @@ -132,6 +135,9 @@ define(["dojo/_base/declare", "dojo/dom-construct", "dojo/_base/array", "dojo/co } if (id.match("CAT:")) { + tnode.rowNode.setAttribute('data-feed-id', bare_id); + tnode.rowNode.setAttribute('data-is-cat', "true"); + tnode.loadingNode = dojo.create('img', { className: 'loadingNode', src: 'images/blank_icon.gif'}); domConstruct.place(tnode.loadingNode, tnode.labelNode, 'after'); } diff --git a/js/Feeds.js b/js/Feeds.js index 33a1fa3dc..7b6366959 100644 --- a/js/Feeds.js +++ b/js/Feeds.js @@ -113,7 +113,7 @@ const Feeds = { this.hideOrShowFeeds(App.getInitParam("hide_read_feeds")); this._counters_prev = elems; - PluginHost.run(PluginHost.HOOK_COUNTERS_PROCESSED); + PluginHost.run(PluginHost.HOOK_COUNTERS_PROCESSED, elems); }, reloadCurrent: function(method) { if (this.getActive() != undefined) { @@ -311,18 +311,22 @@ const Feeds = { setActive: function(id, is_cat) { console.log('setActive', id, is_cat); - if ('requestIdleCallback' in window) - window.requestIdleCallback(() => { - App.Hash.set({f: id, c: is_cat ? 1 : 0}); - }); - else + window.requestIdleCallback(() => { App.Hash.set({f: id, c: is_cat ? 1 : 0}); + }); this._active_feed_id = id; this._active_feed_is_cat = is_cat; - App.byId("headlines-frame").setAttribute("feed-id", id); - App.byId("headlines-frame").setAttribute("is-cat", is_cat ? 1 : 0); + const container = App.byId("headlines-frame"); + + // TODO @deprecated: these two should be removed (replaced with data- attributes below) + container.setAttribute("feed-id", id); + container.setAttribute("is-cat", is_cat ? 1 : 0); + // ^ + + container.setAttribute("data-feed-id", id); + container.setAttribute("data-is-cat", is_cat ? "true" : "false"); this.select(id, is_cat); diff --git a/js/Headlines.js b/js/Headlines.js index 28e43be1f..58348aca7 100755 --- a/js/Headlines.js +++ b/js/Headlines.js @@ -17,17 +17,27 @@ const Headlines = { sticky_header_observer: new IntersectionObserver( (entries, observer) => { entries.forEach((entry) => { - const header = entry.target.nextElementSibling; + const header = entry.target.closest('.cdm').querySelector(".header"); - if (entry.intersectionRatio == 0) { - header.setAttribute("stuck", "1"); - - } else if (entry.intersectionRatio == 1) { - header.removeAttribute("stuck"); + if (entry.isIntersecting) { + header.removeAttribute("data-is-stuck"); + } else { + header.setAttribute("data-is-stuck", "true"); } - //console.log(entry.target, header, entry.intersectionRatio); + //console.log(entry.target, entry.intersectionRatio, entry.isIntersecting, entry.boundingClientRect.top); + }); + }, + {threshold: [0, 1], root: document.querySelector("#headlines-frame")} + ), + sticky_content_observer: new IntersectionObserver( + (entries, observer) => { + entries.forEach((entry) => { + const header = entry.target.closest('.cdm').querySelector(".header"); + + header.style.position = entry.isIntersecting ? "sticky" : "unset"; + //console.log(entry.target, entry.intersectionRatio, entry.isIntersecting, entry.boundingClientRect.top); }); }, {threshold: [0, 1], root: document.querySelector("#headlines-frame")} @@ -72,14 +82,13 @@ const Headlines = { } }); + PluginHost.run(PluginHost.HOOK_HEADLINE_MUTATIONS, mutations); + Headlines.updateSelectedPrompt(); - if ('requestIdleCallback' in window) - window.requestIdleCallback(() => { - Headlines.syncModified(modified); - }); - else + window.requestIdleCallback(() => { Headlines.syncModified(modified); + }); }), syncModified: function (modified) { const ops = { @@ -173,14 +182,14 @@ const Headlines = { }); } - Promise.all(promises).then((results) => { + Promise.allSettled(promises).then((results) => { let feeds = []; let labels = []; results.forEach((res) => { if (res) { try { - const obj = JSON.parse(res); + const obj = JSON.parse(res.value); if (obj.feeds) feeds = feeds.concat(obj.feeds); @@ -198,6 +207,8 @@ const Headlines = { console.log('requesting counters for', feeds, labels); Feeds.requestCounters(feeds, labels); } + + PluginHost.run(PluginHost.HOOK_HEADLINE_MUTATIONS_SYNCED, results); }); }, click: function (event, id, in_body) { @@ -371,6 +382,9 @@ const Headlines = { } } } + + PluginHost.run(PluginHost.HOOK_HEADLINES_SCROLL_HANDLER); + } catch (e) { console.warn("scrollHandler", e); } @@ -378,11 +392,17 @@ const Headlines = { objectById: function (id) { return this.headlines[id]; }, - setCommonClasses: function () { - App.byId("headlines-frame").removeClassName("cdm"); - App.byId("headlines-frame").removeClassName("normal"); + setCommonClasses: function (headlines_count) { + const container = App.byId("headlines-frame"); - App.byId("headlines-frame").addClassName(App.isCombinedMode() ? "cdm" : "normal"); + container.removeClassName("cdm"); + container.removeClassName("normal"); + + container.addClassName(App.isCombinedMode() ? "cdm" : "normal"); + container.setAttribute("data-enable-grid", App.getInitParam("cdm_enable_grid") ? "true" : "false"); + container.setAttribute("data-headlines-count", parseInt(headlines_count)); + container.setAttribute("data-is-cdm", App.isCombinedMode() ? "true" : "false"); + container.setAttribute("data-is-cdm-expanded", App.getInitParam("cdm_expanded")); // for floating title because it's placed outside of headlines-frame App.byId("main").removeClassName("expandable"); @@ -393,7 +413,7 @@ const Headlines = { }, renderAgain: function () { // TODO: wrap headline elements into a knockoutjs model to prevent all this stuff - Headlines.setCommonClasses(); + Headlines.setCommonClasses(this.headlines.filter((h) => h.id).length); App.findAll("#headlines-frame > div[id*=RROW]").forEach((row) => { const id = row.getAttribute("data-article-id"); @@ -422,11 +442,18 @@ const Headlines = { this.sticky_header_observer.observe(e) }); + App.findAll(".cdm .content").forEach((e) => { + this.sticky_content_observer.observe(e) + }); + if (App.getInitParam("cdm_expanded")) App.findAll("#headlines-frame > div[id*=RROW].cdm").forEach((e) => { this.unpack_observer.observe(e) }); + dijit.byId('main').resize(); + + PluginHost.run(PluginHost.HOOK_HEADLINES_RENDERED); }, render: function (headlines, hl) { let row = null; @@ -494,9 +521,10 @@ const Headlines = { <span class="updated" title="${hl.imported}">${hl.updated}</span> <div class="right"> + <i class="material-icons icon-grid-span" title="${__("Span all columns")}" onclick="Article.cdmToggleGridSpan(${hl.id})">fullscreen</i> <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}})"> + <span class="icon-feed" title="${App.escapeHtml(hl.feed_title)}" onclick="Feeds.open({feed:${hl.feed_id}})"> ${Feeds.renderIcon(hl.feed_id, hl.has_icon)} </span> </div> @@ -560,7 +588,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)}">${Feeds.renderIcon(hl.feed_id, hl.has_icon)}</span> + <span onclick="Feeds.open({feed:${hl.feed_id}})" class="icon-feed" title="${App.escapeHtml(hl.feed_title)}">${Feeds.renderIcon(hl.feed_id, hl.has_icon)}</span> </div> </div> `; @@ -614,7 +642,7 @@ const Headlines = { </span> <span class='right'> <span id='selected_prompt'></span> - <div dojoType='fox.form.DropDownButton' title='"${__('Select articles')}'> + <div class='select-articles-dropdown' 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> @@ -671,11 +699,15 @@ const Headlines = { console.log('infscroll_disabled=', Feeds.infscroll_disabled); // also called in renderAgain() after view mode switch - Headlines.setCommonClasses(); + Headlines.setCommonClasses(headlines_count); + /** TODO: remove @deprecated */ App.byId("headlines-frame").setAttribute("is-vfeed", reply['headlines']['is_vfeed'] ? 1 : 0); + App.byId("headlines-frame").setAttribute("data-is-vfeed", + reply['headlines']['is_vfeed'] ? "true" : "false"); + Article.setActive(0); try { @@ -799,6 +831,10 @@ const Headlines = { this.sticky_header_observer.observe(e) }); + App.findAll(".cdm .content").forEach((e) => { + this.sticky_content_observer.observe(e) + }); + if (App.getInitParam("cdm_expanded")) App.findAll("#headlines-frame > div[id*=RROW].cdm").forEach((e) => { this.unpack_observer.observe(e) @@ -816,6 +852,10 @@ const Headlines = { // unpack visible articles, fill buffer more, etc this.scrollHandler(); + dijit.byId('main').resize(); + + PluginHost.run(PluginHost.HOOK_HEADLINES_RENDERED); + Notify.close(); }, reverse: function () { diff --git a/js/PluginHost.js b/js/PluginHost.js index caee79d58..deb7c0645 100644 --- a/js/PluginHost.js +++ b/js/PluginHost.js @@ -17,6 +17,10 @@ const PluginHost = { HOOK_HEADLINE_RENDERED: 12, HOOK_COUNTERS_RECEIVED: 13, HOOK_COUNTERS_PROCESSED: 14, + HOOK_HEADLINE_MUTATIONS: 15, + HOOK_HEADLINE_MUTATIONS_SYNCED: 16, + HOOK_HEADLINES_RENDERED: 17, + HOOK_HEADLINES_SCROLL_HANDLER: 18, hooks: [], register: function (name, callback) { if (typeof(this.hooks[name]) == 'undefined') @@ -25,7 +29,7 @@ const PluginHost = { this.hooks[name].push(callback); }, run: function (name, args) { - //console.warn('PluginHost::run ' + name); + //console.warn('PluginHost.run', name); if (typeof(this.hooks[name]) != 'undefined') for (let i = 0; i < this.hooks[name].length; i++) { diff --git a/js/PrefHelpers.js b/js/PrefHelpers.js index 3f738aa95..cd831d4d0 100644 --- a/js/PrefHelpers.js +++ b/js/PrefHelpers.js @@ -368,15 +368,16 @@ const Helpers = { // 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.") : ""}"> + <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> + <span class="description ${plugin.is_system ? "text-info" : ""}"> + ${plugin.description} + </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"), "", @@ -510,12 +511,10 @@ const Helpers = { search: function() { this.search_query = this.attr('value').search.toLowerCase(); - if ('requestIdleCallback' in window) - window.requestIdleCallback(() => { - this.render_contents(); - }); - else + window.requestIdleCallback(() => { this.render_contents(); + }); + }, render_contents: function() { const container = dialog.domNode.querySelector(".contents"); |