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
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.");
33 * hints_enumerate is a generator of natural numbers in the base defined
36 function hints_enumerate () {
37 var base
= hint_digits
.length
;
41 yield n
.map(function (x
) hint_digits
[x
]).join("");
44 while (n
[i
] >= base
&& i
> 0) {
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
) {
62 var base
= hint_digits
.length
;
64 for (var i
= 0, p
= str
.length
- 1; p
>= 0; i
++, p
--) {
65 n
+= hint_digits
.indexOf(str
[i
]) * Math
.pow(base
, p
);
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
);
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.");
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
99 function hint_manager (window
, xpath_expr
, focused_frame
, focused_element
) {
100 this.window
= window
;
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;
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
;
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
)
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
,
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]);
175 if (!rect
|| rect
.left
> maxX
|| rect
.right
< minX
|| rect
.top
> maxY
|| rect
.bottom
< minY
)
177 let style
= topwin
.getComputedStyle(elem
, "");
178 if (style
.display
== "none" || style
.visibility
== "hidden")
180 if (! (elem
instanceof Ci
.nsIDOMHTMLAreaElement
))
181 rect
= elem
.getClientRects()[0];
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();
197 if (elem
instanceof Ci
.nsIDOMHTMLInputElement
|| elem
instanceof Ci
.nsIDOMHTMLTextAreaElement
)
199 else if (elem
instanceof Ci
.nsIDOMHTMLSelectElement
) {
200 if (elem
.selectedIndex
>= 0)
201 text
= elem
.item(elem
.selectedIndex
).text
;
204 } else if (elem
instanceof Ci
.nsIDOMHTMLFrameElement
) {
205 text
= elem
.name
? elem
.name
: "";
206 } else if (/^\s*$/.test(elem
.textContent
) &&
208 elem
.childNodes
.item(0) instanceof Ci
.nsIDOMHTMLImageElement
) {
209 text
= elem
.childNodes
.item(0).alt
;
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(),
225 show_text
: show_text
};
227 hint
.saved_color
= elem
.style
.color
;
228 hint
.saved_bgcolor
= elem
.style
.backgroundColor
;
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";
244 var frames
= doc
.getElementsByTagName(frametag
);
245 for (var i
= 0, nframes
= frames
.length
; i
< nframes
; ++i
) {
247 rect
= elem
.getBoundingClientRect();
248 if (!rect
|| rect
.left
> maxX
|| rect
.right
< minX
|| rect
.top
> maxY
|| rect
.bottom
< minY
)
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
= [];
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
;
271 for (var i
= 0, h
; (h
= this.hints
[i
]); ++i
) {
276 for (var j
= 0, ntokens
= tokens
.length
; j
< ntokens
; ++j
) {
277 if (! hints_text_match(text
, tokens
[j
])) {
280 h
.hint
.style
.display
= "none";
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
;
294 if (h
== this.last_selected_hint
&& active_number
== -1)
295 this.current_hint_number
= active_number
= cur_number
;
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
)
307 rect
= img_elem
.getBoundingClientRect();
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
);
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
;
336 h
.elem
.style
.color
= "black";
340 label
= number_generator
.next();
343 if (h
.elem
instanceof Ci
.nsIDOMHTMLFrameElement
) {
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
);
362 if (active_number
== -1)
366 select_hint: function (index
) {
367 var old_index
= this.current_hint_number
;
368 if (index
== old_index
)
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];
375 h
.img_hint
.style
.backgroundColor
= img_hint_background_color
;
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
) {
384 h
.img_hint
.style
.backgroundColor
= active_img_hint_background_color
;
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
) {
395 if (h
.saved_color
!= null) {
396 h
.elem
.style
.color
= h
.saved_color
;
397 h
.elem
.style
.backgroundColor
= h
.saved_bgcolor
;
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
;
413 h
.img_hint
.parentNode
.removeChild(h
.img_hint
);
414 h
.hint
.parentNode
.removeChild(h
.hint
);
417 this.valid_hints
= [];
422 * Display the URL and other information for the currently selected node.
424 function hints_minibuffer_annotation (hints
, window
) {
426 this.input
= window
.minibuffer
.input_element
;
428 hints_minibuffer_annotation
.prototype = {
429 constructor: hints_minibuffer_annotation
,
431 update: function () {
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"))
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() + ":" +
448 var spec
= load_spec(elem
);
449 var uri
= load_spec_uri_string(spec
);
455 this.input
.annotation
= s
.join(" ");
459 this.input
.annotate
= true;
463 unload: function () {
464 this.input
.annotate
= false;
468 define_global_mode("hints_minibuffer_annotation_mode",
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);
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,
511 basic_minibuffer_state
.prototype.load
.call(this);
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
;
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
;
557 let delay
= ambiguous
? hints_ambiguous_auto_exit_delay
: hints_auto_exit_delay
;
559 this.auto_exit_timer_ID
= window
.setTimeout(function () { hints_exit(window
, s
); },
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",
592 if (e
.type
!= "keypress")
594 if (e
.charCode
== 48) //0 is special
597 if (hint_digits
.indexOf(String
.fromCharCode(e
.charCode
)) > -1)
599 } else if (e
.charCode
>= 49 && e
.charCode
<= 57)
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.",
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")
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;
625 hints_exit(I
.window
, s
);
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
;
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,
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();
661 var cur
= s
.manager
.current_hint_number
- 1;
662 var vh
= s
.manager
.valid_hints
;
663 var vl
= s
.manager
.valid_hints
.length
;
665 cur
= (cur
+ count
) % vl
;
668 s
.manager
.select_hint(cur
+ 1);
670 s
.update_minibuffer(window
.minibuffer
);
672 interactive("hints-next", null,
674 hints_next(I
.window
, I
.minibuffer
.check_state(hints_minibuffer_state
), I
.p
);
677 interactive("hints-previous", null,
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
;
685 if (cur
> 0 && cur
<= s
.manager
.valid_hints
.length
)
686 elem
= s
.manager
.valid_hints
[cur
- 1].elem
;
688 elem
= window
.buffers
.current
.top_frame
;
690 var c
= s
.continuation
;
691 delete s
.continuation
;
692 window
.minibuffer
.pop_state();
698 interactive("hints-exit", null,
700 hints_exit(I
.window
, I
.minibuffer
.check_state(hints_minibuffer_state
));
703 interactive("hints-quote-next", null,
705 I
.overlay_keymap
= hint_quote_next_keymap
;
710 define_keywords("$buffer");
711 minibuffer
.prototype.read_hinted_element = function () {
713 var buf
= arguments
.$buffer
;
714 var s
= new hints_minibuffer_state(this, (yield CONTINUATION
), buf
, forward_keywords(arguments
));
716 var result
= yield SUSPEND
;
717 yield co_return(result
);