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 "url-prefix(https://pay.reddit.com)," +
26 "url-prefix(https://www.reddit.com) {" +
27 "body>.content .last-clicked {" +
28 " background-color: #bfb !important;" +
29 " border: 0px !important;"+
34 * Scroll, if necessary, to make the given element visible
36 function reddit_scroll_into_view (window, element) {
37 var rect = element.getBoundingClientRect();
38 if (rect.top < 0 || rect.bottom > window.innerHeight)
39 element.scrollIntoView();
44 * Select the next entry down from the currently highlighted one.
45 * Checks the URL to figure out if one a link page or comment page.
47 function reddit_next (I) {
48 var doc = I.buffer.document;
49 if (doc.URL.search("/comments/") == -1) {
50 // Not on comment page, so highlight next link
53 // On comment page, so highlight next comment
54 reddit_next_comment(I, true);
57 interactive("reddit-next",
58 "Move the 'cursor' to the next reddit entry.",
63 * Selects the next parent comment if on a comment page.
65 function reddit_next_parent_comment (I) {
66 var doc = I.buffer.document;
67 if (doc.URL.search("/comments/") != -1)
68 reddit_next_comment(I, false);
70 interactive("reddit-next-parent-comment",
71 "Move the 'cursor' to the next comment which isn't "+
72 "a child of another comment.",
73 reddit_next_parent_comment);
77 * Move select the next link down from the currently highlighted one.
78 * When the end of the page is reached, the behavior is controlled by
79 * the variable reddit_end_behavior.
81 function reddit_next_link (I) {
82 var doc = I.buffer.document;
83 // the behavior of this command depends on whether we have downloaded
84 // enough of the page to include all of the article links.
85 var complete = doc.getElementsByClassName('footer').length > 0;
86 var links = doc.querySelectorAll("body>.content .link");
90 for (var i = 0, llen = links.length; i < llen; i++) {
91 if (links[i].style.display == 'none')
99 if (links[i].className.indexOf("last-clicked") >= 0)
102 // The following situations are null-ops:
103 // 1) there are no links on the page.
104 // 2) page is incomplete and the current link is the last link.
105 if (! first || (current && !next && !complete))
109 if (reddit_end_behavior == 'stop')
111 if (reddit_end_behavior == 'wrap')
113 if (reddit_end_behavior == 'page') {
114 let (xpr = doc.evaluate(
115 '//span[@class="nextprev"]/a[contains(text(),"next")]', doc, null,
116 Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null))
119 if (xpr && (nextpage = xpr.iterateNext())) {
120 dom_remove_class(current, "last-clicked");
121 browser_object_follow(I.buffer, FOLLOW_DEFAULT, nextpage);
127 // Page may or may not be complete. If the page is not
128 // complete, it is safe to assume that there is no current
129 // link because a current link can only persist on a
130 // cached page, which would load instantaneously, not
131 // giving the user the opportunity to run this command.
136 // ordinaries (highlight new, maybe dehighlight old)
138 dom_remove_class(current, "last-clicked");
139 dom_add_class(next, "last-clicked");
140 var anchor = doc.querySelector("body>.content .last-clicked a.title");
141 browser_set_element_focus(I.buffer, anchor);
142 reddit_scroll_into_view(I.buffer.focused_frame, next);
144 interactive("reddit-next-link",
145 "Move the 'cursor' to the next reddit link.",
150 * Checks if comment is a child of parent. Used on collapsed
151 * parents, to determine whether the child should be selected or
154 function comment_is_child (parent, comment) {
155 var parent_comments = parent.querySelectorAll(".comment");
156 for (var i = 0, llen = parent_comments.length; i < llen; i++) {
157 if (parent_comments[i].getAttribute("data-fullname") ==
158 comment.getAttribute("data-fullname"))
168 * Returns entries (top link + comments) that are visible (are not
171 function get_entries_without_collapsed_comments (entries) {
172 var entries_without_collapsed = [];
173 var collapsed_parent = null;
174 for (var i = 0, elen = entries.length; i < elen; i++) {
175 if (collapsed_parent) {
176 // Discard the 'load more comments' buttons
177 var current_classname = entries[i].getElementsByTagName("span")[0].className;
178 if (!comment_is_child(collapsed_parent, entries[i].parentNode) &&
179 current_classname != "morecomments")
181 collapsed_parent = null;
182 } else { // Skip collapsed comments
188 entries[i].getElementsByTagName("div")[1].style.display == "none")
190 collapsed_parent = entries[i].parentNode;
192 entries_without_collapsed.push(entries[i]);
194 return entries_without_collapsed;
199 * Select the next comment down from the currently highlighted one.
200 * When select_all_comments is true, select the next comment. When
201 * it's false select the next comment which isn't a child of another
204 function reddit_next_comment (I, select_all_comments) {
205 var doc = I.buffer.document;
206 // Get all comments plus the top link
207 var entries = doc.querySelectorAll("body>.content .entry");
208 // Remove all the collapsed comments
209 entries = get_entries_without_collapsed_comments(entries);
210 // Get the div which contains all comments
211 var comments_div = doc.getElementsByClassName("nestedlisting")[0];
215 for (var i = 0, elen = entries.length; i < elen && !next; i++) {
216 var parent_div_current = entries[i].parentNode.parentNode;
217 // Next link/comment can be selected if either:
218 // 1) All comments have to be selected
219 // 2) It's the first entry, which is the top link
220 // 3) It's a top level comment
221 if (select_all_comments || i == 0 ||
222 parent_div_current.id == comments_div.id)
229 if (entries[i].className.indexOf("last-clicked") >= 0)
230 current = entries[i];
232 // There are no comments on the page
235 // Last comment on page, try to load more
236 if (current && ! next) {
237 var load_more_link = comments_div.querySelector(
238 ".nestedlisting > .morechildren .button");
239 if (load_more_link) {
240 // Go to the previous comment first, since the current one will disappear
241 reddit_prev_comment(I, true);
242 browser_object_follow(I.buffer, FOLLOW_DEFAULT, load_more_link);
246 // No next yet, because there is no current. So make the first entry the next one
251 dom_remove_class(current, "last-clicked");
252 // Highlight the next comment
253 dom_add_class(next, "last-clicked");
254 // Focus the link on the comment page
255 var anchor = doc.querySelector("body>.content .last-clicked a.title");
256 browser_set_element_focus(I.buffer, anchor);
257 reddit_scroll_into_view(I.buffer.focused_frame, next);
259 interactive("reddit-next-comment",
260 "Move the 'cursor' to the next reddit comment.",
261 reddit_next_comment);
265 * Select the next entry up from the currently highlighted one.
266 * Checks the URL to figure out if one a link page or comment page.
268 function reddit_prev (I) {
269 var doc = I.buffer.document;
270 if (doc.URL.search("/comments/") == -1) {
271 // Not on comment page, so highlight prev link
274 // On comment page, so highlight prev comment
275 reddit_prev_comment(I, true);
278 interactive("reddit-prev",
279 "Move the 'cursor' to the previous reddit entry.",
283 function reddit_prev_parent_comment (I) {
284 var doc = I.buffer.document;
285 if (doc.URL.search("/comments/") != -1)
286 reddit_prev_comment(I, false);
288 interactive("reddit-prev-parent-comment",
289 "Move the 'cursor' to the previous comment which isn't "+
290 "a child of another comment.",
291 reddit_prev_parent_comment);
295 * Select the link before the currently highlighted one. When the
296 * beginning of the page is reached, behavior is controlled by the
297 * variable reddit_end_behavior.
299 function reddit_prev_link (I) {
300 var doc = I.buffer.document;
301 // the behavior of this command depends on whether we have downloaded
302 // enough of the page to include all of the article links.
303 var complete = doc.getElementsByClassName('footer').length > 0;
304 var links = doc.querySelectorAll("body>.content .link");
305 var llen = links.length;
309 for (var i = 0; i < llen; i++) {
310 if (links[i].style.display == 'none')
314 if (links[i].className.indexOf("last-clicked") >= 0) {
320 if (! first || // no links were found at all.
321 (!current && !complete)) // don't know where current is.
326 // the first visible link is the `current' link.
327 // dispatch on reddit_end_behavior.
328 if (reddit_end_behavior == 'stop') {
330 } else if (reddit_end_behavior == 'wrap') {
331 // need to get last link on page.
333 for (var i = 0; i < llen; i++) {
334 if (links[i].style.display == 'none')
339 } else if (reddit_end_behavior == 'page') {
340 let (xpr = doc.evaluate(
341 '//span[@class="nextprev"]/a[contains(text(),"prev")]', doc, null,
342 Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null))
345 if (xpr && (prevpage = xpr.iterateNext())) {
346 dom_remove_class(current, "last-clicked");
347 browser_object_follow(I.buffer, FOLLOW_DEFAULT, prevpage);
353 // ordinaries (highlight new, maybe dehighlight old)
355 dom_remove_class(current, "last-clicked");
356 dom_add_class(prev, "last-clicked");
357 var anchor = doc.querySelector("body>.content .last-clicked a.title");
358 browser_set_element_focus(I.buffer, anchor);
359 reddit_scroll_into_view(I.buffer.focused_frame, prev);
361 interactive("reddit-prev-link",
362 "Move the 'cursor' to the previous reddit link.",
367 * Select the prev comment down from the currently highlighted
368 * one. When select_all_comments is true, select the previous
369 * comment. When it's false select the previous comment which
370 * isn't a child of another comment.
372 function reddit_prev_comment (I, select_all_comments) {
373 var doc = I.buffer.document;
374 // Get all comments plus the top link
375 var entries = doc.querySelectorAll("body>.content .entry");
376 // Remove all the collapsed comments
377 entries = get_entries_without_collapsed_comments(entries);
378 // Get the div which contains all comments
379 var comments_div = doc.getElementsByClassName("nestedlisting")[0];
382 var prev_parent = null;
383 for (var i = 0, elen = entries.length; i < elen && !current; i++) {
384 if (entries[i].className.indexOf("last-clicked") >= 0) {
385 current = entries[i];
386 // Don't bother if the top link is selected, since
387 // that means there is no previous entry
389 if (select_all_comments)
390 prev = entries[i - 1];
395 var parent_div_current = entries[i].parentNode.parentNode;
396 // Remember the last parent comment and consider the top
397 // link to be a parent comment
398 if (i == 0 || parent_div_current.id == comments_div.id)
399 prev_parent = entries[i];
401 // Nothing is selected yet or there are no comments on the page.
406 dom_remove_class(current, "last-clicked");
407 // Highlight the prev comment
408 dom_add_class(prev, "last-clicked");
409 reddit_scroll_into_view(I.buffer.focused_frame, prev);
411 interactive("reddit-prev-comment",
412 "Move the 'cursor' to the previous reddit comment.",
413 reddit_prev_comment);
416 function reddit_open_comments (I, target) {
417 var doc = I.buffer.document;
418 var link = doc.querySelector("body>.content .last-clicked a.comments");
420 browser_object_follow(I.buffer, target || FOLLOW_DEFAULT, link);
422 function reddit_open_comments_new_buffer (I) {
423 reddit_open_comments(I, OPEN_NEW_BUFFER);
425 function reddit_open_comments_new_window (I) {
426 reddit_open_comments(I, OPEN_NEW_WINDOW);
428 interactive("reddit-open-comments",
429 "Open the comments-page associated with the currently selected link.",
430 alternates(reddit_open_comments,
431 reddit_open_comments_new_buffer,
432 reddit_open_comments_new_window));
435 function reddit_vote_up (I) {
436 var doc = I.buffer.document;
437 if (doc.URL.search("/comments/") == -1)
438 reddit_vote_link(I, true);
440 reddit_vote_comment(I, true);
442 interactive("reddit-vote-up",
443 "Vote the currently selected entry up.",
447 function reddit_vote_down (I) {
448 var doc = I.buffer.document;
449 if (doc.URL.search("/comments/") == -1)
450 reddit_vote_link(I, false);
452 reddit_vote_comment(I, false);
454 interactive("reddit-vote-down",
455 "Vote the currently selected entry 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;
463 var arrow_class = ".up";
465 arrow_class = ".down";
466 var link = doc.querySelector(
467 "body>.content .last-clicked .midcol " + arrow_class);
469 browser_object_follow(I.buffer, FOLLOW_DEFAULT, link);
472 function reddit_vote_comment (I, upvote) {
473 // get the current entry and send a click to its vote button.
474 var doc = I.buffer.document;
475 var link = doc.querySelector("body>.content .last-clicked");
477 var arrow_class = ".up";
479 arrow_class = ".down";
480 // Is there anything selected?
481 if (link && link.getElementsByTagName("span")[0].className != "morecomments") {
482 // Get the vote arrow
483 link = link.parentNode.getElementsByClassName("midcol")[0]
484 .querySelector(arrow_class);
486 browser_object_follow(I.buffer, FOLLOW_DEFAULT, link);
490 define_browser_object_class("reddit-current",
492 function (I, prompt) {
493 var doc = I.buffer.document;
494 var link = doc.querySelector("body>.content .last-clicked .entry p.title a");
495 yield co_return(link);
499 define_keymap("reddit_keymap", $display_name = "reddit");
500 define_key(reddit_keymap, "j", "reddit-next");
501 define_key(reddit_keymap, "J", "reddit-next-parent-comment");
502 define_key(reddit_keymap, "k", "reddit-prev");
503 define_key(reddit_keymap, "K", "reddit-prev-parent-comment");
504 define_key(reddit_keymap, ",", "reddit-vote-up");
505 define_key(reddit_keymap, ".", "reddit-vote-down");
506 define_key(reddit_keymap, "h", "reddit-open-comments");
509 var reddit_link_commands =
510 ["follow-current", "follow-current-new-buffer",
511 "follow-current-new-buffer-background",
512 "follow-current-new-window", "copy"];
515 var reddit_modality = {
516 normal: reddit_keymap
520 define_page_mode("reddit-mode",
521 build_url_regexp($domain = /([a-zA-Z0-9\-]*\.)*reddit/),
522 function enable (buffer) {
523 for each (var c in reddit_link_commands) {
524 buffer.default_browser_object_classes[c] =
525 browser_object_reddit_current;
527 buffer.content_modalities.push(reddit_modality);
529 function disable (buffer) {
530 for each (var c in reddit_link_commands) {
531 delete buffer.default_browser_object_classes[c];
533 var i = buffer.content_modalities.indexOf(reddit_modality);
535 buffer.content_modalities.splice(i, 1);
537 $display_name = "reddit",
538 $doc = "reddit page-mode: keyboard navigation for reddit.");
540 page_mode_activate(reddit_mode);