2 * (C) Copyright 2008 Martin Dybdal
3 * (C) Copyright 2009-2010,2012 John Foerch
4 * (C) Copyright 2013 Joren Van Onder
6 * Use, modification, and distribution are subject to the terms specified in the
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(
24 "@-moz-document url-prefix(http://www.reddit.com/) {" +
25 "body>.content .last-clicked {" +
26 " background-color: #bfb !important;" +
27 " border: 0px !important;"+
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.
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)
48 // On comment page, so highlight next comment
50 reddit_next_comment(I, true);
52 interactive("reddit-next",
53 "Move the 'cursor' to the next reddit entry.",
56 /* Selects the next parent comment if on a comment page.
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.
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");
81 for (var i = 0, llen = links.length; i < llen; i++) {
82 if (links[i].style.display == 'none')
90 if (links[i].className.indexOf("last-clicked") >= 0)
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))
100 if (reddit_end_behavior == 'stop')
102 if (reddit_end_behavior == 'wrap')
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))
110 if (xpr && (nextpage = xpr.iterateNext())) {
111 dom_remove_class(current, "last-clicked");
112 browser_object_follow(I.buffer, FOLLOW_DEFAULT, nextpage);
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.
127 // ordinaries (highlight new, maybe dehighlight old)
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.",
139 /* Checks if comment is a child of parent. Used on collapsed
140 * parents, to determine whether the child should be selected or
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"))
154 /* Returns entries (top link + comments) that are visible (are not
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
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]);
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
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];
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) {
217 if (entries[i].className.indexOf("last-clicked") >= 0)
218 current = entries[i];
221 // There are no comments on the page
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);
238 // No next yet, because there is no current. So make the first entry the next one
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.
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)
269 // On comment page, so highlight prev comment
271 reddit_prev_comment(I, true);
273 interactive("reddit-prev",
274 "Move the 'cursor' to the previous reddit entry.",
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.
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;
301 for (var i = 0; i < llen; i++) {
302 if (links[i].style.display == 'none')
306 if (links[i].className.indexOf("last-clicked") >= 0) {
312 if (! first || // no links were found at all.
313 (!current && !complete)) // don't know where current is.
316 // the first visible link is the `current' link.
317 // dispatch on reddit_end_behavior.
318 if (reddit_end_behavior == 'stop')
320 else if (reddit_end_behavior == 'wrap') {
321 // need to get last link on page.
323 for (var i = 0; i < llen; i++) {
324 if (links[i].style.display == 'none')
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))
335 if (xpr && (prevpage = xpr.iterateNext())) {
336 dom_remove_class(current, "last-clicked");
337 browser_object_follow(I.buffer, FOLLOW_DEFAULT, prevpage);
343 // ordinaries (highlight new, maybe dehighlight old)
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.",
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.
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];
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
382 if (select_all_comments)
383 prev = entries[i - 1];
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];
397 // Nothing is selected yet or there are no comments on the page.
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");
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);
441 reddit_vote_comment(I, true);
443 interactive("reddit-vote-up",
444 "Vote the currently selected entry 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);
454 reddit_vote_comment(I, false);
456 interactive("reddit-vote-down",
457 "Vote the currently selected entry 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 = "";
468 arrow_class = ".down";
470 var link = doc.querySelector("body>.content .last-clicked .midcol " + arrow_class);
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 = "";
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)
492 browser_object_follow(I.buffer, FOLLOW_DEFAULT, link);
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);
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;
532 buffer.content_modalities.push(reddit_modality);
534 function disable (buffer) {
535 for each (var c in reddit_link_commands) {
536 delete buffer.default_browser_object_classes[c];
538 var i = buffer.content_modalities.indexOf(reddit_modality);
540 buffer.content_modalities.splice(i, 1);
542 $display_name = "reddit",
543 $doc = "reddit page-mode: keyboard navigation for reddit.");
545 page_mode_activate(reddit_mode);