Change all occurrences of del.icio.us to delicious.com
[conkeror.git] / modules / hints.js
blobb3a5f9c265520ba767d29faecf0435a2a2090611
1 /**
2  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3  * (C) Copyright 2009 John J. Foerch
4  *
5  * Portions of this file are derived from Vimperator,
6  * (C) Copyright 2006-2007 Martin Stubenschrott.
7  *
8  * Use, modification, and distribution are subject to the terms specified in the
9  * COPYING file.
10 **/
12 define_variable("active_img_hint_background_color", "#88FF00",
13     "Color for the active image hint background.");
15 define_variable("img_hint_background_color", "yellow",
16     "Color for inactive image hint backgrounds.");
18 define_variable("active_hint_background_color", "#88FF00",
19     "Color for the active hint background.");
21 define_variable("hint_background_color", "yellow",
22     "Color for the inactive hint.");
25 /**
26  * Register hints style sheet
27  */
28 const hints_stylesheet = "chrome://conkeror-gui/content/hints.css";
29 register_user_stylesheet(hints_stylesheet);
32 function hints_simple_text_match (text, pattern) {
33     var pos = text.indexOf(pattern);
34     if (pos == -1)
35         return false;
36     return [pos, pos + pattern.length];
39 define_variable('hints_text_match', hints_simple_text_match,
40     "A function which takes a string and a pattern (another string) "+
41     "and returns an array of [start, end] indices if the pattern was "+
42     "found in the string, or false if it was not.");
45 /**
46  *   In the hints interaction, a node can be selected either by typing
47  * the number of its associated hint, or by typing substrings of the
48  * text content of the node.  In the case of selecting by text
49  * content, multiple substrings can be given by separating them with
50  * spaces.
51  */
52 function hint_manager (window, xpath_expr, focused_frame, focused_element) {
53     this.window = window;
54     this.hints = [];
55     this.valid_hints = [];
56     this.xpath_expr = xpath_expr;
57     this.focused_frame = focused_frame;
58     this.focused_element = focused_element;
59     this.last_selected_hint = null;
61     // Generate
62     this.generate_hints();
65 hint_manager.prototype = {
66     current_hint_string: "",
67     current_hint_number: -1,
69     /**
70      * Create an initially hidden hint span element absolutely
71      * positioned over each element that matches
72      * hint_xpath_expression.  This is done recursively for all frames
73      * and iframes.  Information about the resulting hints are also
74      * stored in the hints array.
75      */
76     generate_hints: function () {
77         var topwin = this.window;
78         var top_height = topwin.innerHeight;
79         var top_width = topwin.innerWidth;
80         var hints = this.hints;
81         var xpath_expr = this.xpath_expr;
82         var focused_frame_hint = null, focused_element_hint = null;
83         var focused_frame = this.focused_frame;
84         var focused_element = this.focused_element;
86         function helper (window, offsetX, offsetY) {
87             var win_height = window.height;
88             var win_width = window.width;
90             // Bounds
91             var minX = offsetX < 0 ? -offsetX : 0;
92             var minY = offsetY < 0 ? -offsetY : 0;
93             var maxX = offsetX + win_width > top_width ? top_width - offsetX : top_width;
94             var maxY = offsetY + win_height > top_height ? top_height - offsetY : top_height;
96             var scrollX = window.scrollX;
97             var scrollY = window.scrollY;
99             var doc = window.document;
100             var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
101                                    Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE,
102                                    null /* existing results */);
104             var base_node = doc.createElementNS(XHTML_NS, "span");
105             base_node.className = "__conkeror_hint";
107             var fragment = doc.createDocumentFragment();
108             var rect, elem, text, node, show_text;
109             while (true) {
110                 try {
111                     elem = res.iterateNext();
112                     if (!elem)
113                         break;
114                 } catch (e) {
115                     break; // Iterator may have been invalidated by page load activity
116                 }
117                 rect = elem.getBoundingClientRect();
118                 if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
119                     rect = { top: rect.top,
120                              left: rect.left,
121                              bottom: rect.bottom,
122                              right: rect.right };
123                     var coords = elem.getAttribute("coords")
124                         .match(/^(-?\d+)\D+(-?\d+)/);
125                     if (coords.length == 3) {
126                         rect.left += parseInt(coords[1]);
127                         rect.top += parseInt(coords[2]);
128                     }
129                 }
130                 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
131                     continue;
132                 let style = topwin.getComputedStyle(elem, "");
133                 if (style.display == "none" || style.visibility == "hidden")
134                     continue;
135                 if (! (elem instanceof Ci.nsIDOMHTMLAreaElement))
136                     rect = elem.getClientRects()[0];
137                 if (!rect)
138                     continue;
139                 show_text = false;
140                 if (elem instanceof Ci.nsIDOMHTMLInputElement || elem instanceof Ci.nsIDOMHTMLTextAreaElement)
141                     text = elem.value;
142                 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
143                     if (elem.selectedIndex >= 0)
144                         text = elem.item(elem.selectedIndex).text;
145                     else
146                         text = "";
147                 } else if (elem instanceof Ci.nsIDOMHTMLFrameElement) {
148                     text = elem.name ? elem.name : "";
149                 } else if (/^\s*$/.test(elem.textContent) &&
150                            elem.childNodes.length == 1 &&
151                            elem.childNodes.item(0) instanceof Ci.nsIDOMHTMLImageElement) {
152                     text = elem.childNodes.item(0).alt;
153                     show_text = true;
154                 } else
155                     text = elem.textContent;
156                 text = text.toLowerCase();
158                 node = base_node.cloneNode(true);
159                 node.style.left = (rect.left + scrollX) + "px";
160                 node.style.top = (rect.top + scrollY) + "px";
161                 fragment.appendChild(node);
163                 let hint = { text: text,
164                              elem: elem,
165                              hint: node,
166                              img_hint: null,
167                              visible: false,
168                              show_text: show_text };
169                 if (elem.style) {
170                     hint.saved_color = elem.style.color;
171                     hint.saved_bgcolor = elem.style.backgroundColor;
172                 }
173                 hints.push(hint);
175                 if (elem == focused_element)
176                     focused_element_hint = hint;
177                 else if ((elem instanceof Ci.nsIDOMHTMLFrameElement ||
178                           elem instanceof Ci.nsIDOMHTMLIFrameElement) &&
179                          elem.contentWindow == focused_frame)
180                     focused_frame_hint = hint;
181             }
182             doc.documentElement.appendChild(fragment);
184             /* Recurse into any IFRAME or FRAME elements */
185             var frametag = "frame";
186             while (true) {
187                 var frames = doc.getElementsByTagName(frametag);
188                 for (var i = 0, nframes = frames.length; i < nframes; ++i) {
189                     elem = frames[i];
190                     rect = elem.getBoundingClientRect();
191                     if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
192                         continue;
193                     helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
194                 }
195                 if (frametag == "frame") frametag = "iframe"; else break;
196             }
197         }
198         helper(topwin, 0, 0);
199         this.last_selected_hint = focused_element_hint || focused_frame_hint;
200     },
202     /* Updates valid_hints and also re-numbers and re-displays all hints. */
203     update_valid_hints: function () {
204         this.valid_hints = [];
205         var active_number = this.current_hint_number;
207         var tokens = this.current_hint_string.split(" ");
208         var rect, h, text, img_hint, doc, scrollX, scrollY;
209         var hints = this.hints;
211     outer:
212         for (var i = 0, nhints = hints.length; i < nhints; ++i) {
213             h = hints[i];
214             text = h.text;
215             for (var j = 0, ntokens = tokens.length; j < ntokens; ++j) {
216                 if (! hints_text_match(text, tokens[j])) {
217                     if (h.visible) {
218                         h.visible = false;
219                         h.hint.style.display = "none";
220                         if (h.img_hint)
221                             h.img_hint.style.display = "none";
222                         if (h.saved_color != null) {
223                             h.elem.style.backgroundColor = h.saved_bgcolor;
224                             h.elem.style.color = h.saved_color;
225                         }
226                     }
227                     continue outer;
228                 }
229             }
231             var cur_number = this.valid_hints.length + 1;
232             h.visible = true;
234             if (h == this.last_selected_hint && active_number == -1)
235                 this.current_hint_number = active_number = cur_number;
237             var img_elem = null;
239             if (text.length == 0 && h.elem.firstChild &&
240                 h.elem.firstChild instanceof Ci.nsIDOMHTMLImageElement)
241                 img_elem = h.elem.firstChild;
242             else if (h.elem instanceof Ci.nsIDOMHTMLImageElement)
243                 img_elem = h.elem;
245             if (img_elem) {
246                 if (!h.img_hint) {
247                     rect = img_elem.getBoundingClientRect();
248                     if (rect) {
249                         doc = h.elem.ownerDocument;
250                         scrollX = doc.defaultView.scrollX;
251                         scrollY = doc.defaultView.scrollY;
252                         img_hint = doc.createElementNS(XHTML_NS, "span");
253                         img_hint.className = "__conkeror_img_hint";
254                         img_hint.style.left = (rect.left + scrollX) + "px";
255                         img_hint.style.top = (rect.top + scrollY) + "px";
256                         img_hint.style.width = (rect.right - rect.left) + "px";
257                         img_hint.style.height = (rect.bottom - rect.top) + "px";
258                         h.img_hint = img_hint;
259                         doc.documentElement.appendChild(img_hint);
260                     } else
261                         img_elem = null;
262                 }
263                 if (img_elem) {
264                     var bgcolor = (active_number == cur_number) ?
265                         active_img_hint_background_color : img_hint_background_color;
266                     h.img_hint.style.backgroundColor = bgcolor;
267                     h.img_hint.style.display = "inline";
268                 }
269             }
271             if (!h.img_hint && h.elem.style)
272                 h.elem.style.backgroundColor = (active_number == cur_number) ?
273                     active_hint_background_color : hint_background_color;
275             if (h.elem.style)
276                 h.elem.style.color = "black";
278             var label = "" + cur_number;
279             if (h.elem instanceof Ci.nsIDOMHTMLFrameElement) {
280                 label +=  " " + text;
281             } else if (h.show_text && !/^\s*$/.test(text)) {
282                 let substrs = [[0,4]];
283                 for (j = 0; j < ntokens; ++j) {
284                     let m = hints_text_match(text, tokens[j]);
285                     if (m == false) continue;
286                     splice_range(substrs, m[0], m[1] + 2);
287                 }
288                 label += " " + substrs.map(function (x) {
289                     return text.substring(x[0],Math.min(x[1], text.length));
290                 }).join("..") + "..";
291             }
292             h.hint.textContent = label;
293             h.hint.style.display = "inline";
294             this.valid_hints.push(h);
295         }
297         if (active_number == -1)
298             this.select_hint(1);
299     },
301     select_hint: function (index) {
302         var old_index = this.current_hint_number;
303         if (index == old_index)
304             return;
305         var vh = this.valid_hints;
306         if (old_index >= 1 && old_index <= vh.length) {
307             var h = vh[old_index - 1];
308             if (h.img_hint)
309                 h.img_hint.style.backgroundColor = img_hint_background_color;
310             if (h.elem.style)
311                 h.elem.style.backgroundColor = hint_background_color;
312         }
313         this.current_hint_number = index;
314         if (index >= 1 && index <= vh.length) {
315             var h = vh[index - 1];
316             if (h.img_hint)
317                 h.img_hint.style.backgroundColor = active_img_hint_background_color;
318             if (h.elem.style)
319                 h.elem.style.backgroundColor = active_hint_background_color;
320             this.last_selected_hint = h;
321         }
322     },
324     hide_hints: function () {
325         for (var i = 0, nhints = this.hints.length; i < nhints; ++i) {
326             var h = this.hints[i];
327             if (h.visible) {
328                 h.visible = false;
329                 if (h.saved_color != null) {
330                     h.elem.style.color = h.saved_color;
331                     h.elem.style.backgroundColor = h.saved_bgcolor;
332                 }
333                 if (h.img_hint)
334                     h.img_hint.style.display = "none";
335                 h.hint.style.display = "none";
336             }
337         }
338     },
340     remove: function () {
341         for (var i = 0, nhints = this.hints.length; i < nhints; ++i) {
342             var h = this.hints[i];
343             if (h.visible && h.saved_color != null) {
344                 h.elem.style.color = h.saved_color;
345                 h.elem.style.backgroundColor = h.saved_bgcolor;
346             }
347             if (h.img_hint)
348                 h.img_hint.parentNode.removeChild(h.img_hint);
349             h.hint.parentNode.removeChild(h.hint);
350         }
351         this.hints = [];
352         this.valid_hints = [];
353     }
356 /* Show panel with currently selected URL. */
357 function hints_url_panel (hints, window) {
358     var g = new dom_generator(window.document, XUL_NS);
360     var p = g.element("hbox", "class", "panel url", "flex", "0");
361     g.element("label", p, "value", "URL:", "class", "url-panel-label");
362     var url_value = g.element("label", p, "class", "url-panel-value",
363                               "crop", "end", "flex", "1");
364     window.minibuffer.insert_before(p);
366     p.update = function () {
367         url_value.value = "";
368         if (hints.manager && hints.manager.last_selected_hint) {
369             var spec;
370             try {
371                 spec = load_spec(hints.manager.last_selected_hint.elem);
372             } catch (e) {}
373             if (spec) {
374                 var uri = load_spec_uri_string(spec);
375                 if (uri) url_value.value = uri;
376             }
377         }
378     };
380     p.destroy = function () {
381         this.parentNode.removeChild(this);
382     };
384     return p;
387 define_variable("hints_display_url_panel", false,
388     "When selecting a hint, the URL can be displayed in a panel above "+
389     "the minibuffer.  This is useful for confirming that the correct "+
390     "link is selected and that the URL is not evil.  This option is "+
391     "most useful when hints_auto_exit_delay is long or disabled.");
394  * keyword arguments:
396  * $prompt
397  * $callback
398  * $abort_callback
399  */
400 define_keywords("$keymap", "$auto", "$hint_xpath_expression", "$multiple");
401 function hints_minibuffer_state (window, continuation, buffer) {
402     keywords(arguments, $keymap = hint_keymap, $auto);
403     basic_minibuffer_state.call(this, window, $prompt = arguments.$prompt);
404     if (hints_display_url_panel)
405         this.url_panel = hints_url_panel(this, buffer.window);
406     this.original_prompt = arguments.$prompt;
407     this.continuation = continuation;
408     this.keymap = arguments.$keymap;
409     this.auto_exit = arguments.$auto ? true : false;
410     this.xpath_expr = arguments.$hint_xpath_expression;
411     this.auto_exit_timer_ID = null;
412     this.multiple = arguments.$multiple;
413     this.focused_element = buffer.focused_element;
414     this.focused_frame = buffer.focused_frame;
416 hints_minibuffer_state.prototype = {
417     __proto__: basic_minibuffer_state.prototype,
418     manager: null,
419     typed_string: "",
420     typed_number: "",
421     load: function (window) {
422         basic_minibuffer_state.prototype.load.call(this, window);
423         if (!this.manager) {
424             var buf = window.buffers.current;
425             this.manager = new hint_manager(buf.top_frame, this.xpath_expr,
426                                             this.focused_frame, this.focused_element);
427         }
428         this.manager.update_valid_hints();
429         this.window = window;
430         if (this.url_panel)
431             this.url_panel.update();
432     },
433     clear_auto_exit_timer: function () {
434         if (this.auto_exit_timer_ID != null) {
435             this.window.clearTimeout(this.auto_exit_timer_ID);
436             this.auto_exit_timer_ID = null;
437         }
438     },
439     unload: function (window) {
440         this.clear_auto_exit_timer();
441         this.manager.hide_hints();
442         delete this.window;
443         basic_minibuffer_state.prototype.unload.call(this, window);
444     },
445     destroy: function (window) {
446         this.clear_auto_exit_timer();
447         this.manager.remove();
448         if (this.url_panel)
449             this.url_panel.destroy();
450         basic_minibuffer_state.prototype.destroy.call(this, window);
451     },
452     update_minibuffer: function (m) {
453         if (this.typed_number.length > 0)
454             m.prompt = this.original_prompt + " #" + this.typed_number;
455         else
456             m.prompt = this.original_prompt;
457         if (this.url_panel)
458             this.url_panel.update();
459     },
461     handle_auto_exit: function (m, ambiguous) {
462         let window = m.window;
463         var num = this.manager.current_hint_number;
464         if (!this.auto_exit)
465             return;
466         let s = this;
467         let delay = ambiguous ? hints_ambiguous_auto_exit_delay : hints_auto_exit_delay;
468         if (delay > 0)
469             this.auto_exit_timer_ID = window.setTimeout(function () { hints_exit(window, s); },
470                                                         delay);
471     },
473     ran_minibuffer_command: function (m) {
474         this.handle_input(m);
475     },
477     handle_input: function (m) {
478         m._set_selection();
479         this.clear_auto_exit_timer();
480         this.typed_number = "";
481         this.typed_string = m._input_text;
482         this.manager.current_hint_string = this.typed_string;
483         this.manager.current_hint_number = -1;
484         this.manager.update_valid_hints();
485         if (this.manager.valid_hints.length == 1)
486             this.handle_auto_exit(m, false /* unambiguous */);
487         else if (this.manager.valid_hints.length > 1)
488         this.handle_auto_exit(m, true /* ambiguous */);
489         this.update_minibuffer(m);
490     }
493 define_variable("hints_auto_exit_delay", 0,
494     "Delay (in milliseconds) after the most recent key stroke before a "+
495     "sole matching element is automatically selected.  When zero, "+
496     "automatic selection is disabled.  A value of 500 is a good "+
497     "starting point for an average-speed typist.");
499 define_variable("hints_ambiguous_auto_exit_delay", 0,
500     "Delay (in milliseconds) after the most recent key stroke before the "+
501     "first of an ambiguous match is automatically selected.  If this is "+
502     "set to 0, automatic selection in ambiguous matches is disabled.");
504 interactive("hints-handle-number", null,
505     function (I) {
506         let s = I.minibuffer.check_state(hints_minibuffer_state);
507         s.clear_auto_exit_timer();
508         var ch = String.fromCharCode(I.event.charCode);
509         var auto_exit_ambiguous = null; // null -> no auto exit; false -> not ambiguous; true -> ambiguous
510         /* TODO: implement number escaping */
511         // Number entered
512         s.typed_number += ch;
514         s.manager.select_hint(parseInt(s.typed_number));
515         var num = s.manager.current_hint_number;
516         if (num > 0 && num <= s.manager.valid_hints.length)
517             auto_exit_ambiguous = num * 10 > s.manager.valid_hints.length ? false : true;
518         else if (num == 0) {
519             if (!s.multiple) {
520                 hints_exit(I.window, s);
521                 return;
522             }
523             auto_exit_ambiguous = false;
524         }
525         if (auto_exit_ambiguous !== null)
526             s.handle_auto_exit(I.minibuffer, auto_exit_ambiguous);
527         s.update_minibuffer(I.minibuffer);
528     });
530 function hints_backspace (window, s) {
531     let m = window.minibuffer;
532     s.clear_auto_exit_timer();
533     if (s.typed_number.length > 0) {
534         s.typed_number = s.typed_number.substring(0, s.typed_number.length - 1);
535         var num = s.typed_number.length > 0 ? parseInt(s.typed_number) : 1;
536         s.manager.select_hint(num);
537     } else if (s.typed_string.length > 0) {
538         s.typed_string = s.typed_string.substring(0, s.typed_string.length - 1);
539         m._input_text = s.typed_string;
540         m._set_selection();
541         s.manager.current_hint_string = s.typed_string;
542         s.manager.current_hint_number = -1;
543         s.manager.update_valid_hints();
544     }
545     s.update_minibuffer(m);
547 interactive("hints-backspace", null,
548     function (I) {
549         hints_backspace(I.window, I.minibuffer.check_state(hints_minibuffer_state));
550     });
552 function hints_next (window, s, count) {
553     s.clear_auto_exit_timer();
554     s.typed_number = "";
555     var cur = s.manager.current_hint_number - 1;
556     var vh = s.manager.valid_hints;
557     if (vh.length > 0) {
558         cur = (cur + count) % vh.length;
559         if (cur < 0)
560             cur += vh.length;
561         s.manager.select_hint(cur + 1);
562     }
563     s.update_minibuffer(window);
565 interactive("hints-next", null,
566     function (I) {
567         hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), I.p);
568     });
570 interactive("hints-previous", null,
571     function (I) {
572         hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), -I.p);
573     });
575 function hints_exit (window, s) {
576     var cur = s.manager.current_hint_number;
577     var elem = null;
578     if (cur > 0 && cur <= s.manager.valid_hints.length) {
579         elem = s.manager.valid_hints[cur - 1].elem;
580     } else if (cur == 0) {
581         elem = window.buffers.current.top_frame;
582     }
583     if (elem !== null) {
584         var c = s.continuation;
585         delete s.continuation;
586         window.minibuffer.pop_state();
587         if (c)
588             c(elem);
589     }
592 interactive("hints-exit", null,
593     function (I) {
594         hints_exit(I.window, I.minibuffer.check_state(hints_minibuffer_state));
595     });
598 define_keywords("$buffer");
599 minibuffer.prototype.read_hinted_element = function () {
600     keywords(arguments);
601     var buf = arguments.$buffer;
602     var s = new hints_minibuffer_state(this.window, (yield CONTINUATION), buf, forward_keywords(arguments));
603     this.push_state(s);
604     var result = yield SUSPEND;
605     yield co_return(result);