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 rect = elem.getClientRects()[0];
99 if (elem instanceof Ci.nsIDOMHTMLInputElement || elem instanceof Ci.nsIDOMHTMLTextAreaElement)
101 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
102 if (elem.selectedIndex >= 0)
103 text = elem.item(elem.selectedIndex).text;
106 } else if (elem instanceof Ci.nsIDOMHTMLFrameElement) {
107 text = elem.name ? elem.name : "";
108 } else if (/^\s*$/.test(elem.textContent) &&
109 elem.childNodes.length == 1 &&
110 elem.childNodes.item(0) instanceof Ci.nsIDOMHTMLImageElement) {
111 text = elem.childNodes.item(0).alt;
114 text = elem.textContent;
115 text = text.toLowerCase();
117 node = base_node.cloneNode(true);
118 node.style.left = (rect.left + scrollX) + "px";
119 node.style.top = (rect.top + scrollY) + "px";
120 fragment.appendChild(node);
122 let hint = {text: text,
127 show_text: show_text};
129 hint.saved_color = elem.style.color;
130 hint.saved_bgcolor = elem.style.backgroundColor;
134 if (elem == focused_element)
135 focused_element_hint = hint;
136 else if ((elem instanceof Ci.nsIDOMHTMLFrameElement ||
137 elem instanceof Ci.nsIDOMHTMLIFrameElement) &&
138 elem.contentWindow == focused_frame)
139 focused_frame_hint = hint;
141 doc.documentElement.appendChild(fragment);
143 /* Recurse into any IFRAME or FRAME elements */
144 var frametag = "frame";
146 var frames = doc.getElementsByTagName(frametag);
147 for (var i = 0; i < frames.length; ++i)
150 rect = elem.getBoundingClientRect();
151 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
153 helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
155 if (frametag == "frame") frametag = "iframe"; else break;
158 helper(topwin, 0, 0);
159 this.last_selected_hint = focused_element_hint || focused_frame_hint;
162 /* Updates valid_hints and also re-numbers and re-displays all hints. */
163 update_valid_hints : function () {
164 this.valid_hints = [];
165 var active_number = this.current_hint_number;
167 var tokens = this.current_hint_string.split(" ");
168 var rect, h, text, img_hint, doc, scrollX, scrollY;
169 var hints = this.hints;
172 for (var i = 0; i < hints.length; ++i)
176 for (var j = 0; j < tokens.length; ++j)
178 if (text.indexOf(tokens[j]) == -1)
183 h.hint.style.display = "none";
185 h.img_hint.style.display = "none";
186 if (h.saved_color != null) {
187 h.elem.style.backgroundColor = h.saved_bgcolor;
188 h.elem.style.color = h.saved_color;
195 var cur_number = this.valid_hints.length + 1;
198 if (h == this.last_selected_hint && active_number == -1)
199 this.current_hint_number = active_number = cur_number;
203 if (text.length == 0 && h.elem.firstChild &&
204 h.elem.firstChild instanceof Ci.nsIDOMHTMLImageElement)
205 img_elem = h.elem.firstChild;
206 else if (h.elem instanceof Ci.nsIDOMHTMLImageElement)
213 rect = img_elem.getBoundingClientRect();
215 doc = h.elem.ownerDocument;
216 scrollX = doc.defaultView.scrollX;
217 scrollY = doc.defaultView.scrollY;
218 img_hint = doc.createElementNS(XHTML_NS, "span");
219 img_hint.className = "__conkeror_img_hint";
220 img_hint.style.left = (rect.left + scrollX) + "px";
221 img_hint.style.top = (rect.top + scrollY) + "px";
222 img_hint.style.width = (rect.right - rect.left) + "px";
223 img_hint.style.height = (rect.bottom - rect.top) + "px";
224 h.img_hint = img_hint;
225 doc.documentElement.appendChild(img_hint);
230 var bgcolor = (active_number == cur_number) ?
231 active_img_hint_background_color : img_hint_background_color;
232 h.img_hint.style.backgroundColor = bgcolor;
233 h.img_hint.style.display = "inline";
237 if (!h.img_hint && h.elem.style)
238 h.elem.style.backgroundColor = (active_number == cur_number) ?
239 active_hint_background_color : hint_background_color;
242 h.elem.style.color = "black";
244 var label = "" + cur_number;
245 if (h.elem instanceof Ci.nsIDOMHTMLFrameElement) {
247 } else if (h.show_text && !/^\s*$/.test(text)) {
248 let substrs = [[0,4]];
249 for (j = 0; j < tokens.length; ++j)
251 let pos = text.indexOf(tokens[j]);
252 if(pos == -1) continue;
253 splice_range(substrs, pos, pos + tokens[j].length + 2);
255 label += " " + substrs.map(function(x) {
256 return text.substring(x[0],Math.min(x[1], text.length));
257 }).join("..") + "..";
259 h.hint.textContent = label;
260 h.hint.style.display = "inline";
261 this.valid_hints.push(h);
264 if (active_number == -1)
268 select_hint : function (index) {
269 var old_index = this.current_hint_number;
270 if (index == old_index)
272 var vh = this.valid_hints;
273 if (old_index >= 1 && old_index <= vh.length)
275 var h = vh[old_index - 1];
277 h.img_hint.style.backgroundColor = img_hint_background_color;
279 h.elem.style.backgroundColor = hint_background_color;
281 this.current_hint_number = index;
282 if (index >= 1 && index <= vh.length)
284 var h = vh[index - 1];
286 h.img_hint.style.backgroundColor = active_img_hint_background_color;
288 h.elem.style.backgroundColor = active_hint_background_color;
289 this.last_selected_hint = h;
293 hide_hints : function () {
294 for (var i = 0; i < this.hints.length; ++i)
296 var h = this.hints[i];
299 if (h.saved_color != null)
301 h.elem.style.color = h.saved_color;
302 h.elem.style.backgroundColor = h.saved_bgcolor;
305 h.img_hint.style.display = "none";
306 h.hint.style.display = "none";
311 remove : function () {
312 for (var i = 0; i < this.hints.length; ++i)
314 var h = this.hints[i];
315 if (h.visible && h.saved_color != null) {
316 h.elem.style.color = h.saved_color;
317 h.elem.style.backgroundColor = h.saved_bgcolor;
320 h.img_hint.parentNode.removeChild(h.img_hint);
321 h.hint.parentNode.removeChild(h.hint);
324 this.valid_hints = [];
335 define_keywords("$keymap", "$auto", "$hint_xpath_expression", "$multiple");
336 function hints_minibuffer_state(continuation, buffer)
338 keywords(arguments, $keymap = hint_keymap, $auto);
339 basic_minibuffer_state.call(this, $prompt = arguments.$prompt);
340 this.original_prompt = arguments.$prompt;
341 this.continuation = continuation;
342 this.keymap = arguments.$keymap;
343 this.auto_exit = arguments.$auto ? true : false;
344 this.xpath_expr = arguments.$hint_xpath_expression;
345 this.auto_exit_timer_ID = null;
346 this.multiple = arguments.$multiple;
347 this.focused_element = buffer.focused_element;
348 this.focused_frame = buffer.focused_frame;
350 hints_minibuffer_state.prototype = {
351 __proto__: basic_minibuffer_state.prototype,
355 load : function (window) {
357 var buf = window.buffers.current;
358 this.manager = new hint_manager(buf.top_frame, this.xpath_expr,
359 this.focused_frame, this.focused_element);
361 this.manager.update_valid_hints();
363 unload : function (window) {
364 if (this.auto_exit_timer_ID) {
365 window.clearTimeout(this.auto_exit_timer_ID);
366 this.auto_exit_timer_ID = null;
368 this.manager.hide_hints();
370 destroy : function () {
371 if (this.auto_exit_timer_ID) {
372 this.manager.window.clearTimeout(this.auto_exit_timer_ID);
373 this.auto_exit_timer_ID = null;
375 this.manager.remove();
377 update_minibuffer : function (m) {
378 if (this.typed_number.length > 0)
379 m.prompt = this.original_prompt + " #" + this.typed_number;
381 m.prompt = this.original_prompt;
384 handle_auto_exit : function (m, delay) {
385 let window = m.window;
386 var num = this.manager.current_hint_number;
387 if (this.auto_exit_timer_ID)
388 window.clearTimeout(this.auto_exit_timer_ID);
390 this.auto_exit_timer_ID = window.setTimeout(function() { hints_exit(window, s); },
394 handle_input : function (m) {
397 this.typed_number = "";
398 this.typed_string = m._input_text;
399 this.manager.current_hint_string = this.typed_string;
400 this.manager.current_hint_number = -1;
401 this.manager.update_valid_hints();
402 if (this.auto_exit) {
403 if (this.manager.valid_hints.length == 1)
404 this.handle_auto_exit(m, hints_auto_exit_delay);
405 else if (this.manager.valid_hints.length > 1
406 && hints_ambiguous_auto_exit_delay > 0)
407 this.handle_auto_exit(m, hints_ambiguous_auto_exit_delay);
409 this.update_minibuffer(m);
413 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.");
415 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.");
417 interactive("hints-handle-number", null, function (I) {
418 let s = I.minibuffer.check_state(hints_minibuffer_state);
419 var ch = String.fromCharCode(I.event.charCode);
420 var auto_exit = false;
421 /* TODO: implement number escaping */
423 s.typed_number += ch;
425 s.manager.select_hint(parseInt(s.typed_number));
426 var num = s.manager.current_hint_number;
428 if (num > 0 && num <= s.manager.valid_hints.length) {
429 if (num * 10 > s.manager.valid_hints.length)
430 auto_exit = hints_auto_exit_delay;
431 else if (hints_ambiguous_auto_exit_delay > 0)
432 auto_exit = hints_ambiguous_auto_exit_delay;
436 hints_exit(I.window, s);
439 auto_exit = hints_auto_exit_delay;
443 s.handle_auto_exit(I.minibuffer, auto_exit);
444 s.update_minibuffer(I.minibuffer);
447 function hints_backspace(window, s) {
448 let m = window.minibuffer;
449 if (s.auto_exit_timer_ID) {
450 window.clearTimeout(s.auto_exit_timer_ID);
451 s.auto_exit_timer_ID = null;
453 if (s.typed_number.length > 0) {
454 s.typed_number = s.typed_number.substring(0, s.typed_number.length - 1);
455 var num = s.typed_number.length > 0 ? parseInt(s.typed_number) : 1;
456 s.manager.select_hint(num);
457 } else if (s.typed_string.length > 0) {
458 s.typed_string = s.typed_string.substring(0, s.typed_string.length - 1);
459 m._input_text = s.typed_string;
461 s.manager.current_hint_string = s.typed_string;
462 s.manager.current_hint_number = -1;
463 s.manager.update_valid_hints();
465 s.update_minibuffer(m);
467 interactive("hints-backspace", null, function (I) {
468 hints_backspace(I.window, I.minibuffer.check_state(hints_minibuffer_state));
471 function hints_next(window, s, count) {
472 if (s.auto_exit_timer_ID) {
473 window.clearTimeout(s.auto_exit_timer_ID);
474 s.auto_exit_timer_ID = null;
477 var cur = s.manager.current_hint_number - 1;
478 var vh = s.manager.valid_hints;
480 cur = (cur + count) % vh.length;
483 s.manager.select_hint(cur + 1);
485 s.update_minibuffer(window);
487 interactive("hints-next", null, function (I) {
488 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), I.p);
491 interactive("hints-previous", null, function (I) {
492 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), -I.p);
495 function hints_exit(window, s)
497 if (s.auto_exit_timer_ID) {
498 window.clearTimeout(s.auto_exit_timer_ID);
499 s.auto_exit_timer_ID = null;
501 var cur = s.manager.current_hint_number;
503 if (cur > 0 && cur <= s.manager.valid_hints.length)
504 elem = s.manager.valid_hints[cur - 1].elem;
506 elem = window.buffers.current.top_frame;
508 var c = s.continuation;
509 delete s.continuation;
510 window.minibuffer.pop_state();
516 interactive("hints-exit", null, function (I) {
517 hints_exit(I.window, I.minibuffer.check_state(hints_minibuffer_state));
521 /* FIXME: figure out why this needs to have a bunch of duplication */
523 "hints_xpath_expressions",
525 images: {def: "//img | //xhtml:img"},
526 frames: {def: "//iframe | //frame | //xhtml:iframe | //xhtml:frame"},
528 "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
529 "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
530 "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
531 "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | " +
532 "//xhtml:button | //xhtml:select"},
533 mathml: {def: "//m:math"}
535 "XPath expressions for each object class.");
537 minibuffer_auto_complete_preferences["media"] = true;
539 define_keywords("$buffer");
540 minibuffer.prototype.read_hinted_element = function () {
542 var buf = arguments.$buffer;
543 var s = new hints_minibuffer_state((yield CONTINUATION), buf, forward_keywords(arguments));
545 var result = yield SUSPEND;
546 yield co_return(result);