summaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
Diffstat (limited to 'js')
-rw-r--r--js/App.js54
-rw-r--r--js/Article.js18
-rwxr-xr-xjs/FeedTree.js6
-rw-r--r--js/Feeds.js20
-rwxr-xr-xjs/Headlines.js86
-rw-r--r--js/PluginHost.js6
-rw-r--r--js/PrefHelpers.js17
7 files changed, 144 insertions, 63 deletions
diff --git a/js/App.js b/js/App.js
index 20498e692..a9d796968 100644
--- a/js/App.js
+++ b/js/App.js
@@ -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");