]> git.donarmstrong.com Git - dactyl.git/blob - common/content/hints.js
Import 1.0rc1 supporting Firefox up to 11.*
[dactyl.git] / common / content / hints.js
1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
4 //
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
7 /* use strict */
8
9 /** @scope modules */
10 /** @instance hints */
11
12 var HintSession = Class("HintSession", CommandMode, {
13     get extendedMode() modes.HINTS,
14
15     init: function init(mode, opts) {
16         init.supercall(this);
17
18         opts = opts || {};
19
20         if (!opts.window)
21             opts.window = modes.getStack(0).params.window;
22
23         this.hintMode = hints.modes[mode];
24         dactyl.assert(this.hintMode);
25
26         this.activeTimeout = null; // needed for hinttimeout > 0
27         this.continue = Boolean(opts.continue);
28         this.docs = [];
29         this.hintKeys = DOM.Event.parse(options["hintkeys"]).map(DOM.Event.closure.stringify);
30         this.hintNumber = 0;
31         this.hintString = opts.filter || "";
32         this.pageHints = [];
33         this.prevInput = "";
34         this.usedTabKey = false;
35         this.validHints = []; // store the indices of the "hints" array with valid elements
36
37         mappings.pushCommand();
38         this.open();
39
40         this.top = opts.window || content;
41         this.top.addEventListener("resize", this.closure._onResize, true);
42         this.top.addEventListener("dactyl-commandupdate", this.closure._onResize, false, true);
43
44         this.generate();
45
46         this.show();
47         this.magic = true;
48
49         if (this.validHints.length == 0) {
50             dactyl.beep();
51             modes.pop();
52         }
53         else if (this.validHints.length == 1 && !this.continue)
54             this.process(false);
55         else
56             this.checkUnique();
57     },
58
59     Hint: {
60         get active() this._active,
61         set active(val) {
62             this._active = val;
63             if (val)
64                 this.span.setAttribute("active", true);
65             else
66                 this.span.removeAttribute("active");
67
68             hints.setClass(this.elem, this.valid ? val : null);
69             if (this.imgSpan)
70                 hints.setClass(this.imgSpan, this.valid ? val : null);
71         },
72
73         get ambiguous() this.span.hasAttribute("ambiguous"),
74         set ambiguous(val) {
75             let meth = val ? "setAttribute" : "removeAttribute";
76             this.elem[meth]("ambiguous", "true");
77             this.span[meth]("ambiguous", "true");
78             if (this.imgSpan)
79                 this.imgSpan[meth]("ambiguous", "true");
80         },
81
82         get valid() this._valid,
83         set valid(val) {
84             this._valid = val,
85
86             this.span.style.display = (val ? "" : "none");
87             if (this.imgSpan)
88                 this.imgSpan.style.display = (val ? "" : "none");
89             this.active = this.active;
90         }
91     },
92
93     get mode() modes.HINTS,
94
95     get prompt() ["Question", UTF8(this.hintMode.prompt) + ": "],
96
97     leave: function leave(stack) {
98         leave.superapply(this, arguments);
99
100         if (!stack.push) {
101             mappings.popCommand();
102
103             if (hints.hintSession == this)
104                 hints.hintSession = null;
105             if (this.top) {
106                 this.top.removeEventListener("resize", this.closure._onResize, true);
107                 this.top.removeEventListener("dactyl-commandupdate", this.closure._onResize, true);
108             }
109
110             this.removeHints(0);
111         }
112     },
113
114     checkUnique: function _checkUnique() {
115         if (this.hintNumber == 0)
116             return;
117         dactyl.assert(this.hintNumber <= this.validHints.length);
118
119         // if we write a numeric part like 3, but we have 45 hints, only follow
120         // the hint after a timeout, as the user might have wanted to follow link 34
121         if (this.hintNumber > 0 && this.hintNumber * this.hintKeys.length <= this.validHints.length) {
122             let timeout = options["hinttimeout"];
123             if (timeout > 0)
124                 this.activeTimeout = this.timeout(function () {
125                     this.process(true);
126                 }, timeout);
127         }
128         else // we have a unique hint
129             this.process(true);
130     },
131
132     /**
133      * Clear any timeout which might be active after pressing a number
134      */
135     clearTimeout: function () {
136         if (this.activeTimeout)
137             this.activeTimeout.cancel();
138         this.activeTimeout = null;
139     },
140
141     _escapeNumbers: false,
142     get escapeNumbers() this._escapeNumbers,
143     set escapeNumbers(val) {
144         this.clearTimeout();
145         this._escapeNumbers = !!val;
146         if (val && this.usedTabKey)
147             this.hintNumber = 0;
148
149         this.updateStatusline();
150     },
151
152     /**
153      * Returns the hint string for a given number based on the values of
154      * the 'hintkeys' option.
155      *
156      * @param {number} n The number to transform.
157      * @returns {string}
158      */
159     getHintString: function getHintString(n) {
160         let res = [], len = this.hintKeys.length;
161         do {
162             res.push(this.hintKeys[n % len]);
163             n = Math.floor(n / len);
164         }
165         while (n > 0);
166         return res.reverse().join("");
167     },
168
169     /**
170      * The reverse of {@link #getHintString}. Given a hint string,
171      * returns its index.
172      *
173      * @param {string} str The hint's string.
174      * @returns {number} The hint's index.
175      */
176     getHintNumber: function getHintNumber(str) {
177         let base = this.hintKeys.length;
178         let res = 0;
179         for (let char in values(str))
180             res = res * base + this.hintKeys.indexOf(char);
181         return res;
182     },
183
184     /**
185      * Returns true if the given key string represents a
186      * pseudo-hint-number.
187      *
188      * @param {string} key The key to test.
189      * @returns {boolean} Whether the key represents a hint number.
190      */
191     isHintKey: function isHintKey(key) this.hintKeys.indexOf(key) >= 0,
192
193     /**
194      * Gets the actual offset of an imagemap area.
195      *
196      * Only called by {@link #_generate}.
197      *
198      * @param {Object} elem The <area> element.
199      * @param {number} leftPos The left offset of the image.
200      * @param {number} topPos The top offset of the image.
201      * @returns [leftPos, topPos] The updated offsets.
202      */
203     getAreaOffset: function _getAreaOffset(elem, leftPos, topPos) {
204         try {
205             // Need to add the offset to the area element.
206             // Always try to find the top-left point, as per dactyl default.
207             let shape = elem.getAttribute("shape").toLowerCase();
208             let coordStr = elem.getAttribute("coords");
209             // Technically it should be only commas, but hey
210             coordStr = coordStr.replace(/\s+[;,]\s+/g, ",").replace(/\s+/g, ",");
211             let coords = coordStr.split(",").map(Number);
212
213             if ((shape == "rect" || shape == "rectangle") && coords.length == 4) {
214                 leftPos += coords[0];
215                 topPos += coords[1];
216             }
217             else if (shape == "circle" && coords.length == 3) {
218                 leftPos += coords[0] - coords[2] / Math.sqrt(2);
219                 topPos += coords[1] - coords[2] / Math.sqrt(2);
220             }
221             else if ((shape == "poly" || shape == "polygon") && coords.length % 2 == 0) {
222                 let leftBound = Infinity;
223                 let topBound = Infinity;
224
225                 // First find the top-left corner of the bounding rectangle (offset from image topleft can be noticeably suboptimal)
226                 for (let i = 0; i < coords.length; i += 2) {
227                     leftBound = Math.min(coords[i], leftBound);
228                     topBound = Math.min(coords[i + 1], topBound);
229                 }
230
231                 let curTop = null;
232                 let curLeft = null;
233                 let curDist = Infinity;
234
235                 // Then find the closest vertex. (we could generalize to nearest point on an edge, but I doubt there is a need)
236                 for (let i = 0; i < coords.length; i += 2) {
237                     let leftOffset = coords[i] - leftBound;
238                     let topOffset = coords[i + 1] - topBound;
239                     let dist = Math.sqrt(leftOffset * leftOffset + topOffset * topOffset);
240                     if (dist < curDist) {
241                         curDist = dist;
242                         curLeft = coords[i];
243                         curTop = coords[i + 1];
244                     }
245                 }
246
247                 // If we found a satisfactory offset, let's use it.
248                 if (curDist < Infinity)
249                     return [leftPos + curLeft, topPos + curTop];
250             }
251         }
252         catch (e) {} // badly formed document, or shape == "default" in which case we don't move the hint
253         return [leftPos, topPos];
254     },
255
256     // the containing block offsets with respect to the viewport
257     getContainerOffsets: function _getContainerOffsets(doc) {
258         let body = doc.body || doc.documentElement;
259         // TODO: getComputedStyle returns null for Facebook channel_iframe doc - probable Gecko bug.
260         let style = DOM(body).style;
261
262         if (style && /^(absolute|fixed|relative)$/.test(style.position)) {
263             let rect = body.getClientRects()[0];
264             return [-rect.left, -rect.top];
265         }
266         else
267             return [doc.defaultView.scrollX, doc.defaultView.scrollY];
268     },
269
270     /**
271      * Generate the hints in a window.
272      *
273      * Pushes the hints into the pageHints object, but does not display them.
274      *
275      * @param {Window} win The window for which to generate hints.
276      * @default content
277      */
278     generate: function _generate(win, offsets) {
279         if (!win)
280             win = this.top;
281
282         let doc = win.document;
283
284         memoize(doc, "dactylLabels", function ()
285             iter([l.getAttribute("for"), l]
286                  for (l in array.iterValues(doc.querySelectorAll("label[for]"))))
287              .toObject());
288
289         let [offsetX, offsetY] = this.getContainerOffsets(doc);
290
291         offsets = offsets || { left: 0, right: 0, top: 0, bottom: 0 };
292         offsets.right  = win.innerWidth  - offsets.right;
293         offsets.bottom = win.innerHeight - offsets.bottom;
294
295         function isVisible(elem) {
296             let rect = elem.getBoundingClientRect();
297             if (!rect ||
298                 rect.top > offsets.bottom || rect.bottom < offsets.top ||
299                 rect.left > offsets.right || rect.right < offsets.left)
300                 return false;
301
302             if (!rect.width || !rect.height)
303                 if (!Array.some(elem.childNodes, function (elem) elem instanceof Element && DOM(elem).style.float != "none" && isVisible(elem)))
304                     return false;
305
306             let computedStyle = doc.defaultView.getComputedStyle(elem, null);
307             if (computedStyle.visibility != "visible" || computedStyle.display == "none")
308                 return false;
309             return true;
310         }
311
312         let body = doc.body || doc.querySelector("body");
313         if (body) {
314             let fragment = DOM(<div highlight="hints"/>, doc).appendTo(body);
315             fragment.style.height; // Force application of binding.
316             let container = doc.getAnonymousElementByAttribute(fragment[0], "anonid", "hints") || fragment[0];
317
318             let baseNode = DOM(<span highlight="Hint" style="display: none;"/>, doc)[0];
319
320             let mode = this.hintMode;
321             let res = mode.matcher(doc);
322
323             let start = this.pageHints.length;
324             let _hints = [];
325             for (let elem in res)
326                 if (isVisible(elem) && (!mode.filter || mode.filter(elem)))
327                     _hints.push({
328                         elem: elem,
329                         rect: elem.getClientRects()[0] || elem.getBoundingClientRect(),
330                         showText: false,
331                         __proto__: this.Hint
332                     });
333
334             for (let hint in values(_hints)) {
335                 let { elem, rect } = hint;
336
337                 if (elem.hasAttributeNS(NS, "hint"))
338                     [hint.text, hint.showText] = [elem.getAttributeNS(NS, "hint"), true];
339                 else if (isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement]))
340                     [hint.text, hint.showText] = hints.getInputHint(elem, doc);
341                 else if (elem.firstElementChild instanceof HTMLImageElement && /^\s*$/.test(elem.textContent))
342                     [hint.text, hint.showText] = [elem.firstElementChild.alt || elem.firstElementChild.title, true];
343                 else
344                     hint.text = elem.textContent.toLowerCase();
345
346                 hint.span = baseNode.cloneNode(false);
347
348                 let leftPos = Math.max((rect.left + offsetX), offsetX);
349                 let topPos  = Math.max((rect.top + offsetY), offsetY);
350
351                 if (elem instanceof HTMLAreaElement)
352                     [leftPos, topPos] = this.getAreaOffset(elem, leftPos, topPos);
353
354                 hint.span.setAttribute("style", ["display: none; left:", leftPos, "px; top:", topPos, "px"].join(""));
355                 container.appendChild(hint.span);
356
357                 this.pageHints.push(hint);
358             }
359
360             this.docs.push({ doc: doc, start: start, end: this.pageHints.length - 1 });
361         }
362
363         Array.forEach(win.frames, function (f) {
364             if (isVisible(f.frameElement)) {
365                 let rect = f.frameElement.getBoundingClientRect();
366                 this.generate(f, {
367                     left: Math.max(offsets.left - rect.left, 0),
368                     right: Math.max(rect.right - offsets.right, 0),
369                     top: Math.max(offsets.top - rect.top, 0),
370                     bottom: Math.max(rect.bottom - offsets.bottom, 0)
371                 });
372             }
373         }, this);
374
375         return true;
376     },
377
378     /**
379      * Handle user input.
380      *
381      * Will update the filter on displayed hints and follow the final hint if
382      * necessary.
383      *
384      * @param {Event} event The keypress event.
385      */
386     onChange: function onChange(event) {
387         this.prevInput = "text";
388
389         this.clearTimeout();
390
391         this.hintNumber = 0;
392         this.hintString = commandline.command;
393         this.updateStatusline();
394         this.show();
395         if (this.validHints.length == 1)
396             this.process(false);
397     },
398
399     /**
400      * Handle a hints mode event.
401      *
402      * @param {Event} event The event to handle.
403      */
404     onKeyPress: function onKeyPress(eventList) {
405         const KILL = false, PASS = true;
406         let key = DOM.Event.stringify(eventList[0]);
407
408         this.clearTimeout();
409
410         if (!this.escapeNumbers && this.isHintKey(key)) {
411             this.prevInput = "number";
412
413             let oldHintNumber = this.hintNumber;
414             if (this.usedTabKey) {
415                 this.hintNumber = 0;
416                 this.usedTabKey = false;
417             }
418             this.hintNumber = this.hintNumber * this.hintKeys.length +
419                 this.hintKeys.indexOf(key);
420
421             this.updateStatusline();
422
423             if (this.docs.length)
424                 this.updateValidNumbers();
425             else {
426                 this.generate();
427                 this.show();
428             }
429
430             this.showActiveHint(this.hintNumber, oldHintNumber || 1);
431
432             dactyl.assert(this.hintNumber != 0);
433
434             this.checkUnique();
435             return KILL;
436         }
437
438         return PASS;
439     },
440
441     onResize: function onResize() {
442         this.removeHints(0);
443         this.generate(this.top);
444         this.show();
445     },
446
447     _onResize: function _onResize() {
448         if (this.magic)
449             hints.resizeTimer.tell();
450     },
451
452     /**
453      * Finish hinting.
454      *
455      * Called when there are one or zero hints in order to possibly activate it
456      * and, if activated, to clean up the rest of the hinting system.
457      *
458      * @param {boolean} followFirst Whether to force the following of the first
459      *     link (when 'followhints' is 1 or 2)
460      *
461      */
462     process: function _processHints(followFirst) {
463         dactyl.assert(this.validHints.length > 0);
464
465         // This "followhints" option is *too* confusing. For me, and
466         // presumably for users, too. --Kris
467         if (options["followhints"] > 0 && !followFirst)
468             return; // no return hit; don't examine uniqueness
469
470         if (!followFirst) {
471             let firstHref = this.validHints[0].elem.getAttribute("href") || null;
472             if (firstHref) {
473                 if (this.validHints.some(function (h) h.elem.getAttribute("href") != firstHref))
474                     return;
475             }
476             else if (this.validHints.length > 1)
477                 return;
478         }
479
480         let timeout = followFirst || events.feedingKeys ? 0 : 500;
481         let activeIndex = (this.hintNumber ? this.hintNumber - 1 : 0);
482         let elem = this.validHints[activeIndex].elem;
483         let top = this.top;
484
485         if (this.continue)
486             this._reset();
487         else
488             this.removeHints(timeout);
489
490         let n = 5;
491         (function next() {
492             let hinted = n || this.validHints.some(function (h) h.elem === elem);
493             if (!hinted)
494                 hints.setClass(elem, null);
495             else if (n)
496                 hints.setClass(elem, n % 2);
497             else
498                 hints.setClass(elem, this.validHints[Math.max(0, this.hintNumber - 1)].elem === elem);
499
500             if (n--)
501                 this.timeout(next, 50);
502         }).call(this);
503
504         mappings.pushCommand();
505         if (!this.continue) {
506             modes.pop();
507             if (timeout)
508                 modes.push(modes.IGNORE, modes.HINTS);
509         }
510
511         dactyl.trapErrors("action", this.hintMode,
512                           elem, elem.href || elem.src || "",
513                           this.extendedhintCount, top);
514         mappings.popCommand();
515
516         this.timeout(function () {
517             if (modes.main == modes.IGNORE && !this.continue)
518                 modes.pop();
519             commandline.lastEcho = null; // Hack.
520             if (this.continue && this.top)
521                 this.show();
522         }, timeout);
523     },
524
525     /**
526      * Remove all hints from the document, and reset the completions.
527      *
528      * Lingers on the active hint briefly to confirm the selection to the user.
529      *
530      * @param {number} timeout The number of milliseconds before the active
531      *     hint disappears.
532      */
533     removeHints: function _removeHints(timeout) {
534         for (let { doc, start, end } in values(this.docs)) {
535             // Goddamn stupid fucking Gecko 1.x security manager bullshit.
536             try { delete doc.dactylLabels; } catch (e) { doc.dactylLabels = undefined; }
537
538             for (let elem in DOM.XPath("//*[@dactyl:highlight='hints']", doc))
539                 elem.parentNode.removeChild(elem);
540             for (let i in util.range(start, end + 1)) {
541                 this.pageHints[i].ambiguous = false;
542                 this.pageHints[i].valid = false;
543             }
544         }
545         styles.system.remove("hint-positions");
546
547         this.reset();
548     },
549
550     reset: function reset() {
551         this.pageHints = [];
552         this.validHints = [];
553         this.docs = [];
554         this.clearTimeout();
555     },
556     _reset: function _reset() {
557         if (!this.usedTabKey)
558             this.hintNumber = 0;
559         if (this.continue && this.validHints.length <= 1) {
560             this.hintString = "";
561             commandline.widgets.command = this.hintString;
562             this.show();
563         }
564         this.updateStatusline();
565     },
566
567     /**
568      * Display the hints in pageHints that are still valid.
569      */
570     show: function _show() {
571         let hintnum = 1;
572         let validHint = hints.hintMatcher(this.hintString.toLowerCase());
573         let activeHint = this.hintNumber || 1;
574         this.validHints = [];
575
576         for (let { doc, start, end } in values(this.docs)) {
577             let [offsetX, offsetY] = this.getContainerOffsets(doc);
578
579         inner:
580             for (let i in (util.interruptibleRange(start, end + 1, 500))) {
581                 let hint = this.pageHints[i];
582
583                 hint.valid = validHint(hint.text);
584                 if (!hint.valid)
585                     continue inner;
586
587                 if (hint.text == "" && hint.elem.firstChild && hint.elem.firstChild instanceof HTMLImageElement) {
588                     if (!hint.imgSpan) {
589                         let rect = hint.elem.firstChild.getBoundingClientRect();
590                         if (!rect)
591                             continue;
592
593                         hint.imgSpan = util.xmlToDom(<span highlight="Hint" dactyl:hl="HintImage" xmlns:dactyl={NS}/>, doc);
594                         hint.imgSpan.style.display = "none";
595                         hint.imgSpan.style.left = (rect.left + offsetX) + "px";
596                         hint.imgSpan.style.top = (rect.top + offsetY) + "px";
597                         hint.imgSpan.style.width = (rect.right - rect.left) + "px";
598                         hint.imgSpan.style.height = (rect.bottom - rect.top) + "px";
599                         hint.span.parentNode.appendChild(hint.imgSpan);
600                     }
601                 }
602
603                 let str = this.getHintString(hintnum);
604                 let text = [];
605                 if (hint.elem instanceof HTMLInputElement)
606                     if (hint.elem.type === "radio")
607                         text.push(UTF8(hint.elem.checked ? "⊙" : "○"));
608                     else if (hint.elem.type === "checkbox")
609                         text.push(UTF8(hint.elem.checked ? "☑" : "☐"));
610                 if (hint.showText && !/^\s*$/.test(hint.text))
611                     text.push(hint.text.substr(0, 50));
612
613                 hint.span.setAttribute("text", str + (text.length ? ": " + text.join(" ") : ""));
614                 hint.span.setAttribute("number", str);
615                 if (hint.imgSpan)
616                     hint.imgSpan.setAttribute("number", str);
617
618                 hint.active = activeHint == hintnum;
619
620                 this.validHints.push(hint);
621                 hintnum++;
622             }
623         }
624
625         let base = this.hintKeys.length;
626         for (let [i, hint] in Iterator(this.validHints))
627             hint.ambiguous = (i + 1) * base <= this.validHints.length;
628
629         if (options["usermode"]) {
630             let css = [];
631             for (let hint in values(this.pageHints)) {
632                 let selector = highlight.selector("Hint") + "[number=" + hint.span.getAttribute("number").quote() + "]";
633                 let imgSpan = "[dactyl|hl=HintImage]";
634                 css.push(selector + ":not(" + imgSpan + ") { " + hint.span.style.cssText + " }");
635                 if (hint.imgSpan)
636                     css.push(selector + imgSpan + " { " + hint.span.style.cssText + " }");
637             }
638             styles.system.add("hint-positions", "*", css.join("\n"));
639         }
640
641         return true;
642     },
643
644     /**
645      * Update the activeHint.
646      *
647      * By default highlights it green instead of yellow.
648      *
649      * @param {number} newId The hint to make active.
650      * @param {number} oldId The currently active hint.
651      */
652     showActiveHint: function _showActiveHint(newId, oldId) {
653         let oldHint = this.validHints[oldId - 1];
654         if (oldHint)
655             oldHint.active = false;
656
657         let newHint = this.validHints[newId - 1];
658         if (newHint)
659             newHint.active = true;
660     },
661
662     backspace: function () {
663         this.clearTimeout();
664         if (this.prevInput !== "number")
665             return Events.PASS;
666
667         if (this.hintNumber > 0 && !this.usedTabKey) {
668             this.hintNumber = Math.floor(this.hintNumber / this.hintKeys.length);
669             if (this.hintNumber == 0)
670                 this.prevInput = "text";
671             this.update(false);
672         }
673         else {
674             this.usedTabKey = false;
675             this.hintNumber = 0;
676             dactyl.beep();
677         }
678         return Events.KILL;
679     },
680
681     updateValidNumbers: function updateValidNumbers(always) {
682         let string = this.getHintString(this.hintNumber);
683         for (let hint in values(this.validHints))
684             hint.valid = always || hint.span.getAttribute("number").indexOf(string) == 0;
685     },
686
687     tab: function tab(previous) {
688         this.clearTimeout();
689         this.usedTabKey = true;
690         if (this.hintNumber == 0)
691             this.hintNumber = 1;
692
693         let oldId = this.hintNumber;
694         if (!previous) {
695             if (++this.hintNumber > this.validHints.length)
696                 this.hintNumber = 1;
697         }
698         else {
699             if (--this.hintNumber < 1)
700                 this.hintNumber = this.validHints.length;
701         }
702
703         this.updateValidNumbers(true);
704         this.showActiveHint(this.hintNumber, oldId);
705         this.updateStatusline();
706     },
707
708     update: function update(followFirst) {
709         this.clearTimeout();
710         this.updateStatusline();
711
712         if (this.docs.length == 0 && this.hintString.length > 0)
713             this.generate();
714
715         this.show();
716         this.process(followFirst);
717     },
718
719     /**
720      * Display the current status to the user.
721      */
722     updateStatusline: function _updateStatusline() {
723         statusline.inputBuffer = (this.escapeNumbers ? options["mapleader"] : "") +
724                                  (this.hintNumber ? this.getHintString(this.hintNumber) : "");
725     },
726 });
727
728 var Hints = Module("hints", {
729     init: function init() {
730         this.resizeTimer = Timer(100, 500, function () {
731             if (isinstance(modes.main, modes.HINTS))
732                 modes.getStack(0).params.onResize();
733         });
734
735         let appContent = document.getElementById("appcontent");
736         if (appContent)
737             events.listen(appContent, "scroll", this.resizeTimer.closure.tell, false);
738
739         const Mode = Hints.Mode;
740         Mode.prototype.__defineGetter__("matcher", function ()
741             options.get("extendedhinttags").getKey(this.name, options.get("hinttags").matcher));
742
743         this.modes = {};
744         this.addMode(";", "Focus hint",                           buffer.closure.focusElement);
745         this.addMode("?", "Show information for hint",            function (elem) buffer.showElementInfo(elem));
746         this.addMode("s", "Save hint",                            function (elem) buffer.saveLink(elem, false));
747         this.addMode("f", "Focus frame",                          function (elem) dactyl.focus(elem.ownerDocument.defaultView));
748         this.addMode("F", "Focus frame or pseudo-frame",          buffer.closure.focusElement, isScrollable);
749         this.addMode("o", "Follow hint",                          function (elem) buffer.followLink(elem, dactyl.CURRENT_TAB));
750         this.addMode("t", "Follow hint in a new tab",             function (elem) buffer.followLink(elem, dactyl.NEW_TAB));
751         this.addMode("b", "Follow hint in a background tab",      function (elem) buffer.followLink(elem, dactyl.NEW_BACKGROUND_TAB));
752         this.addMode("w", "Follow hint in a new window",          function (elem) buffer.followLink(elem, dactyl.NEW_WINDOW));
753         this.addMode("O", "Generate an ‘:open URL’ prompt",       function (elem, loc) CommandExMode().open("open " + loc));
754         this.addMode("T", "Generate a ‘:tabopen URL’ prompt",     function (elem, loc) CommandExMode().open("tabopen " + loc));
755         this.addMode("W", "Generate a ‘:winopen URL’ prompt",     function (elem, loc) CommandExMode().open("winopen " + loc));
756         this.addMode("a", "Add a bookmark",                       function (elem) bookmarks.addSearchKeyword(elem));
757         this.addMode("S", "Add a search keyword",                 function (elem) bookmarks.addSearchKeyword(elem));
758         this.addMode("v", "View hint source",                     function (elem, loc) buffer.viewSource(loc, false));
759         this.addMode("V", "View hint source in external editor",  function (elem, loc) buffer.viewSource(loc, true));
760         this.addMode("y", "Yank hint location",                   function (elem, loc) editor.setRegister(null, loc, true));
761         this.addMode("Y", "Yank hint description",                function (elem) editor.setRegister(null, elem.textContent || "", true));
762         this.addMode("c", "Open context menu",                    function (elem) DOM(elem).contextmenu());
763         this.addMode("i", "Show image",                           function (elem) dactyl.open(elem.src));
764         this.addMode("I", "Show image in a new tab",              function (elem) dactyl.open(elem.src, dactyl.NEW_TAB));
765
766         function isScrollable(elem) isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]) ||
767             Buffer.isScrollable(elem, 0, true) || Buffer.isScrollable(elem, 0, false);
768     },
769
770     hintSession: Modes.boundProperty(),
771
772     /**
773      * Creates a new hints mode.
774      *
775      * @param {string} mode The letter that identifies this mode.
776      * @param {string} prompt The description to display to the user
777      *     about this mode.
778      * @param {function(Node)} action The function to be called with the
779      *     element that matches.
780      * @param {function(Node):boolean} filter A function used to filter
781      *     the returned node set.
782      * @param {[string]} tags A value to add to the default
783      *     'extendedhinttags' value for this mode.
784      *     @optional
785      */
786     addMode: function (mode, prompt, action, filter, tags) {
787         function toString(regexp) RegExp.prototype.toString.call(regexp);
788
789         if (tags != null) {
790             let eht = options.get("extendedhinttags");
791             let update = eht.isDefault;
792
793             let value = eht.parse(Option.quote(util.regexp.escape(mode)) + ":" + tags.map(Option.quote))[0];
794             eht.defaultValue = eht.defaultValue.filter(function (re) toString(re) != toString(value))
795                                   .concat(value);
796
797             if (update)
798                 eht.reset();
799         }
800
801         this.modes[mode] = Hints.Mode(mode, UTF8(prompt), action, filter);
802     },
803
804     /**
805      * Get a hint for "input", "textarea" and "select".
806      *
807      * Tries to use <label>s if possible but does not try to guess that a
808      * neighboring element might look like a label. Only called by
809      * {@link #_generate}.
810      *
811      * If it finds a hint it returns it, if the hint is not the caption of the
812      * element it will return showText=true.
813      *
814      * @param {Object} elem The element used to generate hint text.
815      * @param {Document} doc The containing document.
816      *
817      * @returns [text, showText]
818      */
819     getInputHint: function _getInputHint(elem, doc) {
820         // <input type="submit|button|reset"/>   Always use the value
821         // <input type="radio|checkbox"/>        Use the value if it is not numeric or label or name
822         // <input type="password"/>              Never use the value, use label or name
823         // <input type="text|file"/> <textarea/> Use value if set or label or name
824         // <input type="image"/>                 Use the alt text if present (showText) or label or name
825         // <input type="hidden"/>                Never gets here
826         // <select/>                             Use the text of the selected item or label or name
827
828         let type = elem.type;
829
830         if (DOM(elem).isInput)
831             return [elem.value, false];
832         else {
833             for (let [, option] in Iterator(options["hintinputs"])) {
834                 if (option == "value") {
835                     if (elem instanceof HTMLSelectElement) {
836                         if (elem.selectedIndex >= 0)
837                             return [elem.item(elem.selectedIndex).text.toLowerCase(), false];
838                     }
839                     else if (type == "image") {
840                         if (elem.alt)
841                             return [elem.alt.toLowerCase(), true];
842                     }
843                     else if (elem.value && type != "password") {
844                         // radios and checkboxes often use internal ids as values - maybe make this an option too...
845                         if (! ((type == "radio" || type == "checkbox") && !isNaN(elem.value)))
846                             return [elem.value.toLowerCase(), (type == "radio" || type == "checkbox")];
847                     }
848                 }
849                 else if (option == "label") {
850                     if (elem.id) {
851                         let label = elem.ownerDocument.dactylLabels[elem.id];
852                         if (label)
853                             return [label.textContent.toLowerCase(), true];
854                     }
855                 }
856                 else if (option == "name")
857                     return [elem.name.toLowerCase(), true];
858             }
859         }
860
861         return ["", false];
862     },
863
864     /**
865      * Get the hintMatcher according to user preference.
866      *
867      * @param {string} hintString The currently typed hint.
868      * @returns {hintMatcher}
869      */
870     hintMatcher: function _hintMatcher(hintString) { //{{{
871         /**
872          * Divide a string by a regular expression.
873          *
874          * @param {RegExp|string} pat The pattern to split on.
875          * @param {string} str The string to split.
876          * @returns {Array(string)} The lowercased splits of the splitting.
877          */
878         function tokenize(pat, str) str.split(pat).map(String.toLowerCase);
879
880         /**
881          * Get a hint matcher for hintmatching=contains
882          *
883          * The hintMatcher expects the user input to be space delimited and it
884          * returns true if each set of characters typed can be found, in any
885          * order, in the link.
886          *
887          * @param {string} hintString The string typed by the user.
888          * @returns {function(String):boolean} A function that takes the text
889          *     of a hint and returns true if all the (space-delimited) sets of
890          *     characters typed by the user can be found in it.
891          */
892         function containsMatcher(hintString) { //{{{
893             let tokens = tokenize(/\s+/, hintString);
894             return function (linkText) {
895                 linkText = linkText.toLowerCase();
896                 return tokens.every(function (token) indexOf(linkText, token) >= 0);
897             };
898         } //}}}
899
900         /**
901          * Get a hintMatcher for hintmatching=firstletters|wordstartswith
902          *
903          * The hintMatcher will look for any division of the user input that
904          * would match the first letters of words. It will always only match
905          * words in order.
906          *
907          * @param {string} hintString The string typed by the user.
908          * @param {boolean} allowWordOverleaping Whether to allow non-contiguous
909          *     words to match.
910          * @returns {function(String):boolean} A function that will filter only
911          *     hints that match as above.
912          */
913         function wordStartsWithMatcher(hintString, allowWordOverleaping) { //{{{
914             let hintStrings     = tokenize(/\s+/, hintString);
915             let wordSplitRegexp = util.regexp(options["wordseparators"]);
916
917             /**
918              * Match a set of characters to the start of words.
919              *
920              * What the **** does this do? --Kris
921              * This function matches hintStrings like 'hekho' to links
922              * like 'Hey Kris, how are you?' -> [HE]y [K]ris [HO]w are you
923              * --Daniel
924              *
925              * @param {string} chars The characters to match.
926              * @param {Array(string)} words The words to match them against.
927              * @param {boolean} allowWordOverleaping Whether words may be
928              *     skipped during matching.
929              * @returns {boolean} Whether a match can be found.
930              */
931             function charsAtBeginningOfWords(chars, words, allowWordOverleaping) {
932                 function charMatches(charIdx, chars, wordIdx, words, inWordIdx, allowWordOverleaping) {
933                     let matches = (chars[charIdx] == words[wordIdx][inWordIdx]);
934                     if ((matches == false && allowWordOverleaping) || words[wordIdx].length == 0) {
935                         let nextWordIdx = wordIdx + 1;
936                         if (nextWordIdx == words.length)
937                             return false;
938
939                         return charMatches(charIdx, chars, nextWordIdx, words, 0, allowWordOverleaping);
940                     }
941
942                     if (matches) {
943                         let nextCharIdx = charIdx + 1;
944                         if (nextCharIdx == chars.length)
945                             return true;
946
947                         let nextWordIdx = wordIdx + 1;
948                         let beyondLastWord = (nextWordIdx == words.length);
949                         let charMatched = false;
950                         if (beyondLastWord == false)
951                             charMatched = charMatches(nextCharIdx, chars, nextWordIdx, words, 0, allowWordOverleaping);
952
953                         if (charMatched)
954                             return true;
955
956                         if (charMatched == false || beyondLastWord == true) {
957                             let nextInWordIdx = inWordIdx + 1;
958                             if (nextInWordIdx == words[wordIdx].length)
959                                 return false;
960
961                             return charMatches(nextCharIdx, chars, wordIdx, words, nextInWordIdx, allowWordOverleaping);
962                         }
963                     }
964
965                     return false;
966                 }
967
968                 return charMatches(0, chars, 0, words, 0, allowWordOverleaping);
969             }
970
971             /**
972              * Check whether the array of strings all exist at the start of the
973              * words.
974              *
975              * i.e. ['ro', 'e'] would match ['rollover', 'effect']
976              *
977              * The matches must be in order, and, if allowWordOverleaping is
978              * false, contiguous.
979              *
980              * @param {Array(string)} strings The strings to search for.
981              * @param {Array(string)} words The words to search in.
982              * @param {boolean} allowWordOverleaping Whether matches may be
983              *     non-contiguous.
984              * @returns {boolean} Whether all the strings matched.
985              */
986             function stringsAtBeginningOfWords(strings, words, allowWordOverleaping) {
987                 let strIdx = 0;
988                 for (let [, word] in Iterator(words)) {
989                     if (word.length == 0)
990                         continue;
991
992                     let str = strings[strIdx];
993                     if (str.length == 0 || indexOf(word, str) == 0)
994                         strIdx++;
995                     else if (!allowWordOverleaping)
996                         return false;
997
998                     if (strIdx == strings.length)
999                         return true;
1000                 }
1001
1002                 for (; strIdx < strings.length; strIdx++) {
1003                     if (strings[strIdx].length != 0)
1004                         return false;
1005                 }
1006                 return true;
1007             }
1008
1009             return function (linkText) {
1010                 if (hintStrings.length == 1 && hintStrings[0].length == 0)
1011                     return true;
1012
1013                 let words = tokenize(wordSplitRegexp, linkText);
1014                 if (hintStrings.length == 1)
1015                     return charsAtBeginningOfWords(hintStrings[0], words, allowWordOverleaping);
1016                 else
1017                     return stringsAtBeginningOfWords(hintStrings, words, allowWordOverleaping);
1018             };
1019         } //}}}
1020
1021         let indexOf = String.indexOf;
1022         if (options.get("hintmatching").has("transliterated"))
1023             indexOf = Hints.indexOf;
1024
1025         switch (options["hintmatching"][0]) {
1026         case "contains"      : return containsMatcher(hintString);
1027         case "wordstartswith": return wordStartsWithMatcher(hintString, true);
1028         case "firstletters"  : return wordStartsWithMatcher(hintString, false);
1029         case "custom"        : return dactyl.plugins.customHintMatcher(hintString);
1030         default              : dactyl.echoerr(_("hints.noMatcher", hintMatching));
1031         }
1032         return null;
1033     }, //}}}
1034
1035     open: function open(mode, opts) {
1036         this._extendedhintCount = opts.count;
1037
1038         opts = opts || {};
1039
1040         mappings.pushCommand();
1041         commandline.input(["Normal", mode], null, {
1042             autocomplete: false,
1043             completer: function (context) {
1044                 context.compare = function () 0;
1045                 context.completions = [[k, v.prompt] for ([k, v] in Iterator(hints.modes))];
1046             },
1047             onCancel: mappings.closure.popCommand,
1048             onSubmit: function (arg) {
1049                 if (arg)
1050                     hints.show(arg, opts);
1051                 mappings.popCommand();
1052             },
1053             onChange: function (arg) {
1054                 if (Object.keys(hints.modes).some(function (m) m != arg && m.indexOf(arg) == 0))
1055                     return;
1056
1057                 this.accepted = true;
1058                 modes.pop();
1059             }
1060         });
1061     },
1062
1063     /**
1064      * Toggle the highlight of a hint.
1065      *
1066      * @param {Object} elem The element to toggle.
1067      * @param {boolean} active Whether it is the currently active hint or not.
1068      */
1069     setClass: function _setClass(elem, active) {
1070         if (elem.dactylHighlight == null)
1071             elem.dactylHighlight = elem.getAttributeNS(NS, "highlight") || "";
1072
1073         let prefix = (elem.getAttributeNS(NS, "hl") || "") + " " + elem.dactylHighlight + " ";
1074         if (active)
1075             highlight.highlightNode(elem, prefix + "HintActive");
1076         else if (active != null)
1077             highlight.highlightNode(elem, prefix + "HintElem");
1078         else {
1079             highlight.highlightNode(elem, elem.dactylHighlight);
1080             // delete elem.dactylHighlight fails on Gecko 1.9. Issue #197
1081             elem.dactylHighlight = null;
1082         }
1083     },
1084
1085     show: function show(mode, opts) {
1086         this.hintSession = HintSession(mode, opts);
1087     }
1088 }, {
1089     isVisible: function isVisible(elem, offScreen) {
1090         let rect = elem.getBoundingClientRect();
1091         if (!rect.width || !rect.height)
1092             if (!Array.some(elem.childNodes, function (elem) elem instanceof Element && DOM(elem).style.float != "none" && isVisible(elem)))
1093                 return false;
1094
1095         let win = elem.ownerDocument.defaultView;
1096         if (offScreen && (rect.top + win.scrollY < 0 || rect.left + win.scrollX < 0 ||
1097                           rect.bottom + win.scrollY > win.scrolMaxY + win.innerHeight ||
1098                           rect.right + win.scrollX > win.scrolMaxX + win.innerWidth))
1099             return false;
1100
1101         if (!DOM(elem).isVisible)
1102             return false;
1103         return true;
1104     },
1105
1106     translitTable: Class.Memoize(function () {
1107         const table = {};
1108         [
1109             [0x00c0, 0x00c6, ["A"]], [0x00c7, 0x00c7, ["C"]],
1110             [0x00c8, 0x00cb, ["E"]], [0x00cc, 0x00cf, ["I"]],
1111             [0x00d1, 0x00d1, ["N"]], [0x00d2, 0x00d6, ["O"]],
1112             [0x00d8, 0x00d8, ["O"]], [0x00d9, 0x00dc, ["U"]],
1113             [0x00dd, 0x00dd, ["Y"]], [0x00e0, 0x00e6, ["a"]],
1114             [0x00e7, 0x00e7, ["c"]], [0x00e8, 0x00eb, ["e"]],
1115             [0x00ec, 0x00ef, ["i"]], [0x00f1, 0x00f1, ["n"]],
1116             [0x00f2, 0x00f6, ["o"]], [0x00f8, 0x00f8, ["o"]],
1117             [0x00f9, 0x00fc, ["u"]], [0x00fd, 0x00fd, ["y"]],
1118             [0x00ff, 0x00ff, ["y"]], [0x0100, 0x0105, ["A", "a"]],
1119             [0x0106, 0x010d, ["C", "c"]], [0x010e, 0x0111, ["D", "d"]],
1120             [0x0112, 0x011b, ["E", "e"]], [0x011c, 0x0123, ["G", "g"]],
1121             [0x0124, 0x0127, ["H", "h"]], [0x0128, 0x0130, ["I", "i"]],
1122             [0x0132, 0x0133, ["IJ", "ij"]], [0x0134, 0x0135, ["J", "j"]],
1123             [0x0136, 0x0136, ["K", "k"]], [0x0139, 0x0142, ["L", "l"]],
1124             [0x0143, 0x0148, ["N", "n"]], [0x0149, 0x0149, ["n"]],
1125             [0x014c, 0x0151, ["O", "o"]], [0x0152, 0x0153, ["OE", "oe"]],
1126             [0x0154, 0x0159, ["R", "r"]], [0x015a, 0x0161, ["S", "s"]],
1127             [0x0162, 0x0167, ["T", "t"]], [0x0168, 0x0173, ["U", "u"]],
1128             [0x0174, 0x0175, ["W", "w"]], [0x0176, 0x0178, ["Y", "y", "Y"]],
1129             [0x0179, 0x017e, ["Z", "z"]], [0x0180, 0x0183, ["b", "B", "B", "b"]],
1130             [0x0187, 0x0188, ["C", "c"]], [0x0189, 0x0189, ["D"]],
1131             [0x018a, 0x0192, ["D", "D", "d", "F", "f"]],
1132             [0x0193, 0x0194, ["G"]],
1133             [0x0197, 0x019b, ["I", "K", "k", "l", "l"]],
1134             [0x019d, 0x01a1, ["N", "n", "O", "O", "o"]],
1135             [0x01a4, 0x01a5, ["P", "p"]], [0x01ab, 0x01ab, ["t"]],
1136             [0x01ac, 0x01b0, ["T", "t", "T", "U", "u"]],
1137             [0x01b2, 0x01d2, ["V", "Y", "y", "Z", "z", "D", "L", "N", "A", "a",
1138                "I", "i", "O", "o"]],
1139             [0x01d3, 0x01dc, ["U", "u"]], [0x01de, 0x01e1, ["A", "a"]],
1140             [0x01e2, 0x01e3, ["AE", "ae"]],
1141             [0x01e4, 0x01ed, ["G", "g", "G", "g", "K", "k", "O", "o", "O", "o"]],
1142             [0x01f0, 0x01f5, ["j", "D", "G", "g"]],
1143             [0x01fa, 0x01fb, ["A", "a"]], [0x01fc, 0x01fd, ["AE", "ae"]],
1144             [0x01fe, 0x0217, ["O", "o", "A", "a", "A", "a", "E", "e", "E", "e",
1145                "I", "i", "I", "i", "O", "o", "O", "o", "R", "r", "R", "r", "U",
1146                "u", "U", "u"]],
1147             [0x0253, 0x0257, ["b", "c", "d", "d"]],
1148             [0x0260, 0x0269, ["g", "h", "h", "i", "i"]],
1149             [0x026b, 0x0273, ["l", "l", "l", "l", "m", "n", "n"]],
1150             [0x027c, 0x028b, ["r", "r", "r", "r", "s", "t", "u", "u", "v"]],
1151             [0x0290, 0x0291, ["z"]], [0x029d, 0x02a0, ["j", "q"]],
1152             [0x1e00, 0x1e09, ["A", "a", "B", "b", "B", "b", "B", "b", "C", "c"]],
1153             [0x1e0a, 0x1e13, ["D", "d"]], [0x1e14, 0x1e1d, ["E", "e"]],
1154             [0x1e1e, 0x1e21, ["F", "f", "G", "g"]], [0x1e22, 0x1e2b, ["H", "h"]],
1155             [0x1e2c, 0x1e8f, ["I", "i", "I", "i", "K", "k", "K", "k", "K", "k",
1156                "L", "l", "L", "l", "L", "l", "L", "l", "M", "m", "M", "m", "M",
1157                "m", "N", "n", "N", "n", "N", "n", "N", "n", "O", "o", "O", "o",
1158                "O", "o", "O", "o", "P", "p", "P", "p", "R", "r", "R", "r", "R",
1159                "r", "R", "r", "S", "s", "S", "s", "S", "s", "S", "s", "S", "s",
1160                "T", "t", "T", "t", "T", "t", "T", "t", "U", "u", "U", "u", "U",
1161                "u", "U", "u", "U", "u", "V", "v", "V", "v", "W", "w", "W", "w",
1162                "W", "w", "W", "w", "W", "w", "X", "x", "X", "x", "Y", "y"]],
1163             [0x1e90, 0x1e9a, ["Z", "z", "Z", "z", "Z", "z", "h", "t", "w", "y", "a"]],
1164             [0x1ea0, 0x1eb7, ["A", "a"]], [0x1eb8, 0x1ec7, ["E", "e"]],
1165             [0x1ec8, 0x1ecb, ["I", "i"]], [0x1ecc, 0x1ee3, ["O", "o"]],
1166             [0x1ee4, 0x1ef1, ["U", "u"]], [0x1ef2, 0x1ef9, ["Y", "y"]],
1167             [0x2071, 0x2071, ["i"]], [0x207f, 0x207f, ["n"]],
1168             [0x249c, 0x24b5, "a"], [0x24b6, 0x24cf, "A"],
1169             [0x24d0, 0x24e9, "a"],
1170             [0xfb00, 0xfb06, ["ff", "fi", "fl", "ffi", "ffl", "st", "st"]],
1171             [0xff21, 0xff3a, "A"], [0xff41, 0xff5a, "a"]
1172         ].forEach(function (start, stop, val) {
1173             if (typeof val != "string")
1174                 for (let i = start; i <= stop; i++)
1175                     table[String.fromCharCode(i)] = val[(i - start) % val.length];
1176             else {
1177                 let n = val.charCodeAt(0);
1178                 for (let i = start; i <= stop; i++)
1179                     table[String.fromCharCode(i)] = String.fromCharCode(n + i - start);
1180             }
1181         });
1182         return table;
1183     }),
1184     indexOf: function indexOf(dest, src) {
1185         let table = this.translitTable;
1186         var end = dest.length - src.length;
1187         if (src.length == 0)
1188             return 0;
1189     outer:
1190         for (var i = 0; i < end; i++) {
1191                 var j = i;
1192                 for (var k = 0; k < src.length;) {
1193                     var s = dest[j++];
1194                     s = table[s] || s;
1195                     for (var l = 0; l < s.length; l++, k++) {
1196                         if (s[l] != src[k])
1197                             continue outer;
1198                         if (k == src.length - 1)
1199                             return i;
1200                     }
1201                 }
1202             }
1203         return -1;
1204     },
1205
1206     Mode: Struct("HintMode", "name", "prompt", "action", "filter")
1207             .localize("prompt")
1208 }, {
1209     modes: function initModes() {
1210         initModes.require("commandline");
1211         modes.addMode("HINTS", {
1212             extended: true,
1213             description: "Active when selecting elements with hints",
1214             bases: [modes.COMMAND_LINE],
1215             input: true,
1216             ownsBuffer: true
1217         });
1218     },
1219     mappings: function () {
1220         let bind = function bind(names, description, action, params)
1221             mappings.add(config.browserModes, names, description,
1222                          action, params);
1223
1224         bind(["f"],
1225             "Start Hints mode",
1226             function () { hints.show("o"); });
1227
1228         bind(["F"],
1229             "Start Hints mode, but open link in a new tab",
1230             function () { hints.show(options.get("activate").has("links") ? "t" : "b"); });
1231
1232         bind([";"],
1233             "Start an extended hints mode",
1234             function ({ count }) { hints.open(";", { count: count }); },
1235             { count: true });
1236
1237         bind(["g;"],
1238             "Start an extended hints mode and stay there until <Esc> is pressed",
1239             function ({ count }) { hints.open("g;", { continue: true, count: count }); },
1240             { count: true });
1241
1242         let bind = function bind(names, description, action, params)
1243             mappings.add([modes.HINTS], names, description,
1244                          action, params);
1245
1246         bind(["<Return>"],
1247             "Follow the selected hint",
1248             function ({ self }) { self.update(true); });
1249
1250         bind(["<Tab>"],
1251             "Focus the next matching hint",
1252             function ({ self }) { self.tab(false); });
1253
1254         bind(["<S-Tab>"],
1255             "Focus the previous matching hint",
1256             function ({ self }) { self.tab(true); });
1257
1258         bind(["<BS>", "<C-h>"],
1259             "Delete the previous character",
1260             function ({ self }) self.backspace());
1261
1262         bind(["<Leader>"],
1263             "Toggle hint filtering",
1264             function ({ self }) { self.escapeNumbers = !self.escapeNumbers; });
1265     },
1266     options: function () {
1267         options.add(["extendedhinttags", "eht"],
1268             "XPath or CSS selector strings of hintable elements for extended hint modes",
1269             "regexpmap", {
1270                 "[iI]": "img",
1271                 "[asOTvVWy]": [":-moz-any-link", "area[href]", "img[src]", "iframe[src]"],
1272                 "[f]": "body",
1273                 "[F]": ["body", "code", "div", "html", "p", "pre", "span"],
1274                 "[S]": ["input:not([type=hidden])", "textarea", "button", "select"]
1275             },
1276             {
1277                 keepQuotes: true,
1278                 getKey: function (val, default_)
1279                     let (res = array.nth(this.value, function (re) let (match = re.exec(val)) match && match[0] == val, 0))
1280                         res ? res.matcher : default_,
1281                 setter: function (vals) {
1282                     for (let value in values(vals))
1283                         value.matcher = DOM.compileMatcher(Option.splitList(value.result));
1284                     return vals;
1285                 },
1286                 validator: DOM.validateMatcher
1287             });
1288
1289         options.add(["hinttags", "ht"],
1290             "XPath or CSS selector strings of hintable elements for Hints mode",
1291             "stringlist", ":-moz-any-link,area,button,iframe,input:not([type=hidden]),select,textarea," +
1292                           "[onclick],[onmouseover],[onmousedown],[onmouseup],[oncommand]," +
1293                           "[tabindex],[role=link],[role=button],[contenteditable=true]",
1294             {
1295                 setter: function (values) {
1296                     this.matcher = DOM.compileMatcher(values);
1297                     return values;
1298                 },
1299                 validator: DOM.validateMatcher
1300             });
1301
1302         options.add(["hintkeys", "hk"],
1303             "The keys used to label and select hints",
1304             "string", "0123456789",
1305             {
1306                 values: {
1307                     "0123456789": "Numbers",
1308                     "asdfg;lkjh": "Home Row"
1309                 },
1310                 validator: function (value) {
1311                     let values = DOM.Event.parse(value).map(DOM.Event.closure.stringify);
1312                     return Option.validIf(array.uniq(values).length === values.length && values.length > 1,
1313                                           _("option.hintkeys.duplicate"));
1314                 }
1315             });
1316
1317         options.add(["hinttimeout", "hto"],
1318             "Timeout before automatically following a non-unique numerical hint",
1319             "number", 0,
1320             { validator: function (value) value >= 0 });
1321
1322         options.add(["followhints", "fh"],
1323             "Define the conditions under which selected hints are followed",
1324             "number", 0,
1325             {
1326                 values: {
1327                     "0": "Follow the first hint as soon as typed text uniquely identifies it. Follow the selected hint on <Return>.",
1328                     "1": "Follow the selected hint on <Return>.",
1329                 }
1330             });
1331
1332         options.add(["hintmatching", "hm"],
1333             "How hints are filtered",
1334             "stringlist", "contains",
1335             {
1336                 values: {
1337                     "contains":       "The typed characters are split on whitespace. The resulting groups must all appear in the hint.",
1338                     "custom":         "Delegate to a custom function: dactyl.plugins.customHintMatcher(hintString)",
1339                     "firstletters":   "Behaves like wordstartswith, but all groups must match a sequence of words.",
1340                     "wordstartswith": "The typed characters are split on whitespace. The resulting groups must all match the beginnings of words, in order.",
1341                     "transliterated": UTF8("When true, special latin characters are translated to their ASCII equivalents (e.g., é ⇒ e)")
1342                 },
1343                 validator: function (values) Option.validateCompleter.call(this, values) &&
1344                     1 === values.reduce(function (acc, v) acc + (["contains", "custom", "firstletters", "wordstartswith"].indexOf(v) >= 0), 0)
1345             });
1346
1347         options.add(["wordseparators", "wsp"],
1348             "Regular expression defining which characters separate words when matching hints",
1349             "string", '[.,!?:;/"^$%&?()[\\]{}<>#*+|=~ _-]',
1350             { validator: function (value) RegExp(value) });
1351
1352         options.add(["hintinputs", "hin"],
1353             "Which text is used to filter hints for input elements",
1354             "stringlist", "label,value",
1355             {
1356                 values: {
1357                     "value": "Match against the value of the input field",
1358                     "label": "Match against the text of a label for the input field, if one can be found",
1359                     "name":  "Match against the name of the input field"
1360                 }
1361             });
1362     }
1363 });
1364
1365 // vim: set fdm=marker sw=4 ts=4 et: