hints.js: Fixed bug with using active_index
[conkeror.git] / modules / hints.js
blobc89a26063046043ab8793eacbf84f52de56e9b1b
1 /**
2  * hints module
3  *
4  * Portions are derived from Vimperator (c) 2006-2007: Martin Stubenschrott <stubenschrott@gmx.net>
5  */
7 /* USER PREFERENCE */
8 /* FIXME: figure out why this needs to have a bunch of duplication */
9 var hint_xpath_expression =
10     "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
11     "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
12     "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
13     "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | " +
14     "//xhtml:button | //xhtml:select";
16 var active_img_hint_background_color = "#88FF00";
17 var img_hint_background_color = "yellow";
18 var active_hint_background_color = "#88FF00";
19 var hint_background_color = "yellow";
21 /**
22  * buffer is a browser_buffer
23  *
24  */
25 function hint_manager(window, xpath_expr)
27     this.window = window;
28     this.hints = [];
29     this.valid_hints = [];
30     this.xpath_expr = xpath_expr;
31     this.generate_hints();
34 hint_manager.prototype = {
35     current_hint_string : "",
36     current_hint_number : 1,
38     /**
39      * Create an initially hidden hint span element absolutely
40      * positioned over each element that matches
41      * hint_xpath_expression.  This is done recursively for all frames
42      * and iframes.  Information about the resulting hints are also
43      * stored in the hints array.
44      */
45     generate_hints : function () {
46         var topwin = this.window;
47         var top_height = topwin.innerHeight;
48         var top_width = topwin.innerWidth;
49         var hints = this.hints;
50         var xpath_expr = this.xpath_expr;
51         function helper(window, offsetX, offsetY) {
52             var win_height = window.height;
53             var win_width = window.width;
55             // Bounds
56             var minX = offsetX < 0 ? -offsetX : 0;
57             var minY = offsetY < 0 ? -offsetY : 0;
58             var maxX = offsetX + win_width > top_width ? top_width - offsetX : top_width;
59             var maxY = offsetY + win_height > top_height ? top_height - offsetY : top_height;
61             var scrollX = window.scrollX;
62             var scrollY = window.scrollY;
64             var doc = window.document;
65             var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
66                                    window.XPathResult.UNORDERED_NODE_ITERATOR_TYPE,
67                                    null /* existing results */);
68             
69             var base_node = doc.createElementNS(XHTML_NS, "span");
70             base_node.className = "__conkeror_hint";
72             var fragment = doc.createDocumentFragment();
73             var rect, elem, text, node;
74             while ((elem = res.iterateNext()) != null)
75             {
76                 rect = elem.getBoundingClientRect();
77                 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
78                     continue;
79                 rect = elem.getClientRects()[0];
80                 if (!rect)
81                     continue;
82                 var tagname = elem.localName;
83                 if (tagname == "INPUT" || tagname == "TEXTAREA")
84                     text = elem.value.toLowerCase();
85                 else if (tagname == "SELECT") {
86                     if (elem.selectedIndex >= 0)
87                         text = elem.item(elem.selectedIndex).text.toLowerCase();
88                     else
89                         text = "";
90                 } else if (tagname == "FRAME") {
91                     text = elem.name ? elem.name : "";
92                 } else
93                     text = elem.textContent.toLowerCase();
95                 node = base_node.cloneNode(true);
96                 node.style.left = (rect.left + scrollX) + "px";
97                 node.style.top = (rect.top + scrollY) + "px";
98                 fragment.appendChild(node);
100                 hints.push({text: text,
101                             elem: elem,
102                             hint: node,
103                             img_hint: null,
104                             saved_color: elem.style.color,
105                             saved_bgcolor: elem.style.backgroundColor,
106                             visible : false});
107             }
108             doc.documentElement.appendChild(fragment);
110             /* Recurse into any IFRAME or FRAME elements */
111             var frametag = "FRAME";
112             while (true) {
113                 var frames = doc.getElementsByTagName(frametag);
114                 for (var i = 0; i < frames.length; ++i)
115                 {
116                     elem = frames[i];
117                     rect = elem.getBoundingClientRect();
118                     if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
119                         continue;
120                     helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
121                 }
122                 if (frametag == "FRAME") frametag = "IFRAME"; else break;
123             }
124         }
125         helper(topwin, 0, 0);
126     },
128     /* Updates valid_hints and also re-numbers and re-displays all hints. */
129     update_valid_hints : function () {
130         this.valid_hints = [];
131         var active_number = this.current_hint_number;
132         
133         var tokens = this.current_hint_string.split(" ");
134         var rect, h, text, img_hint, doc, scrollX, scrollY;
135         var hints = this.hints;
137     outer:
138         for (var i = 0; i < hints.length; ++i)
139         {
140             h = hints[i];
141             text = h.text;
142             for (var j = 0; j < tokens.length; ++j)
143             {
144                 if (text.indexOf(tokens[j]) == -1)
145                 {
146                     if (h.visible)
147                     {
148                         h.visible = false;
149                         h.hint.style.display = "none";
150                         if (h.img_hint)
151                             h.img_hint.style.display = "none";
152                         h.elem.style.backgroundColor = h.saved_bgcolor;
153                         h.elem.style.color = h.saved_color;
154                     }
155                     continue outer;
156                 }
157             }
158             h.visible = true;
160             if (text.length == 0 && h.elem.firstChild && h.elem.firstChild.localName == "IMG")
161             {
162                 if (!h.img_hint)
163                 {
164                     rect = h.elem.firstChild.getBoundingClientRect();
165                     if (!rect)
166                         continue;
167                     doc = h.elem.ownerDocument;
168                     scrollX = doc.defaultView.scrollX;
169                     scrollY = doc.defaultView.scrollY;
170                     img_hint = doc.createElementNS(XHTML_NS, "span");
171                     img_hint.className = "__conkeror_img_hint";
172                     img_hint.style.left = (rect.left + scrollX) + "px";
173                     img_hint.style.top = (rect.top + scrollY) + "px";
174                     img_hint.style.width = (rect.right - rect.left) + "px";
175                     img_hint.style.height = (rect.bottom - rect.top) + "px";
176                     h.img_hint = img_hint;
177                     doc.documentElement.appendChild(img_hint);
178                 }
179                 h.img_hint.style.backgroundColor = (active_number == i) ?
180                     active_img_hint_background_color : img_hint_background_color;
181                 h.img_hint.style.display = "inline";
182             }
184             var cur_number = this.valid_hints.length + 1;
186             if (!h.img_hint)
187                 h.elem.style.backgroundColor = (active_number == cur_number) ?
188                     active_hint_background_color : hint_background_color;
189             h.elem.style.color = "black";
190             var label = "" + cur_number;
191             if (h.elem.localName == "FRAME") {
192                 label +=  " " + text;
193             }
194             h.hint.textContent = label;
195             h.hint.style.display = "inline";
196             this.valid_hints.push(h);
197         }
198     },
200     select_hint : function (index) {
201         var old_index = this.current_hint_number;
202         if (index == old_index)
203             return;
204         var vh = this.valid_hints;
205         if (old_index >= 1 && old_index <= vh.length)
206         {
207             var h = vh[old_index - 1];
208             h.elem.style.backgroundColor = hint_background_color;
209             if (h.img_hint)
210                 h.img_hint.style.backgroundColor = img_hint_background_color;
211         }
212         this.current_hint_number = index;
213         if (index >= 1 && index <= vh.length)
214         {
215             var h = vh[index - 1];
216             h.elem.style.backgroundColor = active_hint_background_color;
217             if (h.img_hint)
218                 h.img_hint.style.backgroundColor = active_img_hint_background_color;
219         }
220     },
222     hide_hints : function () {
223         for (var i = 0; i < this.hints.length; ++i)
224         {
225             var h = this.hints[i];
226             if (h.visible) {
227                 h.visible = false;
228                 h.elem.style.color = h.saved_color;
229                 h.elem.style.backgroundColor = h.saved_bgcolor;
230                 if (h.img_hint)
231                     h.img_hint.style.display = "none";
232                 h.hint.style.display = "none";
233             }
234         }
235     },
237     remove : function () {
238         for (var i = 0; i < this.hints.length; ++i)
239         {
240             var h = this.hints[i];
241             if (h.visible) {
242                 h.elem.style.color = h.saved_color;
243                 h.elem.style.backgroundColor = h.saved_bgcolor;
244             }
245             if (h.img_hint)
246                 h.img_hint.parentNode.removeChild(h.img_hint);
247             h.hint.parentNode.removeChild(h.hint);
248         }
249         this.hints = [];
250         this.valid_hints = [];
251     }
254 var hint_keymap = null;
256 function initialize_hint_keymap()
258     hint_keymap = new keymap();
259     define_key(hint_keymap, kbd(match_any_unmodified_key), "hints-handle-character");
260     define_key(hint_keymap, "back_space", "hints-backspace");
261     define_key(hint_keymap, "tab", "hints-next");
262     define_key(hint_keymap, "right", "hints-next");
263     define_key(hint_keymap, "down", "hints-next");
264     define_key(hint_keymap, "S-tab", "hints-previous");
265     define_key(hint_keymap, "left", "hints-previous");
266     define_key(hint_keymap, "up", "hints-previous");
267     define_key(hint_keymap, "escape", "hints-abort");
268     define_key(hint_keymap, "C-g", "hints-abort");
269     define_key(hint_keymap, "return", "hints-exit");
271     // FIXME: this should probably be some better more general
272     // property, i.e. catch_all or something.
273     define_key(hint_keymap, kbd(match_any_key), null);
275 initialize_hint_keymap();
278  * keyword arguments:
280  * $prompt
281  * $callback
282  * $abort_callback
283  */
284 define_keywords("$keymap", "$auto", "$callback", "$abort_callback", "$hint_xpath_expression");
285 function hints_minibuffer_state()
287     keywords(arguments, $keymap = hint_keymap, $hint_xpath_expression = hint_xpath_expression, $auto);
288     basic_minibuffer_state.call(this, $prompt = arguments.$prompt);
289     this.keymap = arguments.$keymap;
290     this.callback = arguments.$callback;
291     this.abort_callback = arguments.$abort_callback;
292     this.auto_exit = arguments.$auto ? true : false;
293     this.xpath_expr = arguments.$hint_xpath_expression;
294     this.auto_exit_timer_ID = null;
296 hints_minibuffer_state.prototype = {
297     __proto__: basic_minibuffer_state.prototype,
298     manager : null,
299     typed_string : "",
300     typed_number : "",
301     load : function (frame) {
302         if (!this.manager)
303             this.manager = new hint_manager(frame.buffers.current.content_window, this.xpath_expr);
304         this.manager.update_valid_hints();
305     },
306     unload : function (frame) {
307         this.manager.hide_hints();
308     },
309     destroy : function () {
310         this.manager.remove();
311     },
312     update_minibuffer : function (frame) {
313         var str = this.typed_string;
314         if (this.typed_number.length > 0) {
315             str += " #" + this.typed_number;
316         }
317         frame.minibuffer._input_text = str;
318         frame.minibuffer._set_selection();
319     }
322 /* USER PREFERENCE */
323 var hints_auto_exit_delay = 800;
325 function hints_handle_character(frame, s, e) {
326     /* Check for numbers */
327     var ch = String.fromCharCode(e.charCode);
328     var auto_exit = false;
329     /* TODO: implement number escaping */
330     if (e.charCode >= 48 && e.charCode <= 57) {
331         // Number entered
332         s.typed_number += ch;
333         s.manager.select_hint(parseInt(s.typed_number));
334         var num = s.manager.current_hint_number;
335         if (s.auto_exit && num > 0 && num <= s.manager.valid_hints.length
336             && num * 10 > s.manager.valid_hints.length)
337             auto_exit = true;
338     } else {
339         s.typed_number = "";
340         s.typed_string += ch;
341         s.manager.current_hint_string = s.typed_string;
342         s.manager_current_hint_number = 1;
343         s.manager.update_valid_hints();
344         if (s.auto_exit && s.manager.valid_hints.length == 1)
345             auto_exit = true;
346     }
347     if (auto_exit) {
348         if (this.auto_exit_timer_ID) {
349             frame.clearTimeout(this.auto_exit_timer_ID);
350         }
351         this.auto_exit_timer_ID = frame.setTimeout(function() { hints_exit(frame, s); },
352                                                    hints_auto_exit_delay);
353     }
354     s.update_minibuffer(frame);
356 interactive("hints-handle-character", hints_handle_character,
357             I.current_frame, I.minibuffer_state(hints_minibuffer_state), I.e);
359 function hints_backspace(frame, s) {
360     if (s.typed_number.length > 0) {
361         s.typed_number = s.typed_number.substring(0, s.typed_number.length - 1);
362         var num = s.typed_number.length > 0 ? parseInt(s.typed_number) : 1;
363         s.manager.select_hint(num);
364     } else if (s.typed_string.length > 0) {
365         s.typed_string = s.typed_string.substring(0, s.typed_string.length - 1);
366         s.manager.current_hint_string = s.typed_string;
367         s.manager_current_hint_number = 1;
368         s.manager.update_valid_hints();
369     }
370     s.update_minibuffer(frame);
372 interactive("hints-backspace", hints_backspace,
373             I.current_frame, I.minibuffer_state(hints_minibuffer_state));
375 function hints_next(frame, s, count) {
376     s.typed_number = "";
377     var cur = s.manager.current_hint_number - 1;
378     var vh = s.manager.valid_hints;
379     if (vh.length > 0) {
380         cur = (cur + count) % vh.length;
381         if (cur < 0)
382             cur += vh.length;
383         s.manager.select_hint(cur + 1);
384     }
385     s.update_minibuffer(frame);
387 interactive("hints-next", hints_next,
388             I.current_frame, I.minibuffer_state(hints_minibuffer_state), I.p);
390 interactive("hints-previous", hints_next,
391             I.current_frame, I.minibuffer_state(hints_minibuffer_state),
392             I.bind(function (x) {return -x;}, I.p));
394 function hints_abort(frame, s) {
395     if (this.auto_exit_timer_ID) {
396         frame.clearTimeout(this.auto_exit_timer_ID);
397         this.auto_exit_timer_ID = null;
398     }
399     frame.minibuffer.pop_state();
400     if (s.abort_callback)
401         s.abort_callback();
404 interactive("hints-abort", hints_abort,
405             I.current_frame, I.minibuffer_state(hints_minibuffer_state));
407 function hints_exit(frame, s)
409     if (this.auto_exit_timer_ID) {
410         frame.clearTimeout(this.auto_exit_timer_ID);
411         this.auto_exit_timer_ID = null;
412     }
413     var cur = s.manager.current_hint_number - 1;
414     if (cur >= 0 && cur < s.manager.valid_hints.length)
415     {
416         var elem = s.manager.valid_hints[cur].elem;
417         frame.minibuffer.pop_state();
418         if (s.callback)
419             s.callback(elem);
420     }
423 interactive("hints-exit", hints_exit,
424             I.current_frame, I.minibuffer_state(hints_minibuffer_state));
426 I.hinted_element = interactive_method(
427     $doc = "DOM element chosen using the hints system",
428     $async = function (ctx, cont) {
429         keywords(arguments);
430         var s = new hints_minibuffer_state(forward_keywords(arguments), $callback = cont);
431         ctx.frame.minibuffer.push_state(s);
432     });
434 function element_focus(buffer, elem)
436     var elemTagName = elem.localName;
437     if (elemTagName == "FRAME" || elemTagName == "IFRAME")
438     {
439         elem.contentWindow.focus();
440         return false;
441     }
443     elem.focus();
445     var doc = elem.ownerDocument;
447     var evt = doc.createEvent("MouseEvents");
448     var x = 0;
449     var y = 0;
450     // for imagemap
451     if (elemTagName == "area")
452     {
453         var coords = elem.getAttribute("coords").split(",");
454         x = Number(coords[0]);
455         y = Number(coords[1]);
456     }
458     var doc = elem.ownerDocument;
460     evt.initMouseEvent("mouseover", true, true, doc.defaultView, 1, x, y, 0, 0, 0, 0, 0, 0, 0, null);
461     elem.dispatchEvent(evt);
463 interactive("hinted-focus-element", element_focus,
464             I.current_buffer, I.hinted_element($prompt = "Focus:"));
466 function element_follow(buffer, elem)
468     var elemTagName = elem.localName;
469     elem.focus();
470     if (elemTagName == "FRAME" || elemTagName == "IFRAME")
471         return;
473     var x = 1, y = 1;
474     // for imagemap
475     if (elemTagName == "area")
476     {
477         var coords = elem.getAttribute("coords").split(",");
478         x = Number(coords[0]) + 1;
479         y = Number(coords[1]) + 1;
480     }
481     
482     var doc = elem.ownerDocument;
483     var view = doc.defaultView;
485     var evt = doc.createEvent("MouseEvents");
486     /* FIXME: maybe use modifiers to indicate new tab/new window etc. behavior */
487     evt.initMouseEvent("mousedown", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
488                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
489     elem.dispatchEvent(evt);
491     evt.initMouseEvent("click", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
492                        /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
493     elem.dispatchEvent(evt);
496 interactive("hinted-follow-element", element_follow,
497             I.current_buffer, I.hinted_element($prompt = "Follow:"));
501 interactive("hinted-focus-frame", element_focus,
502             I.current_buffer,
503             I.hinted_element(
504                 $prompt = "Frame:",
505                 $hint_xpath_expression = "//xhtml:frame | //xhtml:iframe | //iframe | //frame"
506                 ));