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", "Color for the active image hint background.");
12 define_variable("img_hint_background_color", "yellow", "Color for inactive image hint backgrounds.");
13 define_variable("active_hint_background_color", "#88FF00", "Color for the active hint background.");
14 define_variable("hint_background_color", "yellow", "Color for the inactive hint.");
17 * Register hints style sheet
19 const hints_stylesheet = "chrome://conkeror-gui/content/hints.css";
20 register_user_stylesheet(hints_stylesheet);
23 * buffer is a content_buffer
26 function hint_manager(window, xpath_expr, focused_frame, focused_element)
30 this.valid_hints = [];
31 this.xpath_expr = xpath_expr;
32 this.focused_frame = focused_frame;
33 this.focused_element = focused_element;
34 this.last_selected_hint = null;
37 this.generate_hints();
40 hint_manager.prototype = {
41 current_hint_string : "",
42 current_hint_number : -1,
45 * Create an initially hidden hint span element absolutely
46 * positioned over each element that matches
47 * hint_xpath_expression. This is done recursively for all frames
48 * and iframes. Information about the resulting hints are also
49 * stored in the hints array.
51 generate_hints : function () {
52 var topwin = this.window;
53 var top_height = topwin.innerHeight;
54 var top_width = topwin.innerWidth;
55 var hints = this.hints;
56 var xpath_expr = this.xpath_expr;
57 var focused_frame_hint = null, focused_element_hint = null;
58 var focused_frame = this.focused_frame;
59 var focused_element = this.focused_element;
60 function helper(window, offsetX, offsetY) {
61 var win_height = window.height;
62 var win_width = window.width;
65 var minX = offsetX < 0 ? -offsetX : 0;
66 var minY = offsetY < 0 ? -offsetY : 0;
67 var maxX = offsetX + win_width > top_width ? top_width - offsetX : top_width;
68 var maxY = offsetY + win_height > top_height ? top_height - offsetY : top_height;
70 var scrollX = window.scrollX;
71 var scrollY = window.scrollY;
73 var doc = window.document;
74 var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
75 Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE,
76 null /* existing results */);
78 var base_node = doc.createElementNS(XHTML_NS, "span");
79 base_node.className = "__conkeror_hint";
81 var fragment = doc.createDocumentFragment();
82 var rect, elem, text, node, show_text;
86 elem = res.iterateNext();
90 break; // Iterator may have been invalidated by page load activity
92 rect = elem.getBoundingClientRect();
93 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
95 let style = topwin.getComputedStyle(elem, "");
96 if (style.display == "none" || style.visibility == "hidden")
98 rect = elem.getClientRects()[0];
102 if (elem instanceof Ci.nsIDOMHTMLInputElement || elem instanceof Ci.nsIDOMHTMLTextAreaElement)
104 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
105 if (elem.selectedIndex >= 0)
106 text = elem.item(elem.selectedIndex).text;
109 } else if (elem instanceof Ci.nsIDOMHTMLFrameElement) {
110 text = elem.name ? elem.name : "";
111 } else if (/^\s*$/.test(elem.textContent) &&
112 elem.childNodes.length == 1 &&
113 elem.childNodes.item(0) instanceof Ci.nsIDOMHTMLImageElement) {
114 text = elem.childNodes.item(0).alt;
117 text = elem.textContent;
118 text = text.toLowerCase();
120 node = base_node.cloneNode(true);
121 node.style.left = (rect.left + scrollX) + "px";
122 node.style.top = (rect.top + scrollY) + "px";
123 fragment.appendChild(node);
125 let hint = {text: text,
130 show_text: show_text};
132 hint.saved_color = elem.style.color;
133 hint.saved_bgcolor = elem.style.backgroundColor;
137 if (elem == focused_element)
138 focused_element_hint = hint;
139 else if ((elem instanceof Ci.nsIDOMHTMLFrameElement ||
140 elem instanceof Ci.nsIDOMHTMLIFrameElement) &&
141 elem.contentWindow == focused_frame)
142 focused_frame_hint = hint;
144 doc.documentElement.appendChild(fragment);
146 /* Recurse into any IFRAME or FRAME elements */
147 var frametag = "frame";
149 var frames = doc.getElementsByTagName(frametag);
150 for (var i = 0; i < frames.length; ++i)
153 rect = elem.getBoundingClientRect();
154 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
156 helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
158 if (frametag == "frame") frametag = "iframe"; else break;
161 helper(topwin, 0, 0);
162 this.last_selected_hint = focused_element_hint || focused_frame_hint;
165 /* Updates valid_hints and also re-numbers and re-displays all hints. */
166 update_valid_hints : function () {
167 this.valid_hints = [];
168 var active_number = this.current_hint_number;
170 var tokens = this.current_hint_string.split(" ");
171 var rect, h, text, img_hint, doc, scrollX, scrollY;
172 var hints = this.hints;
175 for (var i = 0; i < hints.length; ++i)
179 for (var j = 0; j < tokens.length; ++j)
181 if (text.indexOf(tokens[j]) == -1)
186 h.hint.style.display = "none";
188 h.img_hint.style.display = "none";
189 if (h.saved_color != null) {
190 h.elem.style.backgroundColor = h.saved_bgcolor;
191 h.elem.style.color = h.saved_color;
198 var cur_number = this.valid_hints.length + 1;
201 if (h == this.last_selected_hint && active_number == -1)
202 this.current_hint_number = active_number = cur_number;
206 if (text.length == 0 && h.elem.firstChild &&
207 h.elem.firstChild instanceof Ci.nsIDOMHTMLImageElement)
208 img_elem = h.elem.firstChild;
209 else if (h.elem instanceof Ci.nsIDOMHTMLImageElement)
216 rect = img_elem.getBoundingClientRect();
218 doc = h.elem.ownerDocument;
219 scrollX = doc.defaultView.scrollX;
220 scrollY = doc.defaultView.scrollY;
221 img_hint = doc.createElementNS(XHTML_NS, "span");
222 img_hint.className = "__conkeror_img_hint";
223 img_hint.style.left = (rect.left + scrollX) + "px";
224 img_hint.style.top = (rect.top + scrollY) + "px";
225 img_hint.style.width = (rect.right - rect.left) + "px";
226 img_hint.style.height = (rect.bottom - rect.top) + "px";
227 h.img_hint = img_hint;
228 doc.documentElement.appendChild(img_hint);
233 var bgcolor = (active_number == cur_number) ?
234 active_img_hint_background_color : img_hint_background_color;
235 h.img_hint.style.backgroundColor = bgcolor;
236 h.img_hint.style.display = "inline";
240 if (!h.img_hint && h.elem.style)
241 h.elem.style.backgroundColor = (active_number == cur_number) ?
242 active_hint_background_color : hint_background_color;
245 h.elem.style.color = "black";
247 var label = "" + cur_number;
248 if (h.elem instanceof Ci.nsIDOMHTMLFrameElement) {
250 } else if (h.show_text && !/^\s*$/.test(text)) {
251 let substrs = [[0,4]];
252 for (j = 0; j < tokens.length; ++j)
254 let pos = text.indexOf(tokens[j]);
255 if(pos == -1) continue;
256 splice_range(substrs, pos, pos + tokens[j].length + 2);
258 label += " " + substrs.map(function(x) {
259 return text.substring(x[0],Math.min(x[1], text.length));
260 }).join("..") + "..";
262 h.hint.textContent = label;
263 h.hint.style.display = "inline";
264 this.valid_hints.push(h);
267 if (active_number == -1)
271 select_hint : function (index) {
272 var old_index = this.current_hint_number;
273 if (index == old_index)
275 var vh = this.valid_hints;
276 if (old_index >= 1 && old_index <= vh.length)
278 var h = vh[old_index - 1];
280 h.img_hint.style.backgroundColor = img_hint_background_color;
282 h.elem.style.backgroundColor = hint_background_color;
284 this.current_hint_number = index;
285 if (index >= 1 && index <= vh.length)
287 var h = vh[index - 1];
289 h.img_hint.style.backgroundColor = active_img_hint_background_color;
291 h.elem.style.backgroundColor = active_hint_background_color;
292 this.last_selected_hint = h;
296 hide_hints : function () {
297 for (var i = 0; i < this.hints.length; ++i)
299 var h = this.hints[i];
302 if (h.saved_color != null)
304 h.elem.style.color = h.saved_color;
305 h.elem.style.backgroundColor = h.saved_bgcolor;
308 h.img_hint.style.display = "none";
309 h.hint.style.display = "none";
314 remove : function () {
315 for (var i = 0; i < this.hints.length; ++i)
317 var h = this.hints[i];
318 if (h.visible && h.saved_color != null) {
319 h.elem.style.color = h.saved_color;
320 h.elem.style.backgroundColor = h.saved_bgcolor;
323 h.img_hint.parentNode.removeChild(h.img_hint);
324 h.hint.parentNode.removeChild(h.hint);
327 this.valid_hints = [];
331 /* Show panel with currently selected URL below the minibuffer. */
332 function hints_url_panel(hints, window) {
333 var g = new dom_generator(window.document, XUL_NS);
335 var p = g.element("hbox", "class", "panel url", "flex", "0");
336 g.element("label", p, "value", "URL:", "class", "url-panel-label");
337 var url_value = g.element("label", p, "class", "url-panel-value");
338 window.minibuffer.insert_before(p);
340 p.update = function() {
341 url_value.value = "";
342 if (hints.manager && hints.manager.last_selected_hint) {
343 var spec = element_get_load_spec(
344 hints.manager.last_selected_hint.elem);
346 var uri = load_spec_uri_string(spec);
347 if (uri) url_value.value = uri;
352 p.destroy = function() {
353 this.parentNode.removeChild(this);
359 define_variable("hints_display_url_panel", false, "When selecting a hint, the URL can be displayed in a panel above the minibuffer. This is useful for confirming that the correct link is selected and that the URL is not evil. This option is most useful when hints_auto_exit_delay is long or disabled.");
368 define_keywords("$keymap", "$auto", "$hint_xpath_expression", "$multiple");
369 function hints_minibuffer_state(continuation, buffer)
371 keywords(arguments, $keymap = hint_keymap, $auto);
372 basic_minibuffer_state.call(this, $prompt = arguments.$prompt);
373 if (hints_display_url_panel)
374 this.url_panel = hints_url_panel(this, buffer.window);
375 this.original_prompt = arguments.$prompt;
376 this.continuation = continuation;
377 this.keymap = arguments.$keymap;
378 this.auto_exit = arguments.$auto ? true : false;
379 this.xpath_expr = arguments.$hint_xpath_expression;
380 this.auto_exit_timer_ID = null;
381 this.multiple = arguments.$multiple;
382 this.focused_element = buffer.focused_element;
383 this.focused_frame = buffer.focused_frame;
385 hints_minibuffer_state.prototype = {
386 __proto__: basic_minibuffer_state.prototype,
390 load : function (window) {
392 var buf = window.buffers.current;
393 this.manager = new hint_manager(buf.top_frame, this.xpath_expr,
394 this.focused_frame, this.focused_element);
396 this.manager.update_valid_hints();
397 this.window = window;
399 this.url_panel.update();
401 clear_auto_exit_timer : function () {
402 if (this.auto_exit_timer_ID != null) {
403 this.window.clearTimeout(this.auto_exit_timer_ID);
404 this.auto_exit_timer_ID = null;
407 unload : function (window) {
408 this.clear_auto_exit_timer();
409 this.manager.hide_hints();
412 destroy : function () {
413 this.clear_auto_exit_timer();
414 this.manager.remove();
416 this.url_panel.destroy();
418 update_minibuffer : function (m) {
419 if (this.typed_number.length > 0)
420 m.prompt = this.original_prompt + " #" + this.typed_number;
422 m.prompt = this.original_prompt;
424 this.url_panel.update();
427 handle_auto_exit : function (m, ambiguous) {
428 let window = m.window;
429 var num = this.manager.current_hint_number;
433 let delay = ambiguous ? hints_ambiguous_auto_exit_delay : hints_auto_exit_delay;
435 this.auto_exit_timer_ID = window.setTimeout(function() { hints_exit(window, s); },
439 handle_input : function (m) {
441 this.clear_auto_exit_timer();
442 this.typed_number = "";
443 this.typed_string = m._input_text;
444 this.manager.current_hint_string = this.typed_string;
445 this.manager.current_hint_number = -1;
446 this.manager.update_valid_hints();
447 if (this.manager.valid_hints.length == 1)
448 this.handle_auto_exit(m, false /* unambiguous */);
449 else if (this.manager.valid_hints.length > 1)
450 this.handle_auto_exit(m, true /* ambiguous */);
451 this.update_minibuffer(m);
455 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.");
457 define_variable("hints_ambiguous_auto_exit_delay", 0, "Delay (in milliseconds) after the most recent key stroke before the first of an ambiguous match is automatically selected. If this is set to 0, automatic selection in ambiguous matches is disabled.");
459 interactive("hints-handle-number", null, function (I) {
460 let s = I.minibuffer.check_state(hints_minibuffer_state);
461 s.clear_auto_exit_timer();
462 var ch = String.fromCharCode(I.event.charCode);
463 var auto_exit_ambiguous = null; // null -> no auto exit; false -> not ambiguous; true -> ambiguous
464 /* TODO: implement number escaping */
466 s.typed_number += ch;
468 s.manager.select_hint(parseInt(s.typed_number));
469 var num = s.manager.current_hint_number;
470 if (num > 0 && num <= s.manager.valid_hints.length)
471 auto_exit_ambiguous = num * 10 > s.manager.valid_hints.length ? false : true;
474 hints_exit(I.window, s);
477 auto_exit_ambiguous = false;
479 if (auto_exit_ambiguous !== null)
480 s.handle_auto_exit(I.minibuffer, auto_exit_ambiguous);
481 s.update_minibuffer(I.minibuffer);
484 function hints_backspace(window, s) {
485 let m = window.minibuffer;
486 s.clear_auto_exit_timer();
487 if (s.typed_number.length > 0) {
488 s.typed_number = s.typed_number.substring(0, s.typed_number.length - 1);
489 var num = s.typed_number.length > 0 ? parseInt(s.typed_number) : 1;
490 s.manager.select_hint(num);
491 } else if (s.typed_string.length > 0) {
492 s.typed_string = s.typed_string.substring(0, s.typed_string.length - 1);
493 m._input_text = s.typed_string;
495 s.manager.current_hint_string = s.typed_string;
496 s.manager.current_hint_number = -1;
497 s.manager.update_valid_hints();
499 s.update_minibuffer(m);
501 interactive("hints-backspace", null, function (I) {
502 hints_backspace(I.window, I.minibuffer.check_state(hints_minibuffer_state));
505 function hints_next(window, s, count) {
506 s.clear_auto_exit_timer();
508 var cur = s.manager.current_hint_number - 1;
509 var vh = s.manager.valid_hints;
511 cur = (cur + count) % vh.length;
514 s.manager.select_hint(cur + 1);
516 s.update_minibuffer(window);
518 interactive("hints-next", null, function (I) {
519 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), I.p);
522 interactive("hints-previous", null, function (I) {
523 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), -I.p);
526 function hints_exit(window, s)
528 var cur = s.manager.current_hint_number;
530 if (cur > 0 && cur <= s.manager.valid_hints.length)
531 elem = s.manager.valid_hints[cur - 1].elem;
533 elem = window.buffers.current.top_frame;
535 var c = s.continuation;
536 delete s.continuation;
537 window.minibuffer.pop_state();
543 interactive("hints-exit", null, function (I) {
544 hints_exit(I.window, I.minibuffer.check_state(hints_minibuffer_state));
548 define_keywords("$buffer");
549 minibuffer.prototype.read_hinted_element = function () {
551 var buf = arguments.$buffer;
552 var s = new hints_minibuffer_state((yield CONTINUATION), buf, forward_keywords(arguments));
554 var result = yield SUSPEND;
555 yield co_return(result);