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.");
28 * Register hints style sheet
30 const hints_stylesheet = "chrome://conkeror-gui/content/hints.css";
31 register_user_stylesheet(hints_stylesheet);
34 function hints_simple_text_match (text, pattern) {
35 var pos = text.indexOf(pattern);
38 return [pos, pos + pattern.length];
41 define_variable('hints_text_match', hints_simple_text_match,
42 "A function which takes a string and a pattern (another string) "+
43 "and returns an array of [start, end] indices if the pattern was "+
44 "found in the string, or false if it was not.");
48 * In the hints interaction, a node can be selected either by typing
49 * the number of its associated hint, or by typing substrings of the
50 * text content of the node. In the case of selecting by text
51 * content, multiple substrings can be given by separating them with
54 function hint_manager (window, xpath_expr, focused_frame, focused_element) {
57 this.valid_hints = [];
58 this.xpath_expr = xpath_expr;
59 this.focused_frame = focused_frame;
60 this.focused_element = focused_element;
61 this.last_selected_hint = null;
64 this.generate_hints();
66 hint_manager.prototype = {
67 constructor: hint_manager,
68 current_hint_string: "",
69 current_hint_number: -1,
72 * Create an initially hidden hint span element absolutely
73 * positioned over each element that matches
74 * hint_xpath_expression. This is done recursively for all frames
75 * and iframes. Information about the resulting hints are also
76 * stored in the hints array.
78 generate_hints: function () {
79 var topwin = this.window;
80 var top_height = topwin.innerHeight;
81 var top_width = topwin.innerWidth;
82 var hints = this.hints;
83 var xpath_expr = this.xpath_expr;
84 var focused_frame_hint = null, focused_element_hint = null;
85 var focused_frame = this.focused_frame;
86 var focused_element = this.focused_element;
88 function helper (window, offsetX, offsetY) {
89 var win_height = window.height;
90 var win_width = window.width;
93 var minX = offsetX < 0 ? -offsetX : 0;
94 var minY = offsetY < 0 ? -offsetY : 0;
95 var maxX = offsetX + win_width > top_width ? top_width - offsetX : top_width;
96 var maxY = offsetY + win_height > top_height ? top_height - offsetY : top_height;
98 var scrollX = window.scrollX;
99 var scrollY = window.scrollY;
101 var doc = window.document;
102 var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
103 Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
104 null /* existing results */);
106 var base_node = doc.createElementNS(XHTML_NS, "span");
107 base_node.className = "__conkeror_hint";
109 var fragment = doc.createDocumentFragment();
110 var rect, elem, text, node, show_text;
111 for (var j = 0; j < res.snapshotLength; j++) {
112 elem = res.snapshotItem(j);
113 rect = elem.getBoundingClientRect();
114 if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
115 rect = { top: rect.top,
120 var coords = elem.getAttribute("coords")
121 .match(/^\D*(-?\d+)\D+(-?\d+)/);
122 if (coords.length == 3) {
123 rect.left += parseInt(coords[1]);
124 rect.top += parseInt(coords[2]);
128 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
130 let style = topwin.getComputedStyle(elem, "");
131 if (style.display == "none" || style.visibility == "hidden")
133 if (! (elem instanceof Ci.nsIDOMHTMLAreaElement))
134 rect = elem.getClientRects()[0];
138 if (elem instanceof Ci.nsIDOMHTMLInputElement || elem instanceof Ci.nsIDOMHTMLTextAreaElement)
140 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
141 if (elem.selectedIndex >= 0)
142 text = elem.item(elem.selectedIndex).text;
145 } else if (elem instanceof Ci.nsIDOMHTMLFrameElement) {
146 text = elem.name ? elem.name : "";
147 } else if (/^\s*$/.test(elem.textContent) &&
148 elem.childNodes.length == 1 &&
149 elem.childNodes.item(0) instanceof Ci.nsIDOMHTMLImageElement) {
150 text = elem.childNodes.item(0).alt;
153 text = elem.textContent;
155 node = base_node.cloneNode(true);
156 node.style.left = (rect.left + scrollX) + "px";
157 node.style.top = (rect.top + scrollY) + "px";
158 fragment.appendChild(node);
160 let hint = { text: text,
161 ltext: text.toLowerCase(),
166 show_text: show_text };
168 hint.saved_color = elem.style.color;
169 hint.saved_bgcolor = elem.style.backgroundColor;
173 if (elem == focused_element)
174 focused_element_hint = hint;
175 else if ((elem instanceof Ci.nsIDOMHTMLFrameElement ||
176 elem instanceof Ci.nsIDOMHTMLIFrameElement) &&
177 elem.contentWindow == focused_frame)
178 focused_frame_hint = hint;
180 doc.documentElement.appendChild(fragment);
182 /* Recurse into any IFRAME or FRAME elements */
183 var frametag = "frame";
185 var frames = doc.getElementsByTagName(frametag);
186 for (var i = 0, nframes = frames.length; i < nframes; ++i) {
188 rect = elem.getBoundingClientRect();
189 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
191 helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
193 if (frametag == "frame") frametag = "iframe"; else break;
196 helper(topwin, 0, 0);
197 this.last_selected_hint = focused_element_hint || focused_frame_hint;
200 /* Updates valid_hints and also re-numbers and re-displays all hints. */
201 update_valid_hints: function () {
202 this.valid_hints = [];
203 var active_number = this.current_hint_number;
204 var tokens = this.current_hint_string.split(" ");
205 var case_sensitive = (this.current_hint_string !=
206 this.current_hint_string.toLowerCase());
207 var rect, text, img_hint, doc, scrollX, scrollY;
209 for (var i = 0, h; (h = this.hints[i]); ++i) {
214 for (var j = 0, ntokens = tokens.length; j < ntokens; ++j) {
215 if (! hints_text_match(text, tokens[j])) {
218 h.hint.style.display = "none";
220 h.img_hint.style.display = "none";
221 if (h.saved_color != null) {
222 h.elem.style.backgroundColor = h.saved_bgcolor;
223 h.elem.style.color = h.saved_color;
230 var cur_number = this.valid_hints.length + 1;
233 if (h == this.last_selected_hint && active_number == -1)
234 this.current_hint_number = active_number = cur_number;
238 if (text == "" && h.elem.firstChild &&
239 h.elem.firstChild instanceof Ci.nsIDOMHTMLImageElement)
240 img_elem = h.elem.firstChild;
241 else if (h.elem instanceof Ci.nsIDOMHTMLImageElement)
246 rect = img_elem.getBoundingClientRect();
248 doc = h.elem.ownerDocument;
249 scrollX = doc.defaultView.scrollX;
250 scrollY = doc.defaultView.scrollY;
251 img_hint = doc.createElementNS(XHTML_NS, "span");
252 img_hint.className = "__conkeror_img_hint";
253 img_hint.style.left = (rect.left + scrollX) + "px";
254 img_hint.style.top = (rect.top + scrollY) + "px";
255 img_hint.style.width = (rect.right - rect.left) + "px";
256 img_hint.style.height = (rect.bottom - rect.top) + "px";
257 h.img_hint = img_hint;
258 doc.documentElement.appendChild(img_hint);
263 var bgcolor = (active_number == cur_number) ?
264 active_img_hint_background_color : img_hint_background_color;
265 h.img_hint.style.backgroundColor = bgcolor;
266 h.img_hint.style.display = "inline";
270 if (!h.img_hint && h.elem.style)
271 h.elem.style.backgroundColor = (active_number == cur_number) ?
272 active_hint_background_color : hint_background_color;
275 h.elem.style.color = "black";
277 var label = "" + cur_number;
278 if (h.elem instanceof Ci.nsIDOMHTMLFrameElement) {
280 } else if (h.show_text && !/^\s*$/.test(text)) {
281 let substrs = [[0,4]];
282 for (j = 0; j < ntokens; ++j) {
283 let m = hints_text_match(text, tokens[j]);
284 if (m == false) continue;
285 splice_range(substrs, m[0], m[1] + 2);
287 label += " " + substrs.map(function (x) {
288 return text.substring(x[0],Math.min(x[1], text.length));
289 }).join("..") + "..";
291 h.hint.textContent = label;
292 h.hint.style.display = "inline";
293 this.valid_hints.push(h);
296 if (active_number == -1)
300 select_hint: function (index) {
301 var old_index = this.current_hint_number;
302 if (index == old_index)
304 var vh = this.valid_hints;
305 if (old_index >= 1 && old_index <= vh.length) {
306 var h = vh[old_index - 1];
308 h.img_hint.style.backgroundColor = img_hint_background_color;
310 h.elem.style.backgroundColor = hint_background_color;
312 this.current_hint_number = index;
313 if (index >= 1 && index <= vh.length) {
314 var h = vh[index - 1];
316 h.img_hint.style.backgroundColor = active_img_hint_background_color;
318 h.elem.style.backgroundColor = active_hint_background_color;
319 this.last_selected_hint = h;
323 hide_hints: function () {
324 for (var i = 0, nhints = this.hints.length; i < nhints; ++i) {
325 var h = this.hints[i];
328 if (h.saved_color != null) {
329 h.elem.style.color = h.saved_color;
330 h.elem.style.backgroundColor = h.saved_bgcolor;
333 h.img_hint.style.display = "none";
334 h.hint.style.display = "none";
339 remove: function () {
340 for (var i = 0, nhints = this.hints.length; i < nhints; ++i) {
341 var h = this.hints[i];
342 if (h.visible && h.saved_color != null) {
343 h.elem.style.color = h.saved_color;
344 h.elem.style.backgroundColor = h.saved_bgcolor;
347 h.img_hint.parentNode.removeChild(h.img_hint);
348 h.hint.parentNode.removeChild(h.hint);
351 this.valid_hints = [];
355 /* Show panel with currently selected URL. */
356 function hints_url_panel (hints, window) {
357 var g = new dom_generator(window.document, XUL_NS);
359 var p = g.element("hbox", "class", "panel url", "flex", "0");
360 g.element("label", p, "value", "URL:", "class", "url-panel-label");
361 var url_value = g.element("label", p, "class", "url-panel-value",
362 "crop", "end", "flex", "1");
363 window.minibuffer.insert_before(p);
365 p.update = function () {
366 url_value.value = "";
367 if (hints.manager && hints.manager.last_selected_hint) {
370 spec = load_spec(hints.manager.last_selected_hint.elem);
373 var uri = load_spec_uri_string(spec);
374 if (uri) url_value.value = uri;
379 p.destroy = function () {
380 this.parentNode.removeChild(this);
386 define_variable("hints_display_url_panel", false,
387 "When selecting a hint, the URL can be displayed in a panel above "+
388 "the minibuffer. This is useful for confirming that the correct "+
389 "link is selected and that the URL is not evil. This option is "+
390 "most useful when hints_auto_exit_delay is long or disabled.");
399 define_keywords("$keymap", "$auto", "$hint_xpath_expression", "$multiple");
400 function hints_minibuffer_state (minibuffer, continuation, buffer) {
401 keywords(arguments, $keymap = hint_keymap, $auto);
402 basic_minibuffer_state.call(this, minibuffer, $prompt = arguments.$prompt,
403 $keymap = arguments.$keymap);
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.auto_exit = arguments.$auto ? true : false;
409 this.xpath_expr = arguments.$hint_xpath_expression;
410 this.auto_exit_timer_ID = null;
411 this.multiple = arguments.$multiple;
412 this.focused_element = buffer.focused_element;
413 this.focused_frame = buffer.focused_frame;
415 hints_minibuffer_state.prototype = {
416 constructor: hints_minibuffer_state,
417 __proto__: basic_minibuffer_state.prototype,
422 basic_minibuffer_state.prototype.load.call(this);
424 var buf = this.minibuffer.window.buffers.current;
425 this.manager = new hint_manager(buf.top_frame, this.xpath_expr,
426 this.focused_frame, this.focused_element);
428 this.manager.update_valid_hints();
430 this.url_panel.update();
432 clear_auto_exit_timer: function () {
433 var window = this.minibuffer.window;
434 if (this.auto_exit_timer_ID != null) {
435 window.clearTimeout(this.auto_exit_timer_ID);
436 this.auto_exit_timer_ID = null;
439 unload: function () {
440 this.clear_auto_exit_timer();
441 this.manager.hide_hints();
442 basic_minibuffer_state.prototype.unload.call(this);
444 destroy: function () {
445 this.clear_auto_exit_timer();
446 this.manager.remove();
448 this.url_panel.destroy();
449 basic_minibuffer_state.prototype.destroy.call(this);
451 update_minibuffer: function (m) {
452 if (this.typed_number.length > 0)
453 m.prompt = this.original_prompt + " #" + this.typed_number;
455 m.prompt = this.original_prompt;
457 this.url_panel.update();
460 handle_auto_exit: function (ambiguous) {
461 var window = this.minibuffer.window;
462 var num = this.manager.current_hint_number;
466 let delay = ambiguous ? hints_ambiguous_auto_exit_delay : hints_auto_exit_delay;
468 this.auto_exit_timer_ID = window.setTimeout(function () { hints_exit(window, s); },
472 handle_input: function (m) {
473 this.clear_auto_exit_timer();
474 this.typed_number = "";
475 this.typed_string = m._input_text;
476 this.manager.current_hint_string = this.typed_string;
477 this.manager.current_hint_number = -1;
478 this.manager.update_valid_hints();
479 if (this.manager.valid_hints.length == 1)
480 this.handle_auto_exit(false /* unambiguous */);
481 else if (this.manager.valid_hints.length > 1)
482 this.handle_auto_exit(true /* ambiguous */);
483 this.update_minibuffer(m);
487 define_variable("hints_auto_exit_delay", 0,
488 "Delay (in milliseconds) after the most recent key stroke before a "+
489 "sole matching element is automatically selected. When zero, "+
490 "automatic selection is disabled. A value of 500 is a good "+
491 "starting point for an average-speed typist.");
493 define_variable("hints_ambiguous_auto_exit_delay", 0,
494 "Delay (in milliseconds) after the most recent key stroke before the "+
495 "first of an ambiguous match is automatically selected. If this is "+
496 "set to 0, automatic selection in ambiguous matches is disabled.");
498 interactive("hints-handle-number", null,
500 let s = I.minibuffer.check_state(hints_minibuffer_state);
501 s.clear_auto_exit_timer();
502 var ch = String.fromCharCode(I.event.charCode);
503 var auto_exit_ambiguous = null; // null -> no auto exit; false -> not ambiguous; true -> ambiguous
504 /* TODO: implement number escaping */
506 s.typed_number += ch;
508 s.manager.select_hint(parseInt(s.typed_number));
509 var num = s.manager.current_hint_number;
510 if (num > 0 && num <= s.manager.valid_hints.length)
511 auto_exit_ambiguous = num * 10 > s.manager.valid_hints.length ? false : true;
514 hints_exit(I.window, s);
517 auto_exit_ambiguous = false;
519 if (auto_exit_ambiguous !== null)
520 s.handle_auto_exit(auto_exit_ambiguous);
521 s.update_minibuffer(I.minibuffer);
524 function hints_backspace (window, s) {
525 let m = window.minibuffer;
526 s.clear_auto_exit_timer();
527 if (s.typed_number.length > 0) {
528 s.typed_number = s.typed_number.substring(0, s.typed_number.length - 1);
529 var num = s.typed_number.length > 0 ? parseInt(s.typed_number) : 1;
530 s.manager.select_hint(num);
531 } else if (s.typed_string.length > 0) {
532 call_builtin_command(window, 'cmd_deleteCharBackward');
533 s.typed_string = m._input_text;
534 //m._set_selection();
535 s.manager.current_hint_string = s.typed_string;
536 s.manager.current_hint_number = -1;
537 s.manager.update_valid_hints();
539 s.update_minibuffer(m);
541 interactive("hints-backspace", null,
543 hints_backspace(I.window, I.minibuffer.check_state(hints_minibuffer_state));
546 function hints_next (window, s, count) {
547 s.clear_auto_exit_timer();
549 var cur = s.manager.current_hint_number - 1;
550 var vh = s.manager.valid_hints;
552 cur = (cur + count) % vh.length;
555 s.manager.select_hint(cur + 1);
557 s.update_minibuffer(window);
559 interactive("hints-next", null,
561 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), I.p);
564 interactive("hints-previous", null,
566 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), -I.p);
569 function hints_exit (window, s) {
570 var cur = s.manager.current_hint_number;
572 if (cur > 0 && cur <= s.manager.valid_hints.length) {
573 elem = s.manager.valid_hints[cur - 1].elem;
574 } else if (cur == 0) {
575 elem = window.buffers.current.top_frame;
578 var c = s.continuation;
579 delete s.continuation;
580 window.minibuffer.pop_state();
586 interactive("hints-exit", null,
588 hints_exit(I.window, I.minibuffer.check_state(hints_minibuffer_state));
592 define_keywords("$buffer");
593 minibuffer.prototype.read_hinted_element = function () {
595 var buf = arguments.$buffer;
596 var s = new hints_minibuffer_state(this, (yield CONTINUATION), buf, forward_keywords(arguments));
598 var result = yield SUSPEND;
599 yield co_return(result);