4 * Portions are derived from Vimperator (c) 2006-2007: Martin Stubenschrott <stubenschrott@gmx.net>
7 define_variable("active_img_hint_background_color", "#88FF00", "Color for the active image hint background.");
8 define_variable("img_hint_background_color", "yellow", "Color for inactive image hint backgrounds.");
9 define_variable("active_hint_background_color", "#88FF00", "Color for the active hint background.");
10 define_variable("hint_background_color", "yellow", "Color for the inactive hint.");
13 * Register hints style sheet
15 const hints_stylesheet = "chrome://conkeror/content/hints.css";
16 function hints_register_stylesheet()
18 var uri = makeURL(hints_stylesheet);
19 var sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService);
20 sss.loadAndRegisterSheet(uri, sss.USER_SHEET);
23 function hints_unregister_stylesheet()
25 var uri = makeURL(hints_stylesheet);
26 var sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService);
27 if (sss.sheetRegistered(uri, sss.USER_SHEET))
28 ss.unregisterSheet(uri, sss.USER_SHEET);
30 hints_register_stylesheet();
33 * buffer is a content_buffer
36 function hint_manager(window, xpath_expr, focused_frame, focused_element)
40 this.valid_hints = [];
41 this.xpath_expr = xpath_expr;
42 this.focused_frame = focused_frame;
43 this.focused_element = focused_element;
44 this.last_selected_hint = null;
47 this.generate_hints();
50 hint_manager.prototype = {
51 current_hint_string : "",
52 current_hint_number : -1,
55 * Create an initially hidden hint span element absolutely
56 * positioned over each element that matches
57 * hint_xpath_expression. This is done recursively for all frames
58 * and iframes. Information about the resulting hints are also
59 * stored in the hints array.
61 generate_hints : function () {
62 var topwin = this.window;
63 var top_height = topwin.innerHeight;
64 var top_width = topwin.innerWidth;
65 var hints = this.hints;
66 var xpath_expr = this.xpath_expr;
67 var focused_frame_hint = null, focused_element_hint = null;
68 var focused_frame = this.focused_frame;
69 var focused_element = this.focused_element;
70 function helper(window, offsetX, offsetY) {
71 var win_height = window.height;
72 var win_width = window.width;
75 var minX = offsetX < 0 ? -offsetX : 0;
76 var minY = offsetY < 0 ? -offsetY : 0;
77 var maxX = offsetX + win_width > top_width ? top_width - offsetX : top_width;
78 var maxY = offsetY + win_height > top_height ? top_height - offsetY : top_height;
80 var scrollX = window.scrollX;
81 var scrollY = window.scrollY;
83 var doc = window.document;
84 var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
85 Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE,
86 null /* existing results */);
88 var base_node = doc.createElementNS(XHTML_NS, "span");
89 base_node.className = "__conkeror_hint";
91 var fragment = doc.createDocumentFragment();
92 var rect, elem, text, node;
93 while ((elem = res.iterateNext()) != null)
95 rect = elem.getBoundingClientRect();
96 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
98 rect = elem.getClientRects()[0];
101 if (elem instanceof Ci.nsIDOMHTMLInputElement || elem instanceof Ci.nsIDOMHTMLTextAreaElement)
102 text = elem.value.toLowerCase();
103 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
104 if (elem.selectedIndex >= 0)
105 text = elem.item(elem.selectedIndex).text.toLowerCase();
108 } else if (elem instanceof Ci.nsIDOMHTMLFrameElement) {
109 text = elem.name ? elem.name : "";
111 text = elem.textContent.toLowerCase();
113 node = base_node.cloneNode(true);
114 node.style.left = (rect.left + scrollX) + "px";
115 node.style.top = (rect.top + scrollY) + "px";
116 fragment.appendChild(node);
118 var hint = {text: text,
124 hint.saved_color = elem.style.color;
125 hint.saved_bgcolor = elem.style.backgroundColor;
129 if (elem == focused_element)
130 focused_element_hint = hint;
131 else if ((elem instanceof Ci.nsIDOMHTMLFrameElement ||
132 elem instanceof Ci.nsIDOMHTMLIFrameElement) &&
133 elem.contentWindow == focused_frame)
134 focused_frame_hint = hint;
136 doc.documentElement.appendChild(fragment);
138 /* Recurse into any IFRAME or FRAME elements */
139 var frametag = "frame";
141 var frames = doc.getElementsByTagName(frametag);
142 for (var i = 0; i < frames.length; ++i)
145 rect = elem.getBoundingClientRect();
146 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
148 helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
150 if (frametag == "frame") frametag = "iframe"; else break;
153 helper(topwin, 0, 0);
154 this.last_selected_hint = focused_element_hint || focused_frame_hint;
157 /* Updates valid_hints and also re-numbers and re-displays all hints. */
158 update_valid_hints : function () {
159 this.valid_hints = [];
160 var active_number = this.current_hint_number;
162 var tokens = this.current_hint_string.split(" ");
163 var rect, h, text, img_hint, doc, scrollX, scrollY;
164 var hints = this.hints;
167 for (var i = 0; i < hints.length; ++i)
171 for (var j = 0; j < tokens.length; ++j)
173 if (text.indexOf(tokens[j]) == -1)
178 h.hint.style.display = "none";
180 h.img_hint.style.display = "none";
181 if (h.saved_color != null) {
182 h.elem.style.backgroundColor = h.saved_bgcolor;
183 h.elem.style.color = h.saved_color;
190 var cur_number = this.valid_hints.length + 1;
193 if (h == this.last_selected_hint && active_number == -1)
194 this.current_hint_number = active_number = cur_number;
198 if (text.length == 0 && h.elem.firstChild &&
199 h.elem.firstChild instanceof Ci.nsIDOMHTMLImageElement)
200 img_elem = h.elem.firstChild;
201 else if (h.elem instanceof Ci.nsIDOMHTMLImageElement)
208 rect = img_elem.getBoundingClientRect();
210 doc = h.elem.ownerDocument;
211 scrollX = doc.defaultView.scrollX;
212 scrollY = doc.defaultView.scrollY;
213 img_hint = doc.createElementNS(XHTML_NS, "span");
214 img_hint.className = "__conkeror_img_hint";
215 img_hint.style.left = (rect.left + scrollX) + "px";
216 img_hint.style.top = (rect.top + scrollY) + "px";
217 img_hint.style.width = (rect.right - rect.left) + "px";
218 img_hint.style.height = (rect.bottom - rect.top) + "px";
219 h.img_hint = img_hint;
220 doc.documentElement.appendChild(img_hint);
225 var bgcolor = (active_number == cur_number) ?
226 active_img_hint_background_color : img_hint_background_color;
227 h.img_hint.style.backgroundColor = bgcolor;
228 h.img_hint.style.display = "inline";
232 if (!h.img_hint && h.elem.style)
233 h.elem.style.backgroundColor = (active_number == cur_number) ?
234 active_hint_background_color : hint_background_color;
237 h.elem.style.color = "black";
239 var label = "" + cur_number;
240 if (h.elem instanceof Ci.nsIDOMHTMLFrameElement) {
243 h.hint.textContent = label;
244 h.hint.style.display = "inline";
245 this.valid_hints.push(h);
248 if (active_number == -1)
252 select_hint : function (index) {
253 var old_index = this.current_hint_number;
254 if (index == old_index)
256 var vh = this.valid_hints;
257 if (old_index >= 1 && old_index <= vh.length)
259 var h = vh[old_index - 1];
261 h.img_hint.style.backgroundColor = img_hint_background_color;
263 h.elem.style.backgroundColor = hint_background_color;
265 this.current_hint_number = index;
266 if (index >= 1 && index <= vh.length)
268 var h = vh[index - 1];
270 h.img_hint.style.backgroundColor = active_img_hint_background_color;
272 h.elem.style.backgroundColor = active_hint_background_color;
273 this.last_selected_hint = h;
277 hide_hints : function () {
278 for (var i = 0; i < this.hints.length; ++i)
280 var h = this.hints[i];
283 if (h.saved_color != null)
285 h.elem.style.color = h.saved_color;
286 h.elem.style.backgroundColor = h.saved_bgcolor;
289 h.img_hint.style.display = "none";
290 h.hint.style.display = "none";
295 remove : function () {
296 for (var i = 0; i < this.hints.length; ++i)
298 var h = this.hints[i];
299 if (h.visible && h.saved_color != null) {
300 h.elem.style.color = h.saved_color;
301 h.elem.style.backgroundColor = h.saved_bgcolor;
304 h.img_hint.parentNode.removeChild(h.img_hint);
305 h.hint.parentNode.removeChild(h.hint);
308 this.valid_hints = [];
312 var hint_keymap = null;
314 function initialize_hint_keymap()
316 hint_keymap = new keymap();
318 initialize_hint_keymap();
327 define_keywords("$keymap", "$auto", "$hint_xpath_expression", "$multiple");
328 function hints_minibuffer_state(continuation, buffer)
330 keywords(arguments, $keymap = hint_keymap, $auto);
331 basic_minibuffer_state.call(this, $prompt = arguments.$prompt);
332 this.continuation = continuation;
333 this.keymap = arguments.$keymap;
334 this.auto_exit = arguments.$auto ? true : false;
335 this.xpath_expr = arguments.$hint_xpath_expression;
336 this.auto_exit_timer_ID = null;
337 this.multiple = arguments.$multiple;
338 this.focused_element = buffer.focused_element;
339 this.focused_frame = buffer.focused_frame;
341 hints_minibuffer_state.prototype = {
342 __proto__: basic_minibuffer_state.prototype,
346 load : function (window) {
348 var buf = window.buffers.current;
349 this.manager = new hint_manager(buf.top_frame, this.xpath_expr,
350 this.focused_frame, this.focused_element);
352 this.manager.update_valid_hints();
354 unload : function (window) {
355 if (this.auto_exit_timer_ID) {
356 window.clearTimeout(this.auto_exit_timer_ID);
357 this.auto_exit_timer_ID = null;
359 this.manager.hide_hints();
361 destroy : function () {
362 if (this.auto_exit_timer_ID) {
363 window.clearTimeout(this.auto_exit_timer_ID);
364 this.auto_exit_timer_ID = null;
366 this.manager.remove();
368 update_minibuffer : function (window) {
369 var str = this.typed_string;
370 if (this.typed_number.length > 0) {
371 str += " #" + this.typed_number;
373 window.minibuffer._input_text = str;
374 window.minibuffer._set_selection();
378 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.");
380 function hints_handle_character(window, s, e) {
381 /* Check for numbers */
382 var ch = String.fromCharCode(e.charCode);
383 var auto_exit = false;
384 /* TODO: implement number escaping */
385 if (e.charCode >= 48 && e.charCode <= 57) {
387 s.typed_number += ch;
388 s.manager.select_hint(parseInt(s.typed_number));
389 var num = s.manager.current_hint_number;
391 if (num > 0 && num <= s.manager.valid_hints.length && num * 10 > s.manager.valid_hints.length)
395 hints_exit(window, s);
403 s.typed_string += ch;
404 s.manager.current_hint_string = s.typed_string;
405 s.manager.current_hint_number = -1;
406 s.manager.update_valid_hints();
407 if (s.auto_exit && s.manager.valid_hints.length == 1)
411 if (this.auto_exit_timer_ID) {
412 window.clearTimeout(this.auto_exit_timer_ID);
414 this.auto_exit_timer_ID = window.setTimeout(function() { hints_exit(window, s); },
415 hints_auto_exit_delay);
417 s.update_minibuffer(window);
419 interactive("hints-handle-character", function (I) {
420 hints_handle_character(I.window, I.minibuffer.check_state(hints_minibuffer_state), I.event);
423 function hints_backspace(window, s) {
424 if (this.auto_exit_timer_ID) {
425 window.clearTimeout(this.auto_exit_timer_ID);
426 this.auto_exit_timer_ID = null;
428 if (s.typed_number.length > 0) {
429 s.typed_number = s.typed_number.substring(0, s.typed_number.length - 1);
430 var num = s.typed_number.length > 0 ? parseInt(s.typed_number) : 1;
431 s.manager.select_hint(num);
432 } else if (s.typed_string.length > 0) {
433 s.typed_string = s.typed_string.substring(0, s.typed_string.length - 1);
434 s.manager.current_hint_string = s.typed_string;
435 s.manager_current_hint_number = -1;
436 s.manager.update_valid_hints();
438 s.update_minibuffer(window);
440 interactive("hints-backspace", function (I) {
441 hints_backspace(I.window, I.minibuffer.check_state(hints_minibuffer_state));
444 function hints_next(window, s, count) {
445 if (this.auto_exit_timer_ID) {
446 window.clearTimeout(this.auto_exit_timer_ID);
447 this.auto_exit_timer_ID = null;
450 var cur = s.manager.current_hint_number - 1;
451 var vh = s.manager.valid_hints;
453 cur = (cur + count) % vh.length;
456 s.manager.select_hint(cur + 1);
458 s.update_minibuffer(window);
460 interactive("hints-next", function (I) {
461 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), I.p);
464 interactive("hints-previous", function (I) {
465 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), -I.p);
468 function hints_exit(window, s)
470 if (this.auto_exit_timer_ID) {
471 window.clearTimeout(this.auto_exit_timer_ID);
472 this.auto_exit_timer_ID = null;
474 var cur = s.manager.current_hint_number;
476 if (cur > 0 && cur <= s.manager.valid_hints.length)
477 elem = s.manager.valid_hints[cur - 1].elem;
479 elem = window.buffers.current.top_frame;
481 var c = s.continuation;
482 delete s.continuation;
483 window.minibuffer.pop_state();
489 interactive("hints-exit", function (I) {
490 hints_exit(I.window, I.minibuffer.check_state(hints_minibuffer_state));
494 /* FIXME: figure out why this needs to have a bunch of duplication */
496 "hints_xpath_expressions",
498 images: {def: "//img | //xhtml:img"},
499 frames: {def: "//iframe | //frame | //xhtml:iframe | //xhtml:frame"},
501 "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
502 "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
503 "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
504 "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | " +
505 "//xhtml:button | //xhtml:select"},
506 mathml: {def: "//m:math"}
508 "XPath expressions for each object class.");
510 define_keywords("$object_class", "$buffer");
511 minibuffer.prototype.read_hinted_element = function () {
513 var buf = arguments.$buffer;
514 // FIXME: clean this up and replace with proper object class declaration
515 var object_class = arguments.$object_class;
516 if (object_class == "top")
517 yield co_return(buf.top_frame);
519 if (object_class == "frames") {
520 check_buffer(buf, content_buffer);
521 var doc = buf.top_document;
522 if (doc.getElementsByTagName("frame").length == 0 &&
523 doc.getElementsByTagName("iframe").length == 0)
525 // only one frame (the top-level one), no need to use the hints system
526 yield co_return(buf.top_frame);
529 var s = new hints_minibuffer_state((yield CONTINUATION), buf, forward_keywords(arguments));
531 var result = yield SUSPEND;
532 yield co_return(result);