page-modes with default browser objects, remove on disable
[conkeror.git] / modules / page-modes / reddit.js
blobf4e84edf5d6f3a915a8419f130867e41fc6b6d81
1 /**
2  * (C) Copyright 2008 Martin Dybdal
3  * (C) Copyright 2009-2010 John Foerch
4  *
5  * Use, modification, and distribution are subject to the terms specified in the
6  * COPYING file.
7 **/
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(
21     "data:text/css," +
22         escape (
23             "@-moz-document url-prefix(http://www.reddit.com/) {" +
24                 ".last-clicked {" +
25                 " background-color: #bfb !important;" +
26                 " border: 0px !important;"+
27                 "}}"));
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();
38 /* Move select the next link down from the currently highlighted one.
39  * When the end of the page is reached, the behavior is controlled by
40  * the variable reddit_end_behavior.
41  */
42 function reddit_next (I) {
43     var doc = I.buffer.document;
44     // the behavior of this command depends on whether we have downloaded
45     // enough of the page to include all of the article links.
46     var complete = doc.getElementsByClassName('footer').length > 0;
47     var links = doc.getElementsByClassName('link');
48     var first = null;
49     var current = null;
50     var next = null;
51     for (var i = 0, llen = links.length; i < llen; i++) {
52         if (links[i].style.display == 'none')
53             continue;
54         if (! first)
55             first = links[i];
56         if (current) {
57             next = links[i];
58             break;
59         }
60         if (links[i].className.indexOf("last-clicked") >= 0)
61             current = links[i];
62     }
63     // The following situations are null-ops:
64     //  1) there are no links on the page.
65     //  2) page is incomplete and the current link is the last link.
66     if (!first || (current && !next && !complete))
67         return;
68     if (! next) {
69         if (current) {
70             if (reddit_end_behavior == 'stop')
71                 return;
72             if (reddit_end_behavior == 'wrap')
73                 next = first;
74             if (reddit_end_behavior == 'page') {
75                 let (xpr = doc.evaluate(
76                     '//p[@class="nextprev"]/a[text()="next"]', doc, null,
77                     Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null))
78                 {
79                     let nextpage;
80                     if (xpr && (nextpage = xpr.iterateNext())) {
81                         dom_remove_class(current, "last-clicked");
82                         browser_object_follow(I.buffer, FOLLOW_DEFAULT, nextpage);
83                         return;
84                     }
85                 }
86             }
87         } else {
88             // Page may or may not be complete.  If the page is not
89             // complete, it is safe to assume that there is no current
90             // link because a current link can only persist on a
91             // cached page, which would load instantaneously, not
92             // giving the user the opportunity to run this command.
93             //
94             next = first;
95         }
96     }
97     // ordinaries (highlight new, maybe dehighlight old)
98     if (current)
99         dom_remove_class(current, "last-clicked");
100     dom_add_class(next, "last-clicked");
101     let (anchor = doc.evaluate(
102         '//*[contains(@class,"last-clicked")]//a[contains(@class,"title")]',
103         next, null, Ci.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null))
104     {
105         browser_set_element_focus(I.buffer, anchor.singleNodeValue);
106     }
107     reddit_scroll_into_view(I.buffer.focused_frame, next);
109 interactive("reddit-next-link",
110             "Move the 'cursor' to the next reddit entry.",
111             reddit_next);
114 /* Select the link before the currently highlighted one.  When the
115  * beginning of the page is reached, behavior is controlled by the
116  * variable reddit_end_behavior.
117  */
118 function reddit_prev (I) {
119     var doc = I.buffer.document;
120     // the behavior of this command depends on whether we have downloaded
121     // enough of the page to include all of the article links.
122     var complete = doc.getElementsByClassName('footer').length > 0;
123     var links = doc.getElementsByClassName('link');
124     var llen = links.length;
125     var first = null;
126     var prev = null;
127     var current = null;
128     for (var i = 0; i < llen; i++) {
129         if (links[i].style.display == 'none')
130             continue;
131         if (! first)
132             first = links[i];
133         if (links[i].className.indexOf("last-clicked") >= 0) {
134             current = links[i];
135             break;
136         }
137         prev = links[i];
138     }
139     if (! first || // no links were found at all.
140         (!current && !complete)) // don't know where current is.
141         return;
142     if (! prev) {
143         // the first visible link is the `current' link.
144         // dispatch on reddit_end_behavior.
145         if (reddit_end_behavior == 'stop')
146             return;
147         else if (reddit_end_behavior == 'wrap') {
148             // need to get last link on page.
149             if (complete) {
150                 for (var i = 0; i < llen; i++) {
151                     if (links[i].style.display == 'none')
152                         continue;
153                     prev = links[i];
154                 }
155             }
156         } else if (reddit_end_behavior == 'page') {
157             let (xpr = doc.evaluate(
158                 '//p[@class="nextprev"]/a[text()="prev"]', doc, null,
159                 Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null))
160             {
161                 let prevpage;
162                 if (xpr && (prevpage = xpr.iterateNext())) {
163                     dom_remove_class(current, "last-clicked");
164                     browser_object_follow(I.buffer, FOLLOW_DEFAULT, prevpage);
165                     return;
166                 }
167             }
168         }
169     }
170     // ordinaries (highlight new, maybe dehighlight old)
171     if (current)
172         dom_remove_class(current, "last-clicked");
173     dom_add_class(prev, "last-clicked");
174     let (anchor = doc.evaluate(
175         '//*[contains(@class,"last-clicked")]//a[contains(@class,"title")]',
176         prev, null, Ci.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null))
177     {
178         browser_set_element_focus(I.buffer, anchor.singleNodeValue);
179     }
180     reddit_scroll_into_view(I.buffer.focused_frame, prev);
182 interactive("reddit-prev-link",
183             "Move the 'cursor' to the previous reddit entry.",
184             reddit_prev);
187 function reddit_open_comments (I, target) {
188     var xpr = I.buffer.document.evaluate(
189         '//*[contains(@class,"last-clicked")]/descendant::a[@class="comments"]',
190         I.buffer.document, null,
191         Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
192     let link;
193     if (xpr && (link = xpr.iterateNext()))
194         browser_object_follow(I.buffer, target || FOLLOW_DEFAULT, link);
196 function reddit_open_comments_new_buffer (I) {
197     reddit_open_comments(I, OPEN_NEW_BUFFER);
199 function reddit_open_comments_new_window (I) {
200     reddit_open_comments(I, OPEN_NEW_WINDOW);
202 interactive("reddit-open-comments",
203             "Open the comments-page associated with the currently selected link.",
204             alternates(reddit_open_comments,
205                        reddit_open_comments_new_buffer,
206                        reddit_open_comments_new_window));
209 function reddit_vote_up (I) {
210     // get the current article and send a click to its vote-up button.
211     var xpr = I.buffer.document.evaluate(
212         '//*[contains(@class,"last-clicked")]/div[contains(@class,"midcol")]/div[contains(@class,"up")]',
213         I.buffer.document, null,
214         Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
215     let link;
216     if (xpr && (link = xpr.iterateNext()))
217         browser_object_follow(I.buffer, FOLLOW_DEFAULT, link);
219 interactive("reddit-vote-up",
220             "Vote the currently selected link up.",
221             reddit_vote_up);
224 function reddit_vote_down (I) {
225     // get the current article and send a click to its vote-down button.
226     var xpr = I.buffer.document.evaluate(
227         '//*[contains(@class,"last-clicked")]/div[contains(@class,"midcol")]/div[contains(@class,"down")]',
228         I.buffer.document, null,
229         Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
230     let link;
231     if (xpr && (link = xpr.iterateNext()))
232         browser_object_follow(I.buffer, FOLLOW_DEFAULT, link);
234 interactive("reddit-vote-down",
235             "Vote the currently selected link down.",
236             reddit_vote_down);
239 define_browser_object_class("reddit-current", null,
240     function (I, prompt) {
241         var xpr = I.buffer.document.evaluate(
242             '//*[contains(@class,"last-clicked")]/*[contains(@class,"entry")]/p[@class="title"]/a',
243             I.buffer.document, null,
244             Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
245         yield co_return(xpr.iterateNext());
246     });
249 define_keymap("reddit_keymap", $display_name = "reddit");
250 define_key(reddit_keymap, "j", "reddit-next-link");
251 define_key(reddit_keymap, "k", "reddit-prev-link");
252 define_key(reddit_keymap, ",", "reddit-vote-up");
253 define_key(reddit_keymap, ".", "reddit-vote-down");
254 define_key(reddit_keymap, "h", "reddit-open-comments");
257 var reddit_link_commands =
258     ["follow-current", "follow-current-new-buffer",
259      "follow-current-new-buffer-background",
260      "follow-current-new-window", "copy"];
263 var reddit_modality = {
264     normal: reddit_keymap
268 define_page_mode("reddit-mode",
269     build_url_regexp($domain = /([a-zA-Z0-9\-]*\.)*reddit/),
270     function enable (buffer) {
271         for each (var c in reddit_link_commands) {
272             buffer.default_browser_object_classes[c] =
273                 browser_object_reddit_current;
274         }
275         buffer.content_modalities.push(reddit_modality);
276     },
277     function disable (buffer) {
278         for each (var c in reddit_link_commands) {
279             delete buffer.default_browser_object_classes[c];
280         }
281         var i = buffer.content_modalities.indexOf(reddit_modality);
282         if (i > -1)
283             buffer.content_modalities.splice(i, 1);
284     },
285     $display_name = "reddit",
286     $doc = "reddit page-mode: keyboard navigation for reddit.");
288 page_mode_activate(reddit_mode);
290 provide("reddit");