Delete file user_variables.js (duplicates content in utils.js)
[conkeror.git] / modules / hints.js
blob52f0ed5a4fa297e5e9c334a99fb4b0c5d287cbfd
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 function hints_register_stylesheet()
18     var uri = makeURL(hints_stylesheet);
19     var sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService);
20     sss.loadAndRegisterSheet(uri, sss.USER_SHEET);
23 function hints_unregister_stylesheet()
25     var uri = makeURL(hints_stylesheet);
26     var sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService);
27     if (sss.sheetRegistered(uri, sss.USER_SHEET))
28         ss.unregisterSheet(uri, sss.USER_SHEET);
30 hints_register_stylesheet();
32 /**
33  * buffer is a content_buffer
34  *
35  */
36 function hint_manager(window, xpath_expr, focused_frame, focused_element)
38     this.window = window;
39     this.hints = [];
40     this.valid_hints = [];
41     this.xpath_expr = xpath_expr;
42     this.focused_frame = focused_frame;
43     this.focused_element = focused_element;
44     this.last_selected_hint = null;
46     // Generate
47     this.generate_hints();
50 hint_manager.prototype = {
51     current_hint_string : "",
52     current_hint_number : -1,
54     /**
55      * Create an initially hidden hint span element absolutely
56      * positioned over each element that matches
57      * hint_xpath_expression.  This is done recursively for all frames
58      * and iframes.  Information about the resulting hints are also
59      * stored in the hints array.
60      */
61     generate_hints : function () {
62         var topwin = this.window;
63         var top_height = topwin.innerHeight;
64         var top_width = topwin.innerWidth;
65         var hints = this.hints;
66         var xpath_expr = this.xpath_expr;
67         var focused_frame_hint = null, focused_element_hint = null;
68         var focused_frame = this.focused_frame;
69         var focused_element = this.focused_element;
70         function helper(window, offsetX, offsetY) {
71             var win_height = window.height;
72             var win_width = window.width;
74             // Bounds
75             var minX = offsetX < 0 ? -offsetX : 0;
76             var minY = offsetY < 0 ? -offsetY : 0;
77             var maxX = offsetX + win_width > top_width ? top_width - offsetX : top_width;
78             var maxY = offsetY + win_height > top_height ? top_height - offsetY : top_height;
80             var scrollX = window.scrollX;
81             var scrollY = window.scrollY;
83             var doc = window.document;
84             var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
85                                    Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE,
86                                    null /* existing results */);
88             var base_node = doc.createElementNS(XHTML_NS, "span");
89             base_node.className = "__conkeror_hint";
91             var fragment = doc.createDocumentFragment();
92             var rect, elem, text, node;
93             while ((elem = res.iterateNext()) != null)
94             {
95                 rect = elem.getBoundingClientRect();
96                 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
97                     continue;
98                 rect = elem.getClientRects()[0];
99                 if (!rect)
100                     continue;
101                 if (elem instanceof Ci.nsIDOMHTMLInputElement || elem instanceof Ci.nsIDOMHTMLTextAreaElement)
102                     text = elem.value.toLowerCase();
103                 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
104                     if (elem.selectedIndex >= 0)
105                         text = elem.item(elem.selectedIndex).text.toLowerCase();
106                     else
107                         text = "";
108                 } else if (elem instanceof Ci.nsIDOMHTMLFrameElement) {
109                     text = elem.name ? elem.name : "";
110                 } else
111                     text = elem.textContent.toLowerCase();
113                 node = base_node.cloneNode(true);
114                 node.style.left = (rect.left + scrollX) + "px";
115                 node.style.top = (rect.top + scrollY) + "px";
116                 fragment.appendChild(node);
118                 var hint = {text: text,
119                             elem: elem,
120                             hint: node,
121                             img_hint: null,
122                             visible : false};
123                 if (elem.style) {
124                     hint.saved_color = elem.style.color;
125                     hint.saved_bgcolor = elem.style.backgroundColor;
126                 }
127                 hints.push(hint);
129                 if (elem == focused_element)
130                     focused_element_hint = hint;
131                 else if ((elem instanceof Ci.nsIDOMHTMLFrameElement ||
132                           elem instanceof Ci.nsIDOMHTMLIFrameElement) &&
133                          elem.contentWindow == focused_frame)
134                     focused_frame_hint = hint;
135             }
136             doc.documentElement.appendChild(fragment);
138             /* Recurse into any IFRAME or FRAME elements */
139             var frametag = "frame";
140             while (true) {
141                 var frames = doc.getElementsByTagName(frametag);
142                 for (var i = 0; i < frames.length; ++i)
143                 {
144                     elem = frames[i];
145                     rect = elem.getBoundingClientRect();
146                     if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
147                         continue;
148                     helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
149                 }
150                 if (frametag == "frame") frametag = "iframe"; else break;
151             }
152         }
153         helper(topwin, 0, 0);
154         this.last_selected_hint = focused_element_hint || focused_frame_hint;
155     },
157     /* Updates valid_hints and also re-numbers and re-displays all hints. */
158     update_valid_hints : function () {
159         this.valid_hints = [];
160         var active_number = this.current_hint_number;
162         var tokens = this.current_hint_string.split(" ");
163         var rect, h, text, img_hint, doc, scrollX, scrollY;
164         var hints = this.hints;
166     outer:
167         for (var i = 0; i < hints.length; ++i)
168         {
169             h = hints[i];
170             text = h.text;
171             for (var j = 0; j < tokens.length; ++j)
172             {
173                 if (text.indexOf(tokens[j]) == -1)
174                 {
175                     if (h.visible)
176                     {
177                         h.visible = false;
178                         h.hint.style.display = "none";
179                         if (h.img_hint)
180                             h.img_hint.style.display = "none";
181                         if (h.saved_color != null) {
182                             h.elem.style.backgroundColor = h.saved_bgcolor;
183                             h.elem.style.color = h.saved_color;
184                         }
185                     }
186                     continue outer;
187                 }
188             }
190             var cur_number = this.valid_hints.length + 1;
191             h.visible = true;
193             if (h == this.last_selected_hint && active_number == -1)
194                 this.current_hint_number = active_number = cur_number;
196             var img_elem = null;
198             if (text.length == 0 && h.elem.firstChild &&
199                 h.elem.firstChild instanceof Ci.nsIDOMHTMLImageElement)
200                 img_elem = h.elem.firstChild;
201             else if (h.elem instanceof Ci.nsIDOMHTMLImageElement)
202                 img_elem = h.elem;
204             if (img_elem)
205             {
206                 if (!h.img_hint)
207                 {
208                     rect = img_elem.getBoundingClientRect();
209                     if (rect) {
210                         doc = h.elem.ownerDocument;
211                         scrollX = doc.defaultView.scrollX;
212                         scrollY = doc.defaultView.scrollY;
213                         img_hint = doc.createElementNS(XHTML_NS, "span");
214                         img_hint.className = "__conkeror_img_hint";
215                         img_hint.style.left = (rect.left + scrollX) + "px";
216                         img_hint.style.top = (rect.top + scrollY) + "px";
217                         img_hint.style.width = (rect.right - rect.left) + "px";
218                         img_hint.style.height = (rect.bottom - rect.top) + "px";
219                         h.img_hint = img_hint;
220                         doc.documentElement.appendChild(img_hint);
221                     } else
222                         img_elem = null;
223                 }
224                 if (img_elem) {
225                     var bgcolor = (active_number == cur_number) ? 
226                         active_img_hint_background_color : img_hint_background_color;
227                     h.img_hint.style.backgroundColor = bgcolor;
228                     h.img_hint.style.display = "inline";
229                 }
230             }
232             if (!h.img_hint && h.elem.style)
233                 h.elem.style.backgroundColor = (active_number == cur_number) ?
234                     active_hint_background_color : hint_background_color;
236             if (h.elem.style)
237                 h.elem.style.color = "black";
239             var label = "" + cur_number;
240             if (h.elem instanceof Ci.nsIDOMHTMLFrameElement) {
241                 label +=  " " + text;
242             }
243             h.hint.textContent = label;
244             h.hint.style.display = "inline";
245             this.valid_hints.push(h);
246         }
248         if (active_number == -1)
249             this.select_hint(1);
250     },
252     select_hint : function (index) {
253         var old_index = this.current_hint_number;
254         if (index == old_index)
255             return;
256         var vh = this.valid_hints;
257         if (old_index >= 1 && old_index <= vh.length)
258         {
259             var h = vh[old_index - 1];
260             if (h.img_hint)
261                 h.img_hint.style.backgroundColor = img_hint_background_color;
262             if (h.elem.style)
263                 h.elem.style.backgroundColor = hint_background_color;
264         }
265         this.current_hint_number = index;
266         if (index >= 1 && index <= vh.length)
267         {
268             var h = vh[index - 1];
269             if (h.img_hint)
270                 h.img_hint.style.backgroundColor = active_img_hint_background_color;
271             if (h.elem.style)
272                 h.elem.style.backgroundColor = active_hint_background_color;
273             this.last_selected_hint = h;
274         }
275     },
277     hide_hints : function () {
278         for (var i = 0; i < this.hints.length; ++i)
279         {
280             var h = this.hints[i];
281             if (h.visible) {
282                 h.visible = false;
283                 if (h.saved_color != null)
284                 {
285                     h.elem.style.color = h.saved_color;
286                     h.elem.style.backgroundColor = h.saved_bgcolor;
287                 }
288                 if (h.img_hint)
289                     h.img_hint.style.display = "none";
290                 h.hint.style.display = "none";
291             }
292         }
293     },
295     remove : function () {
296         for (var i = 0; i < this.hints.length; ++i)
297         {
298             var h = this.hints[i];
299             if (h.visible && h.saved_color != null) {
300                 h.elem.style.color = h.saved_color;
301                 h.elem.style.backgroundColor = h.saved_bgcolor;
302             }
303             if (h.img_hint)
304                 h.img_hint.parentNode.removeChild(h.img_hint);
305             h.hint.parentNode.removeChild(h.hint);
306         }
307         this.hints = [];
308         this.valid_hints = [];
309     }
312 var hint_keymap = null;
314 function initialize_hint_keymap()
316     hint_keymap = new keymap();
318 initialize_hint_keymap();
321  * keyword arguments:
323  * $prompt
324  * $callback
325  * $abort_callback
326  */
327 define_keywords("$keymap", "$auto", "$hint_xpath_expression", "$multiple");
328 function hints_minibuffer_state(continuation, buffer)
330     keywords(arguments, $keymap = hint_keymap, $auto);
331     basic_minibuffer_state.call(this, $prompt = arguments.$prompt);
332     this.continuation = continuation;
333     this.keymap = arguments.$keymap;
334     this.auto_exit = arguments.$auto ? true : false;
335     this.xpath_expr = arguments.$hint_xpath_expression;
336     this.auto_exit_timer_ID = null;
337     this.multiple = arguments.$multiple;
338     this.focused_element = buffer.focused_element;
339     this.focused_frame = buffer.focused_frame;
341 hints_minibuffer_state.prototype = {
342     __proto__: basic_minibuffer_state.prototype,
343     manager : null,
344     typed_string : "",
345     typed_number : "",
346     load : function (window) {
347         if (!this.manager) {
348             var buf = window.buffers.current;
349             this.manager = new hint_manager(buf.top_frame, this.xpath_expr,
350                                             this.focused_frame, this.focused_element);
351         }
352         this.manager.update_valid_hints();
353     },
354     unload : function (window) {
355         if (this.auto_exit_timer_ID) {
356             window.clearTimeout(this.auto_exit_timer_ID);
357             this.auto_exit_timer_ID = null;
358         }
359         this.manager.hide_hints();
360     },
361     destroy : function () {
362         if (this.auto_exit_timer_ID) {
363             window.clearTimeout(this.auto_exit_timer_ID);
364             this.auto_exit_timer_ID = null;
365         }
366         this.manager.remove();
367     },
368     update_minibuffer : function (window) {
369         var str = this.typed_string;
370         if (this.typed_number.length > 0) {
371             str += " #" + this.typed_number;
372         }
373         window.minibuffer._input_text = str;
374         window.minibuffer._set_selection();
375     }
378 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.");
380 function hints_handle_character(window, s, e) {
381     /* Check for numbers */
382     var ch = String.fromCharCode(e.charCode);
383     var auto_exit = false;
384     /* TODO: implement number escaping */
385     if (e.charCode >= 48 && e.charCode <= 57) {
386         // Number entered
387         s.typed_number += ch;
388         s.manager.select_hint(parseInt(s.typed_number));
389         var num = s.manager.current_hint_number;
390         if (s.auto_exit) {
391             if (num > 0 && num <= s.manager.valid_hints.length && num * 10 > s.manager.valid_hints.length)
392                 auto_exit = true;
393             if (num == 0) {
394                 if (!s.multiple) {
395                     hints_exit(window, s);
396                     return;
397                 }
398                 auto_exit = true;
399             }
400         }
401     } else {
402         s.typed_number = "";
403         s.typed_string += ch;
404         s.manager.current_hint_string = s.typed_string;
405         s.manager.current_hint_number = -1;
406         s.manager.update_valid_hints();
407         if (s.auto_exit && s.manager.valid_hints.length == 1)
408             auto_exit = true;
409     }
410     if (auto_exit) {
411         if (this.auto_exit_timer_ID) {
412             window.clearTimeout(this.auto_exit_timer_ID);
413         }
414         this.auto_exit_timer_ID = window.setTimeout(function() { hints_exit(window, s); },
415                                                    hints_auto_exit_delay);
416     }
417     s.update_minibuffer(window);
419 interactive("hints-handle-character", function (I) {
420     hints_handle_character(I.window, I.minibuffer.check_state(hints_minibuffer_state), I.event);
423 function hints_backspace(window, s) {
424     if (this.auto_exit_timer_ID) {
425         window.clearTimeout(this.auto_exit_timer_ID);
426         this.auto_exit_timer_ID = null;
427     }
428     if (s.typed_number.length > 0) {
429         s.typed_number = s.typed_number.substring(0, s.typed_number.length - 1);
430         var num = s.typed_number.length > 0 ? parseInt(s.typed_number) : 1;
431         s.manager.select_hint(num);
432     } else if (s.typed_string.length > 0) {
433         s.typed_string = s.typed_string.substring(0, s.typed_string.length - 1);
434         s.manager.current_hint_string = s.typed_string;
435         s.manager_current_hint_number = -1;
436         s.manager.update_valid_hints();
437     }
438     s.update_minibuffer(window);
440 interactive("hints-backspace", function (I) {
441     hints_backspace(I.window, I.minibuffer.check_state(hints_minibuffer_state));
444 function hints_next(window, s, count) {
445     if (this.auto_exit_timer_ID) {
446         window.clearTimeout(this.auto_exit_timer_ID);
447         this.auto_exit_timer_ID = null;
448     }
449     s.typed_number = "";
450     var cur = s.manager.current_hint_number - 1;
451     var vh = s.manager.valid_hints;
452     if (vh.length > 0) {
453         cur = (cur + count) % vh.length;
454         if (cur < 0)
455             cur += vh.length;
456         s.manager.select_hint(cur + 1);
457     }
458     s.update_minibuffer(window);
460 interactive("hints-next", function (I) {
461     hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), I.p);
464 interactive("hints-previous", function (I) {
465     hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), -I.p);
468 function hints_exit(window, s)
470     if (this.auto_exit_timer_ID) {
471         window.clearTimeout(this.auto_exit_timer_ID);
472         this.auto_exit_timer_ID = null;
473     }
474     var cur = s.manager.current_hint_number;
475     var elem = null;
476     if (cur > 0 && cur <= s.manager.valid_hints.length)
477         elem = s.manager.valid_hints[cur - 1].elem;
478     else if (cur == 0)
479         elem = window.buffers.current.top_frame;
480     if (elem) {
481         var c = s.continuation;
482         delete s.continuation;
483         window.minibuffer.pop_state();
484         if (c)
485             c(elem);
486     }
489 interactive("hints-exit", function (I) {
490     hints_exit(I.window, I.minibuffer.check_state(hints_minibuffer_state));
494 /* FIXME: figure out why this needs to have a bunch of duplication */
495 define_variable(
496     "hints_xpath_expressions",
497     {
498         images: {def: "//img | //xhtml:img"},
499         frames: {def: "//iframe | //frame | //xhtml:iframe | //xhtml:frame"},
500         links: {def:
501                 "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
502                 "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
503                 "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
504                 "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | " +
505                 "//xhtml:button | //xhtml:select"},
506         mathml: {def: "//m:math"}
507     },
508     "XPath expressions for each object class.");
510 define_keywords("$object_class", "$buffer");
511 minibuffer.prototype.read_hinted_element = function () {
512     keywords(arguments);
513     var buf = arguments.$buffer;
514     // FIXME: clean this up and replace with proper object class declaration
515     var object_class = arguments.$object_class;
516     if (object_class == "top")
517         yield co_return(buf.top_frame);
519     if (object_class == "frames") {
520         check_buffer(buf, content_buffer);
521         var doc = buf.top_document;
522         if (doc.getElementsByTagName("frame").length == 0 &&
523             doc.getElementsByTagName("iframe").length == 0)
524         {
525             // only one frame (the top-level one), no need to use the hints system
526             yield co_return(buf.top_frame);
527         }
528     }
529     var s = new hints_minibuffer_state((yield CONTINUATION), buf, forward_keywords(arguments));
530     this.push_state(s);
531     var result = yield SUSPEND;
532     yield co_return(result);