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/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.text("label", p, "class", "url-panel-value");
338 window.minibuffer.insert_before(p);
340 p.update = function() {
341 url_value.nodeValue = "";
342 if (hints.manager && hints.manager.last_selected_hint) {
343 var elem = hints.manager.last_selected_hint.elem;
344 if (elem instanceof Ci.nsIDOMHTMLAnchorElement)
345 url_value.nodeValue = elem;
349 p.destroy = function() {
350 this.parentNode.removeChild(this);
356 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.");
365 define_keywords("$keymap", "$auto", "$hint_xpath_expression", "$multiple");
366 function hints_minibuffer_state(continuation, buffer)
368 keywords(arguments, $keymap = hint_keymap, $auto);
369 basic_minibuffer_state.call(this, $prompt = arguments.$prompt);
370 if (hints_display_url_panel)
371 this.url_panel = hints_url_panel(this, buffer.window);
372 this.original_prompt = arguments.$prompt;
373 this.continuation = continuation;
374 this.keymap = arguments.$keymap;
375 this.auto_exit = arguments.$auto ? true : false;
376 this.xpath_expr = arguments.$hint_xpath_expression;
377 this.auto_exit_timer_ID = null;
378 this.multiple = arguments.$multiple;
379 this.focused_element = buffer.focused_element;
380 this.focused_frame = buffer.focused_frame;
382 hints_minibuffer_state.prototype = {
383 __proto__: basic_minibuffer_state.prototype,
387 load : function (window) {
389 var buf = window.buffers.current;
390 this.manager = new hint_manager(buf.top_frame, this.xpath_expr,
391 this.focused_frame, this.focused_element);
393 this.manager.update_valid_hints();
395 this.url_panel.update();
397 unload : function (window) {
398 if (this.auto_exit_timer_ID) {
399 window.clearTimeout(this.auto_exit_timer_ID);
400 this.auto_exit_timer_ID = null;
402 this.manager.hide_hints();
404 destroy : function () {
405 if (this.auto_exit_timer_ID) {
406 this.manager.window.clearTimeout(this.auto_exit_timer_ID);
407 this.auto_exit_timer_ID = null;
409 this.manager.remove();
411 this.url_panel.destroy();
413 update_minibuffer : function (m) {
414 if (this.typed_number.length > 0)
415 m.prompt = this.original_prompt + " #" + this.typed_number;
417 m.prompt = this.original_prompt;
419 this.url_panel.update();
422 handle_auto_exit : function (m, delay) {
423 let window = m.window;
424 var num = this.manager.current_hint_number;
425 if (this.auto_exit_timer_ID)
426 window.clearTimeout(this.auto_exit_timer_ID);
428 this.auto_exit_timer_ID = window.setTimeout(function() { hints_exit(window, s); },
432 handle_input : function (m) {
435 this.typed_number = "";
436 this.typed_string = m._input_text;
437 this.manager.current_hint_string = this.typed_string;
438 this.manager.current_hint_number = -1;
439 this.manager.update_valid_hints();
440 if (this.auto_exit) {
441 if (this.manager.valid_hints.length == 1)
442 this.handle_auto_exit(m, hints_auto_exit_delay);
443 else if (this.manager.valid_hints.length > 1
444 && hints_ambiguous_auto_exit_delay > 0)
445 this.handle_auto_exit(m, hints_ambiguous_auto_exit_delay);
447 this.update_minibuffer(m);
451 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.");
453 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.");
455 interactive("hints-handle-number", null, function (I) {
456 let s = I.minibuffer.check_state(hints_minibuffer_state);
457 var ch = String.fromCharCode(I.event.charCode);
458 var auto_exit = false;
459 /* TODO: implement number escaping */
461 s.typed_number += ch;
463 s.manager.select_hint(parseInt(s.typed_number));
464 var num = s.manager.current_hint_number;
466 if (num > 0 && num <= s.manager.valid_hints.length) {
467 if (num * 10 > s.manager.valid_hints.length)
468 auto_exit = hints_auto_exit_delay;
469 else if (hints_ambiguous_auto_exit_delay > 0)
470 auto_exit = hints_ambiguous_auto_exit_delay;
474 hints_exit(I.window, s);
477 auto_exit = hints_auto_exit_delay;
481 s.handle_auto_exit(I.minibuffer, auto_exit);
482 s.update_minibuffer(I.minibuffer);
485 function hints_backspace(window, s) {
486 let m = window.minibuffer;
487 if (s.auto_exit_timer_ID) {
488 window.clearTimeout(s.auto_exit_timer_ID);
489 s.auto_exit_timer_ID = null;
491 if (s.typed_number.length > 0) {
492 s.typed_number = s.typed_number.substring(0, s.typed_number.length - 1);
493 var num = s.typed_number.length > 0 ? parseInt(s.typed_number) : 1;
494 s.manager.select_hint(num);
495 } else if (s.typed_string.length > 0) {
496 s.typed_string = s.typed_string.substring(0, s.typed_string.length - 1);
497 m._input_text = s.typed_string;
499 s.manager.current_hint_string = s.typed_string;
500 s.manager.current_hint_number = -1;
501 s.manager.update_valid_hints();
503 s.update_minibuffer(m);
505 interactive("hints-backspace", null, function (I) {
506 hints_backspace(I.window, I.minibuffer.check_state(hints_minibuffer_state));
509 function hints_next(window, s, count) {
510 if (s.auto_exit_timer_ID) {
511 window.clearTimeout(s.auto_exit_timer_ID);
512 s.auto_exit_timer_ID = null;
515 var cur = s.manager.current_hint_number - 1;
516 var vh = s.manager.valid_hints;
518 cur = (cur + count) % vh.length;
521 s.manager.select_hint(cur + 1);
523 s.update_minibuffer(window);
525 interactive("hints-next", null, function (I) {
526 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), I.p);
529 interactive("hints-previous", null, function (I) {
530 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), -I.p);
533 function hints_exit(window, s)
535 if (s.auto_exit_timer_ID) {
536 window.clearTimeout(s.auto_exit_timer_ID);
537 s.auto_exit_timer_ID = null;
539 var cur = s.manager.current_hint_number;
541 if (cur > 0 && cur <= s.manager.valid_hints.length)
542 elem = s.manager.valid_hints[cur - 1].elem;
544 elem = window.buffers.current.top_frame;
546 var c = s.continuation;
547 delete s.continuation;
548 window.minibuffer.pop_state();
554 interactive("hints-exit", null, function (I) {
555 hints_exit(I.window, I.minibuffer.check_state(hints_minibuffer_state));
559 /* FIXME: figure out why this needs to have a bunch of duplication */
561 "hints_xpath_expressions",
563 images: {def: "//img | //xhtml:img"},
564 frames: {def: "//iframe | //frame | //xhtml:iframe | //xhtml:frame"},
566 "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
567 "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
568 "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
569 "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | " +
570 "//xhtml:button | //xhtml:select"},
571 mathml: {def: "//m:math"}
573 "XPath expressions for each object class.");
575 minibuffer_auto_complete_preferences["media"] = true;
577 define_keywords("$buffer");
578 minibuffer.prototype.read_hinted_element = function () {
580 var buf = arguments.$buffer;
581 var s = new hints_minibuffer_state((yield CONTINUATION), buf, forward_keywords(arguments));
583 var result = yield SUSPEND;
584 yield co_return(result);