]> git.wh0rd.org Git - tt-rss.git/blob - js/viewfeed.js
</hurr>
[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                 next_id = rows[0];
459                 prev_id = rows[rows.length-1]
460         } else {
461                 for (let i = 0; i < rows.length; i++) {
462                         if (rows[i] == getActiveArticleId()) {
463
464                                 // Account for adjacent identical article ids.
465                                 if (i > 0) prev_id = rows[i-1];
466
467                                 for (let j = i+1; j < rows.length; j++) {
468                                         if (rows[j] != getActiveArticleId()) {
469                                                 next_id = rows[j];
470                                                 break;
471                                         }
472                                 }
473                                 break;
474                         }
475                 }
476         }
477
478         console.log("cur: " + getActiveArticleId() + " next: " + next_id);
479
480         if (mode == "next") {
481                 if (next_id || getActiveArticleId()) {
482                         if (isCdmMode()) {
483
484                                 var article = $("RROW-" + getActiveArticleId());
485                                 var ctr = $("headlines-frame");
486
487                                 if (!noscroll && article && article.offsetTop + article.offsetHeight >
488                                                 ctr.scrollTop + ctr.offsetHeight) {
489
490                                         scrollArticle(ctr.offsetHeight/4);
491
492                                 } else if (next_id) {
493                                         cdmScrollToArticleId(next_id, true);
494                                 }
495
496                         } else if (next_id) {
497                                 correctHeadlinesOffset(next_id);
498                                 view(next_id, getActiveFeedId(), noexpand);
499                         }
500                 }
501         }
502
503         if (mode == "prev") {
504                 if (prev_id || getActiveArticleId()) {
505                         if (isCdmMode()) {
506
507                                 var article = $("RROW-" + getActiveArticleId());
508                                 const prev_article = $("RROW-" + prev_id);
509                                 var ctr = $("headlines-frame");
510
511                                 if (!noscroll && article && article.offsetTop < ctr.scrollTop) {
512                                         scrollArticle(-ctr.offsetHeight/3);
513                                 } else if (!noscroll && prev_article &&
514                                                 prev_article.offsetTop < ctr.scrollTop) {
515                                         scrollArticle(-ctr.offsetHeight/4);
516                                 } else if (prev_id) {
517                                         cdmScrollToArticleId(prev_id, noscroll);
518                                 }
519
520                         } else if (prev_id) {
521                                 correctHeadlinesOffset(prev_id);
522                                 view(prev_id, getActiveFeedId(), noexpand);
523                         }
524                 }
525         }
526
527 }
528
529 function toggleSelected(id, force_on) {
530         const row = $("RROW-" + id);
531
532         if (row) {
533                 const cb = dijit.getEnclosingWidget(
534                                 row.getElementsByClassName("rchk")[0]);
535
536                 if (row.hasClassName('Selected') && !force_on) {
537                         row.removeClassName('Selected');
538                         if (cb) cb.attr("checked", false);
539                 } else {
540                         row.addClassName('Selected');
541                         if (cb) cb.attr("checked", true);
542                 }
543         }
544
545         updateSelectedPrompt();
546 }
547
548 function updateSelectedPrompt() {
549         const count = getSelectedArticleIds2().length;
550         const elem = $("selected_prompt");
551
552         if (elem) {
553                 elem.innerHTML = ngettext("%d article selected",
554                                 "%d articles selected", count).replace("%d", count);
555
556                 if (count > 0)
557                         Element.show(elem);
558                 else
559                         Element.hide(elem);
560         }
561
562 }
563
564 function toggleUnread(id, cmode) {
565         const row = $("RROW-" + id);
566         if (row) {
567                 const tmpClassName = row.className;
568
569                 if (cmode == undefined || cmode == 2) {
570                         if (row.hasClassName("Unread")) {
571                                 row.removeClassName("Unread");
572
573                         } else {
574                                 row.addClassName("Unread");
575                         }
576
577                 } else if (cmode == 0) {
578
579                         row.removeClassName("Unread");
580
581                 } else if (cmode == 1) {
582                         row.addClassName("Unread");
583                 }
584
585                 if (tmpClassName != row.className) {
586                         if (cmode == undefined) cmode = 2;
587
588                         const query = {op: "rpc", method: "catchupSelected",
589                                 cmode: cmode, ids: id};
590
591                         xhrPost("backend.php", query, (transport) => {
592                                         handle_rpc_json(transport);
593
594                         });
595                 }
596         }
597 }
598
599 function selectionRemoveLabel(id, ids) {
600         if (!ids) ids = getSelectedArticleIds2();
601
602         if (ids.length == 0) {
603                 alert(__("No articles are selected."));
604                 return;
605         }
606
607         const query = { op: "article", method: "removeFromLabel",
608                 ids: ids.toString(), lid: id };
609
610         xhrPost("backend.php", query, (transport) => {
611                 handle_rpc_json(transport);
612                 show_labels_in_headlines(transport);
613         });
614 }
615
616 function selectionAssignLabel(id, ids) {
617         if (!ids) ids = getSelectedArticleIds2();
618
619         if (ids.length == 0) {
620                 alert(__("No articles are selected."));
621                 return;
622         }
623
624         const query = { op: "article", method: "assignToLabel",
625                 ids: ids.toString(), lid: id };
626
627         xhrPost("backend.php", query, (transport) => {
628                 handle_rpc_json(transport);
629                 show_labels_in_headlines(transport);
630         });
631 }
632
633 function selectionToggleUnread(set_state, callback, no_error, ids) {
634         const rows = ids ? ids : getSelectedArticleIds2();
635
636         if (rows.length == 0 && !no_error) {
637                 alert(__("No articles are selected."));
638                 return;
639         }
640
641         for (let i = 0; i < rows.length; i++) {
642                 const row = $("RROW-" + rows[i]);
643                 if (row) {
644                         if (set_state == undefined) {
645                                 if (row.hasClassName("Unread")) {
646                                         row.removeClassName("Unread");
647                                 } else {
648                                         row.addClassName("Unread");
649                                 }
650                         }
651
652                         if (set_state == false) {
653                                 row.removeClassName("Unread");
654                         }
655
656                         if (set_state == true) {
657                                 row.addClassName("Unread");
658                         }
659                 }
660         }
661
662         updateFloatingTitle(true);
663
664         if (rows.length > 0) {
665
666                 let cmode = "";
667
668                 if (set_state == undefined) {
669                         cmode = "2";
670                 } else if (set_state == true) {
671                         cmode = "1";
672                 } else if (set_state == false) {
673                         cmode = "0";
674                 }
675
676                 const query = {op: "rpc", method: "catchupSelected",
677                         cmode: cmode, ids: rows.toString() };
678
679                 notify_progress("Loading, please wait...");
680
681                 xhrPost("backend.php", query, (transport) => {
682                         handle_rpc_json(transport);
683                         if (callback) callback(transport);
684                 });
685
686         }
687 }
688
689 // sel_state ignored
690 function selectionToggleMarked(sel_state, callback, no_error, ids) {
691         const rows = ids ? ids : getSelectedArticleIds2();
692
693         if (rows.length == 0 && !no_error) {
694                 alert(__("No articles are selected."));
695                 return;
696         }
697
698         for (let i = 0; i < rows.length; i++) {
699                 toggleMark(rows[i], true, true);
700         }
701
702         if (rows.length > 0) {
703                 const query = { op: "rpc", method: "markSelected",
704                         ids:  rows.toString(), cmode: 2 };
705
706                 xhrPost("backend.php", query, (transport) => {
707                         handle_rpc_json(transport);
708                         if (callback) callback(transport);
709                 });
710         }
711 }
712
713 // sel_state ignored
714 function selectionTogglePublished(sel_state, callback, no_error, ids) {
715         const rows = ids ? ids : getSelectedArticleIds2();
716
717         if (rows.length == 0 && !no_error) {
718                 alert(__("No articles are selected."));
719                 return;
720         }
721
722         for (let i = 0; i < rows.length; i++) {
723                 togglePub(rows[i], true, true);
724         }
725
726         if (rows.length > 0) {
727                 const query = { op: "rpc", method: "publishSelected",
728                         ids:  rows.toString(), cmode: 2 };
729
730                 xhrPost("backend.php", query, (transport) => {
731                         handle_rpc_json(transport);
732                         if (callback) callback(transport);
733                 });
734         }
735 }
736
737 function getSelectedArticleIds2() {
738
739         const rv = [];
740
741         $$("#headlines-frame > div[id*=RROW][class*=Selected]").each(
742                 function(child) {
743                         rv.push(child.getAttribute("data-article-id"));
744                 });
745
746         return rv;
747 }
748
749 function getLoadedArticleIds() {
750         const rv = [];
751
752         const children = $$("#headlines-frame > div[id*=RROW-]");
753
754         children.each(function(child) {
755                 if (Element.visible(child)) {
756                         rv.push(child.getAttribute("data-article-id"));
757                 }
758         });
759
760         return rv;
761
762 }
763
764 // mode = all,none,unread,invert,marked,published
765 function selectArticles(mode, query) {
766         if (!query) query = "#headlines-frame > div[id*=RROW]";
767
768         const children = $$(query);
769
770         children.each(function(child) {
771                 //const id = child.getAttribute("data-article-id");
772
773                 const cb = dijit.getEnclosingWidget(
774                                 child.getElementsByClassName("rchk")[0]);
775
776                 if (mode == "all") {
777                         child.addClassName("Selected");
778                         if (cb) cb.attr("checked", true);
779                 } else if (mode == "unread") {
780                         if (child.hasClassName("Unread")) {
781                                 child.addClassName("Selected");
782                                 if (cb) cb.attr("checked", true);
783                         } else {
784                                 child.removeClassName("Selected");
785                                 if (cb) cb.attr("checked", false);
786                         }
787                 } else if (mode == "marked") {
788                         if (child.hasClassName("marked")) {
789                                 child.addClassName("Selected");
790                                 if (cb) cb.attr("checked", true);
791                         } else {
792                                 child.removeClassName("Selected");
793                                 if (cb) cb.attr("checked", false);
794                         }
795                 } else if (mode == "published") {
796                         if (child.hasClassName("published")) {
797                                 child.addClassName("Selected");
798                                 if (cb) cb.attr("checked", true);
799                         } else {
800                                 child.removeClassName("Selected");
801                                 if (cb) cb.attr("checked", false);
802                         }
803
804                 } else if (mode == "invert") {
805                         if (child.hasClassName("Selected")) {
806                                 child.removeClassName("Selected");
807                                 if (cb) cb.attr("checked", false);
808                         } else {
809                                 child.addClassName("Selected");
810                                 if (cb) cb.attr("checked", true);
811                         }
812
813                 } else {
814                         child.removeClassName("Selected");
815                         if (cb) cb.attr("checked", false);
816                 }
817         });
818
819         updateSelectedPrompt();
820 }
821
822 function deleteSelection() {
823
824         const rows = getSelectedArticleIds2();
825
826         if (rows.length == 0) {
827                 alert(__("No articles are selected."));
828                 return;
829         }
830
831         const fn = getFeedName(getActiveFeedId(), activeFeedIsCat());
832         let str;
833
834         if (getActiveFeedId() != 0) {
835                 str = ngettext("Delete %d selected article in %s?", "Delete %d selected articles in %s?", rows.length);
836         } else {
837                 str = ngettext("Delete %d selected article?", "Delete %d selected articles?", rows.length);
838         }
839
840         str = str.replace("%d", rows.length);
841         str = str.replace("%s", fn);
842
843         if (getInitParam("confirm_feed_catchup") == 1 && !confirm(str)) {
844                 return;
845         }
846
847         const query = { op: "rpc", method: "delete", ids: rows.toString() };
848
849         xhrPost("backend.php", query, (transport) => {
850                 handle_rpc_json(transport);
851                 viewCurrentFeed();
852         });
853 }
854
855 function archiveSelection() {
856
857         const rows = getSelectedArticleIds2();
858
859         if (rows.length == 0) {
860                 alert(__("No articles are selected."));
861                 return;
862         }
863
864         const fn = getFeedName(getActiveFeedId(), activeFeedIsCat());
865         let str;
866         let op;
867
868         if (getActiveFeedId() != 0) {
869                 str = ngettext("Archive %d selected article in %s?", "Archive %d selected articles in %s?", rows.length);
870                 op = "archive";
871         } else {
872                 str = ngettext("Move %d archived article back?", "Move %d archived articles back?", rows.length);
873
874                 str += " " + __("Please note that unstarred articles might get purged on next feed update.");
875
876                 op = "unarchive";
877         }
878
879         str = str.replace("%d", rows.length);
880         str = str.replace("%s", fn);
881
882         if (getInitParam("confirm_feed_catchup") == 1 && !confirm(str)) {
883                 return;
884         }
885
886         for (let i = 0; i < rows.length; i++) {
887                 cache_delete("article:" + rows[i]);
888         }
889
890         const query = {op: "rpc", method: op, ids: rows.toString()};
891
892         xhrPost("backend.php", query, (transport) => {
893                 handle_rpc_json(transport);
894                 viewCurrentFeed();
895         });
896 }
897
898 function catchupSelection() {
899
900         const rows = getSelectedArticleIds2();
901
902         if (rows.length == 0) {
903                 alert(__("No articles are selected."));
904                 return;
905         }
906
907         const fn = getFeedName(getActiveFeedId(), activeFeedIsCat());
908
909         let str = ngettext("Mark %d selected article in %s as read?", "Mark %d selected articles in %s as read?", rows.length);
910
911         str = str.replace("%d", rows.length);
912         str = str.replace("%s", fn);
913
914         if (getInitParam("confirm_feed_catchup") == 1 && !confirm(str)) {
915                 return;
916         }
917
918         selectionToggleUnread(false, 'viewCurrentFeed()', true);
919 }
920
921 function editArticleTags(id) {
922         const query = "backend.php?op=article&method=editArticleTags&param=" + param_escape(id);
923
924         if (dijit.byId("editTagsDlg"))
925                 dijit.byId("editTagsDlg").destroyRecursive();
926
927         const dialog = new dijit.Dialog({
928                 id: "editTagsDlg",
929                 title: __("Edit article Tags"),
930                 style: "width: 600px",
931                 execute: function() {
932                         if (this.validate()) {
933                                 const query = dojo.objectToQuery(this.attr('value'));
934
935                                 notify_progress("Saving article tags...", true);
936
937                                 xhrPost("backend.php", this.attr('value'), (transport) => {
938                                         try {
939                                                 notify('');
940                                                 dialog.hide();
941
942                                                 const data = JSON.parse(transport.responseText);
943
944                                                 if (data) {
945                                                         const id = data.id;
946
947                                                         const tags = $("ATSTR-" + id);
948                                                         const tooltip = dijit.byId("ATSTRTIP-" + id);
949
950                                                         if (tags) tags.innerHTML = data.content;
951                                                         if (tooltip) tooltip.attr('label', data.content_full);
952                                                 }
953                                         } catch (e) {
954                                                 exception_error(e);
955                                         }
956                                 });
957                         }
958                 },
959                 href: query
960         });
961
962         var tmph = dojo.connect(dialog, 'onLoad', function() {
963                 dojo.disconnect(tmph);
964
965                 new Ajax.Autocompleter('tags_str', 'tags_choices',
966                    "backend.php?op=article&method=completeTags",
967                    { tokens: ',', paramName: "search" });
968         });
969
970         dialog.show();
971
972 }
973
974 function cdmScrollToArticleId(id, force) {
975         const ctr = $("headlines-frame");
976         const e = $("RROW-" + id);
977
978         if (!e || !ctr) return;
979
980         if (force || e.offsetTop+e.offsetHeight > (ctr.scrollTop+ctr.offsetHeight) ||
981                         e.offsetTop < ctr.scrollTop) {
982
983                 // expanded cdm has a 4px margin now
984                 ctr.scrollTop = parseInt(e.offsetTop) - 4;
985
986                 setActiveArticleId(id);
987
988                 // article is selected manually, set it read
989                 toggleUnread(id, 0);1
990         }
991 }
992
993 function setActiveArticleId(id) {
994         console.log("setActiveArticleId:" + id);
995
996         _active_article_id = id;
997         PluginHost.run(PluginHost.HOOK_ARTICLE_SET_ACTIVE, _active_article_id);
998 }
999
1000 function getActiveArticleId() {
1001         return _active_article_id;
1002 }
1003
1004 function postMouseIn(e, id) {
1005         post_under_pointer = id;
1006 }
1007
1008 function postMouseOut(id) {
1009         post_under_pointer = false;
1010 }
1011
1012 function unpackVisibleHeadlines() {
1013         if (!isCdmMode()) return;
1014
1015         const rows = $$("#headlines-frame div[id*=RROW][data-content]");
1016         const threshold = $("headlines-frame").scrollTop + $("headlines-frame").offsetHeight + 300;
1017
1018         for (let i = 0; i < rows.length; i++) {
1019                 const row = rows[i];
1020
1021                 if (row.offsetTop <= threshold) {
1022                         console.log("unpacking: " + row.id);
1023
1024                         const content = row.getAttribute("data-content");
1025
1026                         row.select(".content-inner")[0].innerHTML = content;
1027                         row.removeAttribute("data-content");
1028
1029                         PluginHost.run(PluginHost.HOOK_ARTICLE_RENDERED_CDM, row);
1030                 } else {
1031                         break;
1032                 }
1033         }
1034 }
1035
1036 function headlines_scroll_handler(e) {
1037         try {
1038
1039                 // rate-limit in case of smooth scrolling and similar abominations
1040                 if (Math.max(e.scrollTop, _headlines_scroll_offset) - Math.min(e.scrollTop, _headlines_scroll_offset) < 25) {
1041                         return;
1042                 }
1043
1044                 _headlines_scroll_offset = e.scrollTop;
1045
1046                 unpackVisibleHeadlines();
1047
1048                 // set topmost child in the buffer as active
1049                 if (isCdmMode() && getInitParam("cdm_auto_catchup") == 1 &&
1050                                 getSelectedArticleIds2().length <= 1) {
1051
1052                         const rows = $$("#headlines-frame > div[id*=RROW]");
1053
1054                         for (let i = 0; i < rows.length; i++) {
1055                                 const row = rows[i];
1056
1057                                 if ($("headlines-frame").scrollTop <= row.offsetTop &&
1058                                         row.offsetTop - $("headlines-frame").scrollTop < 100 &&
1059                                         row.getAttribute("data-article-id") != _active_article_id) {
1060
1061                                         if (_active_article_id) {
1062                                                 const row = $("RROW-" + _active_article_id);
1063                                                 if (row) row.removeClassName("active");
1064                                         }
1065
1066                                         _active_article_id = row.getAttribute("data-article-id");
1067                                         showArticleInHeadlines(_active_article_id, true);
1068                                         updateSelectedPrompt();
1069                                         break;
1070                                 }
1071                         }
1072                 }
1073
1074                 if (!_infscroll_disable) {
1075                         const hsp = $("headlines-spacer");
1076
1077                         if (hsp && hsp.offsetTop - 250 <= e.scrollTop + e.offsetHeight) {
1078
1079                                 hsp.innerHTML = "<span class='loading'><img src='images/indicator_tiny.gif'> " +
1080                                         __("Loading, please wait...") + "</span>";
1081
1082                                 loadMoreHeadlines();
1083                                 return;
1084
1085                         }
1086                 }
1087
1088                 if (isCdmMode()) {
1089                         updateFloatingTitle();
1090                 }
1091
1092                 if (getInitParam("cdm_auto_catchup") == 1) {
1093
1094                         let rows = $$("#headlines-frame > div[id*=RROW][class*=Unread]");
1095
1096                         for (let i = 0; i < rows.length; i++) {
1097                                 const row = rows[i];
1098                                 
1099                                 if ($("headlines-frame").scrollTop > (row.offsetTop + row.offsetHeight/2)) {
1100
1101                                         const id = row.getAttribute("data-article-id")
1102
1103                                         if (catchup_id_batch.indexOf(id) == -1)
1104                                                 catchup_id_batch.push(id);
1105
1106                                         //console.log("auto_catchup_batch: " + catchup_id_batch.toString());
1107                                 } else {
1108                                         break;
1109                                 }
1110                         }
1111
1112                         if (_infscroll_disable) {
1113                                 const row = $$("#headlines-frame div[id*=RROW]").last();
1114
1115                                 if (row && $("headlines-frame").scrollTop >
1116                                                 (row.offsetTop + row.offsetHeight - 50)) {
1117
1118                                         console.log("we seem to be at an end");
1119
1120                                         if (getInitParam("on_catchup_show_next_feed") == "1") {
1121                                                 openNextUnreadFeed();
1122                                         }
1123                                 }
1124                         }
1125                 }
1126
1127         } catch (e) {
1128                 console.warn("headlines_scroll_handler: " + e);
1129         }
1130 }
1131
1132 function openNextUnreadFeed() {
1133         const is_cat = activeFeedIsCat();
1134         const nuf = getNextUnreadFeed(getActiveFeedId(), is_cat);
1135         if (nuf) viewfeed({feed: nuf, is_cat: is_cat});
1136 }
1137
1138 function catchupBatchedArticles() {
1139         if (catchup_id_batch.length > 0 && !_infscroll_request_sent && !_catchup_request_sent) {
1140
1141                 console.log("catchupBatchedArticles, size=", catchup_id_batch.length);
1142
1143                 // make a copy of the array
1144                 const batch = catchup_id_batch.slice();
1145                 const query = { op: "rpc", method: "catchupSelected",
1146                         cmode: 0, ids: batch.toString() };
1147
1148                 _catchup_request_sent = true;
1149
1150                 xhrPost("backend.php", query, (transport) => {
1151                         const reply = handle_rpc_json(transport);
1152
1153                         _catchup_request_sent = false;
1154
1155                         if (reply) {
1156                                 const batch = reply.ids;
1157
1158                                 batch.each(function (id) {
1159                                         const elem = $("RROW-" + id);
1160                                         if (elem) elem.removeClassName("Unread");
1161                                         catchup_id_batch.remove(id);
1162                                 });
1163                         }
1164
1165                         updateFloatingTitle(true);
1166                 });
1167         }
1168 }
1169
1170 function catchupRelativeToArticle(below, id) {
1171
1172         if (!id) id = getActiveArticleId();
1173
1174         if (!id) {
1175                 alert(__("No article is selected."));
1176                 return;
1177         }
1178
1179         const visible_ids = getLoadedArticleIds();
1180
1181         const ids_to_mark = [];
1182
1183         if (!below) {
1184                 for (var i = 0; i < visible_ids.length; i++) {
1185                         if (visible_ids[i] != id) {
1186                                 var e = $("RROW-" + visible_ids[i]);
1187
1188                                 if (e && e.hasClassName("Unread")) {
1189                                         ids_to_mark.push(visible_ids[i]);
1190                                 }
1191                         } else {
1192                                 break;
1193                         }
1194                 }
1195         } else {
1196                 for (var i = visible_ids.length - 1; i >= 0; i--) {
1197                         if (visible_ids[i] != id) {
1198                                 var e = $("RROW-" + visible_ids[i]);
1199
1200                                 if (e && e.hasClassName("Unread")) {
1201                                         ids_to_mark.push(visible_ids[i]);
1202                                 }
1203                         } else {
1204                                 break;
1205                         }
1206                 }
1207         }
1208
1209         if (ids_to_mark.length == 0) {
1210                 alert(__("No articles found to mark"));
1211         } else {
1212                 const msg = ngettext("Mark %d article as read?", "Mark %d articles as read?", ids_to_mark.length).replace("%d", ids_to_mark.length);
1213
1214                 if (getInitParam("confirm_feed_catchup") != 1 || confirm(msg)) {
1215
1216                         for (var i = 0; i < ids_to_mark.length; i++) {
1217                                 var e = $("RROW-" + ids_to_mark[i]);
1218                                 e.removeClassName("Unread");
1219                         }
1220
1221                         const query = { op: "rpc", method: "catchupSelected",
1222                                 cmode: 0, ids: ids_to_mark.toString() };
1223
1224                         xhrPost("backend.php", query, (transport) => {
1225                                 handle_rpc_json(transport);
1226                         });
1227                 }
1228         }
1229 }
1230
1231 function getArticleUnderPointer() {
1232         return post_under_pointer;
1233 }
1234
1235 function scrollArticle(offset) {
1236         if (!isCdmMode()) {
1237                 const ci = $("content-insert");
1238                 if (ci) {
1239                         ci.scrollTop += offset;
1240                 }
1241         } else {
1242                 const hi = $("headlines-frame");
1243                 if (hi) {
1244                         hi.scrollTop += offset;
1245                 }
1246
1247         }
1248 }
1249
1250 function show_labels_in_headlines(transport) {
1251         const data = JSON.parse(transport.responseText);
1252
1253         if (data) {
1254                 data['info-for-headlines'].each(function (elem) {
1255                         $$(".HLLCTR-" + elem.id).each(function (ctr) {
1256                                 ctr.innerHTML = elem.labels;
1257                         });
1258                 });
1259         }
1260 }
1261
1262 function cdmClicked(event, id, in_body) {
1263         //var shift_key = event.shiftKey;
1264
1265         if (!event.ctrlKey && !event.metaKey) {
1266
1267                 let elem = $("RROW-" + getActiveArticleId());
1268
1269                 if (elem) elem.removeClassName("active");
1270
1271                 selectArticles("none");
1272                 toggleSelected(id);
1273
1274                 elem = $("RROW-" + id);
1275                 const article_is_unread = elem.hasClassName("Unread");
1276
1277                 elem.removeClassName("Unread");
1278                 elem.addClassName("active");
1279
1280                 setActiveArticleId(id);
1281
1282                 if (article_is_unread) {
1283                         decrementFeedCounter(getActiveFeedId(), activeFeedIsCat());
1284                         updateFloatingTitle(true);
1285
1286                         const query = {
1287                                 op: "rpc", method: "catchupSelected",
1288                                 cmode: 0, ids: id
1289                         };
1290
1291                         xhrPost("backend.php", query, (transport) => {
1292                                 handle_rpc_json(transport);
1293                         });
1294                 }
1295
1296                 return !event.shiftKey;
1297
1298         } else if (!in_body) {
1299
1300                 toggleSelected(id, true);
1301
1302                 let elem = $("RROW-" + id);
1303                 const article_is_unread = elem.hasClassName("Unread");
1304
1305                 if (article_is_unread) {
1306                         decrementFeedCounter(getActiveFeedId(), activeFeedIsCat());
1307                 }
1308
1309                 toggleUnread(id, 0, false);
1310
1311                 openArticleInNewWindow(id);
1312         } else {
1313                 return true;
1314         }
1315
1316         const unread_in_buffer = $$("#headlines-frame > div[id*=RROW][class*=Unread]").length
1317         request_counters(unread_in_buffer == 0);
1318
1319         return false;
1320 }
1321
1322 function hlClicked(event, id) {
1323         if (event.which == 2) {
1324                 view(id);
1325                 return true;
1326         } else if (event.ctrlKey || event.metaKey) {
1327                 openArticleInNewWindow(id);
1328                 return false;
1329         } else {
1330                 view(id);
1331                 return false;
1332         }
1333 }
1334
1335 function openArticleInNewWindow(id) {
1336         toggleUnread(id, 0, false);
1337
1338         const w = window.open("");
1339         w.opener = null;
1340         w.location = "backend.php?op=article&method=redirect&id=" + id;
1341 }
1342
1343 function isCdmMode() {
1344         return getInitParam("combined_display_mode");
1345 }
1346
1347 function markHeadline(id, marked) {
1348         if (marked == undefined) marked = true;
1349
1350         const row = $("RROW-" + id);
1351         if (row) {
1352                 const check = dijit.getEnclosingWidget(
1353                                 row.getElementsByClassName("rchk")[0]);
1354
1355                 if (check) {
1356                         check.attr("checked", marked);
1357                 }
1358
1359                 if (marked)
1360                         row.addClassName("Selected");
1361                 else
1362                         row.removeClassName("Selected");
1363         }
1364 }
1365
1366 function getRelativePostIds(id, limit) {
1367
1368         const tmp = [];
1369
1370         if (!limit) limit = 6; //3
1371
1372         const ids = getLoadedArticleIds();
1373
1374         for (let i = 0; i < ids.length; i++) {
1375                 if (ids[i] == id) {
1376                         for (let k = 1; k <= limit; k++) {
1377                                 //if (i > k-1) tmp.push(ids[i-k]);
1378                                 if (i < ids.length - k) tmp.push(ids[i + k]);
1379                         }
1380                         break;
1381                 }
1382         }
1383
1384         return tmp;
1385 }
1386
1387 function correctHeadlinesOffset(id) {
1388
1389         const container = $("headlines-frame");
1390         const row = $("RROW-" + id);
1391
1392         if (!container || !row) return;
1393
1394         const viewport = container.offsetHeight;
1395
1396         const rel_offset_top = row.offsetTop - container.scrollTop;
1397         const rel_offset_bottom = row.offsetTop + row.offsetHeight - container.scrollTop;
1398
1399         //console.log("Rtop: " + rel_offset_top + " Rbtm: " + rel_offset_bottom);
1400         //console.log("Vport: " + viewport);
1401
1402         if (rel_offset_top <= 0 || rel_offset_top > viewport) {
1403                 container.scrollTop = row.offsetTop;
1404         } else if (rel_offset_bottom > viewport) {
1405
1406                 /* doesn't properly work with Opera in some cases because
1407                  Opera fucks up element scrolling */
1408
1409                 container.scrollTop = row.offsetTop + row.offsetHeight - viewport;
1410         }
1411 }
1412
1413 function headlineActionsChange(elem) {
1414         eval(elem.value);
1415         elem.attr('value', 'false');
1416 }
1417
1418 function closeArticlePanel() {
1419
1420         if (dijit.byId("content-insert"))
1421                 dijit.byId("headlines-wrap-inner").removeChild(
1422                         dijit.byId("content-insert"));
1423 }
1424
1425 function initFloatingMenu() {
1426         if (!dijit.byId("floatingMenu")) {
1427
1428                 const menu = new dijit.Menu({
1429                         id: "floatingMenu",
1430                         targetNodeIds: ["floatingTitle"]
1431                 });
1432
1433                 headlinesMenuCommon(menu);
1434
1435                 menu.startup();
1436         }
1437 }
1438
1439 function headlinesMenuCommon(menu) {
1440
1441         menu.addChild(new dijit.MenuItem({
1442                 label: __("Open original article"),
1443                 onClick: function (event) {
1444                         openArticleInNewWindow(this.getParent().currentTarget.getAttribute("data-article-id"));
1445                 }
1446         }));
1447
1448         menu.addChild(new dijit.MenuItem({
1449                 label: __("Display article URL"),
1450                 onClick: function (event) {
1451                         displayArticleUrl(this.getParent().currentTarget.getAttribute("data-article-id"));
1452                 }
1453         }));
1454
1455         menu.addChild(new dijit.MenuSeparator());
1456
1457         menu.addChild(new dijit.MenuItem({
1458                 label: __("Toggle unread"),
1459                 onClick: function () {
1460
1461                         let ids = getSelectedArticleIds2();
1462                         // cast to string
1463                         const id = (this.getParent().currentTarget.getAttribute("data-article-id")) + "";
1464                         ids = ids.length != 0 && ids.indexOf(id) != -1 ? ids : [id];
1465
1466                         selectionToggleUnread(undefined, false, true, ids);
1467                 }
1468         }));
1469
1470         menu.addChild(new dijit.MenuItem({
1471                 label: __("Toggle starred"),
1472                 onClick: function () {
1473                         let ids = getSelectedArticleIds2();
1474                         // cast to string
1475                         const id = (this.getParent().currentTarget.getAttribute("data-article-id")) + "";
1476                         ids = ids.length != 0 && ids.indexOf(id) != -1 ? ids : [id];
1477
1478                         selectionToggleMarked(undefined, false, true, ids);
1479                 }
1480         }));
1481
1482         menu.addChild(new dijit.MenuItem({
1483                 label: __("Toggle published"),
1484                 onClick: function () {
1485                         let ids = getSelectedArticleIds2();
1486                         // cast to string
1487                         const id = (this.getParent().currentTarget.getAttribute("data-article-id")) + "";
1488                         ids = ids.length != 0 && ids.indexOf(id) != -1 ? ids : [id];
1489
1490                         selectionTogglePublished(undefined, false, true, ids);
1491                 }
1492         }));
1493
1494         menu.addChild(new dijit.MenuSeparator());
1495
1496         menu.addChild(new dijit.MenuItem({
1497                 label: __("Mark above as read"),
1498                 onClick: function () {
1499                         catchupRelativeToArticle(0, this.getParent().currentTarget.getAttribute("data-article-id"));
1500                 }
1501         }));
1502
1503         menu.addChild(new dijit.MenuItem({
1504                 label: __("Mark below as read"),
1505                 onClick: function () {
1506                         catchupRelativeToArticle(1, this.getParent().currentTarget.getAttribute("data-article-id"));
1507                 }
1508         }));
1509
1510
1511         const labels = getInitParam("labels");
1512
1513         if (labels && labels.length) {
1514
1515                 menu.addChild(new dijit.MenuSeparator());
1516
1517                 const labelAddMenu = new dijit.Menu({ownerMenu: menu});
1518                 const labelDelMenu = new dijit.Menu({ownerMenu: menu});
1519
1520                 labels.each(function (label) {
1521                         const bare_id = label.id;
1522                         const name = label.caption;
1523
1524                         labelAddMenu.addChild(new dijit.MenuItem({
1525                                 label: name,
1526                                 labelId: bare_id,
1527                                 onClick: function () {
1528
1529                                         let ids = getSelectedArticleIds2();
1530                                         // cast to string
1531                                         const id = (this.getParent().ownerMenu.currentTarget.getAttribute("data-article-id")) + "";
1532
1533                                         ids = ids.length != 0 && ids.indexOf(id) != -1 ? ids : [id];
1534
1535                                         selectionAssignLabel(this.labelId, ids);
1536                                 }
1537                         }));
1538
1539                         labelDelMenu.addChild(new dijit.MenuItem({
1540                                 label: name,
1541                                 labelId: bare_id,
1542                                 onClick: function () {
1543                                         let ids = getSelectedArticleIds2();
1544                                         // cast to string
1545                                         const id = (this.getParent().ownerMenu.currentTarget.getAttribute("data-article-id")) + "";
1546
1547                                         ids = ids.length != 0 && ids.indexOf(id) != -1 ? ids : [id];
1548
1549                                         selectionRemoveLabel(this.labelId, ids);
1550                                 }
1551                         }));
1552
1553                 });
1554
1555                 menu.addChild(new dijit.PopupMenuItem({
1556                         label: __("Assign label"),
1557                         popup: labelAddMenu
1558                 }));
1559
1560                 menu.addChild(new dijit.PopupMenuItem({
1561                         label: __("Remove label"),
1562                         popup: labelDelMenu
1563                 }));
1564
1565         }
1566 }
1567
1568 function initHeadlinesMenu() {
1569         if (!dijit.byId("headlinesMenu")) {
1570
1571                 const menu = new dijit.Menu({
1572                         id: "headlinesMenu",
1573                         targetNodeIds: ["headlines-frame"],
1574                         selector: ".hlMenuAttach"
1575                 });
1576
1577                 headlinesMenuCommon(menu);
1578
1579                 menu.startup();
1580         }
1581
1582         /* vgroup feed title menu */
1583
1584         if (!dijit.byId("headlinesFeedTitleMenu")) {
1585
1586                 const menu = new dijit.Menu({
1587                         id: "headlinesFeedTitleMenu",
1588                         targetNodeIds: ["headlines-frame"],
1589                         selector: "div.cdmFeedTitle"
1590                 });
1591
1592                 menu.addChild(new dijit.MenuItem({
1593                         label: __("Select articles in group"),
1594                         onClick: function (event) {
1595                                 selectArticles("all",
1596                                         "#headlines-frame > div[id*=RROW]" +
1597                                         "[data-orig-feed-id='" + this.getParent().currentTarget.getAttribute("data-feed-id") + "']");
1598
1599                         }
1600                 }));
1601
1602                 menu.addChild(new dijit.MenuItem({
1603                         label: __("Mark group as read"),
1604                         onClick: function () {
1605                                 selectArticles("none");
1606                                 selectArticles("all",
1607                                         "#headlines-frame > div[id*=RROW]" +
1608                                         "[data-orig-feed-id='" + this.getParent().currentTarget.getAttribute("data-feed-id") + "']");
1609
1610                                 catchupSelection();
1611                         }
1612                 }));
1613
1614                 menu.addChild(new dijit.MenuItem({
1615                         label: __("Mark feed as read"),
1616                         onClick: function () {
1617                                 catchupFeedInGroup(this.getParent().currentTarget.getAttribute("data-feed-id"));
1618                         }
1619                 }));
1620
1621                 menu.addChild(new dijit.MenuItem({
1622                         label: __("Edit feed"),
1623                         onClick: function () {
1624                                 editFeed(this.getParent().currentTarget.getAttribute("data-feed-id"));
1625                         }
1626                 }));
1627
1628                 menu.startup();
1629         }
1630 }
1631
1632 function cache_set(id, obj) {
1633         //console.log("cache_set: " + id);
1634         if (has_storage)
1635                 try {
1636                         sessionStorage[id] = obj;
1637                 } catch (e) {
1638                         sessionStorage.clear();
1639                 }
1640 }
1641
1642 function cache_get(id) {
1643         if (has_storage)
1644                 return sessionStorage[id];
1645 }
1646
1647 function cache_clear() {
1648         if (has_storage)
1649                 sessionStorage.clear();
1650 }
1651
1652 function cache_delete(id) {
1653         if (has_storage)
1654                 sessionStorage.removeItem(id);
1655 }
1656
1657 function cancelSearch() {
1658         _search_query = "";
1659         viewCurrentFeed();
1660 }
1661
1662 function setSelectionScore() {
1663         const ids = getSelectedArticleIds2();
1664
1665         if (ids.length > 0) {
1666                 console.log(ids);
1667
1668                 const score = prompt(__("Please enter new score for selected articles:"));
1669
1670                 if (score != undefined) {
1671                         const query = { op: "article", method: "setScore", id: ids.toString(),
1672                                 score: score };
1673
1674                         xhrJson("backend.php", query, (reply) => {
1675                                 if (reply) {
1676                                         reply.id.each((id) => {
1677                                                 const row = $("RROW-" + id);
1678
1679                                                 if (row) {
1680                                                         const pic = row.getElementsByClassName("score-pic")[0];
1681
1682                                                         if (pic) {
1683                                                                 pic.src = pic.src.replace(/score_.*?\.png/,
1684                                                                         reply["score_pic"]);
1685                                                                 pic.setAttribute("score", reply["score"]);
1686                                                         }
1687                                                 }
1688                                         });
1689                                 }
1690                         });
1691                 }
1692
1693         } else {
1694                 alert(__("No articles are selected."));
1695         }
1696 }
1697
1698 function changeScore(id, pic) {
1699         const score = pic.getAttribute("score");
1700
1701         const new_score = prompt(__("Please enter new score for this article:"), score);
1702
1703         if (new_score != undefined) {
1704                 const query = { op: "article", method: "setScore", id: id, score: new_score };
1705
1706                 xhrJson("backend.php", query, (reply) => {
1707                         if (reply) {
1708                                 pic.src = pic.src.replace(/score_.*?\.png/, reply["score_pic"]);
1709                                 pic.setAttribute("score", new_score);
1710                                 pic.setAttribute("title", new_score);
1711                         }
1712                 });
1713         }
1714 }
1715
1716 function displayArticleUrl(id) {
1717         const query = { op: "rpc", method: "getlinktitlebyid", id: id };
1718
1719         xhrJson("backend.php", query, (reply) => {
1720                 if (reply && reply.link) {
1721                         prompt(__("Article URL:"), reply.link);
1722                 }
1723         });
1724
1725 }
1726
1727 function scrollToRowId(id) {
1728         const row = $(id);
1729
1730         if (row)
1731                 $("headlines-frame").scrollTop = row.offsetTop - 4;
1732 }
1733
1734 function updateFloatingTitle(unread_only) {
1735         if (!isCdmMode()) return;
1736
1737         const hf = $("headlines-frame");
1738
1739         const elems = $$("#headlines-frame > div[id*=RROW]");
1740
1741         for (let i = 0; i < elems.length; i++) {
1742
1743                 const child = elems[i];
1744
1745                 if (child && child.offsetTop + child.offsetHeight > hf.scrollTop) {
1746
1747                         const header = child.getElementsByClassName("header")[0];
1748
1749                         if (unread_only || child.getAttribute("data-article-id") != $("floatingTitle").getAttribute("data-article-id")) {
1750                                 if (child.getAttribute("data-article-id") != $("floatingTitle").getAttribute("data-article-id")) {
1751
1752                                         $("floatingTitle").setAttribute("data-article-id", child.getAttribute("data-article-id"));
1753                                         $("floatingTitle").innerHTML = header.innerHTML;
1754                                         $("floatingTitle").firstChild.innerHTML = "<img class='anchor markedPic' src='images/page_white_go.png' onclick=\"scrollToRowId('" + child.id + "')\">" + $("floatingTitle").firstChild.innerHTML;
1755
1756                                         initFloatingMenu();
1757
1758                                         const cb = $$("#floatingTitle .dijitCheckBox")[0];
1759
1760                                         if (cb)
1761                                                 cb.parentNode.removeChild(cb);
1762                                 }
1763
1764                                 if (child.hasClassName("Unread"))
1765                                         $("floatingTitle").addClassName("Unread");
1766                                 else
1767                                         $("floatingTitle").removeClassName("Unread");
1768
1769                                 PluginHost.run(PluginHost.HOOK_FLOATING_TITLE, child);
1770                         }
1771
1772                         $("floatingTitle").style.marginRight = hf.offsetWidth - child.offsetWidth + "px";
1773                         if (header.offsetTop + header.offsetHeight < hf.scrollTop + $("floatingTitle").offsetHeight - 5 &&
1774                                 child.offsetTop + child.offsetHeight >= hf.scrollTop + $("floatingTitle").offsetHeight - 5)
1775                                 $("floatingTitle").style.visibility = "visible";
1776                         else
1777                                 $("floatingTitle").style.visibility = "hidden";
1778
1779                         return;
1780
1781                 }
1782         }
1783 }