2 * (C) Copyright 2008 Martin Dybdal
3 * (C) Copyright 2009-2010,2012 John Foerch
5 * Use, modification, and distribution are subject to the terms specified in the
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(
23 "@-moz-document url-prefix(http://www.reddit.com/) {" +
24 "body>.content .last-clicked {" +
25 " background-color: #bfb !important;" +
26 " border: 0px !important;"+
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.
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)
47 // On comment page, so highlight next comment
49 reddit_next_comment(I, true);
51 interactive("reddit-next",
52 "Move the 'cursor' to the next reddit entry.",
55 /* Selects the next parent comment if on a comment page.
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.
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");
80 for (var i = 0, llen = links.length; i < llen; i++) {
81 if (links[i].style.display == 'none')
89 if (links[i].className.indexOf("last-clicked") >= 0)
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))
99 if (reddit_end_behavior == 'stop')
101 if (reddit_end_behavior == 'wrap')
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))
109 if (xpr && (nextpage = xpr.iterateNext())) {
110 dom_remove_class(current, "last-clicked");
111 browser_object_follow(I.buffer, FOLLOW_DEFAULT, nextpage);
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.
126 // ordinaries (highlight new, maybe dehighlight old)
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.",
138 /* Checks if comment is a child of parent. Used on collapsed
139 * parents, to determine whether the child should be selected or
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"))
153 /* Returns entries (top link + comments) that are visible (are not
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
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]);
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
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];
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) {
216 if (entries[i].className.indexOf("last-clicked") >= 0)
217 current = entries[i];
220 // There are no comments on the page
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);
237 // No next yet, because there is no current. So make the first entry the next one
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.
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)
268 // On comment page, so highlight prev comment
270 reddit_prev_comment(I, true);
272 interactive("reddit-prev",
273 "Move the 'cursor' to the previous reddit entry.",
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.
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;
300 for (var i = 0; i < llen; i++) {
301 if (links[i].style.display == 'none')
305 if (links[i].className.indexOf("last-clicked") >= 0) {
311 if (! first || // no links were found at all.
312 (!current && !complete)) // don't know where current is.
315 // the first visible link is the `current' link.
316 // dispatch on reddit_end_behavior.
317 if (reddit_end_behavior == 'stop')
319 else if (reddit_end_behavior == 'wrap') {
320 // need to get last link on page.
322 for (var i = 0; i < llen; i++) {
323 if (links[i].style.display == 'none')
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))
334 if (xpr && (prevpage = xpr.iterateNext())) {
335 dom_remove_class(current, "last-clicked");
336 browser_object_follow(I.buffer, FOLLOW_DEFAULT, prevpage);
342 // ordinaries (highlight new, maybe dehighlight old)
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.",
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.
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];
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
381 if (select_all_comments)
382 prev = entries[i - 1];
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];
396 // Nothing is selected yet or there are no comments on the page.
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");
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);
440 reddit_vote_comment(I, true);
442 interactive("reddit-vote-up",
443 "Vote the currently selected entry 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);
453 reddit_vote_comment(I, false);
455 interactive("reddit-vote-down",
456 "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;
462 var arrow_class = "";
467 arrow_class = ".down";
469 var link = doc.querySelector("body>.content .last-clicked .midcol " + arrow_class);
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 = "";
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)
491 browser_object_follow(I.buffer, FOLLOW_DEFAULT, link);
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);
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;
531 buffer.content_modalities.push(reddit_modality);
533 function disable (buffer) {
534 for each (var c in reddit_link_commands) {
535 delete buffer.default_browser_object_classes[c];
537 var i = buffer.content_modalities.indexOf(reddit_modality);
539 buffer.content_modalities.splice(i, 1);
541 $display_name = "reddit",
542 $doc = "reddit page-mode: keyboard navigation for reddit.");
544 page_mode_activate(reddit_mode);