3 let _active_article_id
= 0;
5 let vgroup_last_feed
= false;
6 let post_under_pointer
= false;
8 let last_requested_article
= 0;
10 let catchup_id_batch
= [];
11 //let catchup_timeout_id = false;
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
;
20 let _catchup_request_sent
= false;
22 let has_storage
= 'sessionStorage' in window
&& window
['sessionStorage'] !== null;
24 function headlines_callback2(transport
, offset
, background
, infscroll_req
) {
25 const reply
= handle_rpc_json(transport
);
27 console
.log("headlines_callback2 [offset=" + offset
+ "] B:" + background
+ " I:" + infscroll_req
);
37 is_cat
= reply
['headlines']['is_cat'];
38 feed_id
= reply
['headlines']['id'];
39 last_search_query
= reply
['headlines']['search_query'];
41 if (feed_id
!= -7 && (feed_id
!= getActiveFeedId() || is_cat
!= activeFeedIsCat()))
45 if (infscroll_req
== false) {
46 $("headlines-frame").scrollTop
= 0;
48 $("floatingTitle").style
.visibility
= "hidden";
49 $("floatingTitle").setAttribute("data-article-id", 0);
50 $("floatingTitle").innerHTML
= "";
54 $("headlines-frame").removeClassName("cdm");
55 $("headlines-frame").removeClassName("normal");
57 $("headlines-frame").addClassName(isCdmMode() ? "cdm" : "normal");
59 const headlines_count
= reply
['headlines-info']['count'];
61 vgroup_last_feed
= reply
['headlines-info']['vgroup_last_feed'];
63 if (parseInt(headlines_count
) < 30) {
64 _infscroll_disable
= 1;
66 _infscroll_disable
= 0;
69 current_first_id
= reply
['headlines']['first_id'];
70 const counters
= reply
['counters'];
71 const articles
= reply
['articles'];
73 if (infscroll_req
== false) {
74 loaded_article_ids
= [];
76 dojo
.html
.set($("headlines-toolbar"),
77 reply
['headlines']['toolbar'],
78 {parseContent
: true});
80 $("headlines-frame").innerHTML
= '';
82 let tmp
= document
.createElement("div");
83 tmp
.innerHTML
= reply
['headlines']['content'];
84 dojo
.parser
.parse(tmp
);
86 while (tmp
.hasChildNodes()) {
87 var row
= tmp
.removeChild(tmp
.firstChild
);
89 if (loaded_article_ids
.indexOf(row
.id
) == -1 || row
.hasClassName("feed-title")) {
90 dijit
.byId("headlines-frame").domNode
.appendChild(row
);
92 loaded_article_ids
.push(row
.id
);
96 let hsp
= $("headlines-spacer");
97 if (!hsp
) hsp
= new Element("DIV", {"id": "headlines-spacer"});
98 dijit
.byId('headlines-frame').domNode
.appendChild(hsp
);
102 if (_infscroll_disable
)
103 hsp
.innerHTML
= "<a href='#' onclick='openNextUnreadFeed()'>" +
104 __("Click to open next unread feed.") + "</a>";
107 $("feed_title").innerHTML
+= "<span id='cancel_search'>" +
108 " (<a href='#' onclick='cancelSearch()'>" + __("Cancel search") + "</a>)" +
112 } else if (headlines_count
> 0 && feed_id
== getActiveFeedId() && is_cat
== activeFeedIsCat()) {
113 console
.log("adding some more headlines: " + headlines_count
);
115 const c
= dijit
.byId("headlines-frame");
116 //const ids = getSelectedArticleIds2();
118 let hsp
= $("headlines-spacer");
121 c
.domNode
.removeChild(hsp
);
123 let tmp
= document
.createElement("div");
124 tmp
.innerHTML
= reply
['headlines']['content'];
125 dojo
.parser
.parse(tmp
);
127 while (tmp
.hasChildNodes()) {
128 let row
= tmp
.removeChild(tmp
.firstChild
);
130 if (loaded_article_ids
.indexOf(row
.id
) == -1 || row
.hasClassName("feed-title")) {
131 dijit
.byId("headlines-frame").domNode
.appendChild(row
);
133 loaded_article_ids
.push(row
.id
);
137 if (!hsp
) hsp
= new Element("DIV", {"id": "headlines-spacer"});
138 c
.domNode
.appendChild(hsp
);
140 if (headlines_count
< 30) _infscroll_disable
= true;
142 /* console.log("restore selected ids: " + ids);
144 for (let i = 0; i < ids.length; i++) {
145 markHeadline(ids[i]);
150 if (_infscroll_disable
) {
151 hsp
.innerHTML
= "<a href='#' onclick='openNextUnreadFeed()'>" +
152 __("Click to open next unread feed.") + "</a>";
156 console
.log("no new headlines received");
158 const first_id_changed
= reply
['headlines']['first_id_changed'];
159 console
.log("first id changed:" + first_id_changed
);
161 let hsp
= $("headlines-spacer");
164 if (first_id_changed
) {
165 hsp
.innerHTML
= "<a href='#' onclick='viewCurrentFeed()'>" +
166 __("New articles found, reload feed to continue.") + "</a>";
168 hsp
.innerHTML
= "<a href='#' onclick='openNextUnreadFeed()'>" +
169 __("Click to open next unread feed.") + "</a>";
177 console
.error("Invalid object received: " + transport
.responseText
);
178 dijit
.byId("headlines-frame").attr('content', "<div class='whiteBox'>" +
179 __('Could not update headlines (invalid object received - see error console for details)') +
183 _infscroll_request_sent
= 0;
184 _last_headlines_update
= new Date().getTime();
186 unpackVisibleHeadlines();
188 // if we have some more space in the buffer, why not try to fill it
190 if (!_infscroll_disable
&& $("headlines-spacer") &&
191 $("headlines-spacer").offsetTop
< $("headlines-frame").offsetHeight
) {
193 window
.setTimeout(function() {
201 function render_article(article
) {
202 cleanup_memory("content-insert");
204 dijit
.byId("headlines-wrap-inner").addChild(
205 dijit
.byId("content-insert"));
207 const c
= dijit
.byId("content-insert");
210 c
.domNode
.scrollTop
= 0;
213 c
.attr('content', article
);
214 PluginHost
.run(PluginHost
.HOOK_ARTICLE_RENDERED
, c
.domNode
);
216 correctHeadlinesOffset(getActiveArticleId());
224 function showArticleInHeadlines(id, noexpand) {
225 const row = $("RROW-" + id);
229 row.removeClassName("Unread");
231 row.addClassName("active");
233 selectArticles('none');
238 function article_callback2(transport, id) {
239 console.log("article_callback2 " + id);
241 const reply = handle_rpc_json(transport);
245 reply.each(function(article) {
246 if (getActiveArticleId() == article['id']) {
247 render_article(article['content']);
249 cids_requested.remove(article['id']);
251 cache_set("article:" + article['id'], article['content']);
255 console.error("Invalid object received: " + transport.responseText);
257 render_article("<div class='whiteBox'>" +
258 __('Could not display article (invalid object received - see error console for details)') + "</div>");
261 const unread_in_buffer = $$("#headlines-frame > div[id*=RROW][class*=Unread]").length;
262 request_counters(unread_in_buffer == 0);
267 function view(id
, noexpand
) {
268 setActiveArticleId(id
);
271 console
.log("loading article", id
);
273 const neighbor_ids
= getRelativePostIds(id
);
276 /* only request uncached articles */
278 neighbor_ids
.each((n
) => {
279 if (!cache_get("article:" + n
))
283 const cached_article
= cache_get("article:" + id
);
285 if (cached_article
) {
286 console
.log('rendering cached', id
);
287 render_article(cached_article
);
291 xhrPost("backend.php", {op
: "article", method
: "view", id
: id
, cids
: cids
.toString()}, (transport
) => {
293 const reply
= handle_rpc_json(transport
);
297 reply
.each(function(article
) {
298 if (getActiveArticleId() == article
['id']) {
299 render_article(article
['content']);
301 //cids_requested.remove(article['id']);
303 cache_set("article:" + article
['id'], article
['content']);
307 console
.error("Invalid object received: " + transport
.responseText
);
309 render_article("<div class='whiteBox'>" +
310 __('Could not display article (invalid object received - see error console for details)') + "</div>");
313 //const unread_in_buffer = $$("#headlines-frame > div[id*=RROW][class*=Unread]").length;
314 //request_counters(unread_in_buffer == 0);
326 /* const oldrow = $("RROW-" + getActiveArticleId());
327 if (oldrow) oldrow.removeClassName("active");
329 const crow = $("RROW-" + id);
333 setActiveArticleId(id);
334 showArticleInHeadlines(id, noexpand);
338 console.log("loading article: " + id);
340 const cached_article = cache_get("article:" + id);
342 console.log("cache check result: " + (cached_article != false));
344 const query = {op: "article", method: "view", id: id};
346 const neighbor_ids = getRelativePostIds(id);
348 /* only request uncached articles */
350 /* const cids_to_request = [];
352 for (let i = 0; i < neighbor_ids.length; i++) {
353 if (cids_requested.indexOf(neighbor_ids[i]) == -1)
354 if (!cache_get("article:" + neighbor_ids[i])) {
355 cids_to_request.push(neighbor_ids[i]);
356 cids_requested.push(neighbor_ids[i]);
360 console.log("additional ids: " + cids_to_request.toString());
362 query.cids = cids_to_request.toString();
364 const article_is_unread = crow.hasClassName("Unread");
366 setActiveArticleId(id);
367 showArticleInHeadlines(id);
369 if (cached_article && article_is_unread) {
370 query.mode = "prefetch";
371 render_article(cached_article);
372 } else if (cached_article) {
373 query.mode = "prefetch_old";
374 render_article(cached_article);
376 // if we don't need to request any relative ids, we might as well skip
377 // the server roundtrip altogether
378 if (cids_to_request.length == 0) {
383 last_requested_article = id;
387 if (article_is_unread) {
388 decrementFeedCounter(getActiveFeedId(), activeFeedIsCat());
391 xhrPost("backend.php", query, (transport) => {
392 article_callback2(transport, id);
399 function toggleMark(id
, client_only
) {
400 const query
= { op
: "rpc", id
: id
, method
: "mark" };
402 const row
= $("RROW-" + id
);
407 const row_imgs
= row
.getElementsByClassName("markedPic");
409 for (let i
= 0; i
< row_imgs
.length
; i
++)
410 imgs
.push(row_imgs
[i
]);
412 const ft
= $("floatingTitle");
414 if (ft
&& ft
.getAttribute("data-article-id") == id
) {
415 const fte
= ft
.getElementsByClassName("markedPic");
417 for (var i
= 0; i
< fte
.length
; i
++)
421 for (i
= 0; i
< imgs
.length
; i
++) {
424 if (!row
.hasClassName("marked")) {
425 img
.src
= img
.src
.replace("mark_unset", "mark_set");
428 img
.src
= img
.src
.replace("mark_set", "mark_unset");
433 row
.toggleClassName("marked");
436 xhrPost("backend.php", query
, (transport
) => {
437 handle_rpc_json(transport
);
441 function togglePub(id
, client_only
, no_effects
, note
) {
442 const query
= { op
: "rpc", id
: id
, method
: "publ" };
444 if (note
!= undefined) {
447 query
.note
= "undefined";
450 const row
= $("RROW-" + id
);
455 const row_imgs
= row
.getElementsByClassName("pubPic");
457 for (let i
= 0; i
< row_imgs
.length
; i
++)
458 imgs
.push(row_imgs
[i
]);
460 const ft
= $("floatingTitle");
462 if (ft
&& ft
.getAttribute("data-article-id") == id
) {
463 const fte
= ft
.getElementsByClassName("pubPic");
465 for (let i
= 0; i
< fte
.length
; i
++)
469 for (let i
= 0; i
< imgs
.length
; i
++) {
472 if (!row
.hasClassName("published") || note
!= undefined) {
473 img
.src
= img
.src
.replace("pub_unset", "pub_set");
476 img
.src
= img
.src
.replace("pub_set", "pub_unset");
481 if (note
!= undefined)
482 row
.addClassName("published");
484 row
.toggleClassName("published");
487 xhrPost("backend.php", query
, (transport
) => {
488 handle_rpc_json(transport
);
492 function moveToPost(mode
, noscroll
, noexpand
) {
493 const rows
= getLoadedArticleIds();
498 if (!$('RROW-' + getActiveArticleId())) {
499 setActiveArticleId(0);
502 if (!getActiveArticleId()) {
504 prev_id
= rows
[rows
.length
-1]
506 for (let i
= 0; i
< rows
.length
; i
++) {
507 if (rows
[i
] == getActiveArticleId()) {
509 // Account for adjacent identical article ids.
510 if (i
> 0) prev_id
= rows
[i
-1];
512 for (let j
= i
+1; j
< rows
.length
; j
++) {
513 if (rows
[j
] != getActiveArticleId()) {
523 console
.log("cur: " + getActiveArticleId() + " next: " + next_id
);
525 if (mode
== "next") {
526 if (next_id
|| getActiveArticleId()) {
529 const article
= $("RROW-" + getActiveArticleId());
530 const ctr
= $("headlines-frame");
532 if (!noscroll
&& article
&& article
.offsetTop
+ article
.offsetHeight
>
533 ctr
.scrollTop
+ ctr
.offsetHeight
) {
535 scrollArticle(ctr
.offsetHeight
/4);
537 } else if (next_id
) {
538 cdmScrollToArticleId(next_id
, true);
539 setActiveArticleId(next_id
);
542 } else if (next_id
) {
543 correctHeadlinesOffset(next_id
);
544 view(next_id
, noexpand
);
549 if (mode
== "prev") {
550 if (prev_id
|| getActiveArticleId()) {
553 const article
= $("RROW-" + getActiveArticleId());
554 const prev_article
= $("RROW-" + prev_id
);
555 const ctr
= $("headlines-frame");
557 if (!noscroll
&& article
&& article
.offsetTop
< ctr
.scrollTop
) {
558 scrollArticle(-ctr
.offsetHeight
/3);
559 } else if (!noscroll
&& prev_article
&&
560 prev_article
.offsetTop
< ctr
.scrollTop
) {
561 scrollArticle(-ctr
.offsetHeight
/4);
562 } else if (prev_id
) {
563 cdmScrollToArticleId(prev_id
, noscroll
);
564 setActiveArticleId(prev_id
);
567 } else if (prev_id
) {
568 correctHeadlinesOffset(prev_id
);
569 view(prev_id
, noexpand
);
576 /* function toggleSelected(id, force_on) {
577 const row = $("RROW-" + id);
580 const cb = dijit.getEnclosingWidget(
581 row.getElementsByClassName("rchk")[0]);
583 if (row.hasClassName('Selected') && !force_on) {
584 row.removeClassName('Selected');
585 if (cb) cb.attr("checked", false);
587 row.addClassName('Selected');
588 if (cb) cb.attr("checked", true);
592 updateSelectedPrompt();
595 function updateSelectedPrompt() {
596 const count
= getSelectedArticleIds2().length
;
597 const elem
= $("selected_prompt");
600 elem
.innerHTML
= ngettext("%d article selected",
601 "%d articles selected", count
).replace("%d", count
);
611 function toggleUnread(id
, cmode
) {
612 const row
= $("RROW-" + id
);
615 const tmpClassName
= row
.className
;
617 if (cmode
== undefined || cmode
== 2) {
618 if (row
.hasClassName("Unread")) {
619 row
.removeClassName("Unread");
622 row
.addClassName("Unread");
625 } else if (cmode
== 0) {
626 row
.removeClassName("Unread");
627 } else if (cmode
== 1) {
628 row
.addClassName("Unread");
631 if (tmpClassName
!= row
.className
) {
632 if (cmode
== undefined) cmode
= 2;
634 const query
= {op
: "rpc", method
: "catchupSelected",
635 cmode
: cmode
, ids
: id
};
637 xhrPost("backend.php", query
, (transport
) => {
638 handle_rpc_json(transport
);
644 function selectionRemoveLabel(id
, ids
) {
645 if (!ids
) ids
= getSelectedArticleIds2();
647 if (ids
.length
== 0) {
648 alert(__("No articles are selected."));
652 const query
= { op
: "article", method
: "removeFromLabel",
653 ids
: ids
.toString(), lid
: id
};
655 xhrPost("backend.php", query
, (transport
) => {
656 handle_rpc_json(transport
);
657 show_labels_in_headlines(transport
);
661 function selectionAssignLabel(id
, ids
) {
662 if (!ids
) ids
= getSelectedArticleIds2();
664 if (ids
.length
== 0) {
665 alert(__("No articles are selected."));
669 const query
= { op
: "article", method
: "assignToLabel",
670 ids
: ids
.toString(), lid
: id
};
672 xhrPost("backend.php", query
, (transport
) => {
673 handle_rpc_json(transport
);
674 show_labels_in_headlines(transport
);
678 function selectionToggleUnread(set_state
, callback
, no_error
, ids
) {
679 const rows
= ids
? ids
: getSelectedArticleIds2();
681 if (rows
.length
== 0 && !no_error
) {
682 alert(__("No articles are selected."));
686 for (let i
= 0; i
< rows
.length
; i
++) {
687 const row
= $("RROW-" + rows
[i
]);
689 if (set_state
== undefined) {
690 if (row
.hasClassName("Unread")) {
691 row
.removeClassName("Unread");
693 row
.addClassName("Unread");
697 if (set_state
== false) {
698 row
.removeClassName("Unread");
701 if (set_state
== true) {
702 row
.addClassName("Unread");
707 updateFloatingTitle(true);
709 if (rows
.length
> 0) {
713 if (set_state
== undefined) {
715 } else if (set_state
== true) {
717 } else if (set_state
== false) {
721 const query
= {op
: "rpc", method
: "catchupSelected",
722 cmode
: cmode
, ids
: rows
.toString() };
724 notify_progress("Loading, please wait...");
726 xhrPost("backend.php", query
, (transport
) => {
727 handle_rpc_json(transport
);
728 if (callback
) callback(transport
);
735 function selectionToggleMarked(sel_state
, callback
, no_error
, ids
) {
736 const rows
= ids
? ids
: getSelectedArticleIds2();
738 if (rows
.length
== 0 && !no_error
) {
739 alert(__("No articles are selected."));
743 for (let i
= 0; i
< rows
.length
; i
++) {
744 toggleMark(rows
[i
], true, true);
747 if (rows
.length
> 0) {
748 const query
= { op
: "rpc", method
: "markSelected",
749 ids
: rows
.toString(), cmode
: 2 };
751 xhrPost("backend.php", query
, (transport
) => {
752 handle_rpc_json(transport
);
753 if (callback
) callback(transport
);
759 function selectionTogglePublished(sel_state
, callback
, no_error
, ids
) {
760 const rows
= ids
? ids
: getSelectedArticleIds2();
762 if (rows
.length
== 0 && !no_error
) {
763 alert(__("No articles are selected."));
767 for (let i
= 0; i
< rows
.length
; i
++) {
768 togglePub(rows
[i
], true, true);
771 if (rows
.length
> 0) {
772 const query
= { op
: "rpc", method
: "publishSelected",
773 ids
: rows
.toString(), cmode
: 2 };
775 xhrPost("backend.php", query
, (transport
) => {
776 handle_rpc_json(transport
);
777 if (callback
) callback(transport
);
782 function getSelectedArticleIds2() {
786 $$("#headlines-frame > div[id*=RROW][class*=Selected]").each(
788 rv
.push(child
.getAttribute("data-article-id"));
791 // i wonder if this is a good idea: consider active article a honorary member
792 // of selected articles
793 if (getActiveArticleId())
794 rv
.push(getActiveArticleId());
799 function getLoadedArticleIds() {
802 const children
= $$("#headlines-frame > div[id*=RROW-]");
804 children
.each(function(child
) {
805 if (Element
.visible(child
)) {
806 rv
.push(child
.getAttribute("data-article-id"));
814 // mode = all,none,unread,invert,marked,published
815 function selectArticles(mode
) {
816 let query
= "#headlines-frame > div[id*=RROW]";
824 query
+= "[class*=marked]";
827 query
+= "[class*=published]";
830 query
+= "[class*=Unread]";
833 console
.warn("selectArticles: unknown mode", mode
);
836 const rows
= $$(query
);
838 for (let i
= 0; i
< rows
.length
; i
++) {
840 const cb
= dijit
.getEnclosingWidget(row
.select(".rchk")[0]);
844 row
.removeClassName("Selected");
846 if (!row
.hasClassName("active"))
847 cb
.attr("checked", false);
850 if (row
.hasClassName("Selected")) {
851 row
.removeClassName("Selected");
853 if (!row
.hasClassName("active"))
854 cb
.attr("checked", false);
856 row
.addClassName("Selected");
857 cb
.attr("checked", true);
861 row
.addClassName("Selected");
862 cb
.attr("checked", true);
865 updateSelectedPrompt();
869 // mode = all,none,unread,invert,marked,published
870 /* function selectArticles(mode, query) {
871 if (!query) query = "#headlines-frame > div[id*=RROW]";
873 const children = $$(query);
875 children.each(function(child) {
876 //const id = child.getAttribute("data-article-id");
878 const cb = dijit.getEnclosingWidget(
879 child.getElementsByClassName("rchk")[0]);
882 child.addClassName("Selected");
883 if (cb) cb.attr("checked", true);
884 } else if (mode == "unread") {
885 if (child.hasClassName("Unread")) {
886 child.addClassName("Selected");
887 if (cb) cb.attr("checked", true);
889 child.removeClassName("Selected");
890 if (cb) cb.attr("checked", false);
892 } else if (mode == "marked") {
893 if (child.hasClassName("marked")) {
894 child.addClassName("Selected");
895 if (cb) cb.attr("checked", true);
897 child.removeClassName("Selected");
898 if (cb) cb.attr("checked", false);
900 } else if (mode == "published") {
901 if (child.hasClassName("published")) {
902 child.addClassName("Selected");
903 if (cb) cb.attr("checked", true);
905 child.removeClassName("Selected");
906 if (cb) cb.attr("checked", false);
909 } else if (mode == "invert") {
910 if (child.hasClassName("Selected")) {
911 child.removeClassName("Selected");
912 if (cb) cb.attr("checked", false);
914 child.addClassName("Selected");
915 if (cb) cb.attr("checked", true);
919 child.removeClassName("Selected");
920 if (cb) cb.attr("checked", false);
924 updateSelectedPrompt();
927 function deleteSelection() {
929 const rows
= getSelectedArticleIds2();
931 if (rows
.length
== 0) {
932 alert(__("No articles are selected."));
936 const fn
= getFeedName(getActiveFeedId(), activeFeedIsCat());
939 if (getActiveFeedId() != 0) {
940 str
= ngettext("Delete %d selected article in %s?", "Delete %d selected articles in %s?", rows
.length
);
942 str
= ngettext("Delete %d selected article?", "Delete %d selected articles?", rows
.length
);
945 str
= str
.replace("%d", rows
.length
);
946 str
= str
.replace("%s", fn
);
948 if (getInitParam("confirm_feed_catchup") == 1 && !confirm(str
)) {
952 const query
= { op
: "rpc", method
: "delete", ids
: rows
.toString() };
954 xhrPost("backend.php", query
, (transport
) => {
955 handle_rpc_json(transport
);
960 function archiveSelection() {
962 const rows
= getSelectedArticleIds2();
964 if (rows
.length
== 0) {
965 alert(__("No articles are selected."));
969 const fn
= getFeedName(getActiveFeedId(), activeFeedIsCat());
973 if (getActiveFeedId() != 0) {
974 str
= ngettext("Archive %d selected article in %s?", "Archive %d selected articles in %s?", rows
.length
);
977 str
= ngettext("Move %d archived article back?", "Move %d archived articles back?", rows
.length
);
979 str
+= " " + __("Please note that unstarred articles might get purged on next feed update.");
984 str
= str
.replace("%d", rows
.length
);
985 str
= str
.replace("%s", fn
);
987 if (getInitParam("confirm_feed_catchup") == 1 && !confirm(str
)) {
991 for (let i
= 0; i
< rows
.length
; i
++) {
992 cache_delete("article:" + rows
[i
]);
995 const query
= {op
: "rpc", method
: op
, ids
: rows
.toString()};
997 xhrPost("backend.php", query
, (transport
) => {
998 handle_rpc_json(transport
);
1003 function catchupSelection() {
1005 const rows
= getSelectedArticleIds2();
1007 if (rows
.length
== 0) {
1008 alert(__("No articles are selected."));
1012 const fn
= getFeedName(getActiveFeedId(), activeFeedIsCat());
1014 let str
= ngettext("Mark %d selected article in %s as read?", "Mark %d selected articles in %s as read?", rows
.length
);
1016 str
= str
.replace("%d", rows
.length
);
1017 str
= str
.replace("%s", fn
);
1019 if (getInitParam("confirm_feed_catchup") == 1 && !confirm(str
)) {
1023 selectionToggleUnread(false, 'viewCurrentFeed()', true);
1026 function editArticleTags(id
) {
1027 const query
= "backend.php?op=article&method=editArticleTags¶m=" + param_escape(id
);
1029 if (dijit
.byId("editTagsDlg"))
1030 dijit
.byId("editTagsDlg").destroyRecursive();
1032 const dialog
= new dijit
.Dialog({
1034 title
: __("Edit article Tags"),
1035 style
: "width: 600px",
1036 execute: function() {
1037 if (this.validate()) {
1038 const query
= dojo
.objectToQuery(this.attr('value'));
1040 notify_progress("Saving article tags...", true);
1042 xhrPost("backend.php", this.attr('value'), (transport
) => {
1047 const data
= JSON
.parse(transport
.responseText
);
1052 const tags
= $("ATSTR-" + id
);
1053 const tooltip
= dijit
.byId("ATSTRTIP-" + id
);
1055 if (tags
) tags
.innerHTML
= data
.content
;
1056 if (tooltip
) tooltip
.attr('label', data
.content_full
);
1067 var tmph
= dojo
.connect(dialog
, 'onLoad', function() {
1068 dojo
.disconnect(tmph
);
1070 new Ajax
.Autocompleter('tags_str', 'tags_choices',
1071 "backend.php?op=article&method=completeTags",
1072 { tokens
: ',', paramName
: "search" });
1079 function cdmScrollToArticleId(id
, force
) {
1080 const ctr
= $("headlines-frame");
1081 const e
= $("RROW-" + id
);
1083 if (!e
|| !ctr
) return;
1085 if (force
|| e
.offsetTop
+e
.offsetHeight
> (ctr
.scrollTop
+ctr
.offsetHeight
) ||
1086 e
.offsetTop
< ctr
.scrollTop
) {
1088 // expanded cdm has a 4px margin now
1089 ctr
.scrollTop
= parseInt(e
.offsetTop
) - 4;
1091 /*setActiveArticleId(id);
1093 // article is selected manually, set it read
1094 toggleUnread(id, 0); */
1098 // for the time being active article does not affect buffer selection (we still re/set the checkbox
1099 // because of getSelectedArticleIds2() hack
1100 function setActiveArticleId(id
) {
1101 console
.log("setActiveArticleId", id
);
1103 $$("div[id*=RROW][class*=active]").each((e
) => {
1104 e
.removeClassName("active");
1106 if (!e
.hasClassName("Selected")) {
1107 const cb
= dijit
.getEnclosingWidget(e
.select(".rchk")[0]);
1108 if (cb
) cb
.attr("checked", false);
1112 _active_article_id
= id
;
1114 const row
= $("RROW-" + id
);
1117 if (row
.hasClassName("Unread")) {
1118 toggleUnread(id
, 0);
1120 decrementFeedCounter(getActiveFeedId(), activeFeedIsCat());
1121 updateFloatingTitle(true);
1124 row
.addClassName("active");
1126 if (!row
.hasClassName("Selected")) {
1127 const cb
= dijit
.getEnclosingWidget(row
.select(".rchk")[0]);
1128 if (cb
) cb
.attr("checked", true);
1131 PluginHost
.run(PluginHost
.HOOK_ARTICLE_SET_ACTIVE
, _active_article_id
);
1135 function getActiveArticleId() {
1136 return _active_article_id
;
1139 function postMouseIn(e
, id
) {
1140 post_under_pointer
= id
;
1143 function postMouseOut(id
) {
1144 post_under_pointer
= false;
1147 function unpackVisibleHeadlines() {
1148 if (!isCdmMode()) return;
1150 const rows
= $$("#headlines-frame div[id*=RROW][data-content]");
1151 const threshold
= $("headlines-frame").scrollTop
+ $("headlines-frame").offsetHeight
+ 300;
1153 for (let i
= 0; i
< rows
.length
; i
++) {
1154 const row
= rows
[i
];
1156 if (row
.offsetTop
<= threshold
) {
1157 console
.log("unpacking: " + row
.id
);
1159 row
.select(".content-inner")[0].innerHTML
= row
.getAttribute("data-content");
1160 row
.removeAttribute("data-content");
1162 PluginHost
.run(PluginHost
.HOOK_ARTICLE_RENDERED_CDM
, row
);
1164 // i wonder if this is a good idea?
1165 if (!getActiveArticleId() && !row
.hasClassName("Unread"))
1166 setActiveArticleId(row
.getAttribute("data-article-id"));
1174 function headlines_scroll_handler(e
) {
1177 // rate-limit in case of smooth scrolling and similar abominations
1178 if (Math
.max(e
.scrollTop
, _headlines_scroll_offset
) - Math
.min(e
.scrollTop
, _headlines_scroll_offset
) < 25) {
1182 _headlines_scroll_offset
= e
.scrollTop
;
1184 unpackVisibleHeadlines();
1186 // set topmost child in the buffer as active
1187 if (isCdmMode() && getInitParam("cdm_auto_catchup") == 1) {
1189 const rows
= $$("#headlines-frame > div[id*=RROW]");
1191 for (let i
= 0; i
< rows
.length
; i
++) {
1192 const row
= rows
[i
];
1194 if ($("headlines-frame").scrollTop
<= row
.offsetTop
&&
1195 row
.offsetTop
- $("headlines-frame").scrollTop
< 100 &&
1196 row
.getAttribute("data-article-id") != getActiveArticleId()) {
1198 /* if (_active_article_id) {
1199 const row = $("RROW-" + _active_article_id);
1200 if (row) row.removeClassName("active");
1203 _active_article_id = row.getAttribute("data-article-id");
1204 showArticleInHeadlines(_active_article_id, true);
1205 updateSelectedPrompt(); */
1207 setActiveArticleId(row
.getAttribute("data-article-id"));
1214 if (!_infscroll_disable
) {
1215 const hsp
= $("headlines-spacer");
1217 if (hsp
&& hsp
.offsetTop
- 250 <= e
.scrollTop
+ e
.offsetHeight
) {
1219 hsp
.innerHTML
= "<span class='loading'><img src='images/indicator_tiny.gif'> " +
1220 __("Loading, please wait...") + "</span>";
1222 loadMoreHeadlines();
1229 updateFloatingTitle();
1232 if (getInitParam("cdm_auto_catchup") == 1) {
1234 let rows
= $$("#headlines-frame > div[id*=RROW][class*=Unread]");
1236 for (let i
= 0; i
< rows
.length
; i
++) {
1237 const row
= rows
[i
];
1239 if ($("headlines-frame").scrollTop
> (row
.offsetTop
+ row
.offsetHeight
/2)) {
1241 const id
= row
.getAttribute("data-article-id")
1243 if (catchup_id_batch
.indexOf(id
) == -1)
1244 catchup_id_batch
.push(id
);
1246 //console.log("auto_catchup_batch: " + catchup_id_batch.toString());
1252 if (_infscroll_disable
) {
1253 const row
= $$("#headlines-frame div[id*=RROW]").last();
1255 if (row
&& $("headlines-frame").scrollTop
>
1256 (row
.offsetTop
+ row
.offsetHeight
- 50)) {
1258 console
.log("we seem to be at an end");
1260 if (getInitParam("on_catchup_show_next_feed") == "1") {
1261 openNextUnreadFeed();
1268 console
.warn("headlines_scroll_handler: " + e
);
1272 function openNextUnreadFeed() {
1273 const is_cat
= activeFeedIsCat();
1274 const nuf
= getNextUnreadFeed(getActiveFeedId(), is_cat
);
1275 if (nuf
) viewfeed({feed
: nuf
, is_cat
: is_cat
});
1278 function catchupBatchedArticles() {
1279 if (catchup_id_batch
.length
> 0 && !_infscroll_request_sent
&& !_catchup_request_sent
) {
1281 console
.log("catchupBatchedArticles, size=", catchup_id_batch
.length
);
1283 // make a copy of the array
1284 const batch
= catchup_id_batch
.slice();
1285 const query
= { op
: "rpc", method
: "catchupSelected",
1286 cmode
: 0, ids
: batch
.toString() };
1288 _catchup_request_sent
= true;
1290 xhrPost("backend.php", query
, (transport
) => {
1291 const reply
= handle_rpc_json(transport
);
1293 _catchup_request_sent
= false;
1296 const batch
= reply
.ids
;
1298 batch
.each(function (id
) {
1299 const elem
= $("RROW-" + id
);
1300 if (elem
) elem
.removeClassName("Unread");
1301 catchup_id_batch
.remove(id
);
1305 updateFloatingTitle(true);
1310 function catchupRelativeToArticle(below
, id
) {
1312 if (!id
) id
= getActiveArticleId();
1315 alert(__("No article is selected."));
1319 const visible_ids
= getLoadedArticleIds();
1321 const ids_to_mark
= [];
1324 for (let i
= 0; i
< visible_ids
.length
; i
++) {
1325 if (visible_ids
[i
] != id
) {
1326 const e
= $("RROW-" + visible_ids
[i
]);
1328 if (e
&& e
.hasClassName("Unread")) {
1329 ids_to_mark
.push(visible_ids
[i
]);
1336 for (let i
= visible_ids
.length
- 1; i
>= 0; i
--) {
1337 if (visible_ids
[i
] != id
) {
1338 const e
= $("RROW-" + visible_ids
[i
]);
1340 if (e
&& e
.hasClassName("Unread")) {
1341 ids_to_mark
.push(visible_ids
[i
]);
1349 if (ids_to_mark
.length
== 0) {
1350 alert(__("No articles found to mark"));
1352 const msg
= ngettext("Mark %d article as read?", "Mark %d articles as read?", ids_to_mark
.length
).replace("%d", ids_to_mark
.length
);
1354 if (getInitParam("confirm_feed_catchup") != 1 || confirm(msg
)) {
1356 for (var i
= 0; i
< ids_to_mark
.length
; i
++) {
1357 var e
= $("RROW-" + ids_to_mark
[i
]);
1358 e
.removeClassName("Unread");
1361 const query
= { op
: "rpc", method
: "catchupSelected",
1362 cmode
: 0, ids
: ids_to_mark
.toString() };
1364 xhrPost("backend.php", query
, (transport
) => {
1365 handle_rpc_json(transport
);
1371 function getArticleUnderPointer() {
1372 return post_under_pointer
;
1375 function scrollArticle(offset
) {
1377 const ci
= $("content-insert");
1379 ci
.scrollTop
+= offset
;
1382 const hi
= $("headlines-frame");
1384 hi
.scrollTop
+= offset
;
1390 function show_labels_in_headlines(transport
) {
1391 const data
= JSON
.parse(transport
.responseText
);
1394 data
['info-for-headlines'].each(function (elem
) {
1395 $$(".HLLCTR-" + elem
.id
).each(function (ctr
) {
1396 ctr
.innerHTML
= elem
.labels
;
1402 function cdmClicked(event
, id
, in_body
) {
1403 if (event
.ctrlKey
&& !in_body
) {
1404 openArticleInNewWindow(id
);
1407 setActiveArticleId(id
);
1409 //var shift_key = event.shiftKey;
1411 /* if (!event.ctrlKey && !event.metaKey) {
1413 let elem = $("RROW-" + getActiveArticleId());
1415 if (elem) elem.removeClassName("active");
1417 selectArticles("none");
1420 elem = $("RROW-" + id);
1421 const article_is_unread = elem.hasClassName("Unread");
1423 elem.removeClassName("Unread");
1424 elem.addClassName("active");
1426 setActiveArticleId(id);
1428 if (article_is_unread) {
1429 decrementFeedCounter(getActiveFeedId(), activeFeedIsCat());
1430 updateFloatingTitle(true);
1433 op: "rpc", method: "catchupSelected",
1437 xhrPost("backend.php", query, (transport) => {
1438 handle_rpc_json(transport);
1442 return !event.shiftKey;
1444 } else if (!in_body) {
1446 toggleSelected(id, true);
1448 let elem = $("RROW-" + id);
1449 const article_is_unread = elem.hasClassName("Unread");
1451 if (article_is_unread) {
1452 decrementFeedCounter(getActiveFeedId(), activeFeedIsCat());
1455 toggleUnread(id, 0, false);
1457 openArticleInNewWindow(id);
1462 const unread_in_buffer = $$("#headlines-frame > div[id*=RROW][class*=Unread]").length
1463 request_counters(unread_in_buffer == 0); */
1468 function hlClicked(event
, id
) {
1469 if (event
.ctrlKey
) {
1470 openArticleInNewWindow(id
);
1471 setActiveArticleId(id
);
1478 /* if (event.which == 2) {
1481 } else if (event.ctrlKey || event.metaKey) {
1482 openArticleInNewWindow(id);
1490 function openArticleInNewWindow(id
) {
1491 const w
= window
.open("");
1493 w
.location
= "backend.php?op=article&method=redirect&id=" + id
;
1496 function isCdmMode() {
1497 return getInitParam("combined_display_mode");
1500 /* function markHeadline(id, marked) {
1501 if (marked == undefined) marked = true;
1503 const row = $("RROW-" + id);
1505 const check = dijit.getEnclosingWidget(
1506 row.getElementsByClassName("rchk")[0]);
1509 check.attr("checked", marked);
1513 row.addClassName("Selected");
1515 row.removeClassName("Selected");
1519 function getRelativePostIds(id
, limit
) {
1523 if (!limit
) limit
= 6; //3
1525 const ids
= getLoadedArticleIds();
1527 for (let i
= 0; i
< ids
.length
; i
++) {
1529 for (let k
= 1; k
<= limit
; k
++) {
1530 //if (i > k-1) tmp.push(ids[i-k]);
1531 if (i
< ids
.length
- k
) tmp
.push(ids
[i
+ k
]);
1540 function correctHeadlinesOffset(id
) {
1541 const container
= $("headlines-frame");
1542 const row
= $("RROW-" + id
);
1544 if (!container
|| !row
) return;
1546 const viewport
= container
.offsetHeight
;
1548 const rel_offset_top
= row
.offsetTop
- container
.scrollTop
;
1549 const rel_offset_bottom
= row
.offsetTop
+ row
.offsetHeight
- container
.scrollTop
;
1551 //console.log("Rtop: " + rel_offset_top + " Rbtm: " + rel_offset_bottom);
1552 //console.log("Vport: " + viewport);
1554 if (rel_offset_top
<= 0 || rel_offset_top
> viewport
) {
1555 container
.scrollTop
= row
.offsetTop
;
1556 } else if (rel_offset_bottom
> viewport
) {
1557 container
.scrollTop
= row
.offsetTop
+ row
.offsetHeight
- viewport
;
1561 function headlineActionsChange(elem
) {
1563 elem
.attr('value', 'false');
1566 function closeArticlePanel() {
1567 if (dijit
.byId("content-insert"))
1568 dijit
.byId("headlines-wrap-inner").removeChild(
1569 dijit
.byId("content-insert"));
1572 function initFloatingMenu() {
1573 if (!dijit
.byId("floatingMenu")) {
1575 const menu
= new dijit
.Menu({
1577 targetNodeIds
: ["floatingTitle"]
1580 headlinesMenuCommon(menu
);
1586 function headlinesMenuCommon(menu
) {
1588 menu
.addChild(new dijit
.MenuItem({
1589 label
: __("Open original article"),
1590 onClick: function (event
) {
1591 openArticleInNewWindow(this.getParent().currentTarget
.getAttribute("data-article-id"));
1595 menu
.addChild(new dijit
.MenuItem({
1596 label
: __("Display article URL"),
1597 onClick: function (event
) {
1598 displayArticleUrl(this.getParent().currentTarget
.getAttribute("data-article-id"));
1602 menu
.addChild(new dijit
.MenuSeparator());
1604 menu
.addChild(new dijit
.MenuItem({
1605 label
: __("Toggle unread"),
1606 onClick: function () {
1608 let ids
= getSelectedArticleIds2();
1610 const id
= (this.getParent().currentTarget
.getAttribute("data-article-id")) + "";
1611 ids
= ids
.length
!= 0 && ids
.indexOf(id
) != -1 ? ids
: [id
];
1613 selectionToggleUnread(undefined, false, true, ids
);
1617 menu
.addChild(new dijit
.MenuItem({
1618 label
: __("Toggle starred"),
1619 onClick: function () {
1620 let ids
= getSelectedArticleIds2();
1622 const id
= (this.getParent().currentTarget
.getAttribute("data-article-id")) + "";
1623 ids
= ids
.length
!= 0 && ids
.indexOf(id
) != -1 ? ids
: [id
];
1625 selectionToggleMarked(undefined, false, true, ids
);
1629 menu
.addChild(new dijit
.MenuItem({
1630 label
: __("Toggle published"),
1631 onClick: function () {
1632 let ids
= getSelectedArticleIds2();
1634 const id
= (this.getParent().currentTarget
.getAttribute("data-article-id")) + "";
1635 ids
= ids
.length
!= 0 && ids
.indexOf(id
) != -1 ? ids
: [id
];
1637 selectionTogglePublished(undefined, false, true, ids
);
1641 menu
.addChild(new dijit
.MenuSeparator());
1643 menu
.addChild(new dijit
.MenuItem({
1644 label
: __("Mark above as read"),
1645 onClick: function () {
1646 catchupRelativeToArticle(0, this.getParent().currentTarget
.getAttribute("data-article-id"));
1650 menu
.addChild(new dijit
.MenuItem({
1651 label
: __("Mark below as read"),
1652 onClick: function () {
1653 catchupRelativeToArticle(1, this.getParent().currentTarget
.getAttribute("data-article-id"));
1658 const labels
= getInitParam("labels");
1660 if (labels
&& labels
.length
) {
1662 menu
.addChild(new dijit
.MenuSeparator());
1664 const labelAddMenu
= new dijit
.Menu({ownerMenu
: menu
});
1665 const labelDelMenu
= new dijit
.Menu({ownerMenu
: menu
});
1667 labels
.each(function (label
) {
1668 const bare_id
= label
.id
;
1669 const name
= label
.caption
;
1671 labelAddMenu
.addChild(new dijit
.MenuItem({
1674 onClick: function () {
1676 let ids
= getSelectedArticleIds2();
1678 const id
= (this.getParent().ownerMenu
.currentTarget
.getAttribute("data-article-id")) + "";
1680 ids
= ids
.length
!= 0 && ids
.indexOf(id
) != -1 ? ids
: [id
];
1682 selectionAssignLabel(this.labelId
, ids
);
1686 labelDelMenu
.addChild(new dijit
.MenuItem({
1689 onClick: function () {
1690 let ids
= getSelectedArticleIds2();
1692 const id
= (this.getParent().ownerMenu
.currentTarget
.getAttribute("data-article-id")) + "";
1694 ids
= ids
.length
!= 0 && ids
.indexOf(id
) != -1 ? ids
: [id
];
1696 selectionRemoveLabel(this.labelId
, ids
);
1702 menu
.addChild(new dijit
.PopupMenuItem({
1703 label
: __("Assign label"),
1707 menu
.addChild(new dijit
.PopupMenuItem({
1708 label
: __("Remove label"),
1715 function initHeadlinesMenu() {
1716 if (!dijit
.byId("headlinesMenu")) {
1718 const menu
= new dijit
.Menu({
1719 id
: "headlinesMenu",
1720 targetNodeIds
: ["headlines-frame"],
1721 selector
: ".hlMenuAttach"
1724 headlinesMenuCommon(menu
);
1729 /* vgroup feed title menu */
1731 if (!dijit
.byId("headlinesFeedTitleMenu")) {
1733 const menu
= new dijit
.Menu({
1734 id
: "headlinesFeedTitleMenu",
1735 targetNodeIds
: ["headlines-frame"],
1736 selector
: "div.cdmFeedTitle"
1739 menu
.addChild(new dijit
.MenuItem({
1740 label
: __("Select articles in group"),
1741 onClick: function (event
) {
1742 selectArticles("all",
1743 "#headlines-frame > div[id*=RROW]" +
1744 "[data-orig-feed-id='" + this.getParent().currentTarget
.getAttribute("data-feed-id") + "']");
1749 menu
.addChild(new dijit
.MenuItem({
1750 label
: __("Mark group as read"),
1751 onClick: function () {
1752 selectArticles("none");
1753 selectArticles("all",
1754 "#headlines-frame > div[id*=RROW]" +
1755 "[data-orig-feed-id='" + this.getParent().currentTarget
.getAttribute("data-feed-id") + "']");
1761 menu
.addChild(new dijit
.MenuItem({
1762 label
: __("Mark feed as read"),
1763 onClick: function () {
1764 catchupFeedInGroup(this.getParent().currentTarget
.getAttribute("data-feed-id"));
1768 menu
.addChild(new dijit
.MenuItem({
1769 label
: __("Edit feed"),
1770 onClick: function () {
1771 editFeed(this.getParent().currentTarget
.getAttribute("data-feed-id"));
1779 function cache_set(id
, obj
) {
1780 //console.log("cache_set: " + id);
1783 sessionStorage
[id
] = obj
;
1785 sessionStorage
.clear();
1789 function cache_get(id
) {
1791 return sessionStorage
[id
];
1794 function cache_clear() {
1796 sessionStorage
.clear();
1799 function cache_delete(id
) {
1801 sessionStorage
.removeItem(id
);
1804 function cancelSearch() {
1809 function setSelectionScore() {
1810 const ids
= getSelectedArticleIds2();
1812 if (ids
.length
> 0) {
1815 const score
= prompt(__("Please enter new score for selected articles:"));
1817 if (score
!= undefined) {
1818 const query
= { op
: "article", method
: "setScore", id
: ids
.toString(),
1821 xhrJson("backend.php", query
, (reply
) => {
1823 reply
.id
.each((id
) => {
1824 const row
= $("RROW-" + id
);
1827 const pic
= row
.getElementsByClassName("score-pic")[0];
1830 pic
.src
= pic
.src
.replace(/score_.*?\.png/,
1831 reply
["score_pic"]);
1832 pic
.setAttribute("score", reply
["score"]);
1841 alert(__("No articles are selected."));
1845 function changeScore(id
, pic
) {
1846 const score
= pic
.getAttribute("score");
1848 const new_score
= prompt(__("Please enter new score for this article:"), score
);
1850 if (new_score
!= undefined) {
1851 const query
= { op
: "article", method
: "setScore", id
: id
, score
: new_score
};
1853 xhrJson("backend.php", query
, (reply
) => {
1855 pic
.src
= pic
.src
.replace(/score_.*?\.png/, reply
["score_pic"]);
1856 pic
.setAttribute("score", new_score
);
1857 pic
.setAttribute("title", new_score
);
1863 function displayArticleUrl(id
) {
1864 const query
= { op
: "rpc", method
: "getlinktitlebyid", id
: id
};
1866 xhrJson("backend.php", query
, (reply
) => {
1867 if (reply
&& reply
.link
) {
1868 prompt(__("Article URL:"), reply
.link
);
1874 // floatingTitle goto button uses this
1875 function scrollToRowId(id
) {
1879 $("headlines-frame").scrollTop
= row
.offsetTop
- 4;
1882 function updateFloatingTitle(unread_only
) {
1883 if (!isCdmMode()) return;
1885 const hf
= $("headlines-frame");
1886 const elems
= $$("#headlines-frame > div[id*=RROW]");
1888 for (let i
= 0; i
< elems
.length
; i
++) {
1890 const child
= elems
[i
];
1892 if (child
&& child
.offsetTop
+ child
.offsetHeight
> hf
.scrollTop
) {
1894 const header
= child
.select(".header")[0];
1896 if (unread_only
|| child
.getAttribute("data-article-id") != $("floatingTitle").getAttribute("data-article-id")) {
1897 if (child
.getAttribute("data-article-id") != $("floatingTitle").getAttribute("data-article-id")) {
1899 $("floatingTitle").setAttribute("data-article-id", child
.getAttribute("data-article-id"));
1900 $("floatingTitle").innerHTML
= header
.innerHTML
;
1901 $("floatingTitle").firstChild
.innerHTML
= "<img class='anchor markedPic' src='images/page_white_go.png' onclick=\"scrollToRowId('" + child
.id
+ "')\">" + $("floatingTitle").firstChild
.innerHTML
;
1905 const cb
= $$("#floatingTitle .dijitCheckBox")[0];
1908 cb
.parentNode
.removeChild(cb
);
1911 if (child
.hasClassName("Unread"))
1912 $("floatingTitle").addClassName("Unread");
1914 $("floatingTitle").removeClassName("Unread");
1916 PluginHost
.run(PluginHost
.HOOK_FLOATING_TITLE
, child
);
1919 $("floatingTitle").style
.marginRight
= hf
.offsetWidth
- child
.offsetWidth
+ "px";
1920 if (header
.offsetTop
+ header
.offsetHeight
< hf
.scrollTop
+ $("floatingTitle").offsetHeight
- 5 &&
1921 child
.offsetTop
+ child
.offsetHeight
>= hf
.scrollTop
+ $("floatingTitle").offsetHeight
- 5)
1922 $("floatingTitle").style
.visibility
= "visible";
1924 $("floatingTitle").style
.visibility
= "hidden";