]> git.wh0rd.org Git - tt-rss.git/blob - js/viewfeed.js
aee32b1d126c90f450a5401e6bc0de6f662517da
[tt-rss.git] / js / viewfeed.js
1 /* global dijit, __ */
2
3 let _active_article_id = 0;
4
5 let vgroup_last_feed = false;
6 let post_under_pointer = false;
7
8 let last_requested_article = 0;
9
10 let catchup_id_batch = [];
11 let catchup_timeout_id = false;
12
13 let cids_requested = [];
14 let loaded_article_ids = [];
15 let _last_headlines_update = 0;
16 let _headlines_scroll_offset = 0;
17 let current_first_id = 0;
18 let last_search_query;
19
20 let _catchup_request_sent = false;
21
22 let has_storage = 'sessionStorage' in window && window['sessionStorage'] !== null;
23
24 function headlines_callback2(transport, offset, background, infscroll_req) {
25         const reply = handle_rpc_json(transport);
26
27         console.log("headlines_callback2 [offset=" + offset + "] B:" + background + " I:" + infscroll_req);
28
29         if (background)
30                 return;
31
32         var is_cat = false;
33         var feed_id = false;
34
35         if (reply) {
36
37                 is_cat = reply['headlines']['is_cat'];
38                 feed_id = reply['headlines']['id'];
39                 last_search_query = reply['headlines']['search_query'];
40
41                 if (feed_id != -7 && (feed_id != getActiveFeedId() || is_cat != activeFeedIsCat()))
42                         return;
43
44                 try {
45                         if (infscroll_req == false) {
46                                 $("headlines-frame").scrollTop = 0;
47
48                                 $("floatingTitle").style.visibility = "hidden";
49                                 $("floatingTitle").setAttribute("data-article-id", 0);
50                                 $("floatingTitle").innerHTML = "";
51                         }
52                 } catch (e) { }
53
54                 $("headlines-frame").removeClassName("cdm");
55                 $("headlines-frame").removeClassName("normal");
56
57                 $("headlines-frame").addClassName(isCdmMode() ? "cdm" : "normal");
58
59                 const headlines_count = reply['headlines-info']['count'];
60
61                 vgroup_last_feed = reply['headlines-info']['vgroup_last_feed'];
62
63                 if (parseInt(headlines_count) < 30) {
64                         _infscroll_disable = 1;
65                 } else {
66                         _infscroll_disable = 0;
67                 }
68
69                 current_first_id = reply['headlines']['first_id'];
70                 const counters = reply['counters'];
71                 const articles = reply['articles'];
72
73                 if (infscroll_req == false) {
74                         loaded_article_ids = [];
75
76                         dojo.html.set($("headlines-toolbar"),
77                                         reply['headlines']['toolbar'],
78                                         {parseContent: true});
79
80                         $("headlines-frame").innerHTML = '';
81
82                         let tmp = document.createElement("div");
83                         tmp.innerHTML = reply['headlines']['content'];
84                         dojo.parser.parse(tmp);
85
86                         while (tmp.hasChildNodes()) {
87                                 var row = tmp.removeChild(tmp.firstChild);
88
89                                 if (loaded_article_ids.indexOf(row.id) == -1 || row.hasClassName("feed-title")) {
90                                         dijit.byId("headlines-frame").domNode.appendChild(row);
91
92                                         loaded_article_ids.push(row.id);
93                                 }
94                         }
95
96                         let hsp = $("headlines-spacer");
97                         if (!hsp) hsp = new Element("DIV", {"id": "headlines-spacer"});
98                         dijit.byId('headlines-frame').domNode.appendChild(hsp);
99
100                         initHeadlinesMenu();
101
102                         if (_infscroll_disable)
103                                 hsp.innerHTML = "<a href='#' onclick='openNextUnreadFeed()'>" +
104                                         __("Click to open next unread feed.") + "</a>";
105
106                         if (_search_query) {
107                                 $("feed_title").innerHTML += "<span id='cancel_search'>" +
108                                         " (<a href='#' onclick='cancelSearch()'>" + __("Cancel search") + "</a>)" +
109                                         "</span>";
110                         }
111
112                 } else if (headlines_count > 0 && feed_id == getActiveFeedId() && is_cat == activeFeedIsCat()) {
113                                 console.log("adding some more headlines: " + headlines_count);
114
115                                 const c = dijit.byId("headlines-frame");
116                                 const ids = getSelectedArticleIds2();
117
118                                 let hsp = $("headlines-spacer");
119
120                                 if (hsp)
121                                         c.domNode.removeChild(hsp);
122
123                                 let tmp = document.createElement("div");
124                                 tmp.innerHTML = reply['headlines']['content'];
125                                 dojo.parser.parse(tmp);
126
127                                 while (tmp.hasChildNodes()) {
128                                         let row = tmp.removeChild(tmp.firstChild);
129
130                                         if (loaded_article_ids.indexOf(row.id) == -1 || row.hasClassName("feed-title")) {
131                                                 dijit.byId("headlines-frame").domNode.appendChild(row);
132
133                                                 loaded_article_ids.push(row.id);
134                                         }
135                                 }
136
137                                 if (!hsp) hsp = new Element("DIV", {"id": "headlines-spacer"});
138                                 c.domNode.appendChild(hsp);
139
140                                 if (headlines_count < 30) _infscroll_disable = true;
141
142                                 console.log("restore selected ids: " + ids);
143
144                                 for (let i = 0; i < ids.length; i++) {
145                                         markHeadline(ids[i]);
146                                 }
147
148                                 initHeadlinesMenu();
149
150                                 if (_infscroll_disable) {
151                                         hsp.innerHTML = "<a href='#' onclick='openNextUnreadFeed()'>" +
152                                         __("Click to open next unread feed.") + "</a>";
153                                 }
154
155                         } else {
156                                 console.log("no new headlines received");
157
158                                 const first_id_changed = reply['headlines']['first_id_changed'];
159                                 console.log("first id changed:" + first_id_changed);
160
161                                 let hsp = $("headlines-spacer");
162
163                                 if (hsp) {
164                                         if (first_id_changed) {
165                                                 hsp.innerHTML = "<a href='#' onclick='viewCurrentFeed()'>" +
166                                                 __("New articles found, reload feed to continue.") + "</a>";
167                                         } else {
168                                                 hsp.innerHTML = "<a href='#' onclick='openNextUnreadFeed()'>" +
169                                                 __("Click to open next unread feed.") + "</a>";
170                                         }
171
172                                 }
173
174                         }
175
176                 if (articles) {
177                         for (let i = 0; i < articles.length; i++) {
178                                 const a_id = articles[i]['id'];
179                                 cache_set("article:" + a_id, articles[i]['content']);
180                         }
181                 } else {
182                         console.log("no cached articles received");
183                 }
184
185                 if (counters)
186                         parse_counters(counters);
187                 else
188                         request_counters();
189
190         } else {
191                 console.error("Invalid object received: " + transport.responseText);
192                 dijit.byId("headlines-frame").attr('content', "<div class='whiteBox'>" +
193                                 __('Could not update headlines (invalid object received - see error console for details)') +
194                                 "</div>");
195         }
196
197         _infscroll_request_sent = 0;
198         _last_headlines_update = new Date().getTime();
199
200         unpackVisibleHeadlines();
201
202         // if we have some more space in the buffer, why not try to fill it
203
204         if (!_infscroll_disable && $("headlines-spacer") &&
205                         $("headlines-spacer").offsetTop < $("headlines-frame").offsetHeight) {
206
207                 window.setTimeout(function() {
208                         loadMoreHeadlines();
209                 }, 250);
210         }
211
212         notify("");
213 }
214
215 function render_article(article) {
216         cleanup_memory("content-insert");
217
218         dijit.byId("headlines-wrap-inner").addChild(
219                         dijit.byId("content-insert"));
220
221         const c = dijit.byId("content-insert");
222
223         try {
224                 c.domNode.scrollTop = 0;
225         } catch (e) { }
226
227         c.attr('content', article);
228         PluginHost.run(PluginHost.HOOK_ARTICLE_RENDERED, c.domNode);
229
230         correctHeadlinesOffset(getActiveArticleId());
231
232         try {
233                 c.focus();
234         } catch (e) { }
235 }
236
237 function showArticleInHeadlines(id, noexpand) {
238         const row = $("RROW-" + id);
239         if (!row) return;
240
241         if (!noexpand)
242                 row.removeClassName("Unread");
243
244         row.addClassName("active");
245
246         selectArticles('none');
247
248         markHeadline(id);
249 }
250
251 function article_callback2(transport, id) {
252         console.log("article_callback2 " + id);
253
254         const reply = handle_rpc_json(transport);
255
256         if (reply) {
257
258                 reply.each(function(article) {
259                         if (getActiveArticleId() == article['id']) {
260                                 render_article(article['content']);
261                         }
262                         cids_requested.remove(article['id']);
263
264                         cache_set("article:" + article['id'], article['content']);
265                 });
266
267         } else {
268                 console.error("Invalid object received: " + transport.responseText);
269
270                 render_article("<div class='whiteBox'>" +
271                                 __('Could not display article (invalid object received - see error console for details)') + "</div>");
272         }
273
274         const unread_in_buffer = $$("#headlines-frame > div[id*=RROW][class*=Unread]").length;
275         request_counters(unread_in_buffer == 0);
276
277         notify("");
278 }
279
280 function view(id, activefeed, noexpand) {
281         const oldrow = $("RROW-" + getActiveArticleId());
282         if (oldrow) oldrow.removeClassName("active");
283
284         const crow = $("RROW-" + id);
285
286         if (!crow) return;
287         if (noexpand) {
288                 setActiveArticleId(id);
289                 showArticleInHeadlines(id, noexpand);
290                 return;
291         }
292
293         console.log("loading article: " + id);
294
295         const cached_article = cache_get("article:" + id);
296
297         console.log("cache check result: " + (cached_article != false));
298
299         const query = {op: "article", method: "view", id: id};
300
301         const neighbor_ids = getRelativePostIds(id);
302
303         /* only request uncached articles */
304
305         const cids_to_request = [];
306
307         for (let i = 0; i < neighbor_ids.length; i++) {
308                 if (cids_requested.indexOf(neighbor_ids[i]) == -1)
309                         if (!cache_get("article:" + neighbor_ids[i])) {
310                                 cids_to_request.push(neighbor_ids[i]);
311                                 cids_requested.push(neighbor_ids[i]);
312                         }
313         }
314
315         console.log("additional ids: " + cids_to_request.toString());
316
317         query.cids = cids_to_request.toString();
318
319         const article_is_unread = crow.hasClassName("Unread");
320
321         setActiveArticleId(id);
322         showArticleInHeadlines(id);
323
324         if (cached_article && article_is_unread) {
325                 query.mode = "prefetch";
326                 render_article(cached_article);
327         } else if (cached_article) {
328                 query.mode = "prefetch_old";
329                 render_article(cached_article);
330
331                 // if we don't need to request any relative ids, we might as well skip
332                 // the server roundtrip altogether
333                 if (cids_to_request.length == 0) {
334                         return;
335                 }
336         }
337
338         last_requested_article = id;
339
340         console.log(query);
341
342         if (article_is_unread) {
343                 decrementFeedCounter(getActiveFeedId(), activeFeedIsCat());
344         }
345
346         xhrPost("backend.php", query, (transport) => {
347                 article_callback2(transport, id);
348         })
349
350         return false;
351
352 }
353
354 function toggleMark(id, client_only) {
355         const query = { op: "rpc", id: id, method: "mark" };
356
357         const row = $("RROW-" + id);
358         if (!row) return;
359
360         const imgs = [];
361
362         const row_imgs = row.getElementsByClassName("markedPic");
363
364         for (let i = 0; i < row_imgs.length; i++)
365                 imgs.push(row_imgs[i]);
366
367         const ft = $("floatingTitle");
368
369         if (ft && ft.getAttribute("data-article-id") == id) {
370                 const fte = ft.getElementsByClassName("markedPic");
371
372                 for (var i = 0; i < fte.length; i++)
373                         imgs.push(fte[i]);
374         }
375
376         for (i = 0; i < imgs.length; i++) {
377                 const img = imgs[i];
378
379                 if (!row.hasClassName("marked")) {
380                         img.src = img.src.replace("mark_unset", "mark_set");
381                         query.mark = 1;
382                 } else {
383                         img.src = img.src.replace("mark_set", "mark_unset");
384                         query.mark = 0;
385                 }
386         }
387
388         row.toggleClassName("marked");
389
390         if (!client_only)
391                 xhrPost("backend.php", query, (transport) => {
392                         handle_rpc_json(transport);
393                 });
394         }
395
396 function togglePub(id, client_only, no_effects, note) {
397         const query = { op: "rpc", id: id, method: "publ" };
398
399         if (note != undefined) {
400                 query.note = note;
401         } else {
402                 query.note = "undefined";
403         }
404
405         const row = $("RROW-" + id);
406         if (!row) return;
407
408         const imgs = [];
409
410         const row_imgs = row.getElementsByClassName("pubPic");
411
412         for (let i = 0; i < row_imgs.length; i++)
413                 imgs.push(row_imgs[i]);
414
415         const ft = $("floatingTitle");
416
417         if (ft && ft.getAttribute("data-article-id") == id) {
418                 const fte = ft.getElementsByClassName("pubPic");
419
420                 for (let i = 0; i < fte.length; i++)
421                         imgs.push(fte[i]);
422         }
423
424         for (let i = 0; i < imgs.length; i++) {
425                 const img = imgs[i];
426
427                 if (!row.hasClassName("published") || note != undefined) {
428                         img.src = img.src.replace("pub_unset", "pub_set");
429                         query.pub = 1;
430                 } else {
431                         img.src = img.src.replace("pub_set", "pub_unset");
432                         query.pub = 0;
433                 }
434         }
435
436         if (note != undefined)
437                 row.addClassName("published");
438         else
439                 row.toggleClassName("published");
440
441         if (!client_only)
442                 xhrPost("backend.php", query, (transport) => {
443                                 handle_rpc_json(transport);
444                 });
445 }
446
447 function moveToPost(mode, noscroll, noexpand) {
448         const rows = getLoadedArticleIds();
449
450         let prev_id = false;
451         let next_id = false;
452
453         if (!$('RROW-' + getActiveArticleId())) {
454                 setActiveArticleId(0);
455         }
456
457         if (!getActiveArticleId()) {
458                 console.log('hurr', rows[0]);
459
460                 next_id = rows[0];
461                 prev_id = rows[rows.length-1]
462         } else {
463                 for (let i = 0; i < rows.length; i++) {
464                         if (rows[i] == getActiveArticleId()) {
465
466                                 // Account for adjacent identical article ids.
467                                 if (i > 0) prev_id = rows[i-1];
468
469                                 for (let j = i+1; j < rows.length; j++) {
470                                         if (rows[j] != getActiveArticleId()) {
471                                                 next_id = rows[j];
472                                                 break;
473                                         }
474                                 }
475                                 break;
476                         }
477                 }
478         }
479
480         console.log("cur: " + getActiveArticleId() + " next: " + next_id);
481
482         if (mode == "next") {
483                 if (next_id || getActiveArticleId()) {
484                         if (isCdmMode()) {
485
486                                 var article = $("RROW-" + getActiveArticleId());
487                                 var ctr = $("headlines-frame");
488
489                                 if (!noscroll && article && article.offsetTop + article.offsetHeight >
490                                                 ctr.scrollTop + ctr.offsetHeight) {
491
492                                         scrollArticle(ctr.offsetHeight/4);
493
494                                 } else if (next_id) {
495                                         cdmScrollToArticleId(next_id, true);
496                                 }
497
498                         } else if (next_id) {
499                                 correctHeadlinesOffset(next_id);
500                                 view(next_id, getActiveFeedId(), noexpand);
501                         }
502                 }
503         }
504
505         if (mode == "prev") {
506                 if (prev_id || getActiveArticleId()) {
507                         if (isCdmMode()) {
508
509                                 var article = $("RROW-" + getActiveArticleId());
510                                 const prev_article = $("RROW-" + prev_id);
511                                 var ctr = $("headlines-frame");
512
513                                 if (!noscroll && article && article.offsetTop < ctr.scrollTop) {
514                                         scrollArticle(-ctr.offsetHeight/3);
515                                 } else if (!noscroll && prev_article &&
516                                                 prev_article.offsetTop < ctr.scrollTop) {
517                                         scrollArticle(-ctr.offsetHeight/4);
518                                 } else if (prev_id) {
519                                         cdmScrollToArticleId(prev_id, noscroll);
520                                 }
521
522                         } else if (prev_id) {
523                                 correctHeadlinesOffset(prev_id);
524                                 view(prev_id, getActiveFeedId(), noexpand);
525                         }
526                 }
527         }
528
529 }
530
531 function toggleSelected(id, force_on) {
532         const row = $("RROW-" + id);
533
534         if (row) {
535                 const cb = dijit.getEnclosingWidget(
536                                 row.getElementsByClassName("rchk")[0]);
537
538                 if (row.hasClassName('Selected') && !force_on) {
539                         row.removeClassName('Selected');
540                         if (cb) cb.attr("checked", false);
541                 } else {
542                         row.addClassName('Selected');
543                         if (cb) cb.attr("checked", true);
544                 }
545         }
546
547         updateSelectedPrompt();
548 }
549
550 function updateSelectedPrompt() {
551         const count = getSelectedArticleIds2().length;
552         const elem = $("selected_prompt");
553
554         if (elem) {
555                 elem.innerHTML = ngettext("%d article selected",
556                                 "%d articles selected", count).replace("%d", count);
557
558                 if (count > 0)
559                         Element.show(elem);
560                 else
561                         Element.hide(elem);
562         }
563
564 }
565
566 function toggleUnread(id, cmode) {
567         const row = $("RROW-" + id);
568         if (row) {
569                 const tmpClassName = row.className;
570
571                 if (cmode == undefined || cmode == 2) {
572                         if (row.hasClassName("Unread")) {
573                                 row.removeClassName("Unread");
574
575                         } else {
576                                 row.addClassName("Unread");
577                         }
578
579                 } else if (cmode == 0) {
580
581                         row.removeClassName("Unread");
582
583                 } else if (cmode == 1) {
584                         row.addClassName("Unread");
585                 }
586
587                 if (tmpClassName != row.className) {
588                         if (cmode == undefined) cmode = 2;
589
590                         const query = {op: "rpc", method: "catchupSelected",
591                                 cmode: cmode, ids: id};
592
593                         xhrPost("backend.php", query, (transport) => {
594                                         handle_rpc_json(transport);
595
596                         });
597                 }
598         }
599 }
600
601 function selectionRemoveLabel(id, ids) {
602         if (!ids) ids = getSelectedArticleIds2();
603
604         if (ids.length == 0) {
605                 alert(__("No articles are selected."));
606                 return;
607         }
608
609         const query = { op: "article", method: "removeFromLabel",
610                 ids: ids.toString(), lid: id };
611
612         xhrPost("backend.php", query, (transport) => {
613                 handle_rpc_json(transport);
614                 show_labels_in_headlines(transport);
615         });
616 }
617
618 function selectionAssignLabel(id, ids) {
619         if (!ids) ids = getSelectedArticleIds2();
620
621         if (ids.length == 0) {
622                 alert(__("No articles are selected."));
623                 return;
624         }
625
626         const query = { op: "article", method: "assignToLabel",
627                 ids: ids.toString(), lid: id };
628
629         xhrPost("backend.php", query, (transport) => {
630                 handle_rpc_json(transport);
631                 show_labels_in_headlines(transport);
632         });
633 }
634
635 function selectionToggleUnread(set_state, callback, no_error, ids) {
636         const rows = ids ? ids : getSelectedArticleIds2();
637
638         if (rows.length == 0 && !no_error) {
639                 alert(__("No articles are selected."));
640                 return;
641         }
642
643         for (let i = 0; i < rows.length; i++) {
644                 const row = $("RROW-" + rows[i]);
645                 if (row) {
646                         if (set_state == undefined) {
647                                 if (row.hasClassName("Unread")) {
648                                         row.removeClassName("Unread");
649                                 } else {
650                                         row.addClassName("Unread");
651                                 }
652                         }
653
654                         if (set_state == false) {
655                                 row.removeClassName("Unread");
656                         }
657
658                         if (set_state == true) {
659                                 row.addClassName("Unread");
660                         }
661                 }
662         }
663
664         updateFloatingTitle(true);
665
666         if (rows.length > 0) {
667
668                 let cmode = "";
669
670                 if (set_state == undefined) {
671                         cmode = "2";
672                 } else if (set_state == true) {
673                         cmode = "1";
674                 } else if (set_state == false) {
675                         cmode = "0";
676                 }
677
678                 const query = {op: "rpc", method: "catchupSelected",
679                         cmode: cmode, ids: rows.toString() };
680
681                 notify_progress("Loading, please wait...");
682
683                 xhrPost("backend.php", query, (transport) => {
684                         handle_rpc_json(transport);
685                         if (callback) callback(transport);
686                 });
687
688         }
689 }
690
691 // sel_state ignored
692 function selectionToggleMarked(sel_state, callback, no_error, ids) {
693         const rows = ids ? ids : getSelectedArticleIds2();
694
695         if (rows.length == 0 && !no_error) {
696                 alert(__("No articles are selected."));
697                 return;
698         }
699
700         for (let i = 0; i < rows.length; i++) {
701                 toggleMark(rows[i], true, true);
702         }
703
704         if (rows.length > 0) {
705                 const query = { op: "rpc", method: "markSelected",
706                         ids:  rows.toString(), cmode: 2 };
707
708                 xhrPost("backend.php", query, (transport) => {
709                         handle_rpc_json(transport);
710                         if (callback) callback(transport);
711                 });
712         }
713 }
714
715 // sel_state ignored
716 function selectionTogglePublished(sel_state, callback, no_error, ids) {
717         const rows = ids ? ids : getSelectedArticleIds2();
718
719         if (rows.length == 0 && !no_error) {
720                 alert(__("No articles are selected."));
721                 return;
722         }
723
724         for (let i = 0; i < rows.length; i++) {
725                 togglePub(rows[i], true, true);
726         }
727
728         if (rows.length > 0) {
729                 const query = { op: "rpc", method: "publishSelected",
730                         ids:  rows.toString(), cmode: 2 };
731
732                 xhrPost("backend.php", query, (transport) => {
733                         handle_rpc_json(transport);
734                         if (callback) callback(transport);
735                 });
736         }
737 }
738
739 function getSelectedArticleIds2() {
740
741         const rv = [];
742
743         $$("#headlines-frame > div[id*=RROW][class*=Selected]").each(
744                 function(child) {
745                         rv.push(child.getAttribute("data-article-id"));
746                 });
747
748         return rv;
749 }
750
751 function getLoadedArticleIds() {
752         const rv = [];
753
754         const children = $$("#headlines-frame > div[id*=RROW-]");
755
756         children.each(function(child) {
757                 if (Element.visible(child)) {
758                         rv.push(child.getAttribute("data-article-id"));
759                 }
760         });
761
762         return rv;
763
764 }
765
766 // mode = all,none,unread,invert,marked,published
767 function selectArticles(mode, query) {
768         if (!query) query = "#headlines-frame > div[id*=RROW]";
769
770         const children = $$(query);
771
772         children.each(function(child) {
773                 //const id = child.getAttribute("data-article-id");
774
775                 const cb = dijit.getEnclosingWidget(
776                                 child.getElementsByClassName("rchk")[0]);
777
778                 if (mode == "all") {
779                         child.addClassName("Selected");
780                         if (cb) cb.attr("checked", true);
781                 } else if (mode == "unread") {
782                         if (child.hasClassName("Unread")) {
783                                 child.addClassName("Selected");
784                                 if (cb) cb.attr("checked", true);
785                         } else {
786                                 child.removeClassName("Selected");
787                                 if (cb) cb.attr("checked", false);
788                         }
789                 } else if (mode == "marked") {
790                         if (child.hasClassName("marked")) {
791                                 child.addClassName("Selected");
792                                 if (cb) cb.attr("checked", true);
793                         } else {
794                                 child.removeClassName("Selected");
795                                 if (cb) cb.attr("checked", false);
796                         }
797                 } else if (mode == "published") {
798                         if (child.hasClassName("published")) {
799                                 child.addClassName("Selected");
800                                 if (cb) cb.attr("checked", true);
801                         } else {
802                                 child.removeClassName("Selected");
803                                 if (cb) cb.attr("checked", false);
804                         }
805
806                 } else if (mode == "invert") {
807                         if (child.hasClassName("Selected")) {
808                                 child.removeClassName("Selected");
809                                 if (cb) cb.attr("checked", false);
810                         } else {
811                                 child.addClassName("Selected");
812                                 if (cb) cb.attr("checked", true);
813                         }
814
815                 } else {
816                         child.removeClassName("Selected");
817                         if (cb) cb.attr("checked", false);
818                 }
819         });
820
821         updateSelectedPrompt();
822 }
823
824 function deleteSelection() {
825
826         const rows = getSelectedArticleIds2();
827
828         if (rows.length == 0) {
829                 alert(__("No articles are selected."));
830                 return;
831         }
832
833         const fn = getFeedName(getActiveFeedId(), activeFeedIsCat());
834         let str;
835
836         if (getActiveFeedId() != 0) {
837                 str = ngettext("Delete %d selected article in %s?", "Delete %d selected articles in %s?", rows.length);
838         } else {
839                 str = ngettext("Delete %d selected article?", "Delete %d selected articles?", rows.length);
840         }
841
842         str = str.replace("%d", rows.length);
843         str = str.replace("%s", fn);
844
845         if (getInitParam("confirm_feed_catchup") == 1 && !confirm(str)) {
846                 return;
847         }
848
849         const query = { op: "rpc", method: "delete", ids: rows.toString() };
850
851         xhrPost("backend.php", query, (transport) => {
852                 handle_rpc_json(transport);
853                 viewCurrentFeed();
854         });
855 }
856
857 function archiveSelection() {
858
859         const rows = getSelectedArticleIds2();
860
861         if (rows.length == 0) {
862                 alert(__("No articles are selected."));
863                 return;
864         }
865
866         const fn = getFeedName(getActiveFeedId(), activeFeedIsCat());
867         let str;
868         let op;
869
870         if (getActiveFeedId() != 0) {
871                 str = ngettext("Archive %d selected article in %s?", "Archive %d selected articles in %s?", rows.length);
872                 op = "archive";
873         } else {
874                 str = ngettext("Move %d archived article back?", "Move %d archived articles back?", rows.length);
875
876                 str += " " + __("Please note that unstarred articles might get purged on next feed update.");
877
878                 op = "unarchive";
879         }
880
881         str = str.replace("%d", rows.length);
882         str = str.replace("%s", fn);
883
884         if (getInitParam("confirm_feed_catchup") == 1 && !confirm(str)) {
885                 return;
886         }
887
888         for (let i = 0; i < rows.length; i++) {
889                 cache_delete("article:" + rows[i]);
890         }
891
892         const query = {op: "rpc", method: op, ids: rows.toString()};
893
894         xhrPost("backend.php", query, (transport) => {
895                 handle_rpc_json(transport);
896                 viewCurrentFeed();
897         });
898 }
899
900 function catchupSelection() {
901
902         const rows = getSelectedArticleIds2();
903
904         if (rows.length == 0) {
905                 alert(__("No articles are selected."));
906                 return;
907         }
908
909         const fn = getFeedName(getActiveFeedId(), activeFeedIsCat());
910
911         let str = ngettext("Mark %d selected article in %s as read?", "Mark %d selected articles in %s as read?", rows.length);
912
913         str = str.replace("%d", rows.length);
914         str = str.replace("%s", fn);
915
916         if (getInitParam("confirm_feed_catchup") == 1 && !confirm(str)) {
917                 return;
918         }
919
920         selectionToggleUnread(false, 'viewCurrentFeed()', true);
921 }
922
923 function editArticleTags(id) {
924         const query = "backend.php?op=article&method=editArticleTags&param=" + param_escape(id);
925
926         if (dijit.byId("editTagsDlg"))
927                 dijit.byId("editTagsDlg").destroyRecursive();
928
929         const dialog = new dijit.Dialog({
930                 id: "editTagsDlg",
931                 title: __("Edit article Tags"),
932                 style: "width: 600px",
933                 execute: function() {
934                         if (this.validate()) {
935                                 const query = dojo.objectToQuery(this.attr('value'));
936
937                                 notify_progress("Saving article tags...", true);
938
939                                 xhrPost("backend.php", this.attr('value'), (transport) => {
940                                         try {
941                                                 notify('');
942                                                 dialog.hide();
943
944                                                 const data = JSON.parse(transport.responseText);
945
946                                                 if (data) {
947                                                         const id = data.id;
948
949                                                         const tags = $("ATSTR-" + id);
950                                                         const tooltip = dijit.byId("ATSTRTIP-" + id);
951
952                                                         if (tags) tags.innerHTML = data.content;
953                                                         if (tooltip) tooltip.attr('label', data.content_full);
954                                                 }
955                                         } catch (e) {
956                                                 exception_error(e);
957                                         }
958                                 });
959                         }
960                 },
961                 href: query
962         });
963
964         var tmph = dojo.connect(dialog, 'onLoad', function() {
965                 dojo.disconnect(tmph);
966
967                 new Ajax.Autocompleter('tags_str', 'tags_choices',
968                    "backend.php?op=article&method=completeTags",
969                    { tokens: ',', paramName: "search" });
970         });
971
972         dialog.show();
973
974 }
975
976 function cdmScrollToArticleId(id, force) {
977         const ctr = $("headlines-frame");
978         const e = $("RROW-" + id);
979
980         if (!e || !ctr) return;
981
982         if (force || e.offsetTop+e.offsetHeight > (ctr.scrollTop+ctr.offsetHeight) ||
983                         e.offsetTop < ctr.scrollTop) {
984
985                 // expanded cdm has a 4px margin now
986                 ctr.scrollTop = parseInt(e.offsetTop) - 4;
987
988                 setActiveArticleId(id);
989
990                 // article is selected manually, set it read
991                 toggleUnread(id, 0);1
992         }
993 }
994
995 function setActiveArticleId(id) {
996         console.log("setActiveArticleId:" + id);
997
998         _active_article_id = id;
999         PluginHost.run(PluginHost.HOOK_ARTICLE_SET_ACTIVE, _active_article_id);
1000 }
1001
1002 function getActiveArticleId() {
1003         return _active_article_id;
1004 }
1005
1006 function postMouseIn(e, id) {
1007         post_under_pointer = id;
1008 }
1009
1010 function postMouseOut(id) {
1011         post_under_pointer = false;
1012 }
1013
1014 function unpackVisibleHeadlines() {
1015         if (!isCdmMode()) return;
1016
1017         const rows = $$("#headlines-frame div[id*=RROW][data-content]");
1018         const threshold = $("headlines-frame").scrollTop + $("headlines-frame").offsetHeight + 300;
1019
1020         for (let i = 0; i < rows.length; i++) {
1021                 const row = rows[i];
1022
1023                 if (row.offsetTop <= threshold) {
1024                         console.log("unpacking: " + row.id);
1025
1026                         const content = row.getAttribute("data-content");
1027
1028                         row.select(".content-inner")[0].innerHTML = content;
1029                         row.removeAttribute("data-content");
1030
1031                         PluginHost.run(PluginHost.HOOK_ARTICLE_RENDERED_CDM, row);
1032                 } else {
1033                         break;
1034                 }
1035         }
1036 }
1037
1038 function headlines_scroll_handler(e) {
1039         try {
1040
1041                 // rate-limit in case of smooth scrolling and similar abominations
1042                 if (Math.max(e.scrollTop, _headlines_scroll_offset) - Math.min(e.scrollTop, _headlines_scroll_offset) < 25) {
1043                         return;
1044                 }
1045
1046                 _headlines_scroll_offset = e.scrollTop;
1047
1048                 unpackVisibleHeadlines();
1049
1050                 // set topmost child in the buffer as active
1051                 if (isCdmMode() && getInitParam("cdm_auto_catchup") == 1 &&
1052                                 getSelectedArticleIds2().length <= 1) {
1053
1054                         const rows = $$("#headlines-frame > div[id*=RROW]");
1055
1056                         for (let i = 0; i < rows.length; i++) {
1057                                 const row = rows[i];
1058
1059                                 if ($("headlines-frame").scrollTop <= row.offsetTop &&
1060                                         row.offsetTop - $("headlines-frame").scrollTop < 100 &&
1061                                         row.getAttribute("data-article-id") != _active_article_id) {
1062
1063                                         if (_active_article_id) {
1064                                                 const row = $("RROW-" + _active_article_id);
1065                                                 if (row) row.removeClassName("active");
1066                                         }
1067
1068                                         _active_article_id = row.getAttribute("data-article-id");
1069                                         showArticleInHeadlines(_active_article_id, true);
1070                                         updateSelectedPrompt();
1071                                         break;
1072                                 }
1073                         }
1074                 }
1075
1076                 if (!_infscroll_disable) {
1077                         const hsp = $("headlines-spacer");
1078
1079                         if (hsp && hsp.offsetTop - 250 <= e.scrollTop + e.offsetHeight) {
1080
1081                                 hsp.innerHTML = "<span class='loading'><img src='images/indicator_tiny.gif'> " +
1082                                         __("Loading, please wait...") + "</span>";
1083
1084                                 loadMoreHeadlines();
1085                                 return;
1086
1087                         }
1088                 }
1089
1090                 if (isCdmMode()) {
1091                         updateFloatingTitle();
1092                 }
1093
1094                 if (getInitParam("cdm_auto_catchup") == 1) {
1095
1096                         let rows = $$("#headlines-frame > div[id*=RROW][class*=Unread]");
1097
1098                         for (let i = 0; i < rows.length; i++) {
1099                                 const row = rows[i];
1100                                 
1101                                 if ($("headlines-frame").scrollTop > (row.offsetTop + row.offsetHeight/2)) {
1102
1103                                         const id = row.getAttribute("data-article-id")
1104
1105                                         if (catchup_id_batch.indexOf(id) == -1)
1106                                                 catchup_id_batch.push(id);
1107
1108                                         //console.log("auto_catchup_batch: " + catchup_id_batch.toString());
1109                                 } else {
1110                                         break;
1111                                 }
1112                         }
1113
1114                         if (_infscroll_disable) {
1115                                 const row = $$("#headlines-frame div[id*=RROW]").last();
1116
1117                                 if (row && $("headlines-frame").scrollTop >
1118                                                 (row.offsetTop + row.offsetHeight - 50)) {
1119
1120                                         console.log("we seem to be at an end");
1121
1122                                         if (getInitParam("on_catchup_show_next_feed") == "1") {
1123                                                 openNextUnreadFeed();
1124                                         }
1125                                 }
1126                         }
1127                 }
1128
1129         } catch (e) {
1130                 console.warn("headlines_scroll_handler: " + e);
1131         }
1132 }
1133
1134 function openNextUnreadFeed() {
1135         const is_cat = activeFeedIsCat();
1136         const nuf = getNextUnreadFeed(getActiveFeedId(), is_cat);
1137         if (nuf) viewfeed({feed: nuf, is_cat: is_cat});
1138 }
1139
1140 function catchupBatchedArticles() {
1141         if (catchup_id_batch.length > 0 && !_infscroll_request_sent && !_catchup_request_sent) {
1142
1143                 console.log("catchupBatchedArticles, size=", catchup_id_batch.length);
1144
1145                 // make a copy of the array
1146                 const batch = catchup_id_batch.slice();
1147                 const query = { op: "rpc", method: "catchupSelected",
1148                         cmode: 0, ids: batch.toString() };
1149
1150                 _catchup_request_sent = true;
1151
1152                 xhrPost("backend.php", query, (transport) => {
1153                         const reply = handle_rpc_json(transport);
1154
1155                         _catchup_request_sent = false;
1156
1157                         if (reply) {
1158                                 const batch = reply.ids;
1159
1160                                 batch.each(function (id) {
1161                                         const elem = $("RROW-" + id);
1162                                         if (elem) elem.removeClassName("Unread");
1163                                         catchup_id_batch.remove(id);
1164                                 });
1165                         }
1166
1167                         updateFloatingTitle(true);
1168                 });
1169         }
1170 }
1171
1172 function catchupRelativeToArticle(below, id) {
1173
1174         if (!id) id = getActiveArticleId();
1175
1176         if (!id) {
1177                 alert(__("No article is selected."));
1178                 return;
1179         }
1180
1181         const visible_ids = getLoadedArticleIds();
1182
1183         const ids_to_mark = [];
1184
1185         if (!below) {
1186                 for (var i = 0; i < visible_ids.length; i++) {
1187                         if (visible_ids[i] != id) {
1188                                 var e = $("RROW-" + visible_ids[i]);
1189
1190                                 if (e && e.hasClassName("Unread")) {
1191                                         ids_to_mark.push(visible_ids[i]);
1192                                 }
1193                         } else {
1194                                 break;
1195                         }
1196                 }
1197         } else {
1198                 for (var i = visible_ids.length - 1; i >= 0; i--) {
1199                         if (visible_ids[i] != id) {
1200                                 var e = $("RROW-" + visible_ids[i]);
1201
1202                                 if (e && e.hasClassName("Unread")) {
1203                                         ids_to_mark.push(visible_ids[i]);
1204                                 }
1205                         } else {
1206                                 break;
1207                         }
1208                 }
1209         }
1210
1211         if (ids_to_mark.length == 0) {
1212                 alert(__("No articles found to mark"));
1213         } else {
1214                 const msg = ngettext("Mark %d article as read?", "Mark %d articles as read?", ids_to_mark.length).replace("%d", ids_to_mark.length);
1215
1216                 if (getInitParam("confirm_feed_catchup") != 1 || confirm(msg)) {
1217
1218                         for (var i = 0; i < ids_to_mark.length; i++) {
1219                                 var e = $("RROW-" + ids_to_mark[i]);
1220                                 e.removeClassName("Unread");
1221                         }
1222
1223                         const query = { op: "rpc", method: "catchupSelected",
1224                                 cmode: 0, ids: ids_to_mark.toString() };
1225
1226                         xhrPost("backend.php", query, (transport) => {
1227                                 handle_rpc_json(transport);
1228                         });
1229                 }
1230         }
1231 }
1232
1233 function getArticleUnderPointer() {
1234         return post_under_pointer;
1235 }
1236
1237 function scrollArticle(offset) {
1238         if (!isCdmMode()) {
1239                 const ci = $("content-insert");
1240                 if (ci) {
1241                         ci.scrollTop += offset;
1242                 }
1243         } else {
1244                 const hi = $("headlines-frame");
1245                 if (hi) {
1246                         hi.scrollTop += offset;
1247                 }
1248
1249         }
1250 }
1251
1252 function show_labels_in_headlines(transport) {
1253         const data = JSON.parse(transport.responseText);
1254
1255         if (data) {
1256                 data['info-for-headlines'].each(function (elem) {
1257                         $$(".HLLCTR-" + elem.id).each(function (ctr) {
1258                                 ctr.innerHTML = elem.labels;
1259                         });
1260                 });
1261         }
1262 }
1263
1264 function cdmClicked(event, id, in_body) {
1265         //var shift_key = event.shiftKey;
1266
1267         if (!event.ctrlKey && !event.metaKey) {
1268
1269                 let elem = $("RROW-" + getActiveArticleId());
1270
1271                 if (elem) elem.removeClassName("active");
1272
1273                 selectArticles("none");
1274                 toggleSelected(id);
1275
1276                 elem = $("RROW-" + id);
1277                 const article_is_unread = elem.hasClassName("Unread");
1278
1279                 elem.removeClassName("Unread");
1280                 elem.addClassName("active");
1281
1282                 setActiveArticleId(id);
1283
1284                 if (article_is_unread) {
1285                         decrementFeedCounter(getActiveFeedId(), activeFeedIsCat());
1286                         updateFloatingTitle(true);
1287
1288                         const query = {
1289                                 op: "rpc", method: "catchupSelected",
1290                                 cmode: 0, ids: id
1291                         };
1292
1293                         xhrPost("backend.php", query, (transport) => {
1294                                 handle_rpc_json(transport);
1295                         });
1296                 }
1297
1298                 return !event.shiftKey;
1299
1300         } else if (!in_body) {
1301
1302                 toggleSelected(id, true);
1303
1304                 let elem = $("RROW-" + id);
1305                 const article_is_unread = elem.hasClassName("Unread");
1306
1307                 if (article_is_unread) {
1308                         decrementFeedCounter(getActiveFeedId(), activeFeedIsCat());
1309                 }
1310
1311                 toggleUnread(id, 0, false);
1312
1313                 openArticleInNewWindow(id);
1314         } else {
1315                 return true;
1316         }
1317
1318         const unread_in_buffer = $$("#headlines-frame > div[id*=RROW][class*=Unread]").length
1319         request_counters(unread_in_buffer == 0);
1320
1321         return false;
1322 }
1323
1324 function hlClicked(event, id) {
1325         if (event.which == 2) {
1326                 view(id);
1327                 return true;
1328         } else if (event.ctrlKey || event.metaKey) {
1329                 openArticleInNewWindow(id);
1330                 return false;
1331         } else {
1332                 view(id);
1333                 return false;
1334         }
1335 }
1336
1337 function openArticleInNewWindow(id) {
1338         toggleUnread(id, 0, false);
1339
1340         const w = window.open("");
1341         w.opener = null;
1342         w.location = "backend.php?op=article&method=redirect&id=" + id;
1343 }
1344
1345 function isCdmMode() {
1346         return getInitParam("combined_display_mode");
1347 }
1348
1349 function markHeadline(id, marked) {
1350         if (marked == undefined) marked = true;
1351
1352         const row = $("RROW-" + id);
1353         if (row) {
1354                 const check = dijit.getEnclosingWidget(
1355                                 row.getElementsByClassName("rchk")[0]);
1356
1357                 if (check) {
1358                         check.attr("checked", marked);
1359                 }
1360
1361                 if (marked)
1362                         row.addClassName("Selected");
1363                 else
1364                         row.removeClassName("Selected");
1365         }
1366 }
1367
1368 function getRelativePostIds(id, limit) {
1369
1370         const tmp = [];
1371
1372         if (!limit) limit = 6; //3
1373
1374         const ids = getLoadedArticleIds();
1375
1376         for (let i = 0; i < ids.length; i++) {
1377                 if (ids[i] == id) {
1378                         for (let k = 1; k <= limit; k++) {
1379                                 //if (i > k-1) tmp.push(ids[i-k]);
1380                                 if (i < ids.length - k) tmp.push(ids[i + k]);
1381                         }
1382                         break;
1383                 }
1384         }
1385
1386         return tmp;
1387 }
1388
1389 function correctHeadlinesOffset(id) {
1390
1391         const container = $("headlines-frame");
1392         const row = $("RROW-" + id);
1393
1394         if (!container || !row) return;
1395
1396         const viewport = container.offsetHeight;
1397
1398         const rel_offset_top = row.offsetTop - container.scrollTop;
1399         const rel_offset_bottom = row.offsetTop + row.offsetHeight - container.scrollTop;
1400
1401         //console.log("Rtop: " + rel_offset_top + " Rbtm: " + rel_offset_bottom);
1402         //console.log("Vport: " + viewport);
1403
1404         if (rel_offset_top <= 0 || rel_offset_top > viewport) {
1405                 container.scrollTop = row.offsetTop;
1406         } else if (rel_offset_bottom > viewport) {
1407
1408                 /* doesn't properly work with Opera in some cases because
1409                  Opera fucks up element scrolling */
1410
1411                 container.scrollTop = row.offsetTop + row.offsetHeight - viewport;
1412         }
1413 }
1414
1415 function headlineActionsChange(elem) {
1416         eval(elem.value);
1417         elem.attr('value', 'false');
1418 }
1419
1420 function closeArticlePanel() {
1421
1422         if (dijit.byId("content-insert"))
1423                 dijit.byId("headlines-wrap-inner").removeChild(
1424                         dijit.byId("content-insert"));
1425 }
1426
1427 function initFloatingMenu() {
1428         if (!dijit.byId("floatingMenu")) {
1429
1430                 const menu = new dijit.Menu({
1431                         id: "floatingMenu",
1432                         targetNodeIds: ["floatingTitle"]
1433                 });
1434
1435                 headlinesMenuCommon(menu);
1436
1437                 menu.startup();
1438         }
1439 }
1440
1441 function headlinesMenuCommon(menu) {
1442
1443         menu.addChild(new dijit.MenuItem({
1444                 label: __("Open original article"),
1445                 onClick: function (event) {
1446                         openArticleInNewWindow(this.getParent().currentTarget.getAttribute("data-article-id"));
1447                 }
1448         }));
1449
1450         menu.addChild(new dijit.MenuItem({
1451                 label: __("Display article URL"),
1452                 onClick: function (event) {
1453                         displayArticleUrl(this.getParent().currentTarget.getAttribute("data-article-id"));
1454                 }
1455         }));
1456
1457         menu.addChild(new dijit.MenuSeparator());
1458
1459         menu.addChild(new dijit.MenuItem({
1460                 label: __("Toggle unread"),
1461                 onClick: function () {
1462
1463                         let ids = getSelectedArticleIds2();
1464                         // cast to string
1465                         const id = (this.getParent().currentTarget.getAttribute("data-article-id")) + "";
1466                         ids = ids.length != 0 && ids.indexOf(id) != -1 ? ids : [id];
1467
1468                         selectionToggleUnread(undefined, false, true, ids);
1469                 }
1470         }));
1471
1472         menu.addChild(new dijit.MenuItem({
1473                 label: __("Toggle starred"),
1474                 onClick: function () {
1475                         let ids = getSelectedArticleIds2();
1476                         // cast to string
1477                         const id = (this.getParent().currentTarget.getAttribute("data-article-id")) + "";
1478                         ids = ids.length != 0 && ids.indexOf(id) != -1 ? ids : [id];
1479
1480                         selectionToggleMarked(undefined, false, true, ids);
1481                 }
1482         }));
1483
1484         menu.addChild(new dijit.MenuItem({
1485                 label: __("Toggle published"),
1486                 onClick: function () {
1487                         let ids = getSelectedArticleIds2();
1488                         // cast to string
1489                         const id = (this.getParent().currentTarget.getAttribute("data-article-id")) + "";
1490                         ids = ids.length != 0 && ids.indexOf(id) != -1 ? ids : [id];
1491
1492                         selectionTogglePublished(undefined, false, true, ids);
1493                 }
1494         }));
1495
1496         menu.addChild(new dijit.MenuSeparator());
1497
1498         menu.addChild(new dijit.MenuItem({
1499                 label: __("Mark above as read"),
1500                 onClick: function () {
1501                         catchupRelativeToArticle(0, this.getParent().currentTarget.getAttribute("data-article-id"));
1502                 }
1503         }));
1504
1505         menu.addChild(new dijit.MenuItem({
1506                 label: __("Mark below as read"),
1507                 onClick: function () {
1508                         catchupRelativeToArticle(1, this.getParent().currentTarget.getAttribute("data-article-id"));
1509                 }
1510         }));
1511
1512
1513         const labels = getInitParam("labels");
1514
1515         if (labels && labels.length) {
1516
1517                 menu.addChild(new dijit.MenuSeparator());
1518
1519                 const labelAddMenu = new dijit.Menu({ownerMenu: menu});
1520                 const labelDelMenu = new dijit.Menu({ownerMenu: menu});
1521
1522                 labels.each(function (label) {
1523                         const bare_id = label.id;
1524                         const name = label.caption;
1525
1526                         labelAddMenu.addChild(new dijit.MenuItem({
1527                                 label: name,
1528                                 labelId: bare_id,
1529                                 onClick: function () {
1530
1531                                         let ids = getSelectedArticleIds2();
1532                                         // cast to string
1533                                         const id = (this.getParent().ownerMenu.currentTarget.getAttribute("data-article-id")) + "";
1534
1535                                         ids = ids.length != 0 && ids.indexOf(id) != -1 ? ids : [id];
1536
1537                                         selectionAssignLabel(this.labelId, ids);
1538                                 }
1539                         }));
1540
1541                         labelDelMenu.addChild(new dijit.MenuItem({
1542                                 label: name,
1543                                 labelId: bare_id,
1544                                 onClick: function () {
1545                                         let ids = getSelectedArticleIds2();
1546                                         // cast to string
1547                                         const id = (this.getParent().ownerMenu.currentTarget.getAttribute("data-article-id")) + "";
1548
1549                                         ids = ids.length != 0 && ids.indexOf(id) != -1 ? ids : [id];
1550
1551                                         selectionRemoveLabel(this.labelId, ids);
1552                                 }
1553                         }));
1554
1555                 });
1556
1557                 menu.addChild(new dijit.PopupMenuItem({
1558                         label: __("Assign label"),
1559                         popup: labelAddMenu
1560                 }));
1561
1562                 menu.addChild(new dijit.PopupMenuItem({
1563                         label: __("Remove label"),
1564                         popup: labelDelMenu
1565                 }));
1566
1567         }
1568 }
1569
1570 function initHeadlinesMenu() {
1571         if (!dijit.byId("headlinesMenu")) {
1572
1573                 const menu = new dijit.Menu({
1574                         id: "headlinesMenu",
1575                         targetNodeIds: ["headlines-frame"],
1576                         selector: ".hlMenuAttach"
1577                 });
1578
1579                 headlinesMenuCommon(menu);
1580
1581                 menu.startup();
1582         }
1583
1584         /* vgroup feed title menu */
1585
1586         if (!dijit.byId("headlinesFeedTitleMenu")) {
1587
1588                 const menu = new dijit.Menu({
1589                         id: "headlinesFeedTitleMenu",
1590                         targetNodeIds: ["headlines-frame"],
1591                         selector: "div.cdmFeedTitle"
1592                 });
1593
1594                 menu.addChild(new dijit.MenuItem({
1595                         label: __("Select articles in group"),
1596                         onClick: function (event) {
1597                                 selectArticles("all",
1598                                         "#headlines-frame > div[id*=RROW]" +
1599                                         "[data-orig-feed-id='" + this.getParent().currentTarget.getAttribute("data-feed-id") + "']");
1600
1601                         }
1602                 }));
1603
1604                 menu.addChild(new dijit.MenuItem({
1605                         label: __("Mark group as read"),
1606                         onClick: function () {
1607                                 selectArticles("none");
1608                                 selectArticles("all",
1609                                         "#headlines-frame > div[id*=RROW]" +
1610                                         "[data-orig-feed-id='" + this.getParent().currentTarget.getAttribute("data-feed-id") + "']");
1611
1612                                 catchupSelection();
1613                         }
1614                 }));
1615
1616                 menu.addChild(new dijit.MenuItem({
1617                         label: __("Mark feed as read"),
1618                         onClick: function () {
1619                                 catchupFeedInGroup(this.getParent().currentTarget.getAttribute("data-feed-id"));
1620                         }
1621                 }));
1622
1623                 menu.addChild(new dijit.MenuItem({
1624                         label: __("Edit feed"),
1625                         onClick: function () {
1626                                 editFeed(this.getParent().currentTarget.getAttribute("data-feed-id"));
1627                         }
1628                 }));
1629
1630                 menu.startup();
1631         }
1632 }
1633
1634 function cache_set(id, obj) {
1635         //console.log("cache_set: " + id);
1636         if (has_storage)
1637                 try {
1638                         sessionStorage[id] = obj;
1639                 } catch (e) {
1640                         sessionStorage.clear();
1641                 }
1642 }
1643
1644 function cache_get(id) {
1645         if (has_storage)
1646                 return sessionStorage[id];
1647 }
1648
1649 function cache_clear() {
1650         if (has_storage)
1651                 sessionStorage.clear();
1652 }
1653
1654 function cache_delete(id) {
1655         if (has_storage)
1656                 sessionStorage.removeItem(id);
1657 }
1658
1659 function cancelSearch() {
1660         _search_query = "";
1661         viewCurrentFeed();
1662 }
1663
1664 function setSelectionScore() {
1665         const ids = getSelectedArticleIds2();
1666
1667         if (ids.length > 0) {
1668                 console.log(ids);
1669
1670                 const score = prompt(__("Please enter new score for selected articles:"));
1671
1672                 if (score != undefined) {
1673                         const query = { op: "article", method: "setScore", id: ids.toString(),
1674                                 score: score };
1675
1676                         xhrJson("backend.php", query, (reply) => {
1677                                 if (reply) {
1678                                         reply.id.each((id) => {
1679                                                 const row = $("RROW-" + id);
1680
1681                                                 if (row) {
1682                                                         const pic = row.getElementsByClassName("score-pic")[0];
1683
1684                                                         if (pic) {
1685                                                                 pic.src = pic.src.replace(/score_.*?\.png/,
1686                                                                         reply["score_pic"]);
1687                                                                 pic.setAttribute("score", reply["score"]);
1688                                                         }
1689                                                 }
1690                                         });
1691                                 }
1692                         });
1693                 }
1694
1695         } else {
1696                 alert(__("No articles are selected."));
1697         }
1698 }
1699
1700 function changeScore(id, pic) {
1701         const score = pic.getAttribute("score");
1702
1703         const new_score = prompt(__("Please enter new score for this article:"), score);
1704
1705         if (new_score != undefined) {
1706                 const query = { op: "article", method: "setScore", id: id, score: new_score };
1707
1708                 xhrJson("backend.php", query, (reply) => {
1709                         if (reply) {
1710                                 pic.src = pic.src.replace(/score_.*?\.png/, reply["score_pic"]);
1711                                 pic.setAttribute("score", new_score);
1712                                 pic.setAttribute("title", new_score);
1713                         }
1714                 });
1715         }
1716 }
1717
1718 function displayArticleUrl(id) {
1719         const query = { op: "rpc", method: "getlinktitlebyid", id: id };
1720
1721         xhrJson("backend.php", query, (reply) => {
1722                 if (reply && reply.link) {
1723                         prompt(__("Article URL:"), reply.link);
1724                 }
1725         });
1726
1727 }
1728
1729 function scrollToRowId(id) {
1730         const row = $(id);
1731
1732         if (row)
1733                 $("headlines-frame").scrollTop = row.offsetTop - 4;
1734 }
1735
1736 function updateFloatingTitle(unread_only) {
1737         if (!isCdmMode()) return;
1738
1739         const hf = $("headlines-frame");
1740
1741         const elems = $$("#headlines-frame > div[id*=RROW]");
1742
1743         for (let i = 0; i < elems.length; i++) {
1744
1745                 const child = elems[i];
1746
1747                 if (child && child.offsetTop + child.offsetHeight > hf.scrollTop) {
1748
1749                         const header = child.getElementsByClassName("header")[0];
1750
1751                         if (unread_only || child.getAttribute("data-article-id") != $("floatingTitle").getAttribute("data-article-id")) {
1752                                 if (child.getAttribute("data-article-id") != $("floatingTitle").getAttribute("data-article-id")) {
1753
1754                                         $("floatingTitle").setAttribute("data-article-id", child.getAttribute("data-article-id"));
1755                                         $("floatingTitle").innerHTML = header.innerHTML;
1756                                         $("floatingTitle").firstChild.innerHTML = "<img class='anchor markedPic' src='images/page_white_go.png' onclick=\"scrollToRowId('" + child.id + "')\">" + $("floatingTitle").firstChild.innerHTML;
1757
1758                                         initFloatingMenu();
1759
1760                                         const cb = $$("#floatingTitle .dijitCheckBox")[0];
1761
1762                                         if (cb)
1763                                                 cb.parentNode.removeChild(cb);
1764                                 }
1765
1766                                 if (child.hasClassName("Unread"))
1767                                         $("floatingTitle").addClassName("Unread");
1768                                 else
1769                                         $("floatingTitle").removeClassName("Unread");
1770
1771                                 PluginHost.run(PluginHost.HOOK_FLOATING_TITLE, child);
1772                         }
1773
1774                         $("floatingTitle").style.marginRight = hf.offsetWidth - child.offsetWidth + "px";
1775                         if (header.offsetTop + header.offsetHeight < hf.scrollTop + $("floatingTitle").offsetHeight - 5 &&
1776                                 child.offsetTop + child.offsetHeight >= hf.scrollTop + $("floatingTitle").offsetHeight - 5)
1777                                 $("floatingTitle").style.visibility = "visible";
1778                         else
1779                                 $("floatingTitle").style.visibility = "hidden";
1780
1781                         return;
1782
1783                 }
1784         }
1785 }