this.open();
this.top = opts.window || content;
- this.top.addEventListener("resize", hints.resizeTimer.closure.tell, true);
- this.top.addEventListener("dactyl-commandupdate", hints.resizeTimer.closure.tell, false, true);
+ this.top.addEventListener("resize", this.closure._onResize, true);
+ this.top.addEventListener("dactyl-commandupdate", this.closure._onResize, false, true);
this.generate();
this.show();
+ this.magic = true;
if (this.validHints.length == 0) {
dactyl.beep();
}
else if (this.validHints.length == 1 && !this.continue)
this.process(false);
- else // Ticket #185
+ else
this.checkUnique();
},
hints.setClass(this.imgSpan, this.valid ? val : null);
},
+ get ambiguous() this.span.hasAttribute("ambiguous"),
+ set ambiguous(val) {
+ let meth = val ? "setAttribute" : "removeAttribute";
+ this.elem[meth]("ambiguous", "true");
+ this.span[meth]("ambiguous", "true");
+ if (this.imgSpan)
+ this.imgSpan[meth]("ambiguous", "true");
+ },
+
get valid() this._valid,
set valid(val) {
this._valid = val,
this.span.style.display = (val ? "" : "none");
if (this.imgSpan)
this.imgSpan.style.display = (val ? "" : "none");
-
this.active = this.active;
}
},
if (hints.hintSession == this)
hints.hintSession = null;
if (this.top) {
- this.top.removeEventListener("resize", hints.resizeTimer.closure.tell, true);
- this.top.removeEventListener("dactyl-commandupdate", hints.resizeTimer.closure.tell, true);
+ this.top.removeEventListener("resize", this.closure._onResize, true);
+ this.top.removeEventListener("dactyl-commandupdate", this.closure._onResize, true);
}
this.removeHints(0);
return res.reverse().join("");
},
+ /**
+ * The reverse of {@link #getHintString}. Given a hint string,
+ * returns its index.
+ *
+ * @param {string} str The hint's string.
+ * @returns {number} The hint's index.
+ */
+ getHintNumber: function getHintNumber(str) {
+ let base = this.hintKeys.length;
+ let res = 0;
+ for (let char in values(str))
+ res = res * base + this.hintKeys.indexOf(char);
+ return res;
+ },
+
/**
* Returns true if the given key string represents a
* pseudo-hint-number.
let doc = win.document;
+ memoize(doc, "dactylLabels", function ()
+ iter([l.getAttribute("for"), l]
+ for (l in array.iterValues(doc.querySelectorAll("label[for]"))))
+ .toObject());
+
let [offsetX, offsetY] = this.getContainerOffsets(doc);
offsets = offsets || { left: 0, right: 0, top: 0, bottom: 0 };
function isVisible(elem) {
let rect = elem.getBoundingClientRect();
- if (!rect || !rect.width || !rect.height ||
+ if (!rect ||
rect.top > offsets.bottom || rect.bottom < offsets.top ||
rect.left > offsets.right || rect.right < offsets.left)
return false;
+ if (!rect.width || !rect.height)
+ if (!Array.some(elem.childNodes, function (elem) elem instanceof Element && util.computedStyle(elem).float != "none" && isVisible(elem)))
+ return false;
+
let computedStyle = doc.defaultView.getComputedStyle(elem, null);
if (computedStyle.visibility != "visible" || computedStyle.display == "none")
return false;
util.computedStyle(fragment).height; // Force application of binding.
let container = doc.getAnonymousElementByAttribute(fragment, "anonid", "hints") || fragment;
- let baseNodeAbsolute = util.xmlToDom(<span highlight="Hint" style="display: none"/>, doc);
+ let baseNodeAbsolute = util.xmlToDom(<span highlight="Hint" style="display: none;"/>, doc);
let mode = this.hintMode;
let res = mode.matcher(doc);
let start = this.pageHints.length;
- for (let elem in res) {
- let hint = { elem: elem, showText: false, __proto__: this.Hint };
-
- if (!isVisible(elem) || mode.filter && !mode.filter(elem))
- continue;
+ let _hints = [];
+ for (let elem in res)
+ if (isVisible(elem) && (!mode.filter || mode.filter(elem)))
+ _hints.push({
+ elem: elem,
+ rect: elem.getClientRects()[0] || elem.getBoundingClientRect(),
+ showText: false,
+ __proto__: this.Hint
+ });
+
+ for (let hint in values(_hints)) {
+ let { elem, rect } = hint;
if (elem.hasAttributeNS(NS, "hint"))
[hint.text, hint.showText] = [elem.getAttributeNS(NS, "hint"), true];
else
hint.text = elem.textContent.toLowerCase();
- hint.span = baseNodeAbsolute.cloneNode(true);
+ hint.span = baseNodeAbsolute.cloneNode(false);
- let rect = elem.getClientRects()[0] || elem.getBoundingClientRect();
let leftPos = Math.max((rect.left + offsetX), offsetX);
let topPos = Math.max((rect.top + offsetY), offsetY);
if (elem instanceof HTMLAreaElement)
[leftPos, topPos] = this.getAreaOffset(elem, leftPos, topPos);
- hint.span.style.left = leftPos + "px";
- hint.span.style.top = topPos + "px";
+ hint.span.setAttribute("style", ["display: none; left:", leftPos, "px; top:", topPos, "px"].join(""));
container.appendChild(hint.span);
this.pageHints.push(hint);
},
/**
- * Handle a hint mode event.
+ * Handle a hints mode event.
*
* @param {Event} event The event to handle.
*/
return PASS;
},
- onResize: function () {
+ onResize: function onResize() {
this.removeHints(0);
this.generate(this.top);
this.show();
},
+ _onResize: function _onResize() {
+ if (this.magic)
+ hints.resizeTimer.tell();
+ },
+
/**
* Finish hinting.
*
// This "followhints" option is *too* confusing. For me, and
// presumably for users, too. --Kris
- if (options["followhints"] > 0) {
- if (!followFirst)
- return; // no return hit; don't examine uniqueness
-
- // OK. return hit. But there's more than one hint, and
- // there's no tab-selected current link. Do not follow in mode 2
- dactyl.assert(options["followhints"] != 2 || this.validHints.length == 1 || this.hintNumber);
- }
+ if (options["followhints"] > 0 && !followFirst)
+ return; // no return hit; don't examine uniqueness
if (!followFirst) {
let firstHref = this.validHints[0].elem.getAttribute("href") || null;
else if (n)
hints.setClass(elem, n % 2);
else
- hints.setClass(elem, this.validHints[Math.max(0, this.hintNumber-1)].elem === elem);
+ hints.setClass(elem, this.validHints[Math.max(0, this.hintNumber - 1)].elem === elem);
if (n--)
this.timeout(next, 50);
modes.push(modes.IGNORE, modes.HINTS);
}
+ dactyl.trapErrors("action", this.hintMode,
+ elem, elem.href || elem.src || "",
+ this.extendedhintCount, top);
+
this.timeout(function () {
- if ((modes.extended & modes.HINTS) && !this.continue)
+ if (modes.main == modes.IGNORE && !this.continue)
modes.pop();
commandline.lastEcho = null; // Hack.
- dactyl.trapErrors("action", this.hintMode,
- elem, elem.href || elem.src || "",
- this.extendedhintCount, top);
if (this.continue && this.top)
this.show();
}, timeout);
*/
removeHints: function _removeHints(timeout) {
for (let { doc, start, end } in values(this.docs)) {
+ // Goddamn stupid fucking Gecko 1.x security manager bullshit.
+ try { delete doc.dactylLabels; } catch (e) { doc.dactylLabels = undefined; }
+
for (let elem in util.evaluateXPath("//*[@dactyl:highlight='hints']", doc))
elem.parentNode.removeChild(elem);
- for (let i in util.range(start, end + 1))
+ for (let i in util.range(start, end + 1)) {
+ this.pageHints[i].ambiguous = false;
this.pageHints[i].valid = false;
+ }
}
styles.system.remove("hint-positions");
text.push(UTF8(hint.elem.checked ? "⊙" : "○"));
else if (hint.elem.type === "checkbox")
text.push(UTF8(hint.elem.checked ? "☑" : "☐"));
- if (hint.showText)
+ if (hint.showText && !/^\s*$/.test(hint.text))
text.push(hint.text.substr(0, 50));
hint.span.setAttribute("text", str + (text.length ? ": " + text.join(" ") : ""));
hint.span.setAttribute("number", str);
if (hint.imgSpan)
hint.imgSpan.setAttribute("number", str);
+
hint.active = activeHint == hintnum;
+
this.validHints.push(hint);
hintnum++;
}
}
+ let base = this.hintKeys.length;
+ for (let [i, hint] in Iterator(this.validHints))
+ hint.ambiguous = (i + 1) * base <= this.validHints.length;
+
if (options["usermode"]) {
let css = [];
for (let hint in values(this.pageHints)) {
events.listen(appContent, "scroll", this.resizeTimer.closure.tell, false);
const Mode = Hints.Mode;
- Mode.defaultValue("tags", function () function () options.get("hinttags").matcher);
Mode.prototype.__defineGetter__("matcher", function ()
- options.get("extendedhinttags").getKey(this.name, this.tags()));
+ options.get("extendedhinttags").getKey(this.name, options.get("hinttags").matcher));
this.modes = {};
this.addMode(";", "Focus hint", buffer.closure.focusElement);
this.addMode("?", "Show information for hint", function (elem) buffer.showElementInfo(elem));
this.addMode("s", "Save hint", function (elem) buffer.saveLink(elem, false));
this.addMode("f", "Focus frame", function (elem) dactyl.focus(elem.ownerDocument.defaultView));
- this.addMode("F", "Focus frame or pseudo-frame", buffer.closure.focusElement, null, isScrollable);
+ this.addMode("F", "Focus frame or pseudo-frame", buffer.closure.focusElement, isScrollable);
this.addMode("o", "Follow hint", function (elem) buffer.followLink(elem, dactyl.CURRENT_TAB));
this.addMode("t", "Follow hint in a new tab", function (elem) buffer.followLink(elem, dactyl.NEW_TAB));
this.addMode("b", "Follow hint in a background tab", function (elem) buffer.followLink(elem, dactyl.NEW_BACKGROUND_TAB));
hintSession: Modes.boundProperty(),
/**
- * Creates a new hint mode.
+ * Creates a new hints mode.
*
* @param {string} mode The letter that identifies this mode.
* @param {string} prompt The description to display to the user
* about this mode.
* @param {function(Node)} action The function to be called with the
* element that matches.
- * @param {function():string} tags The function that returns an
- * XPath expression to decide which elements can be hinted (the
- * default returns options["hinttags"]).
- * @optional
+ * @param {function(Node):boolean} filter A function used to filter
+ * the returned node set.
+ * @param {[string]} tags A value to add to the default
+ * 'extendedhinttags' value for this mode.
+ * @optional
*/
- addMode: function (mode, prompt, action, tags) {
- arguments[1] = UTF8(prompt);
- this.modes[mode] = Hints.Mode.apply(Hints.Mode, arguments);
+ addMode: function (mode, prompt, action, filter, tags) {
+ function toString(regexp) RegExp.prototype.toString.call(regexp);
+
+ if (tags != null) {
+ let eht = options.get("extendedhinttags");
+ let update = eht.isDefault;
+
+ let value = eht.parse(Option.quote(util.regexp.escape(mode)) + ":" + tags.map(Option.quote))[0];
+ eht.defaultValue = eht.defaultValue.filter(function (re) toString(re) != toString(value))
+ .concat(value);
+
+ if (update)
+ eht.reset();
+ }
+
+ this.modes[mode] = Hints.Mode(mode, UTF8(prompt), action, filter);
},
/**
let type = elem.type;
- if (elem instanceof HTMLInputElement && set.has(util.editableInputs, elem.type))
+ if (elem instanceof HTMLInputElement && Set.has(util.editableInputs, elem.type))
return [elem.value, false];
else {
for (let [, option] in Iterator(options["hintinputs"])) {
return [elem.alt.toLowerCase(), true];
}
else if (elem.value && type != "password") {
- // radio's and checkboxes often use internal ids as values - maybe make this an option too...
+ // radios and checkboxes often use internal ids as values - maybe make this an option too...
if (! ((type == "radio" || type == "checkbox") && !isNaN(elem.value)))
return [elem.value.toLowerCase(), (type == "radio" || type == "checkbox")];
}
}
else if (option == "label") {
if (elem.id) {
- // TODO: (possibly) do some guess work for label-like objects
- let label = util.evaluateXPath(["label[@for=" + elem.id.quote() + "]"], doc).snapshotItem(0);
+ let label = elem.ownerDocument.dactylLabels[elem.id];
if (label)
return [label.textContent.toLowerCase(), true];
}
* returns true if each set of characters typed can be found, in any
* order, in the link.
*
- * @param {string} hintString The string typed by the user.
+ * @param {string} hintString The string typed by the user.
* @returns {function(String):boolean} A function that takes the text
* of a hint and returns true if all the (space-delimited) sets of
* characters typed by the user can be found in it.
open: function open(mode, opts) {
this._extendedhintCount = opts.count;
commandline.input(["Normal", mode], "", {
+ autocomplete: false,
completer: function (context) {
context.compare = function () 0;
context.completions = [[k, v.prompt] for ([k, v] in Iterator(hints.modes))];
if (arg)
hints.show(arg, opts);
},
- onChange: function () {
+ onChange: function (arg) {
+ if (Object.keys(hints.modes).some(function (m) m != arg && m.indexOf(arg) == 0))
+ return;
+
this.accepted = true;
modes.pop();
- },
+ }
});
},
return -1;
},
- Mode: Struct("HintMode", "name", "prompt", "action", "tags", "filter")
+ Mode: Struct("HintMode", "name", "prompt", "action", "filter")
.localize("prompt")
}, {
modes: function initModes() {
initModes.require("commandline");
modes.addMode("HINTS", {
extended: true,
- description: "Active when selecting elements in QuickHint or ExtendedHint mode",
+ description: "Active when selecting elements with hints",
bases: [modes.COMMAND_LINE],
input: true,
ownsBuffer: true
mappings: function () {
var myModes = config.browserModes.concat(modes.OUTPUT_MULTILINE);
mappings.add(myModes, ["f"],
- "Start QuickHint mode",
+ "Start Hints mode",
function () { hints.show("o"); });
mappings.add(myModes, ["F"],
- "Start QuickHint mode, but open link in a new tab",
+ "Start Hints mode, but open link in a new tab",
function () { hints.show(options.get("activate").has("links") ? "t" : "b"); });
mappings.add(myModes, [";"],
- "Start an extended hint mode",
+ "Start an extended hints mode",
function ({ count }) { hints.open(";", { count: count }); },
{ count: true });
mappings.add(myModes, ["g;"],
- "Start an extended hint mode and stay there until <Esc> is pressed",
+ "Start an extended hints mode and stay there until <Esc> is pressed",
function ({ count }) { hints.open("g;", { continue: true, count: count }); },
{ count: true });
"XPath or CSS selector strings of hintable elements for extended hint modes",
"regexpmap", {
"[iI]": "img",
- "[asOTivVWy]": ["a[href]", "area[href]", "img[src]", "iframe[src]"],
+ "[asOTvVWy]": ["a[href]", "area[href]", "img[src]", "iframe[src]"],
"[f]": "body",
"[F]": ["body", "code", "div", "html", "p", "pre", "span"],
"[S]": ["input:not([type=hidden])", "textarea", "button", "select"]
{
keepQuotes: true,
getKey: function (val, default_)
- let (res = array.nth(this.value, function (re) re.test(val), 0))
+ let (res = array.nth(this.value, function (re) let (match = re.exec(val)) match && match[0] == val, 0))
res ? res.matcher : default_,
setter: function (vals) {
for (let value in values(vals))
});
options.add(["hinttags", "ht"],
- "XPath string of hintable elements activated by 'f' and 'F'",
- "stringlist", "input:not([type=hidden]),a,area,iframe,textarea,button,select," +
+ "XPath or CSS selector strings of hintable elements for Hints mode",
+ "stringlist", "input:not([type=hidden]),a[href],area,iframe,textarea,button,select," +
"[onclick],[onmouseover],[onmousedown],[onmouseup],[oncommand]," +
- "[tabindex],[role=link],[role=button]",
+ "[tabindex],[role=link],[role=button],[contenteditable=true]",
{
setter: function (values) {
this.matcher = util.compileMatcher(values);
},
validator: function (value) {
let values = events.fromString(value).map(events.closure.toString);
- return Option.validIf(array.uniq(values).length === values.length,
- "Duplicate keys not allowed");
+ return Option.validIf(array.uniq(values).length === values.length && values.length > 1,
+ _("option.hintkeys.duplicate"));
}
});
{ validator: function (value) value >= 0 });
options.add(["followhints", "fh"],
- // FIXME: this description isn't very clear but I can't think of a
- // better one right now.
- "Change the behavior of <Return> in hint mode",
+ "Define the conditions under which selected hints are followed",
"number", 0,
{
values: {
"0": "Follow the first hint as soon as typed text uniquely identifies it. Follow the selected hint on <Return>.",
"1": "Follow the selected hint on <Return>.",
- "2": "Follow the selected hint on <Return> only it's been <Tab>-selected."
}
});