hints.js: Fix minor typo
[conkeror.git] / modules / hints.js
blob4a323ad4e16683a62bb5704b73ced639ce51f854
1 /**
2  * hints module
3  *
4  * Portions are derived from Vimperator (c) 2006-2007: Martin Stubenschrott <stubenschrott@gmx.net>
5  */
7 define_variable("active_img_hint_background_color", "#88FF00", "Color for the active image hint background.");
8 define_variable("img_hint_background_color", "yellow", "Color for inactive image hint backgrounds.");
9 define_variable("active_hint_background_color", "#88FF00", "Color for the active hint background.");
10 define_variable("hint_background_color", "yellow", "Color for the inactive hint.");
12 /**
13  * Register hints style sheet
14  */
15 const hints_stylesheet = "chrome://conkeror/content/hints.css";
16 register_user_stylesheet(hints_stylesheet);
18 /**
19  * buffer is a content_buffer
20  *
21  */
22 function hint_manager(window, xpath_expr, focused_frame, focused_element)
24     this.window = window;
25     this.hints = [];
26     this.valid_hints = [];
27     this.xpath_expr = xpath_expr;
28     this.focused_frame = focused_frame;
29     this.focused_element = focused_element;
30     this.last_selected_hint = null;
32     // Generate
33     this.generate_hints();
36 hint_manager.prototype = {
37     current_hint_string : "",
38     current_hint_number : -1,
40     /**
41      * Create an initially hidden hint span element absolutely
42      * positioned over each element that matches
43      * hint_xpath_expression.  This is done recursively for all frames
44      * and iframes.  Information about the resulting hints are also
45      * stored in the hints array.
46      */
47     generate_hints : function () {
48         var topwin = this.window;
49         var top_height = topwin.innerHeight;
50         var top_width = topwin.innerWidth;
51         var hints = this.hints;
52         var xpath_expr = this.xpath_expr;
53         var focused_frame_hint = null, focused_element_hint = null;
54         var focused_frame = this.focused_frame;
55         var focused_element = this.focused_element;
56         function helper(window, offsetX, offsetY) {
57             var win_height = window.height;
58             var win_width = window.width;
60             // Bounds
61             var minX = offsetX < 0 ? -offsetX : 0;
62             var minY = offsetY < 0 ? -offsetY : 0;
63             var maxX = offsetX + win_width > top_width ? top_width - offsetX : top_width;
64             var maxY = offsetY + win_height > top_height ? top_height - offsetY : top_height;
66             var scrollX = window.scrollX;
67             var scrollY = window.scrollY;
69             var doc = window.document;
70             var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
71                                    Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE,
72                                    null /* existing results */);
74             var base_node = doc.createElementNS(XHTML_NS, "span");
75             base_node.className = "__conkeror_hint";
77             var fragment = doc.createDocumentFragment();
78             var rect, elem, text, node;
79             while ((elem = res.iterateNext()) != null)
80             {
81                 rect = elem.getBoundingClientRect();
82                 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
83                     continue;
84                 rect = elem.getClientRects()[0];
85                 if (!rect)
86                     continue;
87                 if (elem instanceof Ci.nsIDOMHTMLInputElement || elem instanceof Ci.nsIDOMHTMLTextAreaElement)
88                     text = elem.value.toLowerCase();
89                 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
90                     if (elem.selectedIndex >= 0)
91                         text = elem.item(elem.selectedIndex).text.toLowerCase();
92                     else
93                         text = "";
94                 } else if (elem instanceof Ci.nsIDOMHTMLFrameElement) {
95                     text = elem.name ? elem.name : "";
96                 } else
97                     text = elem.textContent.toLowerCase();
99                 node = base_node.cloneNode(true);
100                 node.style.left = (rect.left + scrollX) + "px";
101                 node.style.top = (rect.top + scrollY) + "px";
102                 fragment.appendChild(node);
104                 var hint = {text: text,
105                             elem: elem,
106                             hint: node,
107                             img_hint: null,
108                             visible : false};
109                 if (elem.style) {
110                     hint.saved_color = elem.style.color;
111                     hint.saved_bgcolor = elem.style.backgroundColor;
112                 }
113                 hints.push(hint);
115                 if (elem == focused_element)
116                     focused_element_hint = hint;
117                 else if ((elem instanceof Ci.nsIDOMHTMLFrameElement ||
118                           elem instanceof Ci.nsIDOMHTMLIFrameElement) &&
119                          elem.contentWindow == focused_frame)
120                     focused_frame_hint = hint;
121             }
122             doc.documentElement.appendChild(fragment);
124             /* Recurse into any IFRAME or FRAME elements */
125             var frametag = "frame";
126             while (true) {
127                 var frames = doc.getElementsByTagName(frametag);
128                 for (var i = 0; i < frames.length; ++i)
129                 {
130                     elem = frames[i];
131                     rect = elem.getBoundingClientRect();
132                     if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
133                         continue;
134                     helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
135                 }
136                 if (frametag == "frame") frametag = "iframe"; else break;
137             }
138         }
139         helper(topwin, 0, 0);
140         this.last_selected_hint = focused_element_hint || focused_frame_hint;
141     },
143     /* Updates valid_hints and also re-numbers and re-displays all hints. */
144     update_valid_hints : function () {
145         this.valid_hints = [];
146         var active_number = this.current_hint_number;
148         var tokens = this.current_hint_string.split(" ");
149         var rect, h, text, img_hint, doc, scrollX, scrollY;
150         var hints = this.hints;
152     outer:
153         for (var i = 0; i < hints.length; ++i)
154         {
155             h = hints[i];
156             text = h.text;
157             for (var j = 0; j < tokens.length; ++j)
158             {
159                 if (text.indexOf(tokens[j]) == -1)
160                 {
161                     if (h.visible)
162                     {
163                         h.visible = false;
164                         h.hint.style.display = "none";
165                         if (h.img_hint)
166                             h.img_hint.style.display = "none";
167                         if (h.saved_color != null) {
168                             h.elem.style.backgroundColor = h.saved_bgcolor;
169                             h.elem.style.color = h.saved_color;
170                         }
171                     }
172                     continue outer;
173                 }
174             }
176             var cur_number = this.valid_hints.length + 1;
177             h.visible = true;
179             if (h == this.last_selected_hint && active_number == -1)
180                 this.current_hint_number = active_number = cur_number;
182             var img_elem = null;
184             if (text.length == 0 && h.elem.firstChild &&
185                 h.elem.firstChild instanceof Ci.nsIDOMHTMLImageElement)
186                 img_elem = h.elem.firstChild;
187             else if (h.elem instanceof Ci.nsIDOMHTMLImageElement)
188                 img_elem = h.elem;
190             if (img_elem)
191             {
192                 if (!h.img_hint)
193                 {
194                     rect = img_elem.getBoundingClientRect();
195                     if (rect) {
196                         doc = h.elem.ownerDocument;
197                         scrollX = doc.defaultView.scrollX;
198                         scrollY = doc.defaultView.scrollY;
199                         img_hint = doc.createElementNS(XHTML_NS, "span");
200                         img_hint.className = "__conkeror_img_hint";
201                         img_hint.style.left = (rect.left + scrollX) + "px";
202                         img_hint.style.top = (rect.top + scrollY) + "px";
203                         img_hint.style.width = (rect.right - rect.left) + "px";
204                         img_hint.style.height = (rect.bottom - rect.top) + "px";
205                         h.img_hint = img_hint;
206                         doc.documentElement.appendChild(img_hint);
207                     } else
208                         img_elem = null;
209                 }
210                 if (img_elem) {
211                     var bgcolor = (active_number == cur_number) ?
212                         active_img_hint_background_color : img_hint_background_color;
213                     h.img_hint.style.backgroundColor = bgcolor;
214                     h.img_hint.style.display = "inline";
215                 }
216             }
218             if (!h.img_hint && h.elem.style)
219                 h.elem.style.backgroundColor = (active_number == cur_number) ?
220                     active_hint_background_color : hint_background_color;
222             if (h.elem.style)
223                 h.elem.style.color = "black";
225             var label = "" + cur_number;
226             if (h.elem instanceof Ci.nsIDOMHTMLFrameElement) {
227                 label +=  " " + text;
228             }
229             h.hint.textContent = label;
230             h.hint.style.display = "inline";
231             this.valid_hints.push(h);
232         }
234         if (active_number == -1)
235             this.select_hint(1);
236     },
238     select_hint : function (index) {
239         var old_index = this.current_hint_number;
240         if (index == old_index)
241             return;
242         var vh = this.valid_hints;
243         if (old_index >= 1 && old_index <= vh.length)
244         {
245             var h = vh[old_index - 1];
246             if (h.img_hint)
247                 h.img_hint.style.backgroundColor = img_hint_background_color;
248             if (h.elem.style)
249                 h.elem.style.backgroundColor = hint_background_color;
250         }
251         this.current_hint_number = index;
252         if (index >= 1 && index <= vh.length)
253         {
254             var h = vh[index - 1];
255             if (h.img_hint)
256                 h.img_hint.style.backgroundColor = active_img_hint_background_color;
257             if (h.elem.style)
258                 h.elem.style.backgroundColor = active_hint_background_color;
259             this.last_selected_hint = h;
260         }
261     },
263     hide_hints : function () {
264         for (var i = 0; i < this.hints.length; ++i)
265         {
266             var h = this.hints[i];
267             if (h.visible) {
268                 h.visible = false;
269                 if (h.saved_color != null)
270                 {
271                     h.elem.style.color = h.saved_color;
272                     h.elem.style.backgroundColor = h.saved_bgcolor;
273                 }
274                 if (h.img_hint)
275                     h.img_hint.style.display = "none";
276                 h.hint.style.display = "none";
277             }
278         }
279     },
281     remove : function () {
282         for (var i = 0; i < this.hints.length; ++i)
283         {
284             var h = this.hints[i];
285             if (h.visible && h.saved_color != null) {
286                 h.elem.style.color = h.saved_color;
287                 h.elem.style.backgroundColor = h.saved_bgcolor;
288             }
289             if (h.img_hint)
290                 h.img_hint.parentNode.removeChild(h.img_hint);
291             h.hint.parentNode.removeChild(h.hint);
292         }
293         this.hints = [];
294         this.valid_hints = [];
295     }
299  * keyword arguments:
301  * $prompt
302  * $callback
303  * $abort_callback
304  */
305 define_keywords("$keymap", "$auto", "$hint_xpath_expression", "$multiple");
306 function hints_minibuffer_state(continuation, buffer)
308     keywords(arguments, $keymap = hint_keymap, $auto);
309     basic_minibuffer_state.call(this, $prompt = arguments.$prompt);
310     this.original_prompt = arguments.$prompt;
311     this.continuation = continuation;
312     this.keymap = arguments.$keymap;
313     this.auto_exit = arguments.$auto ? true : false;
314     this.xpath_expr = arguments.$hint_xpath_expression;
315     this.auto_exit_timer_ID = null;
316     this.multiple = arguments.$multiple;
317     this.focused_element = buffer.focused_element;
318     this.focused_frame = buffer.focused_frame;
320 hints_minibuffer_state.prototype = {
321     __proto__: basic_minibuffer_state.prototype,
322     manager : null,
323     typed_string : "",
324     typed_number : "",
325     load : function (window) {
326         if (!this.manager) {
327             var buf = window.buffers.current;
328             this.manager = new hint_manager(buf.top_frame, this.xpath_expr,
329                                             this.focused_frame, this.focused_element);
330         }
331         this.manager.update_valid_hints();
332     },
333     unload : function (window) {
334         if (this.auto_exit_timer_ID) {
335             window.clearTimeout(this.auto_exit_timer_ID);
336             this.auto_exit_timer_ID = null;
337         }
338         this.manager.hide_hints();
339     },
340     destroy : function () {
341         if (this.auto_exit_timer_ID) {
342             this.manager.window.clearTimeout(this.auto_exit_timer_ID);
343             this.auto_exit_timer_ID = null;
344         }
345         this.manager.remove();
346     },
347     update_minibuffer : function (m) {
348         if (this.typed_number.length > 0)
349             m.prompt = this.original_prompt + " #" + this.typed_number;
350         else
351             m.prompt = this.original_prompt;
352     },
354     handle_auto_exit : function (m) {
355         let window = m.window;
357         if (this.auto_exit_timer_ID)
358             window.clearTimeout(this.auto_exit_timer_ID);
359         let s = this;
360         this.auto_exit_timer_ID = window.setTimeout(function() { hints_exit(window, s); },
361                                                     hints_auto_exit_delay);
362     },
364     handle_input : function (m) {
365         m._set_selection();
367         var auto_exit = false;
368         this.typed_number = "";
369         this.typed_string = m._input_text;
370         this.manager.current_hint_string = this.typed_string;
371         this.manager.current_hint_number = -1;
372         this.manager.update_valid_hints();
373         if (this.auto_exit && this.manager.valid_hints.length == 1)
374             this.handle_auto_exit(m);
375         this.update_minibuffer(m);
376     }
379 define_variable("hints_auto_exit_delay", 500, "Delay (in milliseconds) after the most recent key stroke before a sole matching element is automatically selected.  If this is set to 0, automatic selection is disabled.");
381 interactive("hints-handle-number", function (I) {
382     let s = I.minibuffer.check_state(hints_minibuffer_state);
383     var ch = String.fromCharCode(I.event.charCode);
384     var auto_exit = false;
385     /* TODO: implement number escaping */
386     // Number entered
387     s.typed_number += ch;
389     s.manager.select_hint(parseInt(s.typed_number));
390     var num = s.manager.current_hint_number;
391     if (s.auto_exit) {
392         if (num > 0 && num <= s.manager.valid_hints.length && num * 10 > s.manager.valid_hints.length)
393             auto_exit = true;
394         if (num == 0) {
395             if (!s.multiple) {
396                 hints_exit(I.window, s);
397                 return;
398             }
399             auto_exit = true;
400         }
401     }
402     if (auto_exit)
403         s.handle_auto_exit(I.minibuffer);
404     s.update_minibuffer(I.minibuffer);
407 function hints_backspace(window, s) {
408     let m = window.minibuffer;
409     if (s.auto_exit_timer_ID) {
410         window.clearTimeout(s.auto_exit_timer_ID);
411         s.auto_exit_timer_ID = null;
412     }
413     if (s.typed_number.length > 0) {
414         s.typed_number = s.typed_number.substring(0, s.typed_number.length - 1);
415         var num = s.typed_number.length > 0 ? parseInt(s.typed_number) : 1;
416         s.manager.select_hint(num);
417     } else if (s.typed_string.length > 0) {
418         s.typed_string = s.typed_string.substring(0, s.typed_string.length - 1);
419         m._input_text = s.typed_string;
420         m._set_selection();
421         s.manager.current_hint_string = s.typed_string;
422         s.manager.current_hint_number = -1;
423         s.manager.update_valid_hints();
424     }
425     s.update_minibuffer(m);
427 interactive("hints-backspace", function (I) {
428     hints_backspace(I.window, I.minibuffer.check_state(hints_minibuffer_state));
431 function hints_next(window, s, count) {
432     if (s.auto_exit_timer_ID) {
433         window.clearTimeout(s.auto_exit_timer_ID);
434         s.auto_exit_timer_ID = null;
435     }
436     s.typed_number = "";
437     var cur = s.manager.current_hint_number - 1;
438     var vh = s.manager.valid_hints;
439     if (vh.length > 0) {
440         cur = (cur + count) % vh.length;
441         if (cur < 0)
442             cur += vh.length;
443         s.manager.select_hint(cur + 1);
444     }
445     s.update_minibuffer(window);
447 interactive("hints-next", function (I) {
448     hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), I.p);
451 interactive("hints-previous", function (I) {
452     hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), -I.p);
455 function hints_exit(window, s)
457     if (s.auto_exit_timer_ID) {
458         window.clearTimeout(s.auto_exit_timer_ID);
459         s.auto_exit_timer_ID = null;
460     }
461     var cur = s.manager.current_hint_number;
462     var elem = null;
463     if (cur > 0 && cur <= s.manager.valid_hints.length)
464         elem = s.manager.valid_hints[cur - 1].elem;
465     else if (cur == 0)
466         elem = window.buffers.current.top_frame;
467     if (elem) {
468         var c = s.continuation;
469         delete s.continuation;
470         window.minibuffer.pop_state();
471         if (c)
472             c(elem);
473     }
476 interactive("hints-exit", function (I) {
477     hints_exit(I.window, I.minibuffer.check_state(hints_minibuffer_state));
481 /* FIXME: figure out why this needs to have a bunch of duplication */
482 define_variable(
483     "hints_xpath_expressions",
484     {
485         images: {def: "//img | //xhtml:img"},
486         frames: {def: "//iframe | //frame | //xhtml:iframe | //xhtml:frame"},
487         links: {def:
488                 "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
489                 "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
490                 "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
491                 "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | " +
492                 "//xhtml:button | //xhtml:select"},
493         mathml: {def: "//m:math"}
494     },
495     "XPath expressions for each object class.");
497 minibuffer_auto_complete_preferences["media"] = true;
499 define_keywords("$buffer");
500 minibuffer.prototype.read_hinted_element = function () {
501     keywords(arguments);
502     var buf = arguments.$buffer;
503     var s = new hints_minibuffer_state((yield CONTINUATION), buf, forward_keywords(arguments));
504     this.push_state(s);
505     var result = yield SUSPEND;
506     yield co_return(result);