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 = [];
338 define_keywords("$keymap", "$auto", "$hint_xpath_expression", "$multiple");
339 function hints_minibuffer_state(continuation, buffer)
341 keywords(arguments, $keymap = hint_keymap, $auto);
342 basic_minibuffer_state.call(this, $prompt = arguments.$prompt);
343 this.original_prompt = arguments.$prompt;
344 this.continuation = continuation;
345 this.keymap = arguments.$keymap;
346 this.auto_exit = arguments.$auto ? true : false;
347 this.xpath_expr = arguments.$hint_xpath_expression;
348 this.auto_exit_timer_ID = null;
349 this.multiple = arguments.$multiple;
350 this.focused_element = buffer.focused_element;
351 this.focused_frame = buffer.focused_frame;
353 hints_minibuffer_state.prototype = {
354 __proto__: basic_minibuffer_state.prototype,
358 load : function (window) {
360 var buf = window.buffers.current;
361 this.manager = new hint_manager(buf.top_frame, this.xpath_expr,
362 this.focused_frame, this.focused_element);
364 this.manager.update_valid_hints();
366 unload : function (window) {
367 if (this.auto_exit_timer_ID) {
368 window.clearTimeout(this.auto_exit_timer_ID);
369 this.auto_exit_timer_ID = null;
371 this.manager.hide_hints();
373 destroy : function () {
374 if (this.auto_exit_timer_ID) {
375 this.manager.window.clearTimeout(this.auto_exit_timer_ID);
376 this.auto_exit_timer_ID = null;
378 this.manager.remove();
380 update_minibuffer : function (m) {
381 if (this.typed_number.length > 0)
382 m.prompt = this.original_prompt + " #" + this.typed_number;
384 m.prompt = this.original_prompt;
387 handle_auto_exit : function (m, delay) {
388 let window = m.window;
389 var num = this.manager.current_hint_number;
390 if (this.auto_exit_timer_ID)
391 window.clearTimeout(this.auto_exit_timer_ID);
393 this.auto_exit_timer_ID = window.setTimeout(function() { hints_exit(window, s); },
397 handle_input : function (m) {
400 this.typed_number = "";
401 this.typed_string = m._input_text;
402 this.manager.current_hint_string = this.typed_string;
403 this.manager.current_hint_number = -1;
404 this.manager.update_valid_hints();
405 if (this.auto_exit) {
406 if (this.manager.valid_hints.length == 1)
407 this.handle_auto_exit(m, hints_auto_exit_delay);
408 else if (this.manager.valid_hints.length > 1
409 && hints_ambiguous_auto_exit_delay > 0)
410 this.handle_auto_exit(m, hints_ambiguous_auto_exit_delay);
412 this.update_minibuffer(m);
416 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.");
418 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.");
420 interactive("hints-handle-number", null, function (I) {
421 let s = I.minibuffer.check_state(hints_minibuffer_state);
422 var ch = String.fromCharCode(I.event.charCode);
423 var auto_exit = false;
424 /* TODO: implement number escaping */
426 s.typed_number += ch;
428 s.manager.select_hint(parseInt(s.typed_number));
429 var num = s.manager.current_hint_number;
431 if (num > 0 && num <= s.manager.valid_hints.length) {
432 if (num * 10 > s.manager.valid_hints.length)
433 auto_exit = hints_auto_exit_delay;
434 else if (hints_ambiguous_auto_exit_delay > 0)
435 auto_exit = hints_ambiguous_auto_exit_delay;
439 hints_exit(I.window, s);
442 auto_exit = hints_auto_exit_delay;
446 s.handle_auto_exit(I.minibuffer, auto_exit);
447 s.update_minibuffer(I.minibuffer);
450 function hints_backspace(window, s) {
451 let m = window.minibuffer;
452 if (s.auto_exit_timer_ID) {
453 window.clearTimeout(s.auto_exit_timer_ID);
454 s.auto_exit_timer_ID = null;
456 if (s.typed_number.length > 0) {
457 s.typed_number = s.typed_number.substring(0, s.typed_number.length - 1);
458 var num = s.typed_number.length > 0 ? parseInt(s.typed_number) : 1;
459 s.manager.select_hint(num);
460 } else if (s.typed_string.length > 0) {
461 s.typed_string = s.typed_string.substring(0, s.typed_string.length - 1);
462 m._input_text = s.typed_string;
464 s.manager.current_hint_string = s.typed_string;
465 s.manager.current_hint_number = -1;
466 s.manager.update_valid_hints();
468 s.update_minibuffer(m);
470 interactive("hints-backspace", null, function (I) {
471 hints_backspace(I.window, I.minibuffer.check_state(hints_minibuffer_state));
474 function hints_next(window, s, count) {
475 if (s.auto_exit_timer_ID) {
476 window.clearTimeout(s.auto_exit_timer_ID);
477 s.auto_exit_timer_ID = null;
480 var cur = s.manager.current_hint_number - 1;
481 var vh = s.manager.valid_hints;
483 cur = (cur + count) % vh.length;
486 s.manager.select_hint(cur + 1);
488 s.update_minibuffer(window);
490 interactive("hints-next", null, function (I) {
491 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), I.p);
494 interactive("hints-previous", null, function (I) {
495 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), -I.p);
498 function hints_exit(window, s)
500 if (s.auto_exit_timer_ID) {
501 window.clearTimeout(s.auto_exit_timer_ID);
502 s.auto_exit_timer_ID = null;
504 var cur = s.manager.current_hint_number;
506 if (cur > 0 && cur <= s.manager.valid_hints.length)
507 elem = s.manager.valid_hints[cur - 1].elem;
509 elem = window.buffers.current.top_frame;
511 var c = s.continuation;
512 delete s.continuation;
513 window.minibuffer.pop_state();
519 interactive("hints-exit", null, function (I) {
520 hints_exit(I.window, I.minibuffer.check_state(hints_minibuffer_state));
524 /* FIXME: figure out why this needs to have a bunch of duplication */
526 "hints_xpath_expressions",
528 images: {def: "//img | //xhtml:img"},
529 frames: {def: "//iframe | //frame | //xhtml:iframe | //xhtml:frame"},
531 "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
532 "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
533 "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
534 "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | " +
535 "//xhtml:button | //xhtml:select"},
536 mathml: {def: "//m:math"}
538 "XPath expressions for each object class.");
540 minibuffer_auto_complete_preferences["media"] = true;
542 define_keywords("$buffer");
543 minibuffer.prototype.read_hinted_element = function () {
545 var buf = arguments.$buffer;
546 var s = new hints_minibuffer_state((yield CONTINUATION), buf, forward_keywords(arguments));
548 var result = yield SUSPEND;
549 yield co_return(result);