reddit_next/prev_comment: use node.parentNode instead of node.parentElement
[conkeror.git] / modules / page-modes / reddit.js
blob2190b7bc0c5d5710690904199bcb5f9bfc4da5c1
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                 "body>.content .last-clicked {" +
26                 " background-color: #bfb !important;" +
27                 " border: 0px !important;"+
28                 "}}"));
31 /**
32  * Scroll, if necessary, to make the given element visible
33  */
34 function reddit_scroll_into_view (window, element) {
35     var rect = element.getBoundingClientRect();
36     if (rect.top < 0 || rect.bottom > window.innerHeight)
37         element.scrollIntoView();
41 /**
42  * Select the next entry down from the currently highlighted one.
43  * Checks the URL to figure out if one a link page or comment page.
44  */
45 function reddit_next (I) {
46     var doc = I.buffer.document;
47     if (doc.URL.search("/comments/") == -1) {
48         // Not on comment page, so highlight next link
49         reddit_next_link(I);
50     } else {
51         // On comment page, so highlight next comment
52         reddit_next_comment(I, true);
53     }
55 interactive("reddit-next",
56     "Move the 'cursor' to the next reddit entry.",
57     reddit_next);
60 /**
61  * Selects the next parent comment if on a comment page.
62  */
63 function reddit_next_parent_comment (I) {
64     var doc = I.buffer.document;
65     if (doc.URL.search("/comments/") != -1)
66         reddit_next_comment(I, false);
68 interactive("reddit-next-parent-comment",
69     "Move the 'cursor' to the next comment which isn't "+
70     "a child of another comment.",
71     reddit_next_parent_comment);
74 /**
75  * Move select the next link down from the currently highlighted one.
76  * When the end of the page is reached, the behavior is controlled by
77  * the variable reddit_end_behavior.
78  */
79 function reddit_next_link (I) {
80     var doc = I.buffer.document;
81     // the behavior of this command depends on whether we have downloaded
82     // enough of the page to include all of the article links.
83     var complete = doc.getElementsByClassName('footer').length > 0;
84     var links = doc.querySelectorAll("body>.content .link");
85     var first = null;
86     var current = null;
87     var next = null;
88     for (var i = 0, llen = links.length; i < llen; i++) {
89         if (links[i].style.display == 'none')
90             continue;
91         if (! first)
92             first = links[i];
93         if (current) {
94             next = links[i];
95             break;
96         }
97         if (links[i].className.indexOf("last-clicked") >= 0)
98             current = links[i];
99     }
100     // The following situations are null-ops:
101     //  1) there are no links on the page.
102     //  2) page is incomplete and the current link is the last link.
103     if (! first || (current && !next && !complete))
104         return;
105     if (! next) {
106         if (current) {
107             if (reddit_end_behavior == 'stop')
108                 return;
109             if (reddit_end_behavior == 'wrap')
110                 next = first;
111             if (reddit_end_behavior == 'page') {
112                 let (xpr = doc.evaluate(
113                     '//p[@class="nextprev"]/a[contains(text(),"next")]', doc, null,
114                     Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null))
115                 {
116                     var nextpage;
117                     if (xpr && (nextpage = xpr.iterateNext())) {
118                         dom_remove_class(current, "last-clicked");
119                         browser_object_follow(I.buffer, FOLLOW_DEFAULT, nextpage);
120                         return;
121                     }
122                 }
123             }
124         } else {
125             // Page may or may not be complete.  If the page is not
126             // complete, it is safe to assume that there is no current
127             // link because a current link can only persist on a
128             // cached page, which would load instantaneously, not
129             // giving the user the opportunity to run this command.
130             //
131             next = first;
132         }
133     }
134     // ordinaries (highlight new, maybe dehighlight old)
135     if (current)
136         dom_remove_class(current, "last-clicked");
137     dom_add_class(next, "last-clicked");
138     var anchor = doc.querySelector("body>.content .last-clicked a.title");
139     browser_set_element_focus(I.buffer, anchor);
140     reddit_scroll_into_view(I.buffer.focused_frame, next);
142 interactive("reddit-next-link",
143     "Move the 'cursor' to the next reddit link.",
144     reddit_next_link);
148  * Checks if comment is a child of parent. Used on collapsed
149  * parents, to determine whether the child should be selected or
150  * not.
151  */
152 function comment_is_child (parent, comment) {
153     var parent_comments = parent.querySelectorAll(".comment");
154     for (var i = 0, llen = parent_comments.length; i < llen; i++) {
155         if (parent_comments[i].getAttribute("data-fullname") ==
156             comment.getAttribute("data-fullname"))
157         {
158             return true;
159         }
160     }
161     return false;
166  * Returns entries (top link + comments) that are visible (are not
167  * collapsed).
168  */
169 function get_entries_without_collapsed_comments (entries) {
170     var entries_without_collapsed = [];
171     var collapsed_parent = null;
172     for (var i = 0, elen = entries.length; i < elen; i++) {
173         if (collapsed_parent) {
174             // Discard the 'load more comments' buttons
175             var current_classname = entries[i].getElementsByTagName("span")[0].className;
176             if (!comment_is_child(collapsed_parent, entries[i].parentNode) &&
177                 current_classname != "morecomments")
178             {
179                 collapsed_parent = null;
180             } else { // Skip collapsed comments
181                 continue;
182             }
183         }
184         // Collapsed comment
185         if (i != 0 &&
186             entries[i].getElementsByTagName("div")[1].style.display == "none")
187         {
188             collapsed_parent = entries[i].parentNode;
189         }
190         entries_without_collapsed.push(entries[i]);
191     }
192     return entries_without_collapsed;
197  * Select the next comment down from the currently highlighted one.
198  * When select_all_comments is true, select the next comment. When
199  * it's false select the next comment which isn't a child of another
200  * comment.
201  */
202 function reddit_next_comment (I, select_all_comments) {
203     var doc = I.buffer.document;
204     // Get all comments plus the top link
205     var entries = doc.querySelectorAll("body>.content .entry");
206     // Remove all the collapsed comments
207     entries = get_entries_without_collapsed_comments(entries);
208     // Get the div which contains all comments
209     var comments_div = doc.getElementsByClassName("nestedlisting")[0];
210     var first = null;
211     var current = null;
212     var next = null;
213     for (var i = 0, elen = entries.length; i < elen && !next; i++) {
214         var parent_div_current = entries[i].parentNode.parentNode;
215         // Next link/comment can be selected if either:
216         //  1) All comments have to be selected
217         //  2) It's the first entry, which is the top link
218         //  3) It's a top level comment
219         if (select_all_comments || i == 0 ||
220             parent_div_current.id == comments_div.id)
221         {
222             if (! first)
223                 first = entries[i];
224             if (current)
225                 next = entries[i];
226         }
227         if (entries[i].className.indexOf("last-clicked") >= 0)
228             current = entries[i];
229     }
230     // There are no comments on the page
231     if (! first)
232         return;
233     // Last comment on page, try to load more
234     if (current && ! next) {
235         var load_more_link = comments_div.querySelector(
236             ".nestedlisting > .morechildren .button");
237         if (load_more_link) {
238             // Go to the previous comment first, since the current one will disappear
239             reddit_prev_comment(I, true);
240             browser_object_follow(I.buffer, FOLLOW_DEFAULT, load_more_link);
241         }
242         return;
243     }
244     // No next yet, because there is no current. So make the first entry the next one
245     if (! next)
246         next = first;
247     // Dehighlight old
248     if (current)
249         dom_remove_class(current, "last-clicked");
250     // Highlight the next comment
251     dom_add_class(next, "last-clicked");
252     // Focus the link on the comment page
253     var anchor = doc.querySelector("body>.content .last-clicked a.title");
254     browser_set_element_focus(I.buffer, anchor);
255     reddit_scroll_into_view(I.buffer.focused_frame, next);
257 interactive("reddit-next-comment",
258     "Move the 'cursor' to the next reddit comment.",
259     reddit_next_comment);
263  * Select the next entry up from the currently highlighted one.
264  * Checks the URL to figure out if one a link page or comment page.
265  */
266 function reddit_prev (I) {
267     var doc = I.buffer.document;
268     if (doc.URL.search("/comments/") == -1) {
269         // Not on comment page, so highlight prev link
270         reddit_prev_link(I);
271     } else {
272         // On comment page, so highlight prev comment
273         reddit_prev_comment(I, true);
274     }
276 interactive("reddit-prev",
277     "Move the 'cursor' to the previous reddit entry.",
278     reddit_prev);
281 function reddit_prev_parent_comment (I) {
282     var doc = I.buffer.document;
283     if (doc.URL.search("/comments/") != -1)
284         reddit_prev_comment(I, false);
286 interactive("reddit-prev-parent-comment",
287     "Move the 'cursor' to the previous comment which isn't "+
288     "a child of another comment.",
289     reddit_prev_parent_comment);
293  * Select the link before the currently highlighted one.  When the
294  * beginning of the page is reached, behavior is controlled by the
295  * variable reddit_end_behavior.
296  */
297 function reddit_prev_link (I) {
298     var doc = I.buffer.document;
299     // the behavior of this command depends on whether we have downloaded
300     // enough of the page to include all of the article links.
301     var complete = doc.getElementsByClassName('footer').length > 0;
302     var links = doc.querySelectorAll("body>.content .link");
303     var llen = links.length;
304     var first = null;
305     var prev = null;
306     var current = null;
307     for (var i = 0; i < llen; i++) {
308         if (links[i].style.display == 'none')
309             continue;
310         if (! first)
311             first = links[i];
312         if (links[i].className.indexOf("last-clicked") >= 0) {
313             current = links[i];
314             break;
315         }
316         prev = links[i];
317     }
318     if (! first || // no links were found at all.
319         (!current && !complete)) // don't know where current is.
320     {
321         return;
322     }
323     if (! prev) {
324         // the first visible link is the `current' link.
325         // dispatch on reddit_end_behavior.
326         if (reddit_end_behavior == 'stop') {
327             return;
328         } else if (reddit_end_behavior == 'wrap') {
329             // need to get last link on page.
330             if (complete) {
331                 for (var i = 0; i < llen; i++) {
332                     if (links[i].style.display == 'none')
333                         continue;
334                     prev = links[i];
335                 }
336             }
337         } else if (reddit_end_behavior == 'page') {
338             let (xpr = doc.evaluate(
339                 '//p[@class="nextprev"]/a[contains(text(),"prev")]', doc, null,
340                 Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null))
341             {
342                 var prevpage;
343                 if (xpr && (prevpage = xpr.iterateNext())) {
344                     dom_remove_class(current, "last-clicked");
345                     browser_object_follow(I.buffer, FOLLOW_DEFAULT, prevpage);
346                     return;
347                 }
348             }
349         }
350     }
351     // ordinaries (highlight new, maybe dehighlight old)
352     if (current)
353         dom_remove_class(current, "last-clicked");
354     dom_add_class(prev, "last-clicked");
355     var anchor = doc.querySelector("body>.content .last-clicked a.title");
356     browser_set_element_focus(I.buffer, anchor);
357     reddit_scroll_into_view(I.buffer.focused_frame, prev);
359 interactive("reddit-prev-link",
360     "Move the 'cursor' to the previous reddit link.",
361     reddit_prev_link);
365  * Select the prev comment down from the currently highlighted
366  * one.  When select_all_comments is true, select the previous
367  * comment. When it's false select the previous comment which
368  * isn't a child of another comment.
369  */
370 function reddit_prev_comment (I, select_all_comments) {
371     var doc = I.buffer.document;
372     // Get all comments plus the top link
373     var entries = doc.querySelectorAll("body>.content .entry");
374     // Remove all the collapsed comments
375     entries = get_entries_without_collapsed_comments(entries);
376     // Get the div which contains all comments
377     var comments_div = doc.getElementsByClassName("nestedlisting")[0];
378     var current = null;
379     var prev = null;
380     var prev_parent = null;
381     for (var i = 0, elen = entries.length; i < elen && !current; i++) {
382         if (entries[i].className.indexOf("last-clicked") >= 0) {
383             current = entries[i];
384             // Don't bother if the top link is selected, since
385             // that means there is no previous entry
386             if (i != 0) {
387                 if (select_all_comments)
388                     prev = entries[i - 1];
389                 else
390                     prev = prev_parent;
391             }
392         }
393         var parent_div_current = entries[i].parentNode.parentNode;
394         // Remember the last parent comment and consider the top
395         // link to be a parent comment
396         if (i == 0 || parent_div_current.id == comments_div.id)
397             prev_parent = entries[i];
398     }
399     // Nothing is selected yet or there are no comments on the page.
400     if (! prev)
401         return;
402     // Dehighlight old
403     if (current)
404         dom_remove_class(current, "last-clicked");
405     // Highlight the prev comment
406     dom_add_class(prev, "last-clicked");
407     reddit_scroll_into_view(I.buffer.focused_frame, prev);
409 interactive("reddit-prev-comment",
410     "Move the 'cursor' to the previous reddit comment.",
411     reddit_prev_comment);
414 function reddit_open_comments (I, target) {
415     var doc = I.buffer.document;
416     var link = doc.querySelector("body>.content .last-clicked a.comments");
417     if (link)
418         browser_object_follow(I.buffer, target || FOLLOW_DEFAULT, link);
420 function reddit_open_comments_new_buffer (I) {
421     reddit_open_comments(I, OPEN_NEW_BUFFER);
423 function reddit_open_comments_new_window (I) {
424     reddit_open_comments(I, OPEN_NEW_WINDOW);
426 interactive("reddit-open-comments",
427     "Open the comments-page associated with the currently selected link.",
428     alternates(reddit_open_comments,
429                reddit_open_comments_new_buffer,
430                reddit_open_comments_new_window));
433 function reddit_vote_up (I) {
434     var doc = I.buffer.document;
435     if (doc.URL.search("/comments/") == -1)
436         reddit_vote_link(I, true);
437     else
438         reddit_vote_comment(I, true);
440 interactive("reddit-vote-up",
441     "Vote the currently selected entry up.",
442     reddit_vote_up);
445 function reddit_vote_down (I) {
446     var doc = I.buffer.document;
447     if (doc.URL.search("/comments/") == -1)
448         reddit_vote_link(I, false);
449     else
450         reddit_vote_comment(I, false);
452 interactive("reddit-vote-down",
453     "Vote the currently selected entry down.",
454     reddit_vote_down);
457 function reddit_vote_link (I, upvote) {
458     // get the current article and send a click to its vote button.
459     var doc = I.buffer.document;
460     if (upvote)
461         var arrow_class = ".up";
462     else
463         arrow_class = ".down";
464     var link = doc.querySelector(
465         "body>.content .last-clicked .midcol " + arrow_class);
466     if (link)
467         browser_object_follow(I.buffer, FOLLOW_DEFAULT, link);
470 function reddit_vote_comment (I, upvote) {
471     // get the current entry and send a click to its vote button.
472     var doc = I.buffer.document;
473     var link = doc.querySelector("body>.content .last-clicked");
474     if (upvote)
475         var arrow_class = ".up";
476     else
477         arrow_class = ".down";
478     // Is there anything selected?
479     if (link && link.getElementsByTagName("span")[0].className != "morecomments") {
480         // Get the vote arrow
481         link = link.parentNode.getElementsByClassName("midcol")[0]
482             .querySelector(arrow_class);
483         if (link)
484             browser_object_follow(I.buffer, FOLLOW_DEFAULT, link);
485     }
488 define_browser_object_class("reddit-current",
489     null,
490     function (I, prompt) {
491         var doc = I.buffer.document;
492         var link = doc.querySelector("body>.content .last-clicked .entry p.title a");
493         yield co_return(link);
494     });
497 define_keymap("reddit_keymap", $display_name = "reddit");
498 define_key(reddit_keymap, "j", "reddit-next");
499 define_key(reddit_keymap, "J", "reddit-next-parent-comment");
500 define_key(reddit_keymap, "k", "reddit-prev");
501 define_key(reddit_keymap, "K", "reddit-prev-parent-comment");
502 define_key(reddit_keymap, ",", "reddit-vote-up");
503 define_key(reddit_keymap, ".", "reddit-vote-down");
504 define_key(reddit_keymap, "h", "reddit-open-comments");
507 var reddit_link_commands =
508     ["follow-current", "follow-current-new-buffer",
509      "follow-current-new-buffer-background",
510      "follow-current-new-window", "copy"];
513 var reddit_modality = {
514     normal: reddit_keymap
518 define_page_mode("reddit-mode",
519     build_url_regexp($domain = /([a-zA-Z0-9\-]*\.)*reddit/),
520     function enable (buffer) {
521         for each (var c in reddit_link_commands) {
522             buffer.default_browser_object_classes[c] =
523                 browser_object_reddit_current;
524         }
525         buffer.content_modalities.push(reddit_modality);
526     },
527     function disable (buffer) {
528         for each (var c in reddit_link_commands) {
529             delete buffer.default_browser_object_classes[c];
530         }
531         var i = buffer.content_modalities.indexOf(reddit_modality);
532         if (i > -1)
533             buffer.content_modalities.splice(i, 1);
534     },
535     $display_name = "reddit",
536     $doc = "reddit page-mode: keyboard navigation for reddit.");
538 page_mode_activate(reddit_mode);
540 provide("reddit");