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