summaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
Diffstat (limited to 'js')
-rw-r--r--js/AppBase.js63
-rw-r--r--js/Article.js103
-rw-r--r--js/CommonFilters.js55
-rwxr-xr-xjs/Headlines.js327
-rw-r--r--js/PrefFilterTree.js4
-rw-r--r--js/form/ValidationTextArea.js33
-rwxr-xr-xjs/prefs.js1
-rw-r--r--js/tt-rss.js67
8 files changed, 321 insertions, 332 deletions
diff --git a/js/AppBase.js b/js/AppBase.js
index 86cc44e8a..b037a52a2 100644
--- a/js/AppBase.js
+++ b/js/AppBase.js
@@ -7,6 +7,39 @@ define(["dojo/_base/declare"], function (declare) {
hotkey_prefix: 0,
hotkey_prefix_pressed: false,
hotkey_prefix_timeout: 0,
+ Scrollable: {
+ scrollByPages: function (elem, page_offset) {
+ if (!elem) return;
+
+ /* keep a line or so from the previous page */
+ const offset = (elem.offsetHeight - (page_offset > 0 ? 50 : -50)) * page_offset;
+
+ this.scroll(elem, offset);
+ },
+ scroll: function(elem, offset) {
+ if (!elem) return;
+
+ elem.scrollTop += offset;
+ },
+ isChildVisible: function(elem, ctr) {
+ if (!elem) return;
+
+ const ctop = ctr.scrollTop;
+ const cbottom = ctop + ctr.offsetHeight;
+
+ const etop = elem.offsetTop;
+ const ebottom = etop + elem.offsetHeight;
+
+ return etop >= ctop && ebottom <= cbottom ||
+ etop < ctop && ebottom > ctop || ebottom > cbottom && etop < cbottom;
+ },
+ fitsInContainer: function (elem, ctr) {
+ if (!elem) return;
+
+ return elem.offsetTop + elem.offsetHeight <= ctr.scrollTop + ctr.offsetHeight &&
+ elem.offsetTop >= ctr.scrollTop;
+ }
+ },
constructor: function() {
window.onerror = this.Error.onWindowError;
},
@@ -101,6 +134,20 @@ define(["dojo/_base/declare"], function (declare) {
}
},
+ isCombinedMode: function() {
+ return this.getInitParam("combined_display_mode");
+ },
+ getActionByHotkeySequence: function (sequence) {
+ const hotkeys_map = App.getInitParam("hotkeys");
+
+ for (const seq in hotkeys_map[1]) {
+ if (hotkeys_map[1].hasOwnProperty(seq)) {
+ if (seq == sequence) {
+ return hotkeys_map[1][seq];
+ }
+ }
+ }
+ },
keyeventToAction: function(event) {
const hotkeys_map = App.getInitParam("hotkeys");
@@ -144,18 +191,16 @@ define(["dojo/_base/declare"], function (declare) {
hotkey_name = keychar ? keychar : "(" + keycode + ")";
}
- const hotkey_full = this.hotkey_prefix ? this.hotkey_prefix + " " + hotkey_name : hotkey_name;
+ let hotkey_full = this.hotkey_prefix ? this.hotkey_prefix + " " + hotkey_name : hotkey_name;
this.hotkey_prefix = false;
- let action_name = false;
+ let action_name = this.getActionByHotkeySequence(hotkey_full);
- for (const sequence in hotkeys_map[1]) {
- if (hotkeys_map[1].hasOwnProperty(sequence)) {
- if (sequence == hotkey_full) {
- action_name = hotkeys_map[1][sequence];
- break;
- }
- }
+ // check for mode-specific hotkey
+ if (!action_name) {
+ hotkey_full = (App.isCombinedMode() ? "{C}" : "{3}") + hotkey_full;
+
+ action_name = this.getActionByHotkeySequence(hotkey_full);
}
console.log('keyeventToAction', hotkey_full, '=>', action_name);
diff --git a/js/Article.js b/js/Article.js
index 50447c2a1..3056dd666 100644
--- a/js/Article.js
+++ b/js/Article.js
@@ -139,7 +139,7 @@ define(["dojo/_base/declare"], function (declare) {
c.attr('content', article);
PluginHost.run(PluginHost.HOOK_ARTICLE_RENDERED, c.domNode);
- Headlines.correctHeadlinesOffset(Article.getActive());
+ //Headlines.correctHeadlinesOffset(Article.getActive());
try {
c.focus();
@@ -149,14 +149,14 @@ define(["dojo/_base/declare"], function (declare) {
formatComments: function(hl) {
let comments = "";
- if (hl.comments) {
+ if (hl.comments || hl.num_comments > 0) {
let comments_msg = __("comments");
if (hl.num_comments > 0) {
comments_msg = hl.num_comments + " " + ngettext("comment", "comments", hl.num_comments)
}
- comments = `<a href="${escapeHtml(hl.comments)}">(${comments_msg})</a>`;
+ comments = `<a href="${escapeHtml(hl.comments ? hl.comments : hl.link)}">(${comments_msg})</a>`;
}
return comments;
@@ -178,15 +178,29 @@ define(["dojo/_base/declare"], function (declare) {
if (container.textContent.length == 0)
container.innerHTML += "&nbsp;";
+ // in expandable mode, save content for later, so that we can pack unfocused rows back
+ if (App.isCombinedMode() && $("main").hasClassName("expandable"))
+ row.setAttribute("data-content-original", row.getAttribute("data-content"));
+
row.removeAttribute("data-content");
PluginHost.run(PluginHost.HOOK_ARTICLE_RENDERED_CDM, row);
}
},
- view: function (id, noexpand) {
+ pack: function(row) {
+ if (row.hasAttribute("data-content-original")) {
+ console.log("packing", row.id);
+ row.setAttribute("data-content", row.getAttribute("data-content-original"));
+ row.removeAttribute("data-content-original");
+
+ row.querySelector(".content-inner").innerHTML = "&nbsp;";
+ }
+ },
+ view: function (id, no_expand) {
this.setActive(id);
+ Headlines.scrollToArticleId(id);
- if (!noexpand) {
+ if (!no_expand) {
const hl = Headlines.objectById(id);
if (hl) {
@@ -277,49 +291,39 @@ define(["dojo/_base/declare"], function (declare) {
dialog.show();
},
- cdmScrollToId: function (id, force, event) {
- const ctr = $("headlines-frame");
- const e = $("RROW-" + id);
- const is_expanded = App.getInitParam("cdm_expanded");
-
- if (!e || !ctr) return;
+ cdmMoveToId: function (id, params) {
+ params = params || {};
- if (force || is_expanded || e.offsetTop + e.offsetHeight > (ctr.scrollTop + ctr.offsetHeight) ||
- e.offsetTop < ctr.scrollTop) {
+ const force_to_top = params.force_to_top || false;
- if (event && event.repeat || !is_expanded) {
- ctr.addClassName("forbid-smooth-scroll");
- window.clearTimeout(this._scroll_reset_timeout);
-
- this._scroll_reset_timeout = window.setTimeout(() => {
- if (ctr) ctr.removeClassName("forbid-smooth-scroll");
- }, 250)
-
- } else {
- ctr.removeClassName("forbid-smooth-scroll");
- }
+ const ctr = $("headlines-frame");
+ const row = $("RROW-" + id);
- ctr.scrollTop = e.offsetTop;
+ if (!row || !ctr) return;
- Element.hide("floatingTitle");
+ if (force_to_top || !App.Scrollable.fitsInContainer(row, ctr)) {
+ ctr.scrollTop = row.offsetTop;
}
},
setActive: function (id) {
- console.log("setActive", id);
+ if (id != Article.getActive()) {
+ console.log("setActive", id, "was", Article.getActive());
- $$("div[id*=RROW][class*=active]").each((e) => {
- e.removeClassName("active");
- });
+ $$("div[id*=RROW][class*=active]").each((row) => {
+ row.removeClassName("active");
+ Article.pack(row);
+ });
- const row = $("RROW-" + id);
+ const row = $("RROW-" + id);
- if (row) {
- Article.unpack(row);
+ if (row) {
+ Article.unpack(row);
- row.removeClassName("Unread");
- row.addClassName("active");
+ row.removeClassName("Unread");
+ row.addClassName("active");
- PluginHost.run(PluginHost.HOOK_ARTICLE_SET_ACTIVE, row.getAttribute("data-article-id"));
+ PluginHost.run(PluginHost.HOOK_ARTICLE_SET_ACTIVE, row.getAttribute("data-article-id"));
+ }
}
},
getActive: function () {
@@ -330,30 +334,11 @@ define(["dojo/_base/declare"], function (declare) {
else
return 0;
},
- scrollByPages: function (page_offset, event) {
- const elem = App.isCombinedMode() ? $("headlines-frame") : $("content-insert");
-
- const offset = elem.offsetHeight * page_offset * 0.99;
-
- this.scroll(offset, event);
+ scrollByPages: function (page_offset) {
+ App.Scrollable.scrollByPages($("content-insert"), page_offset);
},
- scroll: function (offset, event) {
-
- const elem = App.isCombinedMode() ? $("headlines-frame") : $("content-insert");
-
- if (event && event.repeat) {
- elem.addClassName("forbid-smooth-scroll");
- window.clearTimeout(this._scroll_reset_timeout);
-
- this._scroll_reset_timeout = window.setTimeout(() => {
- if (elem) elem.removeClassName("forbid-smooth-scroll");
- }, 250)
-
- } else {
- elem.removeClassName("forbid-smooth-scroll");
- }
-
- elem.scrollTop += offset;
+ scroll: function (offset) {
+ App.Scrollable.scroll($("content-insert"), offset);
},
mouseIn: function (id) {
this.post_under_pointer = id;
diff --git a/js/CommonFilters.js b/js/CommonFilters.js
index 1538a3fb3..53573b365 100644
--- a/js/CommonFilters.js
+++ b/js/CommonFilters.js
@@ -2,24 +2,6 @@
/* global __, ngettext */
define(["dojo/_base/declare"], function (declare) {
Filters = {
- filterDlgCheckRegExp: function(sender) {
- const tooltip = dijit.byId("filterDlg_regExp_tip").domNode;
-
- try {
- sender.domNode.removeClassName("invalid");
- sender.domNode.removeClassName("valid");
-
- new RegExp("/" + sender.value + "/");
-
- sender.domNode.addClassName("valid");
- tooltip.innerText = __("Regular expression, without outer delimiters (i.e. slashes)");
-
- } catch (e) {
- sender.domNode.addClassName("invalid");
-
- tooltip.innerText = e.message;
- }
- },
filterDlgCheckAction: function(sender) {
const action = sender.value;
@@ -143,9 +125,6 @@ define(["dojo/_base/declare"], function (declare) {
if (dijit.byId("filterNewRuleDlg"))
dijit.byId("filterNewRuleDlg").destroyRecursive();
- const query = "backend.php?op=pref-filters&method=newrule&rule=" +
- encodeURIComponent(ruleStr);
-
const rule_dlg = new dijit.Dialog({
id: "filterNewRuleDlg",
title: ruleStr ? __("Edit rule") : __("Add rule"),
@@ -156,7 +135,15 @@ define(["dojo/_base/declare"], function (declare) {
this.hide();
}
},
- href: query
+ content: __('Loading, please wait...'),
+ });
+
+ const tmph = dojo.connect(rule_dlg, "onShow", null, function (e) {
+ dojo.disconnect(tmph);
+
+ xhrPost("backend.php", {op: 'pref-filters', method: 'newrule', rule: ruleStr}, (transport) => {
+ rule_dlg.attr('content', transport.responseText);
+ });
});
rule_dlg.show();
@@ -183,7 +170,7 @@ define(["dojo/_base/declare"], function (declare) {
rule_dlg.show();
},
- editFilterTest: function(query) {
+ editFilterTest: function(params) {
if (dijit.byId("filterTestDlg"))
dijit.byId("filterTestDlg").destroyRecursive();
@@ -195,12 +182,14 @@ define(["dojo/_base/declare"], function (declare) {
results: 0,
limit: 100,
max_offset: 10000,
- getTestResults: function (query, offset) {
- const updquery = query + "&offset=" + offset + "&limit=" + test_dlg.limit;
+ getTestResults: function (params, offset) {
+ params.method = 'testFilterDo';
+ params.offset = offset;
+ params.limit = test_dlg.limit;
console.log("getTestResults:" + offset);
- xhrPost("backend.php", updquery, (transport) => {
+ xhrPost("backend.php", params, (transport) => {
try {
const result = JSON.parse(transport.responseText);
@@ -216,9 +205,7 @@ define(["dojo/_base/declare"], function (declare) {
console.log(offset + " " + test_dlg.max_offset);
for (let i = 0; i < result.length; i++) {
- const tmp = new Element("table");
- tmp.innerHTML = result[i];
- dojo.parser.parse(tmp);
+ const tmp = dojo.create("table", { innerHTML: result[i]});
$("prefFilterTestResultList").innerHTML += tmp.innerHTML;
}
@@ -227,7 +214,7 @@ define(["dojo/_base/declare"], function (declare) {
// get the next batch
window.setTimeout(function () {
- test_dlg.getTestResults(query, offset + test_dlg.limit);
+ test_dlg.getTestResults(params, offset + test_dlg.limit);
}, 0);
} else {
@@ -262,11 +249,11 @@ define(["dojo/_base/declare"], function (declare) {
});
},
- href: query
+ href: "backend.php?op=pref-filters&method=testFilterDlg"
});
dojo.connect(test_dlg, "onLoad", null, function (e) {
- test_dlg.getTestResults(query, 0);
+ test_dlg.getTestResults(params, 0);
});
test_dlg.show();
@@ -296,9 +283,7 @@ define(["dojo/_base/declare"], function (declare) {
title: __("Create Filter"),
style: "width: 600px",
test: function () {
- const query = "backend.php?" + dojo.formToQuery("filter_new_form") + "&savemode=test";
-
- Filters.editFilterTest(query);
+ Filters.editFilterTest(dojo.formToObject("filter_new_form"));
},
selectRules: function (select) {
Lists.select("filterDlg_Matches", select);
diff --git a/js/Headlines.js b/js/Headlines.js
index 540c400d3..8425dc980 100755
--- a/js/Headlines.js
+++ b/js/Headlines.js
@@ -8,6 +8,36 @@ define(["dojo/_base/declare"], function (declare) {
headlines: [],
current_first_id: 0,
_scroll_reset_timeout: false,
+ default_force_previous: false,
+ default_force_to_top: false,
+ line_scroll_offset: 120, /* px */
+ sticky_header_observer: new IntersectionObserver(
+ (entries, observer) => {
+ entries.forEach((entry) => {
+ const header = entry.target.nextElementSibling;
+
+ if (entry.intersectionRatio == 0) {
+ header.setAttribute("stuck", "1");
+
+ } else if (entry.intersectionRatio == 1) {
+ header.removeAttribute("stuck");
+ }
+
+ //console.log(entry.target, header, entry.intersectionRatio);
+
+ });
+ },
+ {threshold: [0, 1], root: document.querySelector("#headlines-frame")}
+ ),
+ unpack_observer: new IntersectionObserver(
+ (entries, observer) => {
+ entries.forEach((entry) => {
+ if (entry.intersectionRatio > 0)
+ Article.unpack(entry.target);
+ });
+ },
+ {threshold: [0], root: document.querySelector("#headlines-frame")}
+ ),
row_observer: new MutationObserver((mutations) => {
const modified = [];
@@ -40,7 +70,6 @@ define(["dojo/_base/declare"], function (declare) {
});
Headlines.updateSelectedPrompt();
- Headlines.updateFloatingTitle(true);
if ('requestIdleCallback' in window)
window.requestIdleCallback(() => {
@@ -49,7 +78,7 @@ define(["dojo/_base/declare"], function (declare) {
else
Headlines.syncModified(modified);
}),
- syncModified: function(modified) {
+ syncModified: function (modified) {
const ops = {
tmark: [],
tpub: [],
@@ -62,7 +91,7 @@ define(["dojo/_base/declare"], function (declare) {
rescore: {},
};
- modified.each(function(m) {
+ modified.each(function (m) {
if (m.old.marked != m.new.marked)
ops.tmark.push(m.id);
@@ -118,26 +147,26 @@ define(["dojo/_base/declare"], function (declare) {
if (ops.tmark.length != 0)
promises.push(xhrPost("backend.php",
- { op: "rpc", method: "markSelected", ids: ops.tmark.toString(), cmode: 2}));
+ {op: "rpc", method: "markSelected", ids: ops.tmark.toString(), cmode: 2}));
if (ops.tpub.length != 0)
promises.push(xhrPost("backend.php",
- { op: "rpc", method: "publishSelected", ids: ops.tpub.toString(), cmode: 2}));
+ {op: "rpc", method: "publishSelected", ids: ops.tpub.toString(), cmode: 2}));
if (ops.read.length != 0)
promises.push(xhrPost("backend.php",
- { op: "rpc", method: "catchupSelected", ids: ops.read.toString(), cmode: 0}));
+ {op: "rpc", method: "catchupSelected", ids: ops.read.toString(), cmode: 0}));
if (ops.unread.length != 0)
promises.push(xhrPost("backend.php",
- { op: "rpc", method: "catchupSelected", ids: ops.unread.toString(), cmode: 1}));
+ {op: "rpc", method: "catchupSelected", ids: ops.unread.toString(), 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 }));
+ {op: "article", method: "setScore", id: ops.rescore[score].toString(), score: score}));
});
}
@@ -177,15 +206,24 @@ define(["dojo/_base/declare"], function (declare) {
} else if (Article.getActive() != id) {
Headlines.select('none');
+
+ const scroll_position_A = $("RROW-" + id).offsetTop - $("headlines-frame").scrollTop;
+
Article.setActive(id);
if (App.getInitParam("cdm_expanded")) {
+
if (!in_body)
Article.openInNewWindow(id);
Headlines.toggleUnread(id, 0);
} else {
- Article.cdmScrollToId(id);
+ const scroll_position_B = $("RROW-" + id).offsetTop - $("headlines-frame").scrollTop;
+
+ // this would only work if there's enough space
+ $("headlines-frame").scrollTop -= scroll_position_A-scroll_position_B;
+
+ Article.cdmMoveToId(id);
}
} else if (in_body) {
@@ -245,36 +283,22 @@ define(["dojo/_base/declare"], function (declare) {
Feeds.open({feed: Feeds.getActive(), is_cat: Feeds.activeIsCat(), offset: offset, append: true});
},
- isChildVisible: function (elem, ctr) {
- const ctop = ctr.scrollTop;
- const cbottom = ctop + ctr.offsetHeight;
-
- const etop = elem.offsetTop;
- const ebottom = etop + elem.offsetHeight;
-
- return etop >= ctop && ebottom <= cbottom ||
- etop < ctop && ebottom > ctop || ebottom > cbottom && etop < cbottom
-
+ isChildVisible: function (elem) {
+ return App.Scrollable.isChildVisible(elem, $("headlines-frame"));
},
- firstVisible: function() {
+ firstVisible: function () {
const rows = $$("#headlines-frame > div[id*=RROW]");
- const ctr = $("headlines-frame");
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
- if (this.isChildVisible(row, ctr)) {
+ if (this.isChildVisible(row)) {
return row.getAttribute("data-article-id");
}
}
},
scrollHandler: function (/*event*/) {
try {
- Headlines.unpackVisible();
-
- if (App.isCombinedMode())
- Headlines.updateFloatingTitle();
-
if (!Feeds.infscroll_disabled && !Feeds.infscroll_in_progress) {
const hsp = $("headlines-spacer");
const container = $("headlines-frame");
@@ -311,82 +335,25 @@ define(["dojo/_base/declare"], function (declare) {
console.warn("scrollHandler", e);
}
},
- updateFloatingTitle: function (status_only) {
- if (!App.isCombinedMode()/* || !App.getInitParam("cdm_expanded")*/) return;
-
- const safety_offset = 120; /* px, needed for firefox */
- const hf = $("headlines-frame");
- const elems = $$("#headlines-frame > div[id*=RROW]");
- const ft = $("floatingTitle");
-
- for (let i = 0; i < elems.length; i++) {
- const row = elems[i];
-
- if (row && row.offsetTop + row.offsetHeight > hf.scrollTop + safety_offset) {
-
- const header = row.select(".header")[0];
- const id = row.getAttribute("data-article-id");
-
- if (status_only || id != ft.getAttribute("data-article-id")) {
- if (id != ft.getAttribute("data-article-id")) {
-
- ft.setAttribute("data-article-id", id);
- ft.innerHTML = header.innerHTML;
-
- ft.select(".dijitCheckBox")[0].outerHTML = "<i class=\"material-icons icon-anchor\" onclick=\"Article.cdmScrollToId(" + id + ", true)\">expand_more</i>";
-
- this.initFloatingMenu();
-
- }
-
- if (row.hasClassName("Unread"))
- ft.addClassName("Unread");
- else
- ft.removeClassName("Unread");
-
- if (row.hasClassName("marked"))
- ft.addClassName("marked");
- else
- ft.removeClassName("marked");
-
- if (row.hasClassName("published"))
- ft.addClassName("published");
- else
- ft.removeClassName("published");
-
- PluginHost.run(PluginHost.HOOK_FLOATING_TITLE, row);
- }
-
- if (hf.scrollTop - row.offsetTop <= header.offsetHeight + safety_offset)
- ft.fade({duration: 0.2});
- else
- ft.appear({duration: 0.2});
-
- return;
- }
- }
+ objectById: function (id) {
+ return this.headlines[id];
},
- unpackVisible: function () {
- if (!App.isCombinedMode() || !App.getInitParam("cdm_expanded")) return;
+ setCommonClasses: function () {
+ $("headlines-frame").removeClassName("cdm");
+ $("headlines-frame").removeClassName("normal");
- const rows = $$("#headlines-frame div[id*=RROW][data-content]");
- const threshold = $("headlines-frame").scrollTop + $("headlines-frame").offsetHeight + 600;
+ $("headlines-frame").addClassName(App.isCombinedMode() ? "cdm" : "normal");
- for (let i = 0; i < rows.length; i++) {
- const row = rows[i];
+ // for floating title because it's placed outside of headlines-frame
+ $("main").removeClassName("expandable");
+ $("main").removeClassName("expanded");
- if (row.offsetTop <= threshold) {
- Article.unpack(row);
- } else {
- break;
- }
- }
- },
- objectById: function (id){
- return this.headlines[id];
+ if (App.isCombinedMode())
+ $("main").addClassName(App.getInitParam("cdm_expanded") ? " expanded" : " expandable");
},
- renderAgain: function() {
+ renderAgain: function () {
// TODO: wrap headline elements into a knockoutjs model to prevent all this stuff
+ Headlines.setCommonClasses();
$$("#headlines-frame > div[id*=RROW]").each((row) => {
const id = row.getAttribute("data-article-id");
@@ -399,19 +366,27 @@ define(["dojo/_base/declare"], function (declare) {
if (hl.active) {
new_row.addClassName("active");
+ Article.unpack(new_row);
if (App.isCombinedMode())
- Article.cdmScrollToId(id);
+ Article.cdmMoveToId(id, {noscroll: true});
else
Article.view(id);
}
if (hl.selected) this.select("all", id);
-
- Article.unpack(new_row);
-
}
});
+
+ $$(".cdm .header-sticky-guard").each((e) => {
+ this.sticky_header_observer.observe(e)
+ });
+
+ if (App.getInitParam("cdm_expanded"))
+ $$("#headlines-frame > div[id*=RROW].cdm").each((e) => {
+ this.unpack_observer.observe(e)
+ });
+
},
render: function (headlines, hl) {
let row = null;
@@ -453,7 +428,7 @@ define(["dojo/_base/declare"], function (declare) {
data-article-title="${escapeHtml(hl.title)}"
onmouseover="Article.mouseIn(${hl.id})"
onmouseout="Article.mouseOut(${hl.id})">
-
+ <div class="header-sticky-guard"></div>
<div class="header">
<div class="left">
<input dojoType="dijit.form.CheckBox" type="checkbox" onclick="Headlines.onRowChecked(this)" class='rchk'>
@@ -558,7 +533,7 @@ define(["dojo/_base/declare"], function (declare) {
return tmp.firstChild;
},
- updateCurrentUnread: function() {
+ updateCurrentUnread: function () {
if ($("feed_current_unread")) {
const feed_unread = Feeds.getUnread(Feeds.getActive(), Feeds.activeIsCat());
@@ -598,34 +573,18 @@ define(["dojo/_base/declare"], function (declare) {
Feeds.infscroll_disabled = parseInt(headlines_count) != 30;
console.log('infscroll_disabled=', Feeds.infscroll_disabled);
- // TODO: the below needs to be applied again when switching expanded/expandable on the fly
- // via hotkeys, not just on feed load
-
- $("headlines-frame").removeClassName("cdm");
- $("headlines-frame").removeClassName("normal");
-
- $("headlines-frame").addClassName(App.isCombinedMode() ? "cdm" : "normal");
+ // also called in renderAgain() after view mode switch
+ Headlines.setCommonClasses();
$("headlines-frame").setAttribute("is-vfeed",
reply['headlines']['is_vfeed'] ? 1 : 0);
- // for floating title because it's placed outside of headlines-frame
- $("main").removeClassName("expandable");
- $("main").removeClassName("expanded");
-
- if (App.isCombinedMode())
- $("main").addClassName(App.getInitParam("cdm_expanded") ? " expanded" : " expandable");
-
Article.setActive(0);
try {
$("headlines-frame").removeClassName("smooth-scroll");
$("headlines-frame").scrollTop = 0;
$("headlines-frame").addClassName("smooth-scroll");
-
- Element.hide("floatingTitle");
- $("floatingTitle").setAttribute("data-article-id", 0);
- $("floatingTitle").innerHTML = "";
} catch (e) {
console.warn(e);
}
@@ -736,6 +695,15 @@ define(["dojo/_base/declare"], function (declare) {
}
}
+ $$(".cdm .header-sticky-guard").each((e) => {
+ this.sticky_header_observer.observe(e)
+ });
+
+ if (App.getInitParam("cdm_expanded"))
+ $$("#headlines-frame > div[id*=RROW].cdm").each((e) => {
+ this.unpack_observer.observe(e)
+ });
+
} else {
console.error("Invalid object received: " + transport.responseText);
dijit.byId("headlines-frame").attr('content', "<div class='whiteBox'>" +
@@ -837,33 +805,30 @@ define(["dojo/_base/declare"], function (declare) {
move: function (mode, params) {
params = params || {};
- const noscroll = params.noscroll || false;
- const noexpand = params.noexpand || false;
- const event = params.event;
-
- const rows = Headlines.getLoaded();
+ 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;
let prev_id = false;
let next_id = false;
+ let current_id = Article.getActive();
- const active_row = $("RROW-" + Article.getActive());
-
- if (!active_row) {
- Article.setActive(0);
- }
-
- if (!Article.getActive() || (active_row && !Headlines.isChildVisible(active_row, $("headlines-frame")))) {
- next_id = Headlines.firstVisible();
- prev_id = next_id;
+ if (!Headlines.isChildVisible($("RROW-" + current_id))) {
+ console.log('active article is obscured, resetting to first visible...');
+ current_id = Headlines.firstVisible();
+ prev_id = current_id;
+ next_id = current_id;
} else {
+ const rows = Headlines.getLoaded();
+
for (let i = 0; i < rows.length; i++) {
- if (rows[i] == Article.getActive()) {
+ if (rows[i] == current_id) {
// Account for adjacent identical article ids.
if (i > 0) prev_id = rows[i - 1];
for (let j = i + 1; j < rows.length; j++) {
- if (rows[j] != Article.getActive()) {
+ if (rows[j] != current_id) {
next_id = rows[j];
break;
}
@@ -873,51 +838,39 @@ define(["dojo/_base/declare"], function (declare) {
}
}
- console.log("cur: " + Article.getActive() + " next: " + next_id);
+ console.log("cur: " + current_id + " next: " + next_id + " prev:" + prev_id);
if (mode === "next") {
- if (next_id || Article.getActive()) {
+ if (next_id) {
if (App.isCombinedMode()) {
-
- //const row = $("RROW-" + Article.getActive());
- const ctr = $("headlines-frame");
-
- if (!noscroll) {
- Article.scroll(ctr.offsetHeight / 2, event);
- } else if (next_id) {
+ window.requestAnimationFrame(() => {
Article.setActive(next_id);
- Article.cdmScrollToId(next_id, true, event);
- }
-
- } else if (next_id) {
- Headlines.correctHeadlinesOffset(next_id);
- Article.view(next_id, noexpand);
+ Article.cdmMoveToId(next_id, {force_to_top: force_to_top});
+ });
+ } else {
+ Article.view(next_id, no_expand);
}
}
- }
-
- if (mode === "prev") {
- if (prev_id || Article.getActive()) {
+ } else if (mode === "prev") {
+ if (prev_id || current_id) {
if (App.isCombinedMode()) {
+ window.requestAnimationFrame(() => {
+ const row = $("RROW-" + current_id);
+ const ctr = $("headlines-frame");
+ const delta_px = Math.round(row.offsetTop) - Math.round(ctr.scrollTop);
- const row = $("RROW-" + Article.getActive());
- //const prev_row = $("RROW-" + prev_id);
- const ctr = $("headlines-frame");
+ console.log('moving back, delta_px', delta_px);
- if (!noscroll) {
- Article.scroll(-ctr.offsetHeight / 2, event);
- } else {
- if (row && Math.round(row.offsetTop) < Math.round(ctr.scrollTop)) {
- Article.cdmScrollToId(Article.getActive(), noscroll, event);
+ if (!force_previous && row && delta_px < -8) {
+ Article.setActive(current_id);
+ Article.cdmMoveToId(current_id, {force_to_top: force_to_top});
} else if (prev_id) {
Article.setActive(prev_id);
- Article.cdmScrollToId(prev_id, noscroll, event);
+ Article.cdmMoveToId(prev_id, {force_to_top: force_to_top});
}
- }
-
+ });
} else if (prev_id) {
- Headlines.correctHeadlinesOffset(prev_id);
- Article.view(prev_id, noexpand);
+ Article.view(prev_id, no_expand);
}
}
}
@@ -1254,7 +1207,7 @@ define(["dojo/_base/declare"], function (declare) {
eval(elem.value);
elem.attr('value', 'false');
},
- correctHeadlinesOffset: function (id) {
+ scrollToArticleId: function (id) {
const container = $("headlines-frame");
const row = $("RROW-" + id);
@@ -1274,20 +1227,6 @@ define(["dojo/_base/declare"], function (declare) {
container.scrollTop = row.offsetTop + row.offsetHeight - viewport;
}
},
- initFloatingMenu: function () {
- if (!dijit.byId("floatingMenu")) {
-
- const menu = new dijit.Menu({
- id: "floatingMenu",
- selector: ".hlMenuAttach",
- targetNodeIds: ["floatingTitle"]
- });
-
- this.headlinesMenuCommon(menu);
-
- menu.startup();
- }
- },
headlinesMenuCommon: function (menu) {
menu.addChild(new dijit.MenuItem({
@@ -1416,21 +1355,11 @@ define(["dojo/_base/declare"], function (declare) {
}
},
- scrollByPages: function (offset, event) {
- const elem = $("headlines-frame");
-
- if (event && event.repeat) {
- elem.addClassName("forbid-smooth-scroll");
- window.clearTimeout(this._scroll_reset_timeout);
-
- this._scroll_reset_timeout = window.setTimeout(() => {
- if (elem) elem.removeClassName("forbid-smooth-scroll");
- }, 250)
- } else {
- elem.removeClassName("forbid-smooth-scroll");
- }
-
- elem.scrollTop += elem.offsetHeight * offset * 0.99;
+ scrollByPages: function (page_offset) {
+ App.Scrollable.scrollByPages($("headlines-frame"), page_offset);
+ },
+ scroll: function (offset) {
+ App.Scrollable.scroll($("headlines-frame"), offset);
},
initHeadlinesMenu: function () {
if (!dijit.byId("headlinesMenu")) {
diff --git a/js/PrefFilterTree.js b/js/PrefFilterTree.js
index a7c7464fb..5fe0714c0 100644
--- a/js/PrefFilterTree.js
+++ b/js/PrefFilterTree.js
@@ -149,9 +149,7 @@ define(["dojo/_base/declare", "dojo/dom-construct", "lib/CheckBoxTree"], functio
style: "width: 600px",
test: function () {
- const query = "backend.php?" + dojo.formToQuery("filter_edit_form") + "&savemode=test";
-
- Filters.editFilterTest(query);
+ Filters.editFilterTest(dojo.formToObject("filter_edit_form"));
},
selectRules: function (select) {
Lists.select("filterDlg_Matches", select);
diff --git a/js/form/ValidationTextArea.js b/js/form/ValidationTextArea.js
new file mode 100644
index 000000000..a7993f716
--- /dev/null
+++ b/js/form/ValidationTextArea.js
@@ -0,0 +1,33 @@
+// https://stackoverflow.com/questions/19317258/how-to-use-dijit-textarea-validation-dojo-1-9
+
+define(["dojo/_base/declare", "dojo/_base/lang", "dijit/form/SimpleTextarea", "dijit/form/ValidationTextBox"],
+ function(declare, lang, SimpleTextarea, ValidationTextBox) {
+
+ return declare('fox.form.ValidationTextArea', [SimpleTextarea, ValidationTextBox], {
+ constructor: function(params){
+ this.constraints = {};
+ this.baseClass += ' dijitValidationTextArea';
+ },
+ templateString: "<textarea ${!nameAttrSetting} data-dojo-attach-point='focusNode,containerNode,textbox' autocomplete='off'></textarea>",
+ validator: function(value, constraints) {
+ //console.log(this, value, constraints);
+
+ if (this.required && this._isEmpty(value))
+ return false;
+
+ if (this.validregexp) {
+ try {
+ new RegExp("/" + value + "/");
+ } catch (e) {
+ return false;
+ }
+ }
+
+ return value.match(new RegExp(this._computeRegexp(constraints)));
+
+ /*return (new RegExp("^(?:" + this._computeRegexp(constraints) + ")"+(this.required?"":"?")+"$",["m"])).test(value) &&
+ (!this.required || !this._isEmpty(value)) &&
+ (this._isEmpty(value) || this.parse(value, constraints) !== undefined); // Boolean*/
+ }
+ })
+ });
diff --git a/js/prefs.js b/js/prefs.js
index 944e49258..96fcbd948 100755
--- a/js/prefs.js
+++ b/js/prefs.js
@@ -55,6 +55,7 @@ require(["dojo/_base/kernel",
"fox/PrefFilterTree",
"fox/PrefLabelTree",
"fox/Toolbar",
+ "fox/form/ValidationTextArea",
"fox/form/Select",
"fox/form/ComboButton",
"fox/form/DropDownButton"], function (dojo, declare, ready, parser, AppBase) {
diff --git a/js/tt-rss.js b/js/tt-rss.js
index d45dd5748..7c325cedf 100644
--- a/js/tt-rss.js
+++ b/js/tt-rss.js
@@ -56,6 +56,7 @@ require(["dojo/_base/kernel",
"fox/FeedStoreModel",
"fox/FeedTree",
"fox/Toolbar",
+ "fox/form/ValidationTextArea",
"fox/form/Select",
"fox/form/ComboButton",
"fox/form/DropDownButton"], function (dojo, declare, ready, parser, AppBase) {
@@ -203,15 +204,12 @@ require(["dojo/_base/kernel",
return Feeds.reloadCurrent('');
},
- isCombinedMode: function() {
- return App.getInitParam("combined_display_mode");
- },
hotkeyHandler: function(event) {
if (event.target.nodeName == "INPUT" || event.target.nodeName == "TEXTAREA") return;
// Arrow buttons and escape are not reported via keypress, handle them via keydown.
- // escape = 27, left = 37, up = 38, right = 39, down = 40, pgup = 33, pgdn = 34
- if (event.type == "keydown" && event.which != 27 && (event.which < 33 || event.which > 40)) return;
+ // escape = 27, left = 37, up = 38, right = 39, down = 40, pgup = 33, pgdn = 34, insert = 45, delete = 46
+ if (event.type == "keydown" && event.which != 27 && (event.which < 33 || event.which > 46)) return;
const action_name = App.keyeventToAction(event);
@@ -284,26 +282,35 @@ require(["dojo/_base/kernel",
if (rv) Feeds.open({feed: rv[0], is_cat: rv[1], delayed: true})
};
this.hotkey_actions["next_article_or_scroll"] = function (event) {
- Headlines.move('next', {event: event});
+ if (App.isCombinedMode())
+ Headlines.scroll(Headlines.line_scroll_offset, event);
+ else
+ Headlines.move('next');
};
this.hotkey_actions["prev_article_or_scroll"] = function (event) {
- Headlines.move('prev', {event: event});
+ if (App.isCombinedMode())
+ Headlines.scroll(-Headlines.line_scroll_offset, event);
+ else
+ Headlines.move('prev');
};
- this.hotkey_actions["next_article_noscroll"] = function (event) {
- Headlines.move('next', {noscroll: true, event: event});
+ this.hotkey_actions["next_article_noscroll"] = function () {
+ Headlines.move('next');
};
- this.hotkey_actions["prev_article_noscroll"] = function (event) {
- Headlines.move('prev', {noscroll: true, event: event});
+ this.hotkey_actions["prev_article_noscroll"] = function () {
+ Headlines.move('prev');
};
- this.hotkey_actions["next_article_noexpand"] = function (event) {
- Headlines.move('next', {noscroll: true, noexpand: true, event: event});
+ this.hotkey_actions["next_article_noexpand"] = function () {
+ Headlines.move('next', {no_expand: true});
};
- this.hotkey_actions["prev_article_noexpand"] = function (event) {
- Headlines.move('prev', {noscroll: true, noexpand: true, event: event});
+ this.hotkey_actions["prev_article_noexpand"] = function () {
+ Headlines.move('prev', {no_expand: true});
};
this.hotkey_actions["search_dialog"] = function () {
Feeds.search();
};
+ this.hotkey_actions["cancel_search"] = function () {
+ Feeds.cancelSearch();
+ };
this.hotkey_actions["toggle_mark"] = function () {
Headlines.selectionToggleMarked();
};
@@ -331,28 +338,34 @@ require(["dojo/_base/kernel",
Headlines.catchupRelativeTo(0);
};
this.hotkey_actions["article_scroll_down"] = function (event) {
- const ctr = App.isCombinedMode() ? $("headlines-frame") : $("content-insert");
-
- if (ctr)
- Article.scroll(ctr.offsetHeight / 2, event);
+ if (App.isCombinedMode())
+ Headlines.scroll(Headlines.line_scroll_offset, event);
+ else
+ Article.scroll(Headlines.line_scroll_offset, event);
};
this.hotkey_actions["article_scroll_up"] = function (event) {
- const ctr = App.isCombinedMode() ? $("headlines-frame") : $("content-insert");
-
- if (ctr)
- Article.scroll(-ctr.offsetHeight / 2, event);
+ if (App.isCombinedMode())
+ Headlines.scroll(-Headlines.line_scroll_offset, event);
+ else
+ Article.scroll(-Headlines.line_scroll_offset, event);
};
- this.hotkey_actions["next_article_page"] = function (event) {
+ this.hotkey_actions["next_headlines_page"] = function (event) {
Headlines.scrollByPages(1, event);
};
- this.hotkey_actions["prev_article_page"] = function (event) {
+ this.hotkey_actions["prev_headlines_page"] = function (event) {
Headlines.scrollByPages(-1, event);
};
this.hotkey_actions["article_page_down"] = function (event) {
- Article.scrollByPages(1, event);
+ if (App.isCombinedMode())
+ Headlines.scrollByPages(1, event);
+ else
+ Article.scrollByPages(1, event);
};
this.hotkey_actions["article_page_up"] = function (event) {
- Article.scrollByPages(-1, event);
+ if (App.isCombinedMode())
+ Headlines.scrollByPages(-1, event);
+ else
+ Article.scrollByPages(-1, event);
};
this.hotkey_actions["close_article"] = function () {
if (App.isCombinedMode()) {