2 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3 * (C) Copyright 2009-2010 John J. Foerch
5 * Portions of this file are derived from Vimperator,
6 * (C) Copyright 2006-2007 Martin Stubenschrott.
8 * Use, modification, and distribution are subject to the terms specified in the
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 define_variable("hint_digits", null,
26 "Null or a string of the digits to use as the counting base "+
27 "for hint numbers, starting with the digit that represents zero "+
28 "and ascending. If null, base 10 will be used with the normal "+
29 "hindu-arabic numerals.");
31 define_variable("hints_display_alt", true,
32 "Display alt text in hints.");
36 * hints_enumerate is a generator of natural numbers in the base defined
39 function hints_enumerate () {
40 var base = hint_digits.length;
44 yield n.map(function (x) hint_digits[x]).join("");
47 while (n[i] >= base && i > 0) {
60 * hints_parse converts a string that represents a natural number to an
61 * int. When hint_digits is non-null, it defines the base for conversion.
63 function hints_parse (str) {
65 var base = hint_digits.length;
67 for (var i = 0, p = str.length - 1; p >= 0; i++, p--) {
68 n += hint_digits.indexOf(str[i]) * Math.pow(base, p);
76 * Register hints style sheet
78 const hints_stylesheet = "chrome://conkeror-gui/content/hints.css";
79 register_user_stylesheet(hints_stylesheet);
82 function hints_simple_text_match (text, pattern) {
83 var pos = text.indexOf(pattern);
86 return [pos, pos + pattern.length];
89 define_variable('hints_text_match', hints_simple_text_match,
90 "A function which takes a string and a pattern (another string) "+
91 "and returns an array of [start, end] indices if the pattern was "+
92 "found in the string, or false if it was not.");
96 * In the hints interaction, a node can be selected either by typing
97 * the number of its associated hint, or by typing substrings of the
98 * text content of the node. In the case of selecting by text
99 * content, multiple substrings can be given by separating them with
102 function hint_manager (window, xpath_expr, focused_frame, focused_element) {
103 this.window = window;
105 this.valid_hints = [];
106 this.xpath_expr = xpath_expr;
107 this.focused_frame = focused_frame;
108 this.focused_element = focused_element;
109 this.last_selected_hint = null;
112 this.generate_hints();
114 hint_manager.prototype = {
115 constructor: hint_manager,
116 current_hint_string: "",
117 current_hint_number: -1,
120 * Create an initially hidden hint span element absolutely
121 * positioned over each element that matches
122 * hint_xpath_expression. This is done recursively for all frames
123 * and iframes. Information about the resulting hints are also
124 * stored in the hints array.
126 generate_hints: function () {
127 var topwin = this.window;
128 var top_height = topwin.innerHeight;
129 var top_width = topwin.innerWidth;
130 var hints = this.hints;
131 var xpath_expr = this.xpath_expr;
132 var focused_frame_hint = null, focused_element_hint = null;
133 var focused_frame = this.focused_frame;
134 var focused_element = this.focused_element;
136 function helper (window, offsetX, offsetY) {
137 var win_height = window.height;
138 var win_width = window.width;
141 var minX = offsetX < 0 ? -offsetX : 0;
142 var minY = offsetY < 0 ? -offsetY : 0;
143 var maxX = offsetX + win_width > top_width ? top_width - offsetX : top_width;
144 var maxY = offsetY + win_height > top_height ? top_height - offsetY : top_height;
146 var scrollX = window.scrollX;
147 var scrollY = window.scrollY;
149 var doc = window.document;
150 if (! doc.documentElement)
152 var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
153 Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
154 null /* existing results */);
156 var base_node = doc.createElementNS(XHTML_NS, "span");
157 base_node.className = "__conkeror_hint";
159 var fragment = doc.createDocumentFragment();
160 var rect, elem, text, node, show_text;
161 for (var j = 0; j < res.snapshotLength; j++) {
162 elem = res.snapshotItem(j);
163 rect = elem.getBoundingClientRect();
164 if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
165 rect = { top: rect.top,
170 var coords = elem.getAttribute("coords")
171 .match(/^\D*(-?\d+)\D+(-?\d+)/);
172 if (coords.length == 3) {
173 rect.left += parseInt(coords[1]);
174 rect.top += parseInt(coords[2]);
178 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
180 let style = topwin.getComputedStyle(elem, "");
181 if (style.display == "none" || style.visibility == "hidden")
183 if (! (elem instanceof Ci.nsIDOMHTMLAreaElement))
184 rect = elem.getClientRects()[0];
187 var nchildren = elem.childNodes.length;
188 if (elem instanceof Ci.nsIDOMHTMLAnchorElement &&
189 rect.width == 0 && rect.height == 0)
191 for (var c = 0; c < nchildren; ++c) {
192 var cc = elem.childNodes.item(c);
193 if (cc.getBoundingClientRect) {
194 rect = cc.getBoundingClientRect();
200 if (elem instanceof Ci.nsIDOMHTMLInputElement || elem instanceof Ci.nsIDOMHTMLTextAreaElement)
202 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
203 if (elem.selectedIndex >= 0)
204 text = elem.item(elem.selectedIndex).text;
207 } else if (elem instanceof Ci.nsIDOMHTMLFrameElement) {
208 text = elem.name ? elem.name : "";
209 } else if (/^\s*$/.test(elem.textContent) &&
211 elem.childNodes.item(0) instanceof Ci.nsIDOMHTMLImageElement) {
212 text = elem.childNodes.item(0).alt;
213 show_text = hints_display_alt;
215 text = elem.textContent;
217 node = base_node.cloneNode(true);
218 node.style.left = (rect.left + scrollX) + "px";
219 node.style.top = (rect.top + scrollY) + "px";
220 fragment.appendChild(node);
222 let hint = { text: text,
223 ltext: text.toLowerCase(),
228 show_text: show_text };
230 hint.saved_color = elem.style.color;
231 hint.saved_bgcolor = elem.style.backgroundColor;
235 if (elem == focused_element)
236 focused_element_hint = hint;
237 else if ((elem instanceof Ci.nsIDOMHTMLFrameElement ||
238 elem instanceof Ci.nsIDOMHTMLIFrameElement) &&
239 elem.contentWindow == focused_frame)
240 focused_frame_hint = hint;
242 doc.documentElement.appendChild(fragment);
244 /* Recurse into any IFRAME or FRAME elements */
245 var frametag = "frame";
247 var frames = doc.getElementsByTagName(frametag);
248 for (var i = 0, nframes = frames.length; i < nframes; ++i) {
250 rect = elem.getBoundingClientRect();
251 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
253 helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
255 if (frametag == "frame") frametag = "iframe"; else break;
258 helper(topwin, 0, 0);
259 this.last_selected_hint = focused_element_hint || focused_frame_hint;
262 /* Updates valid_hints and also re-numbers and re-displays all hints. */
263 update_valid_hints: function () {
264 this.valid_hints = [];
267 var number_generator = hints_enumerate();
268 var active_number = this.current_hint_number;
269 var tokens = this.current_hint_string.split(" ");
270 var case_sensitive = (this.current_hint_string !=
271 this.current_hint_string.toLowerCase());
272 var rect, text, img_hint, doc, scrollX, scrollY;
274 for (var i = 0, h; (h = this.hints[i]); ++i) {
279 for (var j = 0, ntokens = tokens.length; j < ntokens; ++j) {
280 if (! hints_text_match(text, tokens[j])) {
283 h.hint.style.display = "none";
285 h.img_hint.style.display = "none";
286 if (h.saved_color != null) {
287 h.elem.style.backgroundColor = h.saved_bgcolor;
288 h.elem.style.color = h.saved_color;
297 if (h == this.last_selected_hint && active_number == -1)
298 this.current_hint_number = active_number = cur_number;
302 if (text == "" && h.elem.firstChild &&
303 h.elem.firstChild instanceof Ci.nsIDOMHTMLImageElement)
304 img_elem = h.elem.firstChild;
305 else if (h.elem instanceof Ci.nsIDOMHTMLImageElement)
310 rect = img_elem.getBoundingClientRect();
312 doc = h.elem.ownerDocument;
313 scrollX = doc.defaultView.scrollX;
314 scrollY = doc.defaultView.scrollY;
315 img_hint = doc.createElementNS(XHTML_NS, "span");
316 img_hint.className = "__conkeror_img_hint";
317 img_hint.style.left = (rect.left + scrollX) + "px";
318 img_hint.style.top = (rect.top + scrollY) + "px";
319 img_hint.style.width = (rect.right - rect.left) + "px";
320 img_hint.style.height = (rect.bottom - rect.top) + "px";
321 h.img_hint = img_hint;
322 doc.documentElement.appendChild(img_hint);
327 var bgcolor = (active_number == cur_number) ?
328 active_img_hint_background_color : img_hint_background_color;
329 h.img_hint.style.backgroundColor = bgcolor;
330 h.img_hint.style.display = "inline";
334 if (!h.img_hint && h.elem.style)
335 h.elem.style.backgroundColor = (active_number == cur_number) ?
336 active_hint_background_color : hint_background_color;
339 h.elem.style.color = "black";
343 label = number_generator.next();
346 if (h.elem instanceof Ci.nsIDOMHTMLFrameElement) {
348 } else if (h.show_text && !/^\s*$/.test(text)) {
349 let substrs = [[0,4]];
350 for (j = 0; j < ntokens; ++j) {
351 let m = hints_text_match(text, tokens[j]);
352 if (m == false) continue;
353 splice_range(substrs, m[0], m[1] + 2);
355 label += " " + substrs.map(function (x) {
356 return text.substring(x[0],Math.min(x[1], text.length));
357 }).join("..") + "..";
359 h.hint.textContent = label;
360 h.hint.style.display = "inline";
361 this.valid_hints.push(h);
365 if (active_number == -1)
369 select_hint: function (index) {
370 var old_index = this.current_hint_number;
371 if (index == old_index)
373 var vh = this.valid_hints;
374 var vl = this.valid_hints.length;
375 if (old_index >= 1 && old_index <= vl) {
376 var h = vh[old_index - 1];
378 h.img_hint.style.backgroundColor = img_hint_background_color;
380 h.elem.style.backgroundColor = hint_background_color;
382 this.current_hint_number = index;
383 this.last_selected_hint = null;
384 if (index >= 1 && index <= vl) {
387 h.img_hint.style.backgroundColor = active_img_hint_background_color;
389 h.elem.style.backgroundColor = active_hint_background_color;
390 this.last_selected_hint = h;
394 hide_hints: function () {
395 for (var i = 0, h; h = this.hints[i]; ++i) {
398 if (h.saved_color != null) {
400 h.elem.style.color = h.saved_color;
401 h.elem.style.backgroundColor = h.saved_bgcolor;
402 } catch (e) { /* element may be dead */ }
406 h.img_hint.style.display = "none";
407 } catch (e) { /* element may be dead */ }
410 h.hint.style.display = "none";
411 } catch (e) { /* element may be dead */ }
416 remove: function () {
417 for (var i = 0, h; h = this.hints[i]; ++i) {
418 if (h.visible && h.saved_color != null) {
420 h.elem.style.color = h.saved_color;
421 h.elem.style.backgroundColor = h.saved_bgcolor;
422 } catch (e) { /* element may be dead */ }
426 h.img_hint.parentNode.removeChild(h.img_hint);
427 } catch (e) { /* element may be dead */ }
430 h.hint.parentNode.removeChild(h.hint);
431 } catch (e) { /* element may be dead */ }
434 this.valid_hints = [];
439 * Display the URL and other information for the currently selected node.
441 function hints_minibuffer_annotation (hints, window) {
443 this.input = window.minibuffer.input_element;
445 hints_minibuffer_annotation.prototype = {
446 constructor: hints_minibuffer_annotation,
448 update: function () {
450 if (this.hints.manager && this.hints.manager.last_selected_hint) {
451 var elem = this.hints.manager.last_selected_hint.elem;
452 if (elem.hasAttribute("onmousedown") ||
453 elem.hasAttribute("onclick"))
457 var tag = elem.localName.toLowerCase();
458 if ((tag == "input" || tag == "button") &&
459 elem.type == "submit" && elem.form && elem.form.action)
461 s.push((elem.form.method || "GET").toUpperCase() + ":" +
465 var spec = load_spec(elem);
466 var uri = load_spec_uri_string(spec);
472 this.input.annotation = s.join(" ");
476 this.input.annotate = true;
480 unload: function () {
481 this.input.annotate = false;
485 define_global_mode("hints_minibuffer_annotation_mode",
487 minibuffer_annotation_mode.register(hints_minibuffer_annotation_mode);
489 function disable () {
490 minibuffer_annotation_mode.unregister(hints_minibuffer_annotation_mode);
492 $doc = "Display the URL associated with the currently selected hint in "+
493 "a minibuffer annotation.\nThis mode is most useful when "+
494 "hints_auto_exit_delay is long or disabled.");
496 hints_minibuffer_annotation_mode(true);
505 define_keywords("$keymap", "$auto", "$hint_xpath_expression", "$multiple");
506 function hints_minibuffer_state (minibuffer, buffer) {
507 keywords(arguments, $keymap = hint_keymap, $auto);
508 basic_minibuffer_state.call(this, minibuffer, $prompt = arguments.$prompt,
509 $keymap = arguments.$keymap);
510 if (hints_minibuffer_annotation_mode_enabled)
511 this.hints_minibuffer_annotation = new hints_minibuffer_annotation(this, buffer.window);
512 this.original_prompt = arguments.$prompt;
514 let deferred = Promise.defer();
515 this.deferred = deferred;
516 this.promise = make_simple_cancelable(deferred);
518 this.auto_exit = arguments.$auto ? true : false;
519 this.xpath_expr = arguments.$hint_xpath_expression;
520 this.auto_exit_timer_ID = null;
521 this.multiple = arguments.$multiple;
522 this.focused_element = buffer.focused_element;
523 this.focused_frame = buffer.focused_frame;
525 hints_minibuffer_state.prototype = {
526 constructor: hints_minibuffer_state,
527 __proto__: basic_minibuffer_state.prototype,
532 basic_minibuffer_state.prototype.load.call(this);
534 var buf = this.minibuffer.window.buffers.current;
535 this.manager = new hint_manager(buf.top_frame, this.xpath_expr,
536 this.focused_frame, this.focused_element);
538 this.manager.update_valid_hints();
539 if (this.hints_minibuffer_annotation)
540 this.hints_minibuffer_annotation.load();
542 clear_auto_exit_timer: function () {
543 var window = this.minibuffer.window;
544 if (this.auto_exit_timer_ID != null) {
545 window.clearTimeout(this.auto_exit_timer_ID);
546 this.auto_exit_timer_ID = null;
549 unload: function () {
550 this.clear_auto_exit_timer();
551 this.manager.hide_hints();
552 if (this.hints_minibuffer_annotation)
553 this.hints_minibuffer_annotation.unload();
554 basic_minibuffer_state.prototype.unload.call(this);
556 destroy: function () {
557 this.promise.cancel();
558 this.clear_auto_exit_timer();
559 this.manager.remove();
560 if (this.hints_minibuffer_annotation)
561 this.hints_minibuffer_annotation.unload();
562 basic_minibuffer_state.prototype.destroy.call(this);
564 update_minibuffer: function (m) {
565 if (this.typed_number.length > 0)
566 m.prompt = this.original_prompt + " #" + this.typed_number;
568 m.prompt = this.original_prompt;
569 if (this.hints_minibuffer_annotation)
570 this.hints_minibuffer_annotation.update();
573 handle_auto_exit: function (ambiguous) {
574 var window = this.minibuffer.window;
575 var num = this.manager.current_hint_number;
579 let delay = ambiguous ? hints_ambiguous_auto_exit_delay : hints_auto_exit_delay;
581 this.auto_exit_timer_ID = window.setTimeout(function () { hints_exit(window, s); },
585 handle_input: function (m) {
586 this.clear_auto_exit_timer();
587 this.typed_number = "";
588 this.typed_string = m._input_text;
589 this.manager.current_hint_string = this.typed_string;
590 this.manager.current_hint_number = -1;
591 this.manager.update_valid_hints();
592 if (this.manager.valid_hints.length == 1)
593 this.handle_auto_exit(false /* unambiguous */);
594 else if (this.manager.valid_hints.length > 1)
595 this.handle_auto_exit(true /* ambiguous */);
596 this.update_minibuffer(m);
600 define_variable("hints_auto_exit_delay", 0,
601 "Delay (in milliseconds) after the most recent key stroke before a "+
602 "sole matching element is automatically selected. When zero, "+
603 "automatic selection is disabled. A value of 500 is a good "+
604 "starting point for an average-speed typist.");
606 define_variable("hints_ambiguous_auto_exit_delay", 0,
607 "Delay (in milliseconds) after the most recent key stroke before the "+
608 "first of an ambiguous match is automatically selected. If this is "+
609 "set to 0, automatic selection in ambiguous matches is disabled.");
612 define_key_match_predicate("match_hint_digit", "hint digit",
614 if (e.type != "keypress")
616 if (e.charCode == 48) //0 is special
619 if (hint_digits.indexOf(String.fromCharCode(e.charCode)) > -1)
621 } else if (e.charCode >= 49 && e.charCode <= 57)
626 interactive("hints-handle-number",
627 "This is the handler for numeric keys in hinting mode. Normally, "+
628 "that means '1' through '9' and '0', but the numeric base (and digits) "+
629 "can be configured via the user variable 'hint_digits'. No matter "+
630 "what numeric base is in effect, the character '0' is special, and "+
631 "will always be treated as a number 0, translated into the current "+
632 "base if necessary.",
634 let s = I.minibuffer.check_state(hints_minibuffer_state);
635 s.clear_auto_exit_timer();
636 var ch = String.fromCharCode(I.event.charCode);
637 if (hint_digits && ch == "0")
639 var auto_exit_ambiguous = null; // null -> no auto exit; false -> not ambiguous; true -> ambiguous
640 s.typed_number += ch;
641 s.manager.select_hint(hints_parse(s.typed_number));
642 var num = s.manager.current_hint_number;
643 if (num > 0 && num <= s.manager.valid_hints.length)
644 auto_exit_ambiguous = num * 10 > s.manager.valid_hints.length ? false : true;
647 hints_exit(I.window, s);
650 auto_exit_ambiguous = false;
652 if (auto_exit_ambiguous !== null)
653 s.handle_auto_exit(auto_exit_ambiguous);
654 s.update_minibuffer(I.minibuffer);
657 function hints_backspace (window, s) {
658 let m = window.minibuffer;
659 s.clear_auto_exit_timer();
660 var l = s.typed_number.length;
662 s.typed_number = s.typed_number.substring(0, --l);
663 var num = l > 0 ? hints_parse(s.typed_number) : 1;
664 s.manager.select_hint(num);
665 } else if (s.typed_string.length > 0) {
666 call_builtin_command(window, 'cmd_deleteCharBackward');
667 s.typed_string = m._input_text;
668 //m._set_selection();
669 s.manager.current_hint_string = s.typed_string;
670 s.manager.current_hint_number = -1;
671 s.manager.update_valid_hints();
673 s.update_minibuffer(m);
675 interactive("hints-backspace", null,
677 hints_backspace(I.window, I.minibuffer.check_state(hints_minibuffer_state));
680 function hints_next (window, s, count) {
681 s.clear_auto_exit_timer();
683 var cur = s.manager.current_hint_number - 1;
684 var vh = s.manager.valid_hints;
685 var vl = s.manager.valid_hints.length;
687 cur = (cur + count) % vl;
690 s.manager.select_hint(cur + 1);
692 s.update_minibuffer(window.minibuffer);
694 interactive("hints-next", null,
696 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), I.p);
699 interactive("hints-previous", null,
701 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), -I.p);
704 function hints_exit (window, s) {
705 var cur = s.manager.current_hint_number;
707 if (cur > 0 && cur <= s.manager.valid_hints.length)
708 elem = s.manager.valid_hints[cur - 1].elem;
710 elem = window.buffers.current.top_frame;
712 s.deferred.resolve(elem);
713 window.minibuffer.pop_state();
717 interactive("hints-exit", null,
719 hints_exit(I.window, I.minibuffer.check_state(hints_minibuffer_state));
722 interactive("hints-quote-next", null,
724 I.overlay_keymap = hint_quote_next_keymap;
729 define_keywords("$buffer");
730 minibuffer.prototype.read_hinted_element = function () {
732 var buf = arguments.$buffer;
733 var s = new hints_minibuffer_state(this, buf, forward_keywords(arguments));
735 yield co_return(yield s.promise);