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 register_user_stylesheet(hints_stylesheet);
19 * buffer is a content_buffer
22 function hint_manager(window, xpath_expr, focused_frame, focused_element)
26 this.valid_hints = [];
27 this.xpath_expr = xpath_expr;
28 this.focused_frame = focused_frame;
29 this.focused_element = focused_element;
30 this.last_selected_hint = null;
33 this.generate_hints();
36 hint_manager.prototype = {
37 current_hint_string : "",
38 current_hint_number : -1,
41 * Create an initially hidden hint span element absolutely
42 * positioned over each element that matches
43 * hint_xpath_expression. This is done recursively for all frames
44 * and iframes. Information about the resulting hints are also
45 * stored in the hints array.
47 generate_hints : function () {
48 var topwin = this.window;
49 var top_height = topwin.innerHeight;
50 var top_width = topwin.innerWidth;
51 var hints = this.hints;
52 var xpath_expr = this.xpath_expr;
53 var focused_frame_hint = null, focused_element_hint = null;
54 var focused_frame = this.focused_frame;
55 var focused_element = this.focused_element;
56 function helper(window, offsetX, offsetY) {
57 var win_height = window.height;
58 var win_width = window.width;
61 var minX = offsetX < 0 ? -offsetX : 0;
62 var minY = offsetY < 0 ? -offsetY : 0;
63 var maxX = offsetX + win_width > top_width ? top_width - offsetX : top_width;
64 var maxY = offsetY + win_height > top_height ? top_height - offsetY : top_height;
66 var scrollX = window.scrollX;
67 var scrollY = window.scrollY;
69 var doc = window.document;
70 var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
71 Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE,
72 null /* existing results */);
74 var base_node = doc.createElementNS(XHTML_NS, "span");
75 base_node.className = "__conkeror_hint";
77 var fragment = doc.createDocumentFragment();
78 var rect, elem, text, node;
79 while ((elem = res.iterateNext()) != null)
81 rect = elem.getBoundingClientRect();
82 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
84 rect = elem.getClientRects()[0];
87 if (elem instanceof Ci.nsIDOMHTMLInputElement || elem instanceof Ci.nsIDOMHTMLTextAreaElement)
88 text = elem.value.toLowerCase();
89 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
90 if (elem.selectedIndex >= 0)
91 text = elem.item(elem.selectedIndex).text.toLowerCase();
94 } else if (elem instanceof Ci.nsIDOMHTMLFrameElement) {
95 text = elem.name ? elem.name : "";
97 text = elem.textContent.toLowerCase();
99 node = base_node.cloneNode(true);
100 node.style.left = (rect.left + scrollX) + "px";
101 node.style.top = (rect.top + scrollY) + "px";
102 fragment.appendChild(node);
104 var hint = {text: text,
110 hint.saved_color = elem.style.color;
111 hint.saved_bgcolor = elem.style.backgroundColor;
115 if (elem == focused_element)
116 focused_element_hint = hint;
117 else if ((elem instanceof Ci.nsIDOMHTMLFrameElement ||
118 elem instanceof Ci.nsIDOMHTMLIFrameElement) &&
119 elem.contentWindow == focused_frame)
120 focused_frame_hint = hint;
122 doc.documentElement.appendChild(fragment);
124 /* Recurse into any IFRAME or FRAME elements */
125 var frametag = "frame";
127 var frames = doc.getElementsByTagName(frametag);
128 for (var i = 0; i < frames.length; ++i)
131 rect = elem.getBoundingClientRect();
132 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
134 helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
136 if (frametag == "frame") frametag = "iframe"; else break;
139 helper(topwin, 0, 0);
140 this.last_selected_hint = focused_element_hint || focused_frame_hint;
143 /* Updates valid_hints and also re-numbers and re-displays all hints. */
144 update_valid_hints : function () {
145 this.valid_hints = [];
146 var active_number = this.current_hint_number;
148 var tokens = this.current_hint_string.split(" ");
149 var rect, h, text, img_hint, doc, scrollX, scrollY;
150 var hints = this.hints;
153 for (var i = 0; i < hints.length; ++i)
157 for (var j = 0; j < tokens.length; ++j)
159 if (text.indexOf(tokens[j]) == -1)
164 h.hint.style.display = "none";
166 h.img_hint.style.display = "none";
167 if (h.saved_color != null) {
168 h.elem.style.backgroundColor = h.saved_bgcolor;
169 h.elem.style.color = h.saved_color;
176 var cur_number = this.valid_hints.length + 1;
179 if (h == this.last_selected_hint && active_number == -1)
180 this.current_hint_number = active_number = cur_number;
184 if (text.length == 0 && h.elem.firstChild &&
185 h.elem.firstChild instanceof Ci.nsIDOMHTMLImageElement)
186 img_elem = h.elem.firstChild;
187 else if (h.elem instanceof Ci.nsIDOMHTMLImageElement)
194 rect = img_elem.getBoundingClientRect();
196 doc = h.elem.ownerDocument;
197 scrollX = doc.defaultView.scrollX;
198 scrollY = doc.defaultView.scrollY;
199 img_hint = doc.createElementNS(XHTML_NS, "span");
200 img_hint.className = "__conkeror_img_hint";
201 img_hint.style.left = (rect.left + scrollX) + "px";
202 img_hint.style.top = (rect.top + scrollY) + "px";
203 img_hint.style.width = (rect.right - rect.left) + "px";
204 img_hint.style.height = (rect.bottom - rect.top) + "px";
205 h.img_hint = img_hint;
206 doc.documentElement.appendChild(img_hint);
211 var bgcolor = (active_number == cur_number) ?
212 active_img_hint_background_color : img_hint_background_color;
213 h.img_hint.style.backgroundColor = bgcolor;
214 h.img_hint.style.display = "inline";
218 if (!h.img_hint && h.elem.style)
219 h.elem.style.backgroundColor = (active_number == cur_number) ?
220 active_hint_background_color : hint_background_color;
223 h.elem.style.color = "black";
225 var label = "" + cur_number;
226 if (h.elem instanceof Ci.nsIDOMHTMLFrameElement) {
229 h.hint.textContent = label;
230 h.hint.style.display = "inline";
231 this.valid_hints.push(h);
234 if (active_number == -1)
238 select_hint : function (index) {
239 var old_index = this.current_hint_number;
240 if (index == old_index)
242 var vh = this.valid_hints;
243 if (old_index >= 1 && old_index <= vh.length)
245 var h = vh[old_index - 1];
247 h.img_hint.style.backgroundColor = img_hint_background_color;
249 h.elem.style.backgroundColor = hint_background_color;
251 this.current_hint_number = index;
252 if (index >= 1 && index <= vh.length)
254 var h = vh[index - 1];
256 h.img_hint.style.backgroundColor = active_img_hint_background_color;
258 h.elem.style.backgroundColor = active_hint_background_color;
259 this.last_selected_hint = h;
263 hide_hints : function () {
264 for (var i = 0; i < this.hints.length; ++i)
266 var h = this.hints[i];
269 if (h.saved_color != null)
271 h.elem.style.color = h.saved_color;
272 h.elem.style.backgroundColor = h.saved_bgcolor;
275 h.img_hint.style.display = "none";
276 h.hint.style.display = "none";
281 remove : function () {
282 for (var i = 0; i < this.hints.length; ++i)
284 var h = this.hints[i];
285 if (h.visible && h.saved_color != null) {
286 h.elem.style.color = h.saved_color;
287 h.elem.style.backgroundColor = h.saved_bgcolor;
290 h.img_hint.parentNode.removeChild(h.img_hint);
291 h.hint.parentNode.removeChild(h.hint);
294 this.valid_hints = [];
305 define_keywords("$keymap", "$auto", "$hint_xpath_expression", "$multiple");
306 function hints_minibuffer_state(continuation, buffer)
308 keywords(arguments, $keymap = hint_keymap, $auto);
309 basic_minibuffer_state.call(this, $prompt = arguments.$prompt);
310 this.continuation = continuation;
311 this.keymap = arguments.$keymap;
312 this.auto_exit = arguments.$auto ? true : false;
313 this.xpath_expr = arguments.$hint_xpath_expression;
314 this.auto_exit_timer_ID = null;
315 this.multiple = arguments.$multiple;
316 this.focused_element = buffer.focused_element;
317 this.focused_frame = buffer.focused_frame;
319 hints_minibuffer_state.prototype = {
320 __proto__: basic_minibuffer_state.prototype,
324 load : function (window) {
326 var buf = window.buffers.current;
327 this.manager = new hint_manager(buf.top_frame, this.xpath_expr,
328 this.focused_frame, this.focused_element);
330 this.manager.update_valid_hints();
332 unload : function (window) {
333 if (this.auto_exit_timer_ID) {
334 window.clearTimeout(this.auto_exit_timer_ID);
335 this.auto_exit_timer_ID = null;
337 this.manager.hide_hints();
339 destroy : function () {
340 if (this.auto_exit_timer_ID) {
341 window.clearTimeout(this.auto_exit_timer_ID);
342 this.auto_exit_timer_ID = null;
344 this.manager.remove();
346 update_minibuffer : function (window) {
347 var str = this.typed_string;
348 if (this.typed_number.length > 0) {
349 str += " #" + this.typed_number;
351 window.minibuffer._input_text = str;
352 window.minibuffer._set_selection();
356 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.");
358 function hints_handle_character(window, s, e) {
359 /* Check for numbers */
360 var ch = String.fromCharCode(e.charCode);
361 var auto_exit = false;
362 /* TODO: implement number escaping */
363 if (e.charCode >= 48 && e.charCode <= 57) {
365 s.typed_number += ch;
366 s.manager.select_hint(parseInt(s.typed_number));
367 var num = s.manager.current_hint_number;
369 if (num > 0 && num <= s.manager.valid_hints.length && num * 10 > s.manager.valid_hints.length)
373 hints_exit(window, s);
381 s.typed_string += ch;
382 s.manager.current_hint_string = s.typed_string;
383 s.manager.current_hint_number = -1;
384 s.manager.update_valid_hints();
385 if (s.auto_exit && s.manager.valid_hints.length == 1)
389 if (this.auto_exit_timer_ID) {
390 window.clearTimeout(this.auto_exit_timer_ID);
392 this.auto_exit_timer_ID = window.setTimeout(function() { hints_exit(window, s); },
393 hints_auto_exit_delay);
395 s.update_minibuffer(window);
397 interactive("hints-handle-character", function (I) {
398 hints_handle_character(I.window, I.minibuffer.check_state(hints_minibuffer_state), I.event);
401 function hints_backspace(window, s) {
402 if (this.auto_exit_timer_ID) {
403 window.clearTimeout(this.auto_exit_timer_ID);
404 this.auto_exit_timer_ID = null;
406 if (s.typed_number.length > 0) {
407 s.typed_number = s.typed_number.substring(0, s.typed_number.length - 1);
408 var num = s.typed_number.length > 0 ? parseInt(s.typed_number) : 1;
409 s.manager.select_hint(num);
410 } else if (s.typed_string.length > 0) {
411 s.typed_string = s.typed_string.substring(0, s.typed_string.length - 1);
412 s.manager.current_hint_string = s.typed_string;
413 s.manager_current_hint_number = -1;
414 s.manager.update_valid_hints();
416 s.update_minibuffer(window);
418 interactive("hints-backspace", function (I) {
419 hints_backspace(I.window, I.minibuffer.check_state(hints_minibuffer_state));
422 function hints_next(window, s, count) {
423 if (this.auto_exit_timer_ID) {
424 window.clearTimeout(this.auto_exit_timer_ID);
425 this.auto_exit_timer_ID = null;
428 var cur = s.manager.current_hint_number - 1;
429 var vh = s.manager.valid_hints;
431 cur = (cur + count) % vh.length;
434 s.manager.select_hint(cur + 1);
436 s.update_minibuffer(window);
438 interactive("hints-next", function (I) {
439 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), I.p);
442 interactive("hints-previous", function (I) {
443 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), -I.p);
446 function hints_exit(window, s)
448 if (this.auto_exit_timer_ID) {
449 window.clearTimeout(this.auto_exit_timer_ID);
450 this.auto_exit_timer_ID = null;
452 var cur = s.manager.current_hint_number;
454 if (cur > 0 && cur <= s.manager.valid_hints.length)
455 elem = s.manager.valid_hints[cur - 1].elem;
457 elem = window.buffers.current.top_frame;
459 var c = s.continuation;
460 delete s.continuation;
461 window.minibuffer.pop_state();
467 interactive("hints-exit", function (I) {
468 hints_exit(I.window, I.minibuffer.check_state(hints_minibuffer_state));
472 /* FIXME: figure out why this needs to have a bunch of duplication */
474 "hints_xpath_expressions",
476 images: {def: "//img | //xhtml:img"},
477 frames: {def: "//iframe | //frame | //xhtml:iframe | //xhtml:frame"},
479 "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
480 "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
481 "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
482 "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | " +
483 "//xhtml:button | //xhtml:select"},
484 mathml: {def: "//m:math"}
486 "XPath expressions for each object class.");
488 minibuffer_auto_complete_preferences["media"] = true;
490 define_keywords("$buffer");
491 minibuffer.prototype.read_hinted_element = function () {
493 var buf = arguments.$buffer;
494 var s = new hints_minibuffer_state((yield CONTINUATION), buf, forward_keywords(arguments));
496 var result = yield SUSPEND;
497 yield co_return(result);