new module to enable editing and deleting of bookmarks
[conkeror/arlinius.git] / modules / hints.js
blob71c93f6596c0b58f55f7e3b2771183e7f592f3e3
1 /**
2 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3 * (C) Copyright 2009-2010 John J. Foerch
5 * Portions of this file are derived from Vimperator,
6 * (C) Copyright 2006-2007 Martin Stubenschrott.
8 * Use, modification, and distribution are subject to the terms specified in the
9 * COPYING file.
10 **/
12 define_variable("active_img_hint_background_color", "#88FF00",
13 "Color for the active image hint background.");
15 define_variable("img_hint_background_color", "yellow",
16 "Color for inactive image hint backgrounds.");
18 define_variable("active_hint_background_color", "#88FF00",
19 "Color for the active hint background.");
21 define_variable("hint_background_color", "yellow",
22 "Color for the inactive hint.");
25 define_variable("hint_digits", null,
26 "Null or a string of the digits to use as the counting base "+
27 "for hint numbers, starting with the digit that represents zero "+
28 "and ascending. If null, base 10 will be used with the normal "+
29 "hindu-arabic numerals.");
32 /**
33 * hints_enumerate is a generator of natural numbers in the base defined
34 * by hint_digits.
36 function hints_enumerate () {
37 var base = hint_digits.length;
38 var n = [1];
39 var p = 1;
40 while (true) {
41 yield n.map(function (x) hint_digits[x]).join("");
42 var i = p-1;
43 n[i]++;
44 while (n[i] >= base && i > 0) {
45 n[i] = 0;
46 n[--i]++;
48 if (n[0] >= base) {
49 n[0] = 0;
50 n.unshift(1);
51 p++;
56 /**
57 * hints_parse converts a string that represents a natural number to an
58 * int. When hint_digits is non-null, it defines the base for conversion.
60 function hints_parse (str) {
61 if (hint_digits) {
62 var base = hint_digits.length;
63 var n = 0;
64 for (var i = 0, p = str.length - 1; p >= 0; i++, p--) {
65 n += hint_digits.indexOf(str[i]) * Math.pow(base, p);
67 return n;
68 } else
69 return parseInt(str);
72 /**
73 * Register hints style sheet
75 const hints_stylesheet = "chrome://conkeror-gui/content/hints.css";
76 register_user_stylesheet(hints_stylesheet);
79 function hints_simple_text_match (text, pattern) {
80 var pos = text.indexOf(pattern);
81 if (pos == -1)
82 return false;
83 return [pos, pos + pattern.length];
86 define_variable('hints_text_match', hints_simple_text_match,
87 "A function which takes a string and a pattern (another string) "+
88 "and returns an array of [start, end] indices if the pattern was "+
89 "found in the string, or false if it was not.");
92 /**
93 * In the hints interaction, a node can be selected either by typing
94 * the number of its associated hint, or by typing substrings of the
95 * text content of the node. In the case of selecting by text
96 * content, multiple substrings can be given by separating them with
97 * spaces.
99 function hint_manager (window, xpath_expr, focused_frame, focused_element) {
100 this.window = window;
101 this.hints = [];
102 this.valid_hints = [];
103 this.xpath_expr = xpath_expr;
104 this.focused_frame = focused_frame;
105 this.focused_element = focused_element;
106 this.last_selected_hint = null;
108 // Generate
109 this.generate_hints();
111 hint_manager.prototype = {
112 constructor: hint_manager,
113 current_hint_string: "",
114 current_hint_number: -1,
117 * Create an initially hidden hint span element absolutely
118 * positioned over each element that matches
119 * hint_xpath_expression. This is done recursively for all frames
120 * and iframes. Information about the resulting hints are also
121 * stored in the hints array.
123 generate_hints: function () {
124 var topwin = this.window;
125 var top_height = topwin.innerHeight;
126 var top_width = topwin.innerWidth;
127 var hints = this.hints;
128 var xpath_expr = this.xpath_expr;
129 var focused_frame_hint = null, focused_element_hint = null;
130 var focused_frame = this.focused_frame;
131 var focused_element = this.focused_element;
133 function helper (window, offsetX, offsetY) {
134 var win_height = window.height;
135 var win_width = window.width;
137 // Bounds
138 var minX = offsetX < 0 ? -offsetX : 0;
139 var minY = offsetY < 0 ? -offsetY : 0;
140 var maxX = offsetX + win_width > top_width ? top_width - offsetX : top_width;
141 var maxY = offsetY + win_height > top_height ? top_height - offsetY : top_height;
143 var scrollX = window.scrollX;
144 var scrollY = window.scrollY;
146 var doc = window.document;
147 if (! doc.documentElement)
148 return;
149 var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
150 Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
151 null /* existing results */);
153 var base_node = doc.createElementNS(XHTML_NS, "span");
154 base_node.className = "__conkeror_hint";
156 var fragment = doc.createDocumentFragment();
157 var rect, elem, text, node, show_text;
158 for (var j = 0; j < res.snapshotLength; j++) {
159 elem = res.snapshotItem(j);
160 rect = elem.getBoundingClientRect();
161 if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
162 rect = { top: rect.top,
163 left: rect.left,
164 bottom: rect.bottom,
165 right: rect.right };
166 try {
167 var coords = elem.getAttribute("coords")
168 .match(/^\D*(-?\d+)\D+(-?\d+)/);
169 if (coords.length == 3) {
170 rect.left += parseInt(coords[1]);
171 rect.top += parseInt(coords[2]);
173 } catch (e) {}
175 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
176 continue;
177 let style = topwin.getComputedStyle(elem, "");
178 if (style.display == "none" || style.visibility == "hidden")
179 continue;
180 if (! (elem instanceof Ci.nsIDOMHTMLAreaElement))
181 rect = elem.getClientRects()[0];
182 if (!rect)
183 continue;
184 var nchildren = elem.childNodes.length;
185 if (elem instanceof Ci.nsIDOMHTMLAnchorElement &&
186 rect.width == 0 && rect.height == 0)
188 for (var c = 0; c < nchildren; ++c) {
189 var cc = elem.childNodes.item(c);
190 if (cc.getBoundingClientRect) {
191 rect = cc.getBoundingClientRect();
192 break;
196 show_text = false;
197 if (elem instanceof Ci.nsIDOMHTMLInputElement || elem instanceof Ci.nsIDOMHTMLTextAreaElement)
198 text = elem.value;
199 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
200 if (elem.selectedIndex >= 0)
201 text = elem.item(elem.selectedIndex).text;
202 else
203 text = "";
204 } else if (elem instanceof Ci.nsIDOMHTMLFrameElement) {
205 text = elem.name ? elem.name : "";
206 } else if (/^\s*$/.test(elem.textContent) &&
207 nchildren == 1 &&
208 elem.childNodes.item(0) instanceof Ci.nsIDOMHTMLImageElement) {
209 text = elem.childNodes.item(0).alt;
210 show_text = true;
211 } else
212 text = elem.textContent;
214 node = base_node.cloneNode(true);
215 node.style.left = (rect.left + scrollX) + "px";
216 node.style.top = (rect.top + scrollY) + "px";
217 fragment.appendChild(node);
219 let hint = { text: text,
220 ltext: text.toLowerCase(),
221 elem: elem,
222 hint: node,
223 img_hint: null,
224 visible: false,
225 show_text: show_text };
226 if (elem.style) {
227 hint.saved_color = elem.style.color;
228 hint.saved_bgcolor = elem.style.backgroundColor;
230 hints.push(hint);
232 if (elem == focused_element)
233 focused_element_hint = hint;
234 else if ((elem instanceof Ci.nsIDOMHTMLFrameElement ||
235 elem instanceof Ci.nsIDOMHTMLIFrameElement) &&
236 elem.contentWindow == focused_frame)
237 focused_frame_hint = hint;
239 doc.documentElement.appendChild(fragment);
241 /* Recurse into any IFRAME or FRAME elements */
242 var frametag = "frame";
243 while (true) {
244 var frames = doc.getElementsByTagName(frametag);
245 for (var i = 0, nframes = frames.length; i < nframes; ++i) {
246 elem = frames[i];
247 rect = elem.getBoundingClientRect();
248 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
249 continue;
250 helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
252 if (frametag == "frame") frametag = "iframe"; else break;
255 helper(topwin, 0, 0);
256 this.last_selected_hint = focused_element_hint || focused_frame_hint;
259 /* Updates valid_hints and also re-numbers and re-displays all hints. */
260 update_valid_hints: function () {
261 this.valid_hints = [];
262 var cur_number = 1;
263 if (hint_digits)
264 var number_generator = hints_enumerate();
265 var active_number = this.current_hint_number;
266 var tokens = this.current_hint_string.split(" ");
267 var case_sensitive = (this.current_hint_string !=
268 this.current_hint_string.toLowerCase());
269 var rect, text, img_hint, doc, scrollX, scrollY;
270 outer:
271 for (var i = 0, h; (h = this.hints[i]); ++i) {
272 if (case_sensitive)
273 text = h.text;
274 else
275 text = h.ltext;
276 for (var j = 0, ntokens = tokens.length; j < ntokens; ++j) {
277 if (! hints_text_match(text, tokens[j])) {
278 if (h.visible) {
279 h.visible = false;
280 h.hint.style.display = "none";
281 if (h.img_hint)
282 h.img_hint.style.display = "none";
283 if (h.saved_color != null) {
284 h.elem.style.backgroundColor = h.saved_bgcolor;
285 h.elem.style.color = h.saved_color;
288 continue outer;
292 h.visible = true;
294 if (h == this.last_selected_hint && active_number == -1)
295 this.current_hint_number = active_number = cur_number;
297 var img_elem = null;
299 if (text == "" && h.elem.firstChild &&
300 h.elem.firstChild instanceof Ci.nsIDOMHTMLImageElement)
301 img_elem = h.elem.firstChild;
302 else if (h.elem instanceof Ci.nsIDOMHTMLImageElement)
303 img_elem = h.elem;
305 if (img_elem) {
306 if (!h.img_hint) {
307 rect = img_elem.getBoundingClientRect();
308 if (rect) {
309 doc = h.elem.ownerDocument;
310 scrollX = doc.defaultView.scrollX;
311 scrollY = doc.defaultView.scrollY;
312 img_hint = doc.createElementNS(XHTML_NS, "span");
313 img_hint.className = "__conkeror_img_hint";
314 img_hint.style.left = (rect.left + scrollX) + "px";
315 img_hint.style.top = (rect.top + scrollY) + "px";
316 img_hint.style.width = (rect.right - rect.left) + "px";
317 img_hint.style.height = (rect.bottom - rect.top) + "px";
318 h.img_hint = img_hint;
319 doc.documentElement.appendChild(img_hint);
320 } else
321 img_elem = null;
323 if (img_elem) {
324 var bgcolor = (active_number == cur_number) ?
325 active_img_hint_background_color : img_hint_background_color;
326 h.img_hint.style.backgroundColor = bgcolor;
327 h.img_hint.style.display = "inline";
331 if (!h.img_hint && h.elem.style)
332 h.elem.style.backgroundColor = (active_number == cur_number) ?
333 active_hint_background_color : hint_background_color;
335 if (h.elem.style)
336 h.elem.style.color = "black";
338 var label = "";
339 if (hint_digits)
340 label = number_generator.next();
341 else
342 label += cur_number;
343 if (h.elem instanceof Ci.nsIDOMHTMLFrameElement) {
344 label += " " + text;
345 } else if (h.show_text && !/^\s*$/.test(text)) {
346 let substrs = [[0,4]];
347 for (j = 0; j < ntokens; ++j) {
348 let m = hints_text_match(text, tokens[j]);
349 if (m == false) continue;
350 splice_range(substrs, m[0], m[1] + 2);
352 label += " " + substrs.map(function (x) {
353 return text.substring(x[0],Math.min(x[1], text.length));
354 }).join("..") + "..";
356 h.hint.textContent = label;
357 h.hint.style.display = "inline";
358 this.valid_hints.push(h);
359 cur_number++;
362 if (active_number == -1)
363 this.select_hint(1);
366 select_hint: function (index) {
367 var old_index = this.current_hint_number;
368 if (index == old_index)
369 return;
370 var vh = this.valid_hints;
371 var vl = this.valid_hints.length;
372 if (old_index >= 1 && old_index <= vl) {
373 var h = vh[old_index - 1];
374 if (h.img_hint)
375 h.img_hint.style.backgroundColor = img_hint_background_color;
376 if (h.elem.style)
377 h.elem.style.backgroundColor = hint_background_color;
379 this.current_hint_number = index;
380 this.last_selected_hint = null;
381 if (index >= 1 && index <= vl) {
382 h = vh[index - 1];
383 if (h.img_hint)
384 h.img_hint.style.backgroundColor = active_img_hint_background_color;
385 if (h.elem.style)
386 h.elem.style.backgroundColor = active_hint_background_color;
387 this.last_selected_hint = h;
391 hide_hints: function () {
392 for (var i = 0, h; h = this.hints[i]; ++i) {
393 if (h.visible) {
394 h.visible = false;
395 if (h.saved_color != null) {
396 h.elem.style.color = h.saved_color;
397 h.elem.style.backgroundColor = h.saved_bgcolor;
399 if (h.img_hint)
400 h.img_hint.style.display = "none";
401 h.hint.style.display = "none";
406 remove: function () {
407 for (var i = 0, h; h = this.hints[i]; ++i) {
408 if (h.visible && h.saved_color != null) {
409 h.elem.style.color = h.saved_color;
410 h.elem.style.backgroundColor = h.saved_bgcolor;
412 if (h.img_hint)
413 h.img_hint.parentNode.removeChild(h.img_hint);
414 h.hint.parentNode.removeChild(h.hint);
416 this.hints = [];
417 this.valid_hints = [];
422 * Display the URL and other information for the currently selected node.
424 function hints_minibuffer_annotation (hints, window) {
425 this.hints = hints;
426 this.input = window.minibuffer.input_element;
428 hints_minibuffer_annotation.prototype = {
429 constructor: hints_minibuffer_annotation,
431 update: function () {
432 var s = [];
433 if (this.hints.manager && this.hints.manager.last_selected_hint) {
434 var elem = this.hints.manager.last_selected_hint.elem;
435 if (elem.hasAttribute("onmousedown") ||
436 elem.hasAttribute("onclick"))
438 s.push("[script]");
440 var tag = elem.localName.toLowerCase();
441 if ((tag == "input" || tag == "button") &&
442 elem.type == "submit" && elem.form && elem.form.action)
444 s.push((elem.form.method || "GET").toUpperCase() + ":" +
445 elem.form.action);
446 } else {
447 try {
448 var spec = load_spec(elem);
449 var uri = load_spec_uri_string(spec);
450 if (uri)
451 s.push(uri);
452 } catch (e) {}
455 this.input.annotation = s.join(" ");
458 load: function () {
459 this.input.annotate = true;
460 this.update();
463 unload: function () {
464 this.input.annotate = false;
468 define_global_mode("hints_minibuffer_annotation_mode",
469 function enable () {
470 minibuffer_annotation_mode.register(hints_minibuffer_annotation_mode);
472 function disable () {
473 minibuffer_annotation_mode.unregister(hints_minibuffer_annotation_mode);
475 $doc = "Display the URL associated with the currently selected hint in "+
476 "a minibuffer annotation.\nThis mode is most useful when "+
477 "hints_auto_exit_delay is long or disabled.");
479 hints_minibuffer_annotation_mode(true);
482 * keyword arguments:
484 * $prompt
485 * $callback
486 * $abort_callback
488 define_keywords("$keymap", "$auto", "$hint_xpath_expression", "$multiple");
489 function hints_minibuffer_state (minibuffer, continuation, buffer) {
490 keywords(arguments, $keymap = hint_keymap, $auto);
491 basic_minibuffer_state.call(this, minibuffer, $prompt = arguments.$prompt,
492 $keymap = arguments.$keymap);
493 if (hints_minibuffer_annotation_mode_enabled)
494 this.hints_minibuffer_annotation = new hints_minibuffer_annotation(this, buffer.window);
495 this.original_prompt = arguments.$prompt;
496 this.continuation = continuation;
497 this.auto_exit = arguments.$auto ? true : false;
498 this.xpath_expr = arguments.$hint_xpath_expression;
499 this.auto_exit_timer_ID = null;
500 this.multiple = arguments.$multiple;
501 this.focused_element = buffer.focused_element;
502 this.focused_frame = buffer.focused_frame;
504 hints_minibuffer_state.prototype = {
505 constructor: hints_minibuffer_state,
506 __proto__: basic_minibuffer_state.prototype,
507 manager: null,
508 typed_string: "",
509 typed_number: "",
510 load: function () {
511 basic_minibuffer_state.prototype.load.call(this);
512 if (!this.manager) {
513 var buf = this.minibuffer.window.buffers.current;
514 this.manager = new hint_manager(buf.top_frame, this.xpath_expr,
515 this.focused_frame, this.focused_element);
517 this.manager.update_valid_hints();
518 if (this.hints_minibuffer_annotation)
519 this.hints_minibuffer_annotation.load();
521 clear_auto_exit_timer: function () {
522 var window = this.minibuffer.window;
523 if (this.auto_exit_timer_ID != null) {
524 window.clearTimeout(this.auto_exit_timer_ID);
525 this.auto_exit_timer_ID = null;
528 unload: function () {
529 this.clear_auto_exit_timer();
530 this.manager.hide_hints();
531 if (this.hints_minibuffer_annotation)
532 this.hints_minibuffer_annotation.unload();
533 basic_minibuffer_state.prototype.unload.call(this);
535 destroy: function () {
536 this.clear_auto_exit_timer();
537 this.manager.remove();
538 if (this.hints_minibuffer_annotation)
539 this.hints_minibuffer_annotation.unload();
540 basic_minibuffer_state.prototype.destroy.call(this);
542 update_minibuffer: function (m) {
543 if (this.typed_number.length > 0)
544 m.prompt = this.original_prompt + " #" + this.typed_number;
545 else
546 m.prompt = this.original_prompt;
547 if (this.hints_minibuffer_annotation)
548 this.hints_minibuffer_annotation.update();
551 handle_auto_exit: function (ambiguous) {
552 var window = this.minibuffer.window;
553 var num = this.manager.current_hint_number;
554 if (!this.auto_exit)
555 return;
556 let s = this;
557 let delay = ambiguous ? hints_ambiguous_auto_exit_delay : hints_auto_exit_delay;
558 if (delay > 0)
559 this.auto_exit_timer_ID = window.setTimeout(function () { hints_exit(window, s); },
560 delay);
563 handle_input: function (m) {
564 this.clear_auto_exit_timer();
565 this.typed_number = "";
566 this.typed_string = m._input_text;
567 this.manager.current_hint_string = this.typed_string;
568 this.manager.current_hint_number = -1;
569 this.manager.update_valid_hints();
570 if (this.manager.valid_hints.length == 1)
571 this.handle_auto_exit(false /* unambiguous */);
572 else if (this.manager.valid_hints.length > 1)
573 this.handle_auto_exit(true /* ambiguous */);
574 this.update_minibuffer(m);
578 define_variable("hints_auto_exit_delay", 0,
579 "Delay (in milliseconds) after the most recent key stroke before a "+
580 "sole matching element is automatically selected. When zero, "+
581 "automatic selection is disabled. A value of 500 is a good "+
582 "starting point for an average-speed typist.");
584 define_variable("hints_ambiguous_auto_exit_delay", 0,
585 "Delay (in milliseconds) after the most recent key stroke before the "+
586 "first of an ambiguous match is automatically selected. If this is "+
587 "set to 0, automatic selection in ambiguous matches is disabled.");
590 define_key_match_predicate("match_hint_digit", "hint digit",
591 function (e) {
592 if (e.type != "keypress")
593 return false;
594 if (e.charCode == 48) //0 is special
595 return true;
596 if (hint_digits) {
597 if (hint_digits.indexOf(String.fromCharCode(e.charCode)) > -1)
598 return true;
599 } else if (e.charCode >= 49 && e.charCode <= 57)
600 return true;
601 return false;
604 interactive("hints-handle-number",
605 "This is the handler for numeric keys in hinting mode. Normally, "+
606 "that means '1' through '9' and '0', but the numeric base (and digits) "+
607 "can be configured via the user variable 'hint_digits'. No matter "+
608 "what numeric base is in effect, the character '0' is special, and "+
609 "will always be treated as a number 0, translated into the current "+
610 "base if necessary.",
611 function (I) {
612 let s = I.minibuffer.check_state(hints_minibuffer_state);
613 s.clear_auto_exit_timer();
614 var ch = String.fromCharCode(I.event.charCode);
615 if (hint_digits && ch == "0")
616 ch = hint_digits[0];
617 var auto_exit_ambiguous = null; // null -> no auto exit; false -> not ambiguous; true -> ambiguous
618 s.typed_number += ch;
619 s.manager.select_hint(hints_parse(s.typed_number));
620 var num = s.manager.current_hint_number;
621 if (num > 0 && num <= s.manager.valid_hints.length)
622 auto_exit_ambiguous = num * 10 > s.manager.valid_hints.length ? false : true;
623 else if (num == 0) {
624 if (!s.multiple) {
625 hints_exit(I.window, s);
626 return;
628 auto_exit_ambiguous = false;
630 if (auto_exit_ambiguous !== null)
631 s.handle_auto_exit(auto_exit_ambiguous);
632 s.update_minibuffer(I.minibuffer);
635 function hints_backspace (window, s) {
636 let m = window.minibuffer;
637 s.clear_auto_exit_timer();
638 var l = s.typed_number.length;
639 if (l > 0) {
640 s.typed_number = s.typed_number.substring(0, --l);
641 var num = l > 0 ? hints_parse(s.typed_number) : 1;
642 s.manager.select_hint(num);
643 } else if (s.typed_string.length > 0) {
644 call_builtin_command(window, 'cmd_deleteCharBackward');
645 s.typed_string = m._input_text;
646 //m._set_selection();
647 s.manager.current_hint_string = s.typed_string;
648 s.manager.current_hint_number = -1;
649 s.manager.update_valid_hints();
651 s.update_minibuffer(m);
653 interactive("hints-backspace", null,
654 function (I) {
655 hints_backspace(I.window, I.minibuffer.check_state(hints_minibuffer_state));
658 function hints_next (window, s, count) {
659 s.clear_auto_exit_timer();
660 s.typed_number = "";
661 var cur = s.manager.current_hint_number - 1;
662 var vh = s.manager.valid_hints;
663 var vl = s.manager.valid_hints.length;
664 if (vl > 0) {
665 cur = (cur + count) % vl;
666 if (cur < 0)
667 cur += vl;
668 s.manager.select_hint(cur + 1);
670 s.update_minibuffer(window.minibuffer);
672 interactive("hints-next", null,
673 function (I) {
674 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), I.p);
677 interactive("hints-previous", null,
678 function (I) {
679 hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), -I.p);
682 function hints_exit (window, s) {
683 var cur = s.manager.current_hint_number;
684 var elem = null;
685 if (cur > 0 && cur <= s.manager.valid_hints.length)
686 elem = s.manager.valid_hints[cur - 1].elem;
687 else if (cur == 0)
688 elem = window.buffers.current.top_frame;
689 if (elem !== null) {
690 var c = s.continuation;
691 delete s.continuation;
692 window.minibuffer.pop_state();
693 if (c)
694 c(elem);
698 interactive("hints-exit", null,
699 function (I) {
700 hints_exit(I.window, I.minibuffer.check_state(hints_minibuffer_state));
703 interactive("hints-quote-next", null,
704 function (I) {
705 I.overlay_keymap = hint_quote_next_keymap;
707 $prefix);
710 define_keywords("$buffer");
711 minibuffer.prototype.read_hinted_element = function () {
712 keywords(arguments);
713 var buf = arguments.$buffer;
714 var s = new hints_minibuffer_state(this, (yield CONTINUATION), buf, forward_keywords(arguments));
715 this.push_state(s);
716 var result = yield SUSPEND;
717 yield co_return(result);
720 provide("hints");