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