4 * Portions are derived from Vimperator (c) 2006-2007: Martin Stubenschrott <stubenschrott@gmx.net>
8 /* FIXME: figure out why this needs to have a bunch of duplication */
9 var hint_xpath_expression =
10 "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
11 "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
12 "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
13 "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | " +
14 "//xhtml:button | //xhtml:select";
16 var active_img_hint_background_color = "#88FF00";
17 var img_hint_background_color = "yellow";
18 var active_hint_background_color = "#88FF00";
19 var hint_background_color = "yellow";
22 * buffer is a browser_buffer
25 function hint_manager(window, xpath_expr)
29 this.valid_hints = [];
30 this.xpath_expr = xpath_expr;
31 this.generate_hints();
34 hint_manager.prototype = {
35 current_hint_string : "",
36 current_hint_number : 1,
39 * Create an initially hidden hint span element absolutely
40 * positioned over each element that matches
41 * hint_xpath_expression. This is done recursively for all frames
42 * and iframes. Information about the resulting hints are also
43 * stored in the hints array.
45 generate_hints : function () {
46 var topwin = this.window;
47 var top_height = topwin.innerHeight;
48 var top_width = topwin.innerWidth;
49 var hints = this.hints;
50 var xpath_expr = this.xpath_expr;
51 function helper(window, offsetX, offsetY) {
52 var win_height = window.height;
53 var win_width = window.width;
56 var minX = offsetX < 0 ? -offsetX : 0;
57 var minY = offsetY < 0 ? -offsetY : 0;
58 var maxX = offsetX + win_width > top_width ? top_width - offsetX : top_width;
59 var maxY = offsetY + win_height > top_height ? top_height - offsetY : top_height;
61 var scrollX = window.scrollX;
62 var scrollY = window.scrollY;
64 var doc = window.document;
65 var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
66 window.XPathResult.UNORDERED_NODE_ITERATOR_TYPE,
67 null /* existing results */);
69 var base_node = doc.createElementNS(XHTML_NS, "span");
70 base_node.className = "__conkeror_hint";
72 var fragment = doc.createDocumentFragment();
73 var rect, elem, text, node;
74 while ((elem = res.iterateNext()) != null)
76 rect = elem.getBoundingClientRect();
77 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
79 rect = elem.getClientRects()[0];
82 var tagname = elem.localName;
83 if (tagname == "INPUT" || tagname == "TEXTAREA")
84 text = elem.value.toLowerCase();
85 else if (tagname == "SELECT") {
86 if (elem.selectedIndex >= 0)
87 text = elem.item(elem.selectedIndex).text.toLowerCase();
90 } else if (tagname == "FRAME") {
91 text = elem.name ? elem.name : "";
93 text = elem.textContent.toLowerCase();
95 node = base_node.cloneNode(true);
96 node.style.left = (rect.left + scrollX) + "px";
97 node.style.top = (rect.top + scrollY) + "px";
98 fragment.appendChild(node);
100 hints.push({text: text,
104 saved_color: elem.style.color,
105 saved_bgcolor: elem.style.backgroundColor,
108 doc.documentElement.appendChild(fragment);
110 /* Recurse into any IFRAME or FRAME elements */
111 var frametag = "FRAME";
113 var frames = doc.getElementsByTagName(frametag);
114 for (var i = 0; i < frames.length; ++i)
117 rect = elem.getBoundingClientRect();
118 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
120 helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
122 if (frametag == "FRAME") frametag = "IFRAME"; else break;
125 helper(topwin, 0, 0);
128 /* Updates valid_hints and also re-numbers and re-displays all hints. */
129 update_valid_hints : function () {
130 this.valid_hints = [];
131 var active_number = this.current_hint_number;
133 var tokens = this.current_hint_string.split(" ");
134 var rect, h, text, img_hint, doc, scrollX, scrollY;
135 var hints = this.hints;
138 for (var i = 0; i < hints.length; ++i)
142 for (var j = 0; j < tokens.length; ++j)
144 if (text.indexOf(tokens[j]) == -1)
149 h.hint.style.display = "none";
151 h.img_hint.style.display = "none";
152 h.elem.style.backgroundColor = h.saved_bgcolor;
153 h.elem.style.color = h.saved_color;
160 if (text.length == 0 && h.elem.firstChild && h.elem.firstChild.localName == "IMG")
164 rect = h.elem.firstChild.getBoundingClientRect();
167 doc = h.elem.ownerDocument;
168 scrollX = doc.defaultView.scrollX;
169 scrollY = doc.defaultView.scrollY;
170 img_hint = doc.createElementNS(XHTML_NS, "span");
171 img_hint.className = "__conkeror_img_hint";
172 img_hint.style.left = (rect.left + scrollX) + "px";
173 img_hint.style.top = (rect.top + scrollY) + "px";
174 img_hint.style.width = (rect.right - rect.left) + "px";
175 img_hint.style.height = (rect.bottom - rect.top) + "px";
176 h.img_hint = img_hint;
177 doc.documentElement.appendChild(img_hint);
179 h.img_hint.style.backgroundColor = (active_number == i) ?
180 active_img_hint_background_color : img_hint_background_color;
181 h.img_hint.style.display = "inline";
184 var cur_number = this.valid_hints.length + 1;
187 h.elem.style.backgroundColor = (active_number == cur_number) ?
188 active_hint_background_color : hint_background_color;
189 h.elem.style.color = "black";
190 var label = "" + cur_number;
191 if (h.elem.localName == "FRAME") {
194 h.hint.textContent = label;
195 h.hint.style.display = "inline";
196 this.valid_hints.push(h);
200 select_hint : function (index) {
201 var old_index = this.current_hint_number;
202 if (index == old_index)
204 var vh = this.valid_hints;
205 if (old_index >= 1 && old_index <= vh.length)
207 var h = vh[old_index - 1];
208 h.elem.style.backgroundColor = hint_background_color;
210 h.img_hint.style.backgroundColor = img_hint_background_color;
212 this.current_hint_number = index;
213 if (index >= 1 && index <= vh.length)
215 var h = vh[index - 1];
216 h.elem.style.backgroundColor = active_hint_background_color;
218 h.img_hint.style.backgroundColor = active_img_hint_background_color;
222 hide_hints : function () {
223 for (var i = 0; i < this.hints.length; ++i)
225 var h = this.hints[i];
228 h.elem.style.color = h.saved_color;
229 h.elem.style.backgroundColor = h.saved_bgcolor;
231 h.img_hint.style.display = "none";
232 h.hint.style.display = "none";
237 remove : function () {
238 for (var i = 0; i < this.hints.length; ++i)
240 var h = this.hints[i];
242 h.elem.style.color = h.saved_color;
243 h.elem.style.backgroundColor = h.saved_bgcolor;
246 h.img_hint.parentNode.removeChild(h.img_hint);
247 h.hint.parentNode.removeChild(h.hint);
250 this.valid_hints = [];
254 var hint_keymap = null;
256 function initialize_hint_keymap()
258 hint_keymap = new keymap();
259 define_key(hint_keymap, kbd(match_any_unmodified_key), "hints-handle-character");
260 define_key(hint_keymap, "back_space", "hints-backspace");
261 define_key(hint_keymap, "tab", "hints-next");
262 define_key(hint_keymap, "right", "hints-next");
263 define_key(hint_keymap, "down", "hints-next");
264 define_key(hint_keymap, "S-tab", "hints-previous");
265 define_key(hint_keymap, "left", "hints-previous");
266 define_key(hint_keymap, "up", "hints-previous");
267 define_key(hint_keymap, "escape", "hints-abort");
268 define_key(hint_keymap, "C-g", "hints-abort");
269 define_key(hint_keymap, "return", "hints-exit");
271 // FIXME: this should probably be some better more general
272 // property, i.e. catch_all or something.
273 define_key(hint_keymap, kbd(match_any_key), null);
275 initialize_hint_keymap();
284 define_keywords("$keymap", "$auto", "$callback", "$abort_callback", "$hint_xpath_expression");
285 function hints_minibuffer_state()
287 keywords(arguments, $keymap = hint_keymap, $hint_xpath_expression = hint_xpath_expression, $auto);
288 basic_minibuffer_state.call(this, $prompt = arguments.$prompt);
289 this.keymap = arguments.$keymap;
290 this.callback = arguments.$callback;
291 this.abort_callback = arguments.$abort_callback;
292 this.auto_exit = arguments.$auto ? true : false;
293 this.xpath_expr = arguments.$hint_xpath_expression;
294 this.auto_exit_timer_ID = null;
296 hints_minibuffer_state.prototype = {
297 __proto__: basic_minibuffer_state.prototype,
301 load : function (frame) {
303 this.manager = new hint_manager(frame.buffers.current.content_window, this.xpath_expr);
304 this.manager.update_valid_hints();
306 unload : function (frame) {
307 this.manager.hide_hints();
309 destroy : function () {
310 this.manager.remove();
312 update_minibuffer : function (frame) {
313 var str = this.typed_string;
314 if (this.typed_number.length > 0) {
315 str += " #" + this.typed_number;
317 frame.minibuffer._input_text = str;
318 frame.minibuffer._set_selection();
322 /* USER PREFERENCE */
323 var hints_auto_exit_delay = 800;
325 function hints_handle_character(frame, s, e) {
326 /* Check for numbers */
327 var ch = String.fromCharCode(e.charCode);
328 var auto_exit = false;
329 /* TODO: implement number escaping */
330 if (e.charCode >= 48 && e.charCode <= 57) {
332 s.typed_number += ch;
333 s.manager.select_hint(parseInt(s.typed_number));
334 var num = s.manager.current_hint_number;
335 if (s.auto_exit && num > 0 && num <= s.manager.valid_hints.length
336 && num * 10 > s.manager.valid_hints.length)
340 s.typed_string += ch;
341 s.manager.current_hint_string = s.typed_string;
342 s.manager_current_hint_number = 1;
343 s.manager.update_valid_hints();
344 if (s.auto_exit && s.manager.valid_hints.length == 1)
348 if (this.auto_exit_timer_ID) {
349 frame.clearTimeout(this.auto_exit_timer_ID);
351 this.auto_exit_timer_ID = frame.setTimeout(function() { hints_exit(frame, s); },
352 hints_auto_exit_delay);
354 s.update_minibuffer(frame);
356 interactive("hints-handle-character", hints_handle_character,
357 I.current_frame, I.minibuffer_state(hints_minibuffer_state), I.e);
359 function hints_backspace(frame, s) {
360 if (s.typed_number.length > 0) {
361 s.typed_number = s.typed_number.substring(0, s.typed_number.length - 1);
362 var num = s.typed_number.length > 0 ? parseInt(s.typed_number) : 1;
363 s.manager.select_hint(num);
364 } else if (s.typed_string.length > 0) {
365 s.typed_string = s.typed_string.substring(0, s.typed_string.length - 1);
366 s.manager.current_hint_string = s.typed_string;
367 s.manager_current_hint_number = 1;
368 s.manager.update_valid_hints();
370 s.update_minibuffer(frame);
372 interactive("hints-backspace", hints_backspace,
373 I.current_frame, I.minibuffer_state(hints_minibuffer_state));
375 function hints_next(frame, s, count) {
377 var cur = s.manager.current_hint_number - 1;
378 var vh = s.manager.valid_hints;
380 cur = (cur + count) % vh.length;
383 s.manager.select_hint(cur + 1);
385 s.update_minibuffer(frame);
387 interactive("hints-next", hints_next,
388 I.current_frame, I.minibuffer_state(hints_minibuffer_state), I.p);
390 interactive("hints-previous", hints_next,
391 I.current_frame, I.minibuffer_state(hints_minibuffer_state),
392 I.bind(function (x) {return -x;}, I.p));
394 function hints_abort(frame, s) {
395 if (this.auto_exit_timer_ID) {
396 frame.clearTimeout(this.auto_exit_timer_ID);
397 this.auto_exit_timer_ID = null;
399 frame.minibuffer.pop_state();
400 if (s.abort_callback)
404 interactive("hints-abort", hints_abort,
405 I.current_frame, I.minibuffer_state(hints_minibuffer_state));
407 function hints_exit(frame, s)
409 if (this.auto_exit_timer_ID) {
410 frame.clearTimeout(this.auto_exit_timer_ID);
411 this.auto_exit_timer_ID = null;
413 var cur = s.manager.current_hint_number - 1;
414 if (cur >= 0 && cur < s.manager.valid_hints.length)
416 var elem = s.manager.valid_hints[cur].elem;
417 frame.minibuffer.pop_state();
423 interactive("hints-exit", hints_exit,
424 I.current_frame, I.minibuffer_state(hints_minibuffer_state));
426 I.hinted_element = interactive_method(
427 $doc = "DOM element chosen using the hints system",
428 $async = function (ctx, cont) {
430 var s = new hints_minibuffer_state(forward_keywords(arguments), $callback = cont);
431 ctx.frame.minibuffer.push_state(s);
434 function element_focus(buffer, elem)
436 var elemTagName = elem.localName;
437 if (elemTagName == "FRAME" || elemTagName == "IFRAME")
439 elem.contentWindow.focus();
445 var doc = elem.ownerDocument;
447 var evt = doc.createEvent("MouseEvents");
451 if (elemTagName == "area")
453 var coords = elem.getAttribute("coords").split(",");
454 x = Number(coords[0]);
455 y = Number(coords[1]);
458 var doc = elem.ownerDocument;
460 evt.initMouseEvent("mouseover", true, true, doc.defaultView, 1, x, y, 0, 0, 0, 0, 0, 0, 0, null);
461 elem.dispatchEvent(evt);
463 interactive("hinted-focus-element", element_focus,
464 I.current_buffer, I.hinted_element($prompt = "Focus:"));
466 function element_follow(buffer, elem)
468 var elemTagName = elem.localName;
470 if (elemTagName == "FRAME" || elemTagName == "IFRAME")
475 if (elemTagName == "area")
477 var coords = elem.getAttribute("coords").split(",");
478 x = Number(coords[0]) + 1;
479 y = Number(coords[1]) + 1;
482 var doc = elem.ownerDocument;
483 var view = doc.defaultView;
485 var evt = doc.createEvent("MouseEvents");
486 /* FIXME: maybe use modifiers to indicate new tab/new window etc. behavior */
487 evt.initMouseEvent("mousedown", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
488 /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
489 elem.dispatchEvent(evt);
491 evt.initMouseEvent("click", true, true, view, 1, x, y, 0, 0, /*ctrl*/ 0, /*event.altKey*/0,
492 /*event.shiftKey*/ 0, /*event.metaKey*/ 0, 0, null);
493 elem.dispatchEvent(evt);
496 interactive("hinted-follow-element", element_follow,
497 I.current_buffer, I.hinted_element($prompt = "Follow:"));
501 interactive("hinted-focus-frame", element_focus,
505 $hint_xpath_expression = "//xhtml:frame | //xhtml:iframe | //iframe | //frame"