2 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
4 * Portions of this file are derived from Vimperator,
5 * (C) Copyright 2006-2007 Martin Stubenschrott.
7 * Use, modification, and distribution are subject to the terms specified in the
11 define_variable("active_img_hint_background_color", "#88FF00",
12 "Color for the active image hint background.");
14 define_variable("img_hint_background_color", "yellow",
15 "Color for inactive image hint backgrounds.");
17 define_variable("active_hint_background_color", "#88FF00",
18 "Color for the active hint background.");
20 define_variable("hint_background_color", "yellow",
21 "Color for the inactive hint.");
25 * Register hints style sheet
27 const hints_stylesheet = "chrome://conkeror-gui/content/hints.css";
28 register_user_stylesheet(hints_stylesheet);
31 * In the hints interaction, a node can be selected either by typing
32 * the number of its associated hint, or by typing substrings of the
33 * text content of the node. In the case of selecting by text
34 * content, multiple substrings can be given by separating them with
37 function hint_manager(window, xpath_expr, focused_frame, focused_element)
41 this.valid_hints = [];
42 this.xpath_expr = xpath_expr;
43 this.focused_frame = focused_frame;
44 this.focused_element = focused_element;
45 this.last_selected_hint = null;
48 this.generate_hints();
51 hint_manager.prototype = {
52 current_hint_string : "",
53 current_hint_number : -1,
56 * Create an initially hidden hint span element absolutely
57 * positioned over each element that matches
58 * hint_xpath_expression. This is done recursively for all frames
59 * and iframes. Information about the resulting hints are also
60 * stored in the hints array.
62 generate_hints : function () {
63 var topwin = this.window;
64 var top_height = topwin.innerHeight;
65 var top_width = topwin.innerWidth;
66 var hints = this.hints;
67 var xpath_expr = this.xpath_expr;
68 var focused_frame_hint = null, focused_element_hint = null;
69 var focused_frame = this.focused_frame;
70 var focused_element = this.focused_element;
71 function helper(window, offsetX, offsetY) {
72 var win_height = window.height;
73 var win_width = window.width;
76 var minX = offsetX < 0 ? -offsetX : 0;
77 var minY = offsetY < 0 ? -offsetY : 0;
78 var maxX = offsetX + win_width > top_width ? top_width - offsetX : top_width;
79 var maxY = offsetY + win_height > top_height ? top_height - offsetY : top_height;
81 var scrollX = window.scrollX;
82 var scrollY = window.scrollY;
84 var doc = window.document;
85 var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
86 Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE,
87 null /* existing results */);
89 var base_node = doc.createElementNS(XHTML_NS, "span");
90 base_node.className = "__conkeror_hint";
92 var fragment = doc.createDocumentFragment();
93 var rect, elem, text, node, show_text;
97 elem = res.iterateNext();
101 break; // Iterator may have been invalidated by page load activity
103 rect = elem.getBoundingClientRect();
104 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
106 let style = topwin.getComputedStyle(elem, "");
107 if (style.display == "none" || style.visibility == "hidden")
109 rect = elem.getClientRects()[0];
113 if (elem instanceof Ci.nsIDOMHTMLInputElement || elem instanceof Ci.nsIDOMHTMLTextAreaElement)
115 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
116 if (elem.selectedIndex >= 0)
117 text = elem.item(elem.selectedIndex).text;
120 } else if (elem instanceof Ci.nsIDOMHTMLFrameElement) {
121 text = elem.name ? elem.name : "";
122 } else if (/^\s*$/.test(elem.textContent) &&
123 elem.childNodes.length == 1 &&
124 elem.childNodes.item(0) instanceof Ci.nsIDOMHTMLImageElement) {
125 text = elem.childNodes.item(0).alt;
128 text = elem.textContent;
129 text = text.toLowerCase();
131 node = base_node.cloneNode(true);
132 node.style.left = (rect.left + scrollX) + "px";
133 node.style.top = (rect.top + scrollY) + "px";
134 fragment.appendChild(node);
136 let hint = {text: text,
141 show_text: show_text};
143 hint.saved_color = elem.style.color;
144 hint.saved_bgcolor = elem.style.backgroundColor;
148 if (elem == focused_element)
149 focused_element_hint = hint;
150 else if ((elem instanceof Ci.nsIDOMHTMLFrameElement ||
151 elem instanceof Ci.nsIDOMHTMLIFrameElement) &&
152 elem.contentWindow == focused_frame)
153 focused_frame_hint = hint;
155 doc.documentElement.appendChild(fragment);
157 /* Recurse into any IFRAME or FRAME elements */
158 var frametag = "frame";
160 var frames = doc.getElementsByTagName(frametag);
161 for (var i = 0; i < frames.length; ++i)
164 rect = elem.getBoundingClientRect();
165 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
167 helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
169 if (frametag == "frame") frametag = "iframe"; else break;
172 helper(topwin, 0, 0);
173 this.last_selected_hint = focused_element_hint || focused_frame_hint;
176 /* Updates valid_hints and also re-numbers and re-displays all hints. */
177 update_valid_hints : function () {
178 this.valid_hints = [];
179 var active_number = this.current_hint_number;
181 var tokens = this.current_hint_string.split(" ");
182 var rect, h, text, img_hint, doc, scrollX, scrollY;
183 var hints = this.hints;
186 for (var i = 0; i < hints.length; ++i)
190 for (var j = 0; j < tokens.length; ++j)
192 if (text.indexOf(tokens[j]) == -1)
197 h.hint.style.display = "none";
199 h.img_hint.style.display = "none";
200 if (h.saved_color != null) {
201 h.elem.style.backgroundColor = h.saved_bgcolor;
202 h.elem.style.color = h.saved_color;
209 var cur_number = this.valid_hints.length + 1;
212 if (h == this.last_selected_hint && active_number == -1)
213 this.current_hint_number = active_number = cur_number;
217 if (text.length == 0 && h.elem.firstChild &&
218 h.elem.firstChild instanceof Ci.nsIDOMHTMLImageElement)
219 img_elem = h.elem.firstChild;
220 else if (h.elem instanceof Ci.nsIDOMHTMLImageElement)
227 rect = img_elem.getBoundingClientRect();
229 doc = h.elem.ownerDocument;
230 scrollX = doc.defaultView.scrollX;
231 scrollY = doc.defaultView.scrollY;
232 img_hint = doc.createElementNS(XHTML_NS, "span");
233 img_hint.className = "__conkeror_img_hint";
234 img_hint.style.left = (rect.left + scrollX) + "px";
235 img_hint.style.top = (rect.top + scrollY) + "px";
236 img_hint.style.width = (rect.right - rect.left) + "px";
237 img_hint.style.height = (rect.bottom - rect.top) + "px";
238 h.img_hint = img_hint;
239 doc.documentElement.appendChild(img_hint);
244 var bgcolor = (active_number == cur_number) ?
245 active_img_hint_background_color : img_hint_background_color;
246 h.img_hint.style.backgroundColor = bgcolor;
247 h.img_hint.style.display = "inline";
251 if (!h.img_hint && h.elem.style)
252 h.elem.style.backgroundColor = (active_number == cur_number) ?
253 active_hint_background_color : hint_background_color;
256 h.elem.style.color = "black";
258 var label = "" + cur_number;
259 if (h.elem instanceof Ci.nsIDOMHTMLFrameElement) {
261 } else if (h.show_text && !/^\s*$/.test(text)) {
262 let substrs = [[0,4]];
263 for (j = 0; j < tokens.length; ++j)
265 let pos = text.indexOf(tokens[j]);
266 if(pos == -1) continue;
267 splice_range(substrs, pos, pos + tokens[j].length + 2);
269 label += " " + substrs.map(function(x) {
270 return text.substring(x[0],Math.min(x[1], text.length));
271 }).join("..") + "..";
273 h.hint.textContent = label;
274 h.hint.style.display = "inline";
275 this.valid_hints.push(h);
278 if (active_number == -1)
282 select_hint : function (index) {
283 var old_index = this.current_hint_number;
284 if (index == old_index)
286 var vh = this.valid_hints;
287 if (old_index >= 1 && old_index <= vh.length)
289 var h = vh[old_index - 1];
291 h.img_hint.style.backgroundColor = img_hint_background_color;
293 h.elem.style.backgroundColor = hint_background_color;
295 this.current_hint_number = index;
296 if (index >= 1 && index <= vh.length)
298 var h = vh[index - 1];
300 h.img_hint.style.backgroundColor = active_img_hint_background_color;
302 h.elem.style.backgroundColor = active_hint_background_color;
303 this.last_selected_hint = h;
307 hide_hints : function () {
308 for (var i = 0; i < this.hints.length; ++i)
310 var h = this.hints[i];
313 if (h.saved_color != null)
315 h.elem.style.color = h.saved_color;
316 h.elem.style.backgroundColor = h.saved_bgcolor;
319 h.img_hint.style.display = "none";
320 h.hint.style.display = "none";
325 remove : function () {
326 for (var i = 0; i < this.hints.length; ++i)
328 var h = this.hints[i];
329 if (h.visible && h.saved_color != null) {
330 h.elem.style.color = h.saved_color;
331 h.elem.style.backgroundColor = h.saved_bgcolor;
334 h.img_hint.parentNode.removeChild(h.img_hint);
335 h.hint.parentNode.removeChild(h.hint);
338 this.valid_hints = [];
342 /* Show panel with currently selected URL below the minibuffer. */
343 function hints_url_panel(hints, window) {
344 var g = new dom_generator(window.document, XUL_NS);
346 var p = g.element("hbox", "class", "panel url", "flex", "0");
347 g.element("label", p, "value", "URL:", "class", "url-panel-label");
348 var url_value = g.element("label", p, "class", "url-panel-value",
349 "crop", "end", "flex", "1");
350 window.minibuffer.insert_before(p);
352 p.update = function() {
353 url_value.value = "";
354 if (hints.manager && hints.manager.last_selected_hint) {
357 spec = load_spec(hints.manager.last_selected_hint.elem);
360 var uri = load_spec_uri_string(spec);
361 if (uri) url_value.value = uri;
366 p.destroy = function() {
367 this.parentNode.removeChild(this);
373 define_variable("hints_display_url_panel", false,
374 "When selecting a hint, the URL can be displayed in a panel above "+
375 "the minibuffer. This is useful for confirming that the correct "+
376 "link is selected and that the URL is not evil. This option is "+
377 "most useful when hints_auto_exit_delay is long or disabled.");
386 define_keywords("$keymap", "$auto", "$hint_xpath_expression", "$multiple");
387 function hints_minibuffer_state(continuation, buffer)
389 keywords(arguments, $keymap = hint_keymap, $auto);
390 basic_minibuffer_state.call(this, $prompt = arguments.$prompt);
391 if (hints_display_url_panel)
392 this.url_panel = hints_url_panel(this, buffer.window);
393 this.original_prompt = arguments.$prompt;
394 this.continuation = continuation;
395 this.keymap = arguments.$keymap;
396 this.auto_exit = arguments.$auto ? true : false;
397 this.xpath_expr = arguments.$hint_xpath_expression;
398 this.auto_exit_timer_ID = null;
399 this.multiple = arguments.$multiple;
400 this.focused_element = buffer.focused_element;
401 this.focused_frame = buffer.focused_frame;
403 hints_minibuffer_state.prototype = {
404 __proto__: basic_minibuffer_state.prototype,
408 load : function (window) {
410 var buf = window.buffers.current;
411 this.manager = new hint_manager(buf.top_frame, this.xpath_expr,
412 this.focused_frame, this.focused_element);
414 this.manager.update_valid_hints();
415 this.window = window;
417 this.url_panel.update();
419 clear_auto_exit_timer : function () {
420 if (this.auto_exit_timer_ID != null) {
421 this.window.clearTimeout(this.auto_exit_timer_ID);
422 this.auto_exit_timer_ID = null;
425 unload : function (window) {
426 this.clear_auto_exit_timer();
427 this.manager.hide_hints();
430 destroy : function () {
431 this.clear_auto_exit_timer();
432 this.manager.remove();
434 this.url_panel.destroy();
436 update_minibuffer : function (m) {
437 if (this.typed_number.length > 0)
438 m.prompt = this.original_prompt + " #" + this.typed_number;
440 m.prompt = this.original_prompt;
442 this.url_panel.update();
445 handle_auto_exit : function (m, ambiguous) {
446 let window = m.window;
447 var num = this.manager.current_hint_number;
451 let delay = ambiguous ? hints_ambiguous_auto_exit_delay : hints_auto_exit_delay;
453 this.auto_exit_timer_ID = window.setTimeout(function() { hints_exit(window, s); },
457 ran_minibuffer_command : function (m) {
458 this.handle_input(m);
461 handle_input : function (m) {
463 this.clear_auto_exit_timer();
464 this.typed_number = "";
465 this.typed_string = m._input_text;
466 this.manager.current_hint_string = this.typed_string;
467 this.manager.current_hint_number = -1;
468 this.manager.update_valid_hints();
469 if (this.manager.valid_hints.length == 1)
470 this.handle_auto_exit(m, false /* unambiguous */);
471 else if (this.manager.valid_hints.length > 1)
472 this.handle_auto_exit(m, true /* ambiguous */);
473 this.update_minibuffer(m);
477 define_variable("hints_auto_exit_delay", 500,
478 "Delay (in milliseconds) after the most recent key stroke before a "+
479 "sole matching element is automatically selected. If this is set to "+
480 "0, automatic selection is disabled.");
482 define_variable("hints_ambiguous_auto_exit_delay", 0,
483 "Delay (in milliseconds) after the most recent key stroke before the "+
484 "first of an ambiguous match is automatically selected. If this is "+
485 "set to 0, automatic selection in ambiguous matches is disabled.");
487 interactive("hints-handle-number", null, function (I) {
488 let s = I.minibuffer.check_state(hints_minibuffer_state);
489 s.clear_auto_exit_timer();
490 var ch = String.fromCharCode(I.event.charCode);
491 var auto_exit_ambiguous = null; // null -> no auto exit; false -> not ambiguous; true -> ambiguous
492 /* TODO: implement number escaping */
494 s.typed_number += ch;
496 s.manager.select_hint(parseInt(s.typed_number));
497 var num = s.manager.current_hint_number;
498 if (num > 0 && num <= s.manager.valid_hints.length)
499 auto_exit_ambiguous = num * 10 > s.manager.valid_hints.length ? false : true;
502 hints_exit(I.window, s);
505 auto_exit_ambiguous = false;
507 if (auto_exit_ambiguous !== null)
508 s.handle_auto_exit(I.minibuffer, auto_exit_ambiguous);
509 s.update_minibuffer(I.minibuffer);
512 function hints_backspace(window, s) {
513 let m = window.minibuffer;
514 s.clear_auto_exit_timer();
515 if (s.typed_number.length > 0) {
516 s.typed_number = s.typed_number.substring(0, s.typed_number.length - 1);
517 var num = s.typed_number.length > 0 ? parseInt(s.typed_number) : 1;
518 s.manager.select_hint(num);
519 } else if (s.typed_string.length > 0) {
520 s.typed_string = s.typed_string.substring(0, s.typed_string.length - 1);
521 m._input_text = s.typed_string;
523 s.manager.current_hint_string = s.typed_string;
524 s.manager.current_hint_number = -1;
525 s.manager.update_valid_hints();
527 s.update_minibuffer(m);
529 interactive("hints-backspace", null, function (I) {
530 hints_backspace(I.window, I.minibuffer.check_state(hints_minibuffer_state));
533 function hints_next(window, s, count) {
534 s.clear_auto_exit_timer();
536 var cur = s.manager.current_hint_number - 1;
537 var vh = s.manager.valid_hints;
539 cur = (cur + count) % vh.length;
542 s.manager.select_hint(cur + 1);
544 s.update_minibuffer(window);
546 interactive("hints-next", null, function (I) {
547 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), I.p);
550 interactive("hints-previous", null, function (I) {
551 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), -I.p);
554 function hints_exit(window, s)
556 var cur = s.manager.current_hint_number;
558 if (cur > 0 && cur <= s.manager.valid_hints.length)
559 elem = s.manager.valid_hints[cur - 1].elem;
561 elem = window.buffers.current.top_frame;
563 var c = s.continuation;
564 delete s.continuation;
565 window.minibuffer.pop_state();
571 interactive("hints-exit", null, function (I) {
572 hints_exit(I.window, I.minibuffer.check_state(hints_minibuffer_state));
576 define_keywords("$buffer");
577 minibuffer.prototype.read_hinted_element = function () {
579 var buf = arguments.$buffer;
580 var s = new hints_minibuffer_state((yield CONTINUATION), buf, forward_keywords(arguments));
582 var result = yield SUSPEND;
583 yield co_return(result);