redesign buffer-modes and page-modes
[conkeror.git] / modules / page-modes / reddit.js
blobdfece3924a848d5b9c9ba77bacf331cfaa629abf
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 in_module(null);
11 require("content-buffer.js");
13 define_variable("reddit_end_behavior", "stop",
14     "Controls the behavior of the commands reddit-next-link and "+
15     "reddit-prev-link when at the last or first link, respectively. "+
16     "Given as a string, the supported values are 'stop', 'wrap', "+
17     "and 'page'.  'stop' means to not move the highlight in any "+
18     "way.  'wrap' means to wrap around to the first (or last) "+
19     "link.  'page' means to navigate the buffer to the next (or "+
20     "previous) page on reddit.");
22 register_user_stylesheet(
23     "data:text/css," +
24         escape (
25             "@-moz-document url-prefix(http://www.reddit.com/) {" +
26                 ".last-clicked {" +
27                 " background-color: #bfb !important;" +
28                 " border: 0px !important;"+
29                 "}}"));
32 /* Scroll, if necessary, to make the given element visible */
33 function reddit_scroll_into_view (window, element) {
34     var rect = element.getBoundingClientRect();
35     if (rect.top < 0 || rect.bottom > window.innerHeight)
36         element.scrollIntoView();
40 /* Move select the next link down from the currently highlighted one.
41  * When the end of the page is reached, the behavior is controlled by
42  * the variable reddit_end_behavior.
43  */
44 function reddit_next (I) {
45     var doc = I.buffer.document;
46     // the behavior of this command depends on whether we have downloaded
47     // enough of the page to include all of the article links.
48     var complete = doc.getElementsByClassName('footer').length > 0;
49     var links = doc.getElementsByClassName('link');
50     var first = null;
51     var current = null;
52     var next = null;
53     for (var i = 0, llen = links.length; i < llen; i++) {
54         if (links[i].style.display == 'none')
55             continue;
56         if (! first)
57             first = links[i];
58         if (current) {
59             next = links[i];
60             break;
61         }
62         if (links[i].className.indexOf("last-clicked") >= 0)
63             current = links[i];
64     }
65     // The following situations are null-ops:
66     //  1) there are no links on the page.
67     //  2) page is incomplete and the current link is the last link.
68     if (!first || (current && !next && !complete))
69         return;
70     if (! next) {
71         if (current) {
72             if (reddit_end_behavior == 'stop')
73                 return;
74             if (reddit_end_behavior == 'wrap')
75                 next = first;
76             if (reddit_end_behavior == 'page') {
77                 let (xpr = doc.evaluate(
78                     '//p[@class="nextprev"]/a[text()="next"]', doc, null,
79                     Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null))
80                 {
81                     let nextpage;
82                     if (xpr && (nextpage = xpr.iterateNext())) {
83                         dom_remove_class(current, "last-clicked");
84                         browser_object_follow(I.buffer, FOLLOW_DEFAULT, nextpage);
85                         return;
86                     }
87                 }
88             }
89         } else {
90             // Page may or may not be complete.  If the page is not
91             // complete, it is safe to assume that there is no current
92             // link because a current link can only persist on a
93             // cached page, which would load instantaneously, not
94             // giving the user the opportunity to run this command.
95             //
96             next = first;
97         }
98     }
99     // ordinaries (highlight new, maybe dehighlight old)
100     if (current)
101         dom_remove_class(current, "last-clicked");
102     dom_add_class(next, "last-clicked");
103     let (anchor = doc.evaluate(
104         '//*[contains(@class,"last-clicked")]//a[contains(@class,"title")]',
105         next, null, Ci.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null))
106     {
107         browser_set_element_focus(I.buffer, anchor.singleNodeValue);
108     }
109     reddit_scroll_into_view(I.buffer.focused_frame, next);
111 interactive("reddit-next-link",
112             "Move the 'cursor' to the next reddit entry.",
113             reddit_next);
116 /* Select the link before the currently highlighted one.  When the
117  * beginning of the page is reached, behavior is controlled by the
118  * variable reddit_end_behavior.
119  */
120 function reddit_prev (I) {
121     var doc = I.buffer.document;
122     // the behavior of this command depends on whether we have downloaded
123     // enough of the page to include all of the article links.
124     var complete = doc.getElementsByClassName('footer').length > 0;
125     var links = doc.getElementsByClassName('link');
126     var llen = links.length;
127     var first = null;
128     var prev = null;
129     var current = null;
130     for (var i = 0; i < llen; i++) {
131         if (links[i].style.display == 'none')
132             continue;
133         if (! first)
134             first = links[i];
135         if (links[i].className.indexOf("last-clicked") >= 0) {
136             current = links[i];
137             break;
138         }
139         prev = links[i];
140     }
141     if (! first || // no links were found at all.
142         (!current && !complete)) // don't know where current is.
143         return;
144     if (! prev) {
145         // the first visible link is the `current' link.
146         // dispatch on reddit_end_behavior.
147         if (reddit_end_behavior == 'stop')
148             return;
149         else if (reddit_end_behavior == 'wrap') {
150             // need to get last link on page.
151             if (complete) {
152                 for (var i = 0; i < llen; i++) {
153                     if (links[i].style.display == 'none')
154                         continue;
155                     prev = links[i];
156                 }
157             }
158         } else if (reddit_end_behavior == 'page') {
159             let (xpr = doc.evaluate(
160                 '//p[@class="nextprev"]/a[text()="prev"]', doc, null,
161                 Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null))
162             {
163                 let prevpage;
164                 if (xpr && (prevpage = xpr.iterateNext())) {
165                     dom_remove_class(current, "last-clicked");
166                     browser_object_follow(I.buffer, FOLLOW_DEFAULT, prevpage);
167                     return;
168                 }
169             }
170         }
171     }
172     // ordinaries (highlight new, maybe dehighlight old)
173     if (current)
174         dom_remove_class(current, "last-clicked");
175     dom_add_class(prev, "last-clicked");
176     let (anchor = doc.evaluate(
177         '//*[contains(@class,"last-clicked")]//a[contains(@class,"title")]',
178         prev, null, Ci.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null))
179     {
180         browser_set_element_focus(I.buffer, anchor.singleNodeValue);
181     }
182     reddit_scroll_into_view(I.buffer.focused_frame, prev);
184 interactive("reddit-prev-link",
185             "Move the 'cursor' to the previous reddit entry.",
186             reddit_prev);
189 function reddit_open_comments (I, target) {
190     var xpr = I.buffer.document.evaluate(
191         '//*[contains(@class,"last-clicked")]/descendant::a[@class="comments"]',
192         I.buffer.document, null,
193         Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
194     let link;
195     if (xpr && (link = xpr.iterateNext()))
196         browser_object_follow(I.buffer, target || FOLLOW_DEFAULT, link);
198 function reddit_open_comments_new_buffer (I) {
199     reddit_open_comments(I, OPEN_NEW_BUFFER);
201 function reddit_open_comments_new_window (I) {
202     reddit_open_comments(I, OPEN_NEW_WINDOW);
204 interactive("reddit-open-comments",
205             "Open the comments-page associated with the currently selected link.",
206             alternates(reddit_open_comments,
207                        reddit_open_comments_new_buffer,
208                        reddit_open_comments_new_window));
211 function reddit_vote_up (I) {
212     // get the current article and send a click to its vote-up button.
213     var xpr = I.buffer.document.evaluate(
214         '//*[contains(@class,"last-clicked")]/div[contains(@class,"midcol")]/div[contains(@class,"up")]',
215         I.buffer.document, null,
216         Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
217     let link;
218     if (xpr && (link = xpr.iterateNext()))
219         browser_object_follow(I.buffer, FOLLOW_DEFAULT, link);
221 interactive("reddit-vote-up",
222             "Vote the currently selected link up.",
223             reddit_vote_up);
226 function reddit_vote_down (I) {
227     // get the current article and send a click to its vote-down button.
228     var xpr = I.buffer.document.evaluate(
229         '//*[contains(@class,"last-clicked")]/div[contains(@class,"midcol")]/div[contains(@class,"down")]',
230         I.buffer.document, null,
231         Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
232     let link;
233     if (xpr && (link = xpr.iterateNext()))
234         browser_object_follow(I.buffer, FOLLOW_DEFAULT, link);
236 interactive("reddit-vote-down",
237             "Vote the currently selected link down.",
238             reddit_vote_down);
241 define_browser_object_class("reddit-current", null,
242     function (I, prompt) {
243         var xpr = I.buffer.document.evaluate(
244             '//*[contains(@class,"last-clicked")]/*[contains(@class,"entry")]/p[@class="title"]/a',
245             I.buffer.document, null,
246             Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
247         yield co_return(xpr.iterateNext());
248     });
251 define_keymap("reddit_keymap", $display_name = "reddit");
252 define_key(reddit_keymap, "j", "reddit-next-link");
253 define_key(reddit_keymap, "k", "reddit-prev-link");
254 define_key(reddit_keymap, ",", "reddit-vote-up");
255 define_key(reddit_keymap, ".", "reddit-vote-down");
256 define_key(reddit_keymap, "h", "reddit-open-comments");
259 var reddit_modality = {
260     normal: reddit_keymap
264 define_page_mode("reddit-mode",
265     build_url_regex($domain = /([a-zA-Z0-9\-]*\.)*reddit/),
266     function enable (buffer) {
267         let (cmds = ["follow-current",
268                      "follow-current-new-buffer",
269                      "follow-current-new-buffer-background",
270                      "follow-current-new-window",
271                      "copy"]) {
272             for each (var c in cmds) {
273                 buffer.default_browser_object_classes[c] =
274                     browser_object_reddit_current;
275             }
276         }
277         buffer.content_modalities.push(reddit_modality);
278     },
279     function disable (buffer) {
280         var i = buffer.content_modalities.indexOf(reddit_modality);
281         if (i > -1)
282             buffer.content_modalities.splice(i, 1);
283     },
284     $display_name = "reddit",
285     $doc = "reddit page-mode: keyboard navigation for reddit.");
287 page_mode_activate(reddit_mode);
289 provide("reddit");