reddit.js: make highlight stylesheet work on SSL
[conkeror.git] / modules / page-modes / reddit.js
blob4e83127a49c14bfe9a06004c14caf46e37cf15d5
1 /**
2  * (C) Copyright 2008 Martin Dybdal
3  * (C) Copyright 2009-2010,2012 John Foerch
4  * (C) Copyright 2013 Joren Van Onder
5  *
6  * Use, modification, and distribution are subject to the terms specified in the
7  * COPYING file.
8 **/
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(
22     "data:text/css," +
23         escape (
24             "@-moz-document url-prefix(http://www.reddit.com/)," +
25                 "url-prefix(https://pay.reddit.com) {" +
26                 "body>.content .last-clicked {" +
27                 " background-color: #bfb !important;" +
28                 " border: 0px !important;"+
29                 "}}"));
32 /**
33  * Scroll, if necessary, to make the given element visible
34  */
35 function reddit_scroll_into_view (window, element) {
36     var rect = element.getBoundingClientRect();
37     if (rect.top < 0 || rect.bottom > window.innerHeight)
38         element.scrollIntoView();
42 /**
43  * Select the next entry down from the currently highlighted one.
44  * Checks the URL to figure out if one a link page or comment page.
45  */
46 function reddit_next (I) {
47     var doc = I.buffer.document;
48     if (doc.URL.search("/comments/") == -1) {
49         // Not on comment page, so highlight next link
50         reddit_next_link(I);
51     } else {
52         // On comment page, so highlight next comment
53         reddit_next_comment(I, true);
54     }
56 interactive("reddit-next",
57     "Move the 'cursor' to the next reddit entry.",
58     reddit_next);
61 /**
62  * Selects the next parent comment if on a comment page.
63  */
64 function reddit_next_parent_comment (I) {
65     var doc = I.buffer.document;
66     if (doc.URL.search("/comments/") != -1)
67         reddit_next_comment(I, false);
69 interactive("reddit-next-parent-comment",
70     "Move the 'cursor' to the next comment which isn't "+
71     "a child of another comment.",
72     reddit_next_parent_comment);
75 /**
76  * Move select the next link down from the currently highlighted one.
77  * When the end of the page is reached, the behavior is controlled by
78  * the variable reddit_end_behavior.
79  */
80 function reddit_next_link (I) {
81     var doc = I.buffer.document;
82     // the behavior of this command depends on whether we have downloaded
83     // enough of the page to include all of the article links.
84     var complete = doc.getElementsByClassName('footer').length > 0;
85     var links = doc.querySelectorAll("body>.content .link");
86     var first = null;
87     var current = null;
88     var next = null;
89     for (var i = 0, llen = links.length; i < llen; i++) {
90         if (links[i].style.display == 'none')
91             continue;
92         if (! first)
93             first = links[i];
94         if (current) {
95             next = links[i];
96             break;
97         }
98         if (links[i].className.indexOf("last-clicked") >= 0)
99             current = links[i];
100     }
101     // The following situations are null-ops:
102     //  1) there are no links on the page.
103     //  2) page is incomplete and the current link is the last link.
104     if (! first || (current && !next && !complete))
105         return;
106     if (! next) {
107         if (current) {
108             if (reddit_end_behavior == 'stop')
109                 return;
110             if (reddit_end_behavior == 'wrap')
111                 next = first;
112             if (reddit_end_behavior == 'page') {
113                 let (xpr = doc.evaluate(
114                     '//p[@class="nextprev"]/a[contains(text(),"next")]', doc, null,
115                     Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null))
116                 {
117                     var nextpage;
118                     if (xpr && (nextpage = xpr.iterateNext())) {
119                         dom_remove_class(current, "last-clicked");
120                         browser_object_follow(I.buffer, FOLLOW_DEFAULT, nextpage);
121                         return;
122                     }
123                 }
124             }
125         } else {
126             // Page may or may not be complete.  If the page is not
127             // complete, it is safe to assume that there is no current
128             // link because a current link can only persist on a
129             // cached page, which would load instantaneously, not
130             // giving the user the opportunity to run this command.
131             //
132             next = first;
133         }
134     }
135     // ordinaries (highlight new, maybe dehighlight old)
136     if (current)
137         dom_remove_class(current, "last-clicked");
138     dom_add_class(next, "last-clicked");
139     var anchor = doc.querySelector("body>.content .last-clicked a.title");
140     browser_set_element_focus(I.buffer, anchor);
141     reddit_scroll_into_view(I.buffer.focused_frame, next);
143 interactive("reddit-next-link",
144     "Move the 'cursor' to the next reddit link.",
145     reddit_next_link);
149  * Checks if comment is a child of parent. Used on collapsed
150  * parents, to determine whether the child should be selected or
151  * not.
152  */
153 function comment_is_child (parent, comment) {
154     var parent_comments = parent.querySelectorAll(".comment");
155     for (var i = 0, llen = parent_comments.length; i < llen; i++) {
156         if (parent_comments[i].getAttribute("data-fullname") ==
157             comment.getAttribute("data-fullname"))
158         {
159             return true;
160         }
161     }
162     return false;
167  * Returns entries (top link + comments) that are visible (are not
168  * collapsed).
169  */
170 function get_entries_without_collapsed_comments (entries) {
171     var entries_without_collapsed = [];
172     var collapsed_parent = null;
173     for (var i = 0, elen = entries.length; i < elen; i++) {
174         if (collapsed_parent) {
175             // Discard the 'load more comments' buttons
176             var current_classname = entries[i].getElementsByTagName("span")[0].className;
177             if (!comment_is_child(collapsed_parent, entries[i].parentNode) &&
178                 current_classname != "morecomments")
179             {
180                 collapsed_parent = null;
181             } else { // Skip collapsed comments
182                 continue;
183             }
184         }
185         // Collapsed comment
186         if (i != 0 &&
187             entries[i].getElementsByTagName("div")[1].style.display == "none")
188         {
189             collapsed_parent = entries[i].parentNode;
190         }
191         entries_without_collapsed.push(entries[i]);
192     }
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
201  * comment.
202  */
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];
211     var first = null;
212     var current = null;
213     var next = null;
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)
222         {
223             if (! first)
224                 first = entries[i];
225             if (current)
226                 next = entries[i];
227         }
228         if (entries[i].className.indexOf("last-clicked") >= 0)
229             current = entries[i];
230     }
231     // There are no comments on the page
232     if (! first)
233         return;
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);
242         }
243         return;
244     }
245     // No next yet, because there is no current. So make the first entry the next one
246     if (! next)
247         next = first;
248     // Dehighlight old
249     if (current)
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.
266  */
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
271         reddit_prev_link(I);
272     } else {
273         // On comment page, so highlight prev comment
274         reddit_prev_comment(I, true);
275     }
277 interactive("reddit-prev",
278     "Move the 'cursor' to the previous reddit entry.",
279     reddit_prev);
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.
297  */
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;
305     var first = null;
306     var prev = null;
307     var current = null;
308     for (var i = 0; i < llen; i++) {
309         if (links[i].style.display == 'none')
310             continue;
311         if (! first)
312             first = links[i];
313         if (links[i].className.indexOf("last-clicked") >= 0) {
314             current = links[i];
315             break;
316         }
317         prev = links[i];
318     }
319     if (! first || // no links were found at all.
320         (!current && !complete)) // don't know where current is.
321     {
322         return;
323     }
324     if (! prev) {
325         // the first visible link is the `current' link.
326         // dispatch on reddit_end_behavior.
327         if (reddit_end_behavior == 'stop') {
328             return;
329         } else if (reddit_end_behavior == 'wrap') {
330             // need to get last link on page.
331             if (complete) {
332                 for (var i = 0; i < llen; i++) {
333                     if (links[i].style.display == 'none')
334                         continue;
335                     prev = links[i];
336                 }
337             }
338         } else if (reddit_end_behavior == 'page') {
339             let (xpr = doc.evaluate(
340                 '//p[@class="nextprev"]/a[contains(text(),"prev")]', doc, null,
341                 Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null))
342             {
343                 var prevpage;
344                 if (xpr && (prevpage = xpr.iterateNext())) {
345                     dom_remove_class(current, "last-clicked");
346                     browser_object_follow(I.buffer, FOLLOW_DEFAULT, prevpage);
347                     return;
348                 }
349             }
350         }
351     }
352     // ordinaries (highlight new, maybe dehighlight old)
353     if (current)
354         dom_remove_class(current, "last-clicked");
355     dom_add_class(prev, "last-clicked");
356     var anchor = doc.querySelector("body>.content .last-clicked a.title");
357     browser_set_element_focus(I.buffer, anchor);
358     reddit_scroll_into_view(I.buffer.focused_frame, prev);
360 interactive("reddit-prev-link",
361     "Move the 'cursor' to the previous reddit link.",
362     reddit_prev_link);
366  * Select the prev comment down from the currently highlighted
367  * one.  When select_all_comments is true, select the previous
368  * comment. When it's false select the previous comment which
369  * isn't a child of another comment.
370  */
371 function reddit_prev_comment (I, select_all_comments) {
372     var doc = I.buffer.document;
373     // Get all comments plus the top link
374     var entries = doc.querySelectorAll("body>.content .entry");
375     // Remove all the collapsed comments
376     entries = get_entries_without_collapsed_comments(entries);
377     // Get the div which contains all comments
378     var comments_div = doc.getElementsByClassName("nestedlisting")[0];
379     var current = null;
380     var prev = null;
381     var prev_parent = null;
382     for (var i = 0, elen = entries.length; i < elen && !current; i++) {
383         if (entries[i].className.indexOf("last-clicked") >= 0) {
384             current = entries[i];
385             // Don't bother if the top link is selected, since
386             // that means there is no previous entry
387             if (i != 0) {
388                 if (select_all_comments)
389                     prev = entries[i - 1];
390                 else
391                     prev = prev_parent;
392             }
393         }
394         var parent_div_current = entries[i].parentNode.parentNode;
395         // Remember the last parent comment and consider the top
396         // link to be a parent comment
397         if (i == 0 || parent_div_current.id == comments_div.id)
398             prev_parent = entries[i];
399     }
400     // Nothing is selected yet or there are no comments on the page.
401     if (! prev)
402         return;
403     // Dehighlight old
404     if (current)
405         dom_remove_class(current, "last-clicked");
406     // Highlight the prev comment
407     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");
418     if (link)
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;
436     if (doc.URL.search("/comments/") == -1)
437         reddit_vote_link(I, true);
438     else
439         reddit_vote_comment(I, true);
441 interactive("reddit-vote-up",
442     "Vote the currently selected entry up.",
443     reddit_vote_up);
446 function reddit_vote_down (I) {
447     var doc = I.buffer.document;
448     if (doc.URL.search("/comments/") == -1)
449         reddit_vote_link(I, false);
450     else
451         reddit_vote_comment(I, false);
453 interactive("reddit-vote-down",
454     "Vote the currently selected entry down.",
455     reddit_vote_down);
458 function reddit_vote_link (I, upvote) {
459     // get the current article and send a click to its vote button.
460     var doc = I.buffer.document;
461     if (upvote)
462         var arrow_class = ".up";
463     else
464         arrow_class = ".down";
465     var link = doc.querySelector(
466         "body>.content .last-clicked .midcol " + arrow_class);
467     if (link)
468         browser_object_follow(I.buffer, FOLLOW_DEFAULT, link);
471 function reddit_vote_comment (I, upvote) {
472     // get the current entry and send a click to its vote button.
473     var doc = I.buffer.document;
474     var link = doc.querySelector("body>.content .last-clicked");
475     if (upvote)
476         var arrow_class = ".up";
477     else
478         arrow_class = ".down";
479     // Is there anything selected?
480     if (link && link.getElementsByTagName("span")[0].className != "morecomments") {
481         // Get the vote arrow
482         link = link.parentNode.getElementsByClassName("midcol")[0]
483             .querySelector(arrow_class);
484         if (link)
485             browser_object_follow(I.buffer, FOLLOW_DEFAULT, link);
486     }
489 define_browser_object_class("reddit-current",
490     null,
491     function (I, prompt) {
492         var doc = I.buffer.document;
493         var link = doc.querySelector("body>.content .last-clicked .entry p.title a");
494         yield co_return(link);
495     });
498 define_keymap("reddit_keymap", $display_name = "reddit");
499 define_key(reddit_keymap, "j", "reddit-next");
500 define_key(reddit_keymap, "J", "reddit-next-parent-comment");
501 define_key(reddit_keymap, "k", "reddit-prev");
502 define_key(reddit_keymap, "K", "reddit-prev-parent-comment");
503 define_key(reddit_keymap, ",", "reddit-vote-up");
504 define_key(reddit_keymap, ".", "reddit-vote-down");
505 define_key(reddit_keymap, "h", "reddit-open-comments");
508 var reddit_link_commands =
509     ["follow-current", "follow-current-new-buffer",
510      "follow-current-new-buffer-background",
511      "follow-current-new-window", "copy"];
514 var reddit_modality = {
515     normal: reddit_keymap
519 define_page_mode("reddit-mode",
520     build_url_regexp($domain = /([a-zA-Z0-9\-]*\.)*reddit/),
521     function enable (buffer) {
522         for each (var c in reddit_link_commands) {
523             buffer.default_browser_object_classes[c] =
524                 browser_object_reddit_current;
525         }
526         buffer.content_modalities.push(reddit_modality);
527     },
528     function disable (buffer) {
529         for each (var c in reddit_link_commands) {
530             delete buffer.default_browser_object_classes[c];
531         }
532         var i = buffer.content_modalities.indexOf(reddit_modality);
533         if (i > -1)
534             buffer.content_modalities.splice(i, 1);
535     },
536     $display_name = "reddit",
537     $doc = "reddit page-mode: keyboard navigation for reddit.");
539 page_mode_activate(reddit_mode);
541 provide("reddit");