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 domain(reddit.com) {" +
25 "body>.content .last-clicked {" +
26 " background-color: #bfb !important;" +
27 " border: 0px !important;"+
32 * Scroll, if necessary, to make the given element visible
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();
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.
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
51 // On comment page, so highlight next comment
52 reddit_next_comment(I, true);
55 interactive("reddit-next",
56 "Move the 'cursor' to the next reddit entry.",
61 * Selects the next parent comment if on a comment page.
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);
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.
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");
88 for (var i = 0, llen = links.length; i < llen; i++) {
89 if (links[i].style.display == 'none')
97 if (links[i].className.indexOf("last-clicked") >= 0)
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))
107 if (reddit_end_behavior == 'stop')
109 if (reddit_end_behavior == 'wrap')
111 if (reddit_end_behavior == 'page') {
113 let xpr = doc.evaluate(
114 '//span[@class="nextprev"]/a[contains(text(),"next")]', doc, null,
115 Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
117 if (xpr && (nextpage = xpr.iterateNext())) {
118 dom_remove_class(current, "last-clicked");
119 browser_object_follow(I.buffer, FOLLOW_DEFAULT, nextpage);
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.
134 // ordinaries (highlight new, maybe dehighlight old)
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.",
148 * Checks if comment is a child of parent. Used on collapsed
149 * parents, to determine whether the child should be selected or
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"))
166 * Returns entries (top link + comments) that are visible (are not
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' links
175 var load_more_comments = entries[i].querySelector(".morecomments");
176 // Discard the 'continue this thread' links
177 var continue_this_thread = entries[i].querySelector(".deepthread");
179 if (!comment_is_child(collapsed_parent, entries[i].parentNode) &&
180 !load_more_comments && !continue_this_thread)
182 collapsed_parent = null;
183 } else { // Skip collapsed comments
188 if (i != 0 && entries[i].parentNode.className.match(/\bcollapsed\b/)) {
189 collapsed_parent = entries[i].parentNode;
191 entries_without_collapsed.push(entries[i]);
193 return entries_without_collapsed;
198 * Select the next comment down from the currently highlighted one.
199 * When select_all_comments is true, select the next comment. When
200 * it's false select the next comment which isn't a child of another
203 function reddit_next_comment (I, select_all_comments) {
204 var doc = I.buffer.document;
205 // Get all comments plus the top link
206 var entries = doc.querySelectorAll("body>.content .entry");
207 // Remove all the collapsed comments
208 entries = get_entries_without_collapsed_comments(entries);
209 // Get the div which contains all comments
210 var comments_div = doc.getElementsByClassName("nestedlisting")[0];
214 for (var i = 0, elen = entries.length; i < elen && !next; i++) {
215 var parent_div_current = entries[i].parentNode.parentNode;
216 // Next link/comment can be selected if either:
217 // 1) All comments have to be selected
218 // 2) It's the first entry, which is the top link
219 // 3) It's a top level comment
220 if (select_all_comments || i == 0 ||
221 parent_div_current.id == comments_div.id)
228 if (entries[i].className.indexOf("last-clicked") >= 0)
229 current = entries[i];
231 // There are no comments on the page
234 // Last comment on page, try to load more
235 if (current && ! next) {
236 var load_more_link = comments_div.querySelector(
237 ".nestedlisting > .morechildren .button");
238 if (load_more_link) {
239 // Go to the previous comment first, since the current one will disappear
240 reddit_prev_comment(I, true);
241 browser_object_follow(I.buffer, FOLLOW_DEFAULT, load_more_link);
245 // No next yet, because there is no current. So make the first entry the next one
250 dom_remove_class(current, "last-clicked");
251 // Highlight the next comment
252 dom_add_class(next, "last-clicked");
253 // Focus the link on the comment page
254 var anchor = doc.querySelector("body>.content .last-clicked a.title");
255 browser_set_element_focus(I.buffer, anchor);
256 reddit_scroll_into_view(I.buffer.focused_frame, next);
258 interactive("reddit-next-comment",
259 "Move the 'cursor' to the next reddit comment.",
260 reddit_next_comment);
264 * Select the next entry up from the currently highlighted one.
265 * Checks the URL to figure out if one a link page or comment page.
267 function reddit_prev (I) {
268 var doc = I.buffer.document;
269 if (doc.URL.search("/comments/") == -1) {
270 // Not on comment page, so highlight prev link
273 // On comment page, so highlight prev comment
274 reddit_prev_comment(I, true);
277 interactive("reddit-prev",
278 "Move the 'cursor' to the previous reddit entry.",
282 function reddit_prev_parent_comment (I) {
283 var doc = I.buffer.document;
284 if (doc.URL.search("/comments/") != -1)
285 reddit_prev_comment(I, false);
287 interactive("reddit-prev-parent-comment",
288 "Move the 'cursor' to the previous comment which isn't "+
289 "a child of another comment.",
290 reddit_prev_parent_comment);
294 * Select the link before the currently highlighted one. When the
295 * beginning of the page is reached, behavior is controlled by the
296 * variable reddit_end_behavior.
298 function reddit_prev_link (I) {
299 var doc = I.buffer.document;
300 // the behavior of this command depends on whether we have downloaded
301 // enough of the page to include all of the article links.
302 var complete = doc.getElementsByClassName('footer').length > 0;
303 var links = doc.querySelectorAll("body>.content .link");
304 var llen = links.length;
308 for (var i = 0; i < llen; i++) {
309 if (links[i].style.display == 'none')
313 if (links[i].className.indexOf("last-clicked") >= 0) {
319 if (! first || // no links were found at all.
320 (!current && !complete)) // don't know where current is.
325 // the first visible link is the `current' link.
326 // dispatch on reddit_end_behavior.
327 if (reddit_end_behavior == 'stop') {
329 } else if (reddit_end_behavior == 'wrap') {
330 // need to get last link on page.
332 prev = links[llen - 1];
334 } else if (reddit_end_behavior == 'page') {
336 let xpr = doc.evaluate(
337 '//span[@class="nextprev"]/a[contains(text(),"prev")]', doc, null,
338 Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
340 if (xpr && (prevpage = xpr.iterateNext())) {
341 dom_remove_class(current, "last-clicked");
342 browser_object_follow(I.buffer, FOLLOW_DEFAULT, prevpage);
349 // ordinaries (highlight new, maybe dehighlight old)
351 dom_remove_class(current, "last-clicked");
352 dom_add_class(prev, "last-clicked");
353 var anchor = doc.querySelector("body>.content .last-clicked a.title");
354 browser_set_element_focus(I.buffer, anchor);
355 reddit_scroll_into_view(I.buffer.focused_frame, prev);
357 interactive("reddit-prev-link",
358 "Move the 'cursor' to the previous reddit link.",
363 * Select the prev comment down from the currently highlighted
364 * one. When select_all_comments is true, select the previous
365 * comment. When it's false select the previous comment which
366 * isn't a child of another comment.
368 function reddit_prev_comment (I, select_all_comments) {
369 var doc = I.buffer.document;
370 // Get all comments plus the top link
371 var entries = doc.querySelectorAll("body>.content .entry");
372 // Remove all the collapsed comments
373 entries = get_entries_without_collapsed_comments(entries);
374 // Get the div which contains all comments
375 var comments_div = doc.getElementsByClassName("nestedlisting")[0];
378 var prev_parent = null;
379 for (var i = 0, elen = entries.length; i < elen && !current; i++) {
380 if (entries[i].className.indexOf("last-clicked") >= 0) {
381 current = entries[i];
382 // Don't bother if the top link is selected, since
383 // that means there is no previous entry
385 if (select_all_comments)
386 prev = entries[i - 1];
391 var parent_div_current = entries[i].parentNode.parentNode;
392 // Remember the last parent comment and consider the top
393 // link to be a parent comment
394 if (i == 0 || parent_div_current.id == comments_div.id)
395 prev_parent = entries[i];
397 // Nothing is selected yet or there are no comments on the page.
402 dom_remove_class(current, "last-clicked");
403 // Highlight the prev comment
404 dom_add_class(prev, "last-clicked");
405 reddit_scroll_into_view(I.buffer.focused_frame, prev);
407 interactive("reddit-prev-comment",
408 "Move the 'cursor' to the previous reddit comment.",
409 reddit_prev_comment);
412 function reddit_open_comments (I, target) {
413 var doc = I.buffer.document;
414 var link = doc.querySelector("body>.content .last-clicked a.comments");
416 browser_object_follow(I.buffer, target || FOLLOW_DEFAULT, link);
418 function reddit_open_comments_new_buffer (I) {
419 reddit_open_comments(I, OPEN_NEW_BUFFER);
421 function reddit_open_comments_new_window (I) {
422 reddit_open_comments(I, OPEN_NEW_WINDOW);
424 interactive("reddit-open-comments",
425 "Open the comments-page associated with the currently selected link.",
426 alternates(reddit_open_comments,
427 reddit_open_comments_new_buffer,
428 reddit_open_comments_new_window));
431 function reddit_vote_up (I) {
432 var doc = I.buffer.document;
433 if (doc.URL.search("/comments/") == -1)
434 reddit_vote_link(I, true);
436 reddit_vote_comment(I, true);
438 interactive("reddit-vote-up",
439 "Vote the currently selected entry up.",
443 function reddit_vote_down (I) {
444 var doc = I.buffer.document;
445 if (doc.URL.search("/comments/") == -1)
446 reddit_vote_link(I, false);
448 reddit_vote_comment(I, false);
450 interactive("reddit-vote-down",
451 "Vote the currently selected entry down.",
455 function reddit_vote_link (I, upvote) {
456 // get the current article and send a click to its vote button.
457 var doc = I.buffer.document;
459 var arrow_class = ".up";
461 arrow_class = ".down";
462 var link = doc.querySelector(
463 "body>.content .last-clicked .midcol " + arrow_class);
465 browser_object_follow(I.buffer, FOLLOW_DEFAULT, link);
468 function reddit_vote_comment (I, upvote) {
469 // get the current entry and send a click to its vote button.
470 var doc = I.buffer.document;
471 var link = doc.querySelector("body>.content .last-clicked");
473 var arrow_class = ".up";
475 arrow_class = ".down";
476 // Is there anything selected?
477 if (link && link.getElementsByTagName("span")[0].className != "morecomments") {
478 // Get the vote arrow
479 link = link.parentNode.getElementsByClassName("midcol")[0]
480 .querySelector(arrow_class);
482 browser_object_follow(I.buffer, FOLLOW_DEFAULT, link);
486 define_browser_object_class("reddit-current",
488 function (I, prompt) {
489 var doc = I.buffer.document;
490 var link = doc.querySelector("body>.content .last-clicked .entry p.title a");
491 yield co_return(link);
495 define_keymap("reddit_keymap", $display_name = "reddit");
496 define_key(reddit_keymap, "j", "reddit-next");
497 define_key(reddit_keymap, "J", "reddit-next-parent-comment");
498 define_key(reddit_keymap, "k", "reddit-prev");
499 define_key(reddit_keymap, "K", "reddit-prev-parent-comment");
500 define_key(reddit_keymap, ",", "reddit-vote-up");
501 define_key(reddit_keymap, ".", "reddit-vote-down");
502 define_key(reddit_keymap, "h", "reddit-open-comments");
505 var reddit_link_commands =
506 ["follow-current", "follow-current-new-buffer",
507 "follow-current-new-buffer-background",
508 "follow-current-new-window", "copy"];
511 var reddit_modality = {
512 normal: reddit_keymap
516 define_page_mode("reddit-mode",
517 build_url_regexp($domain = /([a-zA-Z0-9\-]*\.)*reddit/),
518 function enable (buffer) {
519 for each (var c in reddit_link_commands) {
520 buffer.default_browser_object_classes[c] =
521 browser_object_reddit_current;
523 buffer.content_modalities.push(reddit_modality);
525 function disable (buffer) {
526 for each (var c in reddit_link_commands) {
527 delete buffer.default_browser_object_classes[c];
529 var i = buffer.content_modalities.indexOf(reddit_modality);
531 buffer.content_modalities.splice(i, 1);
533 $display_name = "reddit",
534 $doc = "reddit page-mode: keyboard navigation for reddit.");
536 page_mode_activate(reddit_mode);