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
14 define_variable("active_img_hint_background_color", "#88FF00",
15 "Color for the active image hint background.");
17 define_variable("img_hint_background_color", "yellow",
18 "Color for inactive image hint backgrounds.");
20 define_variable("active_hint_background_color", "#88FF00",
21 "Color for the active hint background.");
23 define_variable("hint_background_color", "yellow",
24 "Color for the inactive hint.");
27 define_variable("hint_digits", null,
28 "Null or a string of the digits to use as the counting base "+
29 "for hint numbers, starting with the digit that represents zero "+
30 "and ascending. If null, base 10 will be used with the normal "+
31 "hindu-arabic numerals.");
35 * hints_enumerate is a generator of natural numbers in the base defined
38 function hints_enumerate () {
39 var base = hint_digits.length;
43 yield n.map(function (x) hint_digits[x]).join("");
46 while (n[i] >= base && i > 0) {
59 * hints_parse converts a string that represents a natural number to an
60 * int. When hint_digits is non-null, it defines the base for conversion.
62 function hints_parse (str) {
64 var base = hint_digits.length;
66 for (var i = 0, p = str.length - 1; p >= 0; i++, p--) {
67 n += hint_digits.indexOf(str[i]) * Math.pow(base, p);
75 * Register hints style sheet
77 const hints_stylesheet = "chrome://conkeror-gui/content/hints.css";
78 register_user_stylesheet(hints_stylesheet);
81 function hints_simple_text_match (text, pattern) {
82 var pos = text.indexOf(pattern);
85 return [pos, pos + pattern.length];
88 define_variable('hints_text_match', hints_simple_text_match,
89 "A function which takes a string and a pattern (another string) "+
90 "and returns an array of [start, end] indices if the pattern was "+
91 "found in the string, or false if it was not.");
95 * In the hints interaction, a node can be selected either by typing
96 * the number of its associated hint, or by typing substrings of the
97 * text content of the node. In the case of selecting by text
98 * content, multiple substrings can be given by separating them with
101 function hint_manager (window, xpath_expr, focused_frame, focused_element) {
102 this.window = window;
104 this.valid_hints = [];
105 this.xpath_expr = xpath_expr;
106 this.focused_frame = focused_frame;
107 this.focused_element = focused_element;
108 this.last_selected_hint = null;
111 this.generate_hints();
113 hint_manager.prototype = {
114 constructor: hint_manager,
115 current_hint_string: "",
116 current_hint_number: -1,
119 * Create an initially hidden hint span element absolutely
120 * positioned over each element that matches
121 * hint_xpath_expression. This is done recursively for all frames
122 * and iframes. Information about the resulting hints are also
123 * stored in the hints array.
125 generate_hints: function () {
126 var topwin = this.window;
127 var top_height = topwin.innerHeight;
128 var top_width = topwin.innerWidth;
129 var hints = this.hints;
130 var xpath_expr = this.xpath_expr;
131 var focused_frame_hint = null, focused_element_hint = null;
132 var focused_frame = this.focused_frame;
133 var focused_element = this.focused_element;
135 function helper (window, offsetX, offsetY) {
136 var win_height = window.height;
137 var win_width = window.width;
140 var minX = offsetX < 0 ? -offsetX : 0;
141 var minY = offsetY < 0 ? -offsetY : 0;
142 var maxX = offsetX + win_width > top_width ? top_width - offsetX : top_width;
143 var maxY = offsetY + win_height > top_height ? top_height - offsetY : top_height;
145 var scrollX = window.scrollX;
146 var scrollY = window.scrollY;
148 var doc = window.document;
149 var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
150 Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
151 null /* existing results */);
153 var base_node = doc.createElementNS(XHTML_NS, "span");
154 base_node.className = "__conkeror_hint";
156 var fragment = doc.createDocumentFragment();
157 var rect, elem, text, node, show_text;
158 for (var j = 0; j < res.snapshotLength; j++) {
159 elem = res.snapshotItem(j);
160 rect = elem.getBoundingClientRect();
161 if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
162 rect = { top: rect.top,
167 var coords = elem.getAttribute("coords")
168 .match(/^\D*(-?\d+)\D+(-?\d+)/);
169 if (coords.length == 3) {
170 rect.left += parseInt(coords[1]);
171 rect.top += parseInt(coords[2]);
175 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
177 let style = topwin.getComputedStyle(elem, "");
178 if (style.display == "none" || style.visibility == "hidden")
180 if (! (elem instanceof Ci.nsIDOMHTMLAreaElement))
181 rect = elem.getClientRects()[0];
185 if (elem instanceof Ci.nsIDOMHTMLInputElement || elem instanceof Ci.nsIDOMHTMLTextAreaElement)
187 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
188 if (elem.selectedIndex >= 0)
189 text = elem.item(elem.selectedIndex).text;
192 } else if (elem instanceof Ci.nsIDOMHTMLFrameElement) {
193 text = elem.name ? elem.name : "";
194 } else if (/^\s*$/.test(elem.textContent) &&
195 elem.childNodes.length == 1 &&
196 elem.childNodes.item(0) instanceof Ci.nsIDOMHTMLImageElement) {
197 text = elem.childNodes.item(0).alt;
200 text = elem.textContent;
202 node = base_node.cloneNode(true);
203 node.style.left = (rect.left + scrollX) + "px";
204 node.style.top = (rect.top + scrollY) + "px";
205 fragment.appendChild(node);
207 let hint = { text: text,
208 ltext: text.toLowerCase(),
213 show_text: show_text };
215 hint.saved_color = elem.style.color;
216 hint.saved_bgcolor = elem.style.backgroundColor;
220 if (elem == focused_element)
221 focused_element_hint = hint;
222 else if ((elem instanceof Ci.nsIDOMHTMLFrameElement ||
223 elem instanceof Ci.nsIDOMHTMLIFrameElement) &&
224 elem.contentWindow == focused_frame)
225 focused_frame_hint = hint;
227 doc.documentElement.appendChild(fragment);
229 /* Recurse into any IFRAME or FRAME elements */
230 var frametag = "frame";
232 var frames = doc.getElementsByTagName(frametag);
233 for (var i = 0, nframes = frames.length; i < nframes; ++i) {
235 rect = elem.getBoundingClientRect();
236 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
238 helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
240 if (frametag == "frame") frametag = "iframe"; else break;
243 helper(topwin, 0, 0);
244 this.last_selected_hint = focused_element_hint || focused_frame_hint;
247 /* Updates valid_hints and also re-numbers and re-displays all hints. */
248 update_valid_hints: function () {
249 this.valid_hints = [];
252 var number_generator = hints_enumerate();
253 var active_number = this.current_hint_number;
254 var tokens = this.current_hint_string.split(" ");
255 var case_sensitive = (this.current_hint_string !=
256 this.current_hint_string.toLowerCase());
257 var rect, text, img_hint, doc, scrollX, scrollY;
259 for (var i = 0, h; (h = this.hints[i]); ++i) {
264 for (var j = 0, ntokens = tokens.length; j < ntokens; ++j) {
265 if (! hints_text_match(text, tokens[j])) {
268 h.hint.style.display = "none";
270 h.img_hint.style.display = "none";
271 if (h.saved_color != null) {
272 h.elem.style.backgroundColor = h.saved_bgcolor;
273 h.elem.style.color = h.saved_color;
282 if (h == this.last_selected_hint && active_number == -1)
283 this.current_hint_number = active_number = cur_number;
287 if (text == "" && h.elem.firstChild &&
288 h.elem.firstChild instanceof Ci.nsIDOMHTMLImageElement)
289 img_elem = h.elem.firstChild;
290 else if (h.elem instanceof Ci.nsIDOMHTMLImageElement)
295 rect = img_elem.getBoundingClientRect();
297 doc = h.elem.ownerDocument;
298 scrollX = doc.defaultView.scrollX;
299 scrollY = doc.defaultView.scrollY;
300 img_hint = doc.createElementNS(XHTML_NS, "span");
301 img_hint.className = "__conkeror_img_hint";
302 img_hint.style.left = (rect.left + scrollX) + "px";
303 img_hint.style.top = (rect.top + scrollY) + "px";
304 img_hint.style.width = (rect.right - rect.left) + "px";
305 img_hint.style.height = (rect.bottom - rect.top) + "px";
306 h.img_hint = img_hint;
307 doc.documentElement.appendChild(img_hint);
312 var bgcolor = (active_number == cur_number) ?
313 active_img_hint_background_color : img_hint_background_color;
314 h.img_hint.style.backgroundColor = bgcolor;
315 h.img_hint.style.display = "inline";
319 if (!h.img_hint && h.elem.style)
320 h.elem.style.backgroundColor = (active_number == cur_number) ?
321 active_hint_background_color : hint_background_color;
324 h.elem.style.color = "black";
328 label = number_generator.next();
331 if (h.elem instanceof Ci.nsIDOMHTMLFrameElement) {
333 } else if (h.show_text && !/^\s*$/.test(text)) {
334 let substrs = [[0,4]];
335 for (j = 0; j < ntokens; ++j) {
336 let m = hints_text_match(text, tokens[j]);
337 if (m == false) continue;
338 splice_range(substrs, m[0], m[1] + 2);
340 label += " " + substrs.map(function (x) {
341 return text.substring(x[0],Math.min(x[1], text.length));
342 }).join("..") + "..";
344 h.hint.textContent = label;
345 h.hint.style.display = "inline";
346 this.valid_hints.push(h);
350 if (active_number == -1)
354 select_hint: function (index) {
355 var old_index = this.current_hint_number;
356 if (index == old_index)
358 var vh = this.valid_hints;
359 var vl = this.valid_hints.length;
360 if (old_index >= 1 && old_index <= vl) {
361 var h = vh[old_index - 1];
363 h.img_hint.style.backgroundColor = img_hint_background_color;
365 h.elem.style.backgroundColor = hint_background_color;
367 this.current_hint_number = index;
368 if (index >= 1 && index <= vl) {
371 h.img_hint.style.backgroundColor = active_img_hint_background_color;
373 h.elem.style.backgroundColor = active_hint_background_color;
374 this.last_selected_hint = h;
378 hide_hints: function () {
379 for (var i = 0, h; h = this.hints[i]; ++i) {
382 if (h.saved_color != null) {
383 h.elem.style.color = h.saved_color;
384 h.elem.style.backgroundColor = h.saved_bgcolor;
387 h.img_hint.style.display = "none";
388 h.hint.style.display = "none";
393 remove: function () {
394 for (var i = 0, h; h = this.hints[i]; ++i) {
395 if (h.visible && h.saved_color != null) {
396 h.elem.style.color = h.saved_color;
397 h.elem.style.backgroundColor = h.saved_bgcolor;
400 h.img_hint.parentNode.removeChild(h.img_hint);
401 h.hint.parentNode.removeChild(h.hint);
404 this.valid_hints = [];
408 /* Show panel with currently selected URL. */
409 function hints_url_panel (hints, window) {
410 var g = new dom_generator(window.document, XUL_NS);
412 var p = g.element("hbox", "class", "panel url", "flex", "0");
413 g.element("label", p, "value", "URL:", "class", "url-panel-label");
414 var url_value = g.element("label", p, "class", "url-panel-value",
415 "crop", "end", "flex", "1");
416 window.minibuffer.insert_before(p);
418 p.update = function () {
419 url_value.value = "";
420 if (hints.manager && hints.manager.last_selected_hint) {
423 spec = load_spec(hints.manager.last_selected_hint.elem);
426 var uri = load_spec_uri_string(spec);
427 if (uri) url_value.value = uri;
432 p.destroy = function () {
433 this.parentNode.removeChild(this);
439 define_variable("hints_display_url_panel", false,
440 "When selecting a hint, the URL can be displayed in a panel above "+
441 "the minibuffer. This is useful for confirming that the correct "+
442 "link is selected and that the URL is not evil. This option is "+
443 "most useful when hints_auto_exit_delay is long or disabled.");
452 define_keywords("$keymap", "$auto", "$hint_xpath_expression", "$multiple");
453 function hints_minibuffer_state (minibuffer, continuation, buffer) {
454 keywords(arguments, $keymap = hint_keymap, $auto);
455 basic_minibuffer_state.call(this, minibuffer, $prompt = arguments.$prompt,
456 $keymap = arguments.$keymap);
457 if (hints_display_url_panel)
458 this.url_panel = hints_url_panel(this, buffer.window);
459 this.original_prompt = arguments.$prompt;
460 this.continuation = continuation;
461 this.auto_exit = arguments.$auto ? true : false;
462 this.xpath_expr = arguments.$hint_xpath_expression;
463 this.auto_exit_timer_ID = null;
464 this.multiple = arguments.$multiple;
465 this.focused_element = buffer.focused_element;
466 this.focused_frame = buffer.focused_frame;
468 hints_minibuffer_state.prototype = {
469 constructor: hints_minibuffer_state,
470 __proto__: basic_minibuffer_state.prototype,
475 basic_minibuffer_state.prototype.load.call(this);
477 var buf = this.minibuffer.window.buffers.current;
478 this.manager = new hint_manager(buf.top_frame, this.xpath_expr,
479 this.focused_frame, this.focused_element);
481 this.manager.update_valid_hints();
483 this.url_panel.update();
485 clear_auto_exit_timer: function () {
486 var window = this.minibuffer.window;
487 if (this.auto_exit_timer_ID != null) {
488 window.clearTimeout(this.auto_exit_timer_ID);
489 this.auto_exit_timer_ID = null;
492 unload: function () {
493 this.clear_auto_exit_timer();
494 this.manager.hide_hints();
495 basic_minibuffer_state.prototype.unload.call(this);
497 destroy: function () {
498 this.clear_auto_exit_timer();
499 this.manager.remove();
501 this.url_panel.destroy();
502 basic_minibuffer_state.prototype.destroy.call(this);
504 update_minibuffer: function (m) {
505 if (this.typed_number.length > 0)
506 m.prompt = this.original_prompt + " #" + this.typed_number;
508 m.prompt = this.original_prompt;
510 this.url_panel.update();
513 handle_auto_exit: function (ambiguous) {
514 var window = this.minibuffer.window;
515 var num = this.manager.current_hint_number;
519 let delay = ambiguous ? hints_ambiguous_auto_exit_delay : hints_auto_exit_delay;
521 this.auto_exit_timer_ID = window.setTimeout(function () { hints_exit(window, s); },
525 handle_input: function (m) {
526 this.clear_auto_exit_timer();
527 this.typed_number = "";
528 this.typed_string = m._input_text;
529 this.manager.current_hint_string = this.typed_string;
530 this.manager.current_hint_number = -1;
531 this.manager.update_valid_hints();
532 if (this.manager.valid_hints.length == 1)
533 this.handle_auto_exit(false /* unambiguous */);
534 else if (this.manager.valid_hints.length > 1)
535 this.handle_auto_exit(true /* ambiguous */);
536 this.update_minibuffer(m);
540 define_variable("hints_auto_exit_delay", 0,
541 "Delay (in milliseconds) after the most recent key stroke before a "+
542 "sole matching element is automatically selected. When zero, "+
543 "automatic selection is disabled. A value of 500 is a good "+
544 "starting point for an average-speed typist.");
546 define_variable("hints_ambiguous_auto_exit_delay", 0,
547 "Delay (in milliseconds) after the most recent key stroke before the "+
548 "first of an ambiguous match is automatically selected. If this is "+
549 "set to 0, automatic selection in ambiguous matches is disabled.");
552 define_key_match_predicate("match_hint_digit", "hint digit",
554 if (e.type != "keypress")
556 if (e.charCode == 48) //0 is special
559 if (hint_digits.indexOf(String.fromCharCode(e.charCode)) > -1)
561 } else if (e.charCode >= 49 && e.charCode <= 57)
566 interactive("hints-handle-number",
567 "This is the handler for numeric keys in hinting mode. Normally, "+
568 "that means '1' through '9' and '0', but the numeric base (and digits) "+
569 "can be configured via the user variable 'hint_digits'. No matter "+
570 "what numeric base is in effect, the character '0' is special, and "+
571 "will always be treated as a number 0, translated into the current "+
572 "base if necessary.",
574 let s = I.minibuffer.check_state(hints_minibuffer_state);
575 s.clear_auto_exit_timer();
576 var ch = String.fromCharCode(I.event.charCode);
577 if (hint_digits && ch == "0")
579 var auto_exit_ambiguous = null; // null -> no auto exit; false -> not ambiguous; true -> ambiguous
580 s.typed_number += ch;
581 s.manager.select_hint(hints_parse(s.typed_number));
582 var num = s.manager.current_hint_number;
583 if (num > 0 && num <= s.manager.valid_hints.length)
584 auto_exit_ambiguous = num * 10 > s.manager.valid_hints.length ? false : true;
587 hints_exit(I.window, s);
590 auto_exit_ambiguous = false;
592 if (auto_exit_ambiguous !== null)
593 s.handle_auto_exit(auto_exit_ambiguous);
594 s.update_minibuffer(I.minibuffer);
597 function hints_backspace (window, s) {
598 let m = window.minibuffer;
599 s.clear_auto_exit_timer();
600 var l = s.typed_number.length;
602 s.typed_number = s.typed_number.substring(0, --l);
603 var num = l > 0 ? hints_parse(s.typed_number) : 1;
604 s.manager.select_hint(num);
605 } else if (s.typed_string.length > 0) {
606 call_builtin_command(window, 'cmd_deleteCharBackward');
607 s.typed_string = m._input_text;
608 //m._set_selection();
609 s.manager.current_hint_string = s.typed_string;
610 s.manager.current_hint_number = -1;
611 s.manager.update_valid_hints();
613 s.update_minibuffer(m);
615 interactive("hints-backspace", null,
617 hints_backspace(I.window, I.minibuffer.check_state(hints_minibuffer_state));
620 function hints_next (window, s, count) {
621 s.clear_auto_exit_timer();
623 var cur = s.manager.current_hint_number - 1;
624 var vh = s.manager.valid_hints;
625 var vl = s.manager.valid_hints.length;
627 cur = (cur + count) % vl;
630 s.manager.select_hint(cur + 1);
632 s.update_minibuffer(window);
634 interactive("hints-next", null,
636 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), I.p);
639 interactive("hints-previous", null,
641 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), -I.p);
644 function hints_exit (window, s) {
645 var cur = s.manager.current_hint_number;
647 if (cur > 0 && cur <= s.manager.valid_hints.length)
648 elem = s.manager.valid_hints[cur - 1].elem;
650 elem = window.buffers.current.top_frame;
652 var c = s.continuation;
653 delete s.continuation;
654 window.minibuffer.pop_state();
660 interactive("hints-exit", null,
662 hints_exit(I.window, I.minibuffer.check_state(hints_minibuffer_state));
665 interactive("hints-quote-next", null,
667 I.overlay_keymap = hint_quote_next_keymap;
672 define_keywords("$buffer");
673 minibuffer.prototype.read_hinted_element = function () {
675 var buf = arguments.$buffer;
676 var s = new hints_minibuffer_state(this, (yield CONTINUATION), buf, forward_keywords(arguments));
678 var result = yield SUSPEND;
679 yield co_return(result);