reddit.js: update XPath expression that finds prev and next buttons
[conkeror.git] / modules / page-modes / reddit.js
blob54c0aa27f38ab2b5cd3076ab5037851f2dbdba6c
1 /**
2  * (C) Copyright 2008 Martin Dybdal
3  * (C) Copyright 2009-2010,2012 John Foerch
4  * (C) Copyright 2013 Joren Van Onder
5  *
6  * Use, modification, and distribution are subject to the terms specified in the
7  * COPYING file.
8 **/
10 require("content-buffer.js");
12 define_variable("reddit_end_behavior", "stop",
13     "Controls the behavior of the commands reddit-next-link and "+
14     "reddit-prev-link when at the last or first link, respectively. "+
15     "Given as a string, the supported values are 'stop', 'wrap', "+
16     "and 'page'.  'stop' means to not move the highlight in any "+
17     "way.  'wrap' means to wrap around to the first (or last) "+
18     "link.  'page' means to navigate the buffer to the next (or "+
19     "previous) page on reddit.");
21 register_user_stylesheet(
22     "data:text/css," +
23         escape (
24             "@-moz-document url-prefix(http://www.reddit.com/)," +
25                 "url-prefix(https://pay.reddit.com)," +
26                 "url-prefix(https://www.reddit.com) {" +
27                 "body>.content .last-clicked {" +
28                 " background-color: #bfb !important;" +
29                 " border: 0px !important;"+
30                 "}}"));
33 /**
34  * Scroll, if necessary, to make the given element visible
35  */
36 function reddit_scroll_into_view (window, element) {
37     var rect = element.getBoundingClientRect();
38     if (rect.top < 0 || rect.bottom > window.innerHeight)
39         element.scrollIntoView();
43 /**
44  * Select the next entry down from the currently highlighted one.
45  * Checks the URL to figure out if one a link page or comment page.
46  */
47 function reddit_next (I) {
48     var doc = I.buffer.document;
49     if (doc.URL.search("/comments/") == -1) {
50         // Not on comment page, so highlight next link
51         reddit_next_link(I);
52     } else {
53         // On comment page, so highlight next comment
54         reddit_next_comment(I, true);
55     }
57 interactive("reddit-next",
58     "Move the 'cursor' to the next reddit entry.",
59     reddit_next);
62 /**
63  * Selects the next parent comment if on a comment page.
64  */
65 function reddit_next_parent_comment (I) {
66     var doc = I.buffer.document;
67     if (doc.URL.search("/comments/") != -1)
68         reddit_next_comment(I, false);
70 interactive("reddit-next-parent-comment",
71     "Move the 'cursor' to the next comment which isn't "+
72     "a child of another comment.",
73     reddit_next_parent_comment);
76 /**
77  * Move select the next link down from the currently highlighted one.
78  * When the end of the page is reached, the behavior is controlled by
79  * the variable reddit_end_behavior.
80  */
81 function reddit_next_link (I) {
82     var doc = I.buffer.document;
83     // the behavior of this command depends on whether we have downloaded
84     // enough of the page to include all of the article links.
85     var complete = doc.getElementsByClassName('footer').length > 0;
86     var links = doc.querySelectorAll("body>.content .link");
87     var first = null;
88     var current = null;
89     var next = null;
90     for (var i = 0, llen = links.length; i < llen; i++) {
91         if (links[i].style.display == 'none')
92             continue;
93         if (! first)
94             first = links[i];
95         if (current) {
96             next = links[i];
97             break;
98         }
99         if (links[i].className.indexOf("last-clicked") >= 0)
100             current = links[i];
101     }
102     // The following situations are null-ops:
103     //  1) there are no links on the page.
104     //  2) page is incomplete and the current link is the last link.
105     if (! first || (current && !next && !complete))
106         return;
107     if (! next) {
108         if (current) {
109             if (reddit_end_behavior == 'stop')
110                 return;
111             if (reddit_end_behavior == 'wrap')
112                 next = first;
113             if (reddit_end_behavior == 'page') {
114                 let (xpr = doc.evaluate(
115                     '//span[@class="nextprev"]/a[contains(text(),"next")]', doc, null,
116                     Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null))
117                 {
118                     var nextpage;
119                     if (xpr && (nextpage = xpr.iterateNext())) {
120                         dom_remove_class(current, "last-clicked");
121                         browser_object_follow(I.buffer, FOLLOW_DEFAULT, nextpage);
122                         return;
123                     }
124                 }
125             }
126         } else {
127             // Page may or may not be complete.  If the page is not
128             // complete, it is safe to assume that there is no current
129             // link because a current link can only persist on a
130             // cached page, which would load instantaneously, not
131             // giving the user the opportunity to run this command.
132             //
133             next = first;
134         }
135     }
136     // ordinaries (highlight new, maybe dehighlight old)
137     if (current)
138         dom_remove_class(current, "last-clicked");
139     dom_add_class(next, "last-clicked");
140     var anchor = doc.querySelector("body>.content .last-clicked a.title");
141     browser_set_element_focus(I.buffer, anchor);
142     reddit_scroll_into_view(I.buffer.focused_frame, next);
144 interactive("reddit-next-link",
145     "Move the 'cursor' to the next reddit link.",
146     reddit_next_link);
150  * Checks if comment is a child of parent. Used on collapsed
151  * parents, to determine whether the child should be selected or
152  * not.
153  */
154 function comment_is_child (parent, comment) {
155     var parent_comments = parent.querySelectorAll(".comment");
156     for (var i = 0, llen = parent_comments.length; i < llen; i++) {
157         if (parent_comments[i].getAttribute("data-fullname") ==
158             comment.getAttribute("data-fullname"))
159         {
160             return true;
161         }
162     }
163     return false;
168  * Returns entries (top link + comments) that are visible (are not
169  * collapsed).
170  */
171 function get_entries_without_collapsed_comments (entries) {
172     var entries_without_collapsed = [];
173     var collapsed_parent = null;
174     for (var i = 0, elen = entries.length; i < elen; i++) {
175         if (collapsed_parent) {
176             // Discard the 'load more comments' buttons
177             var current_classname = entries[i].getElementsByTagName("span")[0].className;
178             if (!comment_is_child(collapsed_parent, entries[i].parentNode) &&
179                 current_classname != "morecomments")
180             {
181                 collapsed_parent = null;
182             } else { // Skip collapsed comments
183                 continue;
184             }
185         }
186         // Collapsed comment
187         if (i != 0 &&
188             entries[i].getElementsByTagName("div")[1].style.display == "none")
189         {
190             collapsed_parent = entries[i].parentNode;
191         }
192         entries_without_collapsed.push(entries[i]);
193     }
194     return entries_without_collapsed;
199  * Select the next comment down from the currently highlighted one.
200  * When select_all_comments is true, select the next comment. When
201  * it's false select the next comment which isn't a child of another
202  * comment.
203  */
204 function reddit_next_comment (I, select_all_comments) {
205     var doc = I.buffer.document;
206     // Get all comments plus the top link
207     var entries = doc.querySelectorAll("body>.content .entry");
208     // Remove all the collapsed comments
209     entries = get_entries_without_collapsed_comments(entries);
210     // Get the div which contains all comments
211     var comments_div = doc.getElementsByClassName("nestedlisting")[0];
212     var first = null;
213     var current = null;
214     var next = null;
215     for (var i = 0, elen = entries.length; i < elen && !next; i++) {
216         var parent_div_current = entries[i].parentNode.parentNode;
217         // Next link/comment can be selected if either:
218         //  1) All comments have to be selected
219         //  2) It's the first entry, which is the top link
220         //  3) It's a top level comment
221         if (select_all_comments || i == 0 ||
222             parent_div_current.id == comments_div.id)
223         {
224             if (! first)
225                 first = entries[i];
226             if (current)
227                 next = entries[i];
228         }
229         if (entries[i].className.indexOf("last-clicked") >= 0)
230             current = entries[i];
231     }
232     // There are no comments on the page
233     if (! first)
234         return;
235     // Last comment on page, try to load more
236     if (current && ! next) {
237         var load_more_link = comments_div.querySelector(
238             ".nestedlisting > .morechildren .button");
239         if (load_more_link) {
240             // Go to the previous comment first, since the current one will disappear
241             reddit_prev_comment(I, true);
242             browser_object_follow(I.buffer, FOLLOW_DEFAULT, load_more_link);
243         }
244         return;
245     }
246     // No next yet, because there is no current. So make the first entry the next one
247     if (! next)
248         next = first;
249     // Dehighlight old
250     if (current)
251         dom_remove_class(current, "last-clicked");
252     // Highlight the next comment
253     dom_add_class(next, "last-clicked");
254     // Focus the link on the comment page
255     var anchor = doc.querySelector("body>.content .last-clicked a.title");
256     browser_set_element_focus(I.buffer, anchor);
257     reddit_scroll_into_view(I.buffer.focused_frame, next);
259 interactive("reddit-next-comment",
260     "Move the 'cursor' to the next reddit comment.",
261     reddit_next_comment);
265  * Select the next entry up from the currently highlighted one.
266  * Checks the URL to figure out if one a link page or comment page.
267  */
268 function reddit_prev (I) {
269     var doc = I.buffer.document;
270     if (doc.URL.search("/comments/") == -1) {
271         // Not on comment page, so highlight prev link
272         reddit_prev_link(I);
273     } else {
274         // On comment page, so highlight prev comment
275         reddit_prev_comment(I, true);
276     }
278 interactive("reddit-prev",
279     "Move the 'cursor' to the previous reddit entry.",
280     reddit_prev);
283 function reddit_prev_parent_comment (I) {
284     var doc = I.buffer.document;
285     if (doc.URL.search("/comments/") != -1)
286         reddit_prev_comment(I, false);
288 interactive("reddit-prev-parent-comment",
289     "Move the 'cursor' to the previous comment which isn't "+
290     "a child of another comment.",
291     reddit_prev_parent_comment);
295  * Select the link before the currently highlighted one.  When the
296  * beginning of the page is reached, behavior is controlled by the
297  * variable reddit_end_behavior.
298  */
299 function reddit_prev_link (I) {
300     var doc = I.buffer.document;
301     // the behavior of this command depends on whether we have downloaded
302     // enough of the page to include all of the article links.
303     var complete = doc.getElementsByClassName('footer').length > 0;
304     var links = doc.querySelectorAll("body>.content .link");
305     var llen = links.length;
306     var first = null;
307     var prev = null;
308     var current = null;
309     for (var i = 0; i < llen; i++) {
310         if (links[i].style.display == 'none')
311             continue;
312         if (! first)
313             first = links[i];
314         if (links[i].className.indexOf("last-clicked") >= 0) {
315             current = links[i];
316             break;
317         }
318         prev = links[i];
319     }
320     if (! first || // no links were found at all.
321         (!current && !complete)) // don't know where current is.
322     {
323         return;
324     }
325     if (! prev) {
326         // the first visible link is the `current' link.
327         // dispatch on reddit_end_behavior.
328         if (reddit_end_behavior == 'stop') {
329             return;
330         } else if (reddit_end_behavior == 'wrap') {
331             // need to get last link on page.
332             if (complete) {
333                 for (var i = 0; i < llen; i++) {
334                     if (links[i].style.display == 'none')
335                         continue;
336                     prev = links[i];
337                 }
338             }
339         } else if (reddit_end_behavior == 'page') {
340             let (xpr = doc.evaluate(
341                 '//span[@class="nextprev"]/a[contains(text(),"prev")]', doc, null,
342                 Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null))
343             {
344                 var prevpage;
345                 if (xpr && (prevpage = xpr.iterateNext())) {
346                     dom_remove_class(current, "last-clicked");
347                     browser_object_follow(I.buffer, FOLLOW_DEFAULT, prevpage);
348                     return;
349                 }
350             }
351         }
352     }
353     // ordinaries (highlight new, maybe dehighlight old)
354     if (current)
355         dom_remove_class(current, "last-clicked");
356     dom_add_class(prev, "last-clicked");
357     var anchor = doc.querySelector("body>.content .last-clicked a.title");
358     browser_set_element_focus(I.buffer, anchor);
359     reddit_scroll_into_view(I.buffer.focused_frame, prev);
361 interactive("reddit-prev-link",
362     "Move the 'cursor' to the previous reddit link.",
363     reddit_prev_link);
367  * Select the prev comment down from the currently highlighted
368  * one.  When select_all_comments is true, select the previous
369  * comment. When it's false select the previous comment which
370  * isn't a child of another comment.
371  */
372 function reddit_prev_comment (I, select_all_comments) {
373     var doc = I.buffer.document;
374     // Get all comments plus the top link
375     var entries = doc.querySelectorAll("body>.content .entry");
376     // Remove all the collapsed comments
377     entries = get_entries_without_collapsed_comments(entries);
378     // Get the div which contains all comments
379     var comments_div = doc.getElementsByClassName("nestedlisting")[0];
380     var current = null;
381     var prev = null;
382     var prev_parent = null;
383     for (var i = 0, elen = entries.length; i < elen && !current; i++) {
384         if (entries[i].className.indexOf("last-clicked") >= 0) {
385             current = entries[i];
386             // Don't bother if the top link is selected, since
387             // that means there is no previous entry
388             if (i != 0) {
389                 if (select_all_comments)
390                     prev = entries[i - 1];
391                 else
392                     prev = prev_parent;
393             }
394         }
395         var parent_div_current = entries[i].parentNode.parentNode;
396         // Remember the last parent comment and consider the top
397         // link to be a parent comment
398         if (i == 0 || parent_div_current.id == comments_div.id)
399             prev_parent = entries[i];
400     }
401     // Nothing is selected yet or there are no comments on the page.
402     if (! prev)
403         return;
404     // Dehighlight old
405     if (current)
406         dom_remove_class(current, "last-clicked");
407     // Highlight the prev comment
408     dom_add_class(prev, "last-clicked");
409     reddit_scroll_into_view(I.buffer.focused_frame, prev);
411 interactive("reddit-prev-comment",
412     "Move the 'cursor' to the previous reddit comment.",
413     reddit_prev_comment);
416 function reddit_open_comments (I, target) {
417     var doc = I.buffer.document;
418     var link = doc.querySelector("body>.content .last-clicked a.comments");
419     if (link)
420         browser_object_follow(I.buffer, target || FOLLOW_DEFAULT, link);
422 function reddit_open_comments_new_buffer (I) {
423     reddit_open_comments(I, OPEN_NEW_BUFFER);
425 function reddit_open_comments_new_window (I) {
426     reddit_open_comments(I, OPEN_NEW_WINDOW);
428 interactive("reddit-open-comments",
429     "Open the comments-page associated with the currently selected link.",
430     alternates(reddit_open_comments,
431                reddit_open_comments_new_buffer,
432                reddit_open_comments_new_window));
435 function reddit_vote_up (I) {
436     var doc = I.buffer.document;
437     if (doc.URL.search("/comments/") == -1)
438         reddit_vote_link(I, true);
439     else
440         reddit_vote_comment(I, true);
442 interactive("reddit-vote-up",
443     "Vote the currently selected entry up.",
444     reddit_vote_up);
447 function reddit_vote_down (I) {
448     var doc = I.buffer.document;
449     if (doc.URL.search("/comments/") == -1)
450         reddit_vote_link(I, false);
451     else
452         reddit_vote_comment(I, false);
454 interactive("reddit-vote-down",
455     "Vote the currently selected entry down.",
456     reddit_vote_down);
459 function reddit_vote_link (I, upvote) {
460     // get the current article and send a click to its vote button.
461     var doc = I.buffer.document;
462     if (upvote)
463         var arrow_class = ".up";
464     else
465         arrow_class = ".down";
466     var link = doc.querySelector(
467         "body>.content .last-clicked .midcol " + arrow_class);
468     if (link)
469         browser_object_follow(I.buffer, FOLLOW_DEFAULT, link);
472 function reddit_vote_comment (I, upvote) {
473     // get the current entry and send a click to its vote button.
474     var doc = I.buffer.document;
475     var link = doc.querySelector("body>.content .last-clicked");
476     if (upvote)
477         var arrow_class = ".up";
478     else
479         arrow_class = ".down";
480     // Is there anything selected?
481     if (link && link.getElementsByTagName("span")[0].className != "morecomments") {
482         // Get the vote arrow
483         link = link.parentNode.getElementsByClassName("midcol")[0]
484             .querySelector(arrow_class);
485         if (link)
486             browser_object_follow(I.buffer, FOLLOW_DEFAULT, link);
487     }
490 define_browser_object_class("reddit-current",
491     null,
492     function (I, prompt) {
493         var doc = I.buffer.document;
494         var link = doc.querySelector("body>.content .last-clicked .entry p.title a");
495         yield co_return(link);
496     });
499 define_keymap("reddit_keymap", $display_name = "reddit");
500 define_key(reddit_keymap, "j", "reddit-next");
501 define_key(reddit_keymap, "J", "reddit-next-parent-comment");
502 define_key(reddit_keymap, "k", "reddit-prev");
503 define_key(reddit_keymap, "K", "reddit-prev-parent-comment");
504 define_key(reddit_keymap, ",", "reddit-vote-up");
505 define_key(reddit_keymap, ".", "reddit-vote-down");
506 define_key(reddit_keymap, "h", "reddit-open-comments");
509 var reddit_link_commands =
510     ["follow-current", "follow-current-new-buffer",
511      "follow-current-new-buffer-background",
512      "follow-current-new-window", "copy"];
515 var reddit_modality = {
516     normal: reddit_keymap
520 define_page_mode("reddit-mode",
521     build_url_regexp($domain = /([a-zA-Z0-9\-]*\.)*reddit/),
522     function enable (buffer) {
523         for each (var c in reddit_link_commands) {
524             buffer.default_browser_object_classes[c] =
525                 browser_object_reddit_current;
526         }
527         buffer.content_modalities.push(reddit_modality);
528     },
529     function disable (buffer) {
530         for each (var c in reddit_link_commands) {
531             delete buffer.default_browser_object_classes[c];
532         }
533         var i = buffer.content_modalities.indexOf(reddit_modality);
534         if (i > -1)
535             buffer.content_modalities.splice(i, 1);
536     },
537     $display_name = "reddit",
538     $doc = "reddit page-mode: keyboard navigation for reddit.");
540 page_mode_activate(reddit_mode);
542 provide("reddit");