X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=common%2Fcontent%2Fhints.js;h=a30ef295057f33655020e429bf58aacb61b918f0;hb=70740024f9c028c1fd63e1a1850ab062ff956054;hp=bcf431ce06175ffa7664bae2304ad5f26d819f64;hpb=eeed0be1a8abf7e3c97f43b63c1d595e940fef21;p=dactyl.git diff --git a/common/content/hints.js b/common/content/hints.js index bcf431c..a30ef29 100644 --- a/common/content/hints.js +++ b/common/content/hints.js @@ -38,12 +38,13 @@ var HintSession = Class("HintSession", CommandMode, { 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(); @@ -51,7 +52,7 @@ var HintSession = Class("HintSession", CommandMode, { } else if (this.validHints.length == 1 && !this.continue) this.process(false); - else // Ticket #185 + else this.checkUnique(); }, @@ -69,6 +70,15 @@ var HintSession = Class("HintSession", CommandMode, { 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, @@ -76,7 +86,6 @@ var HintSession = Class("HintSession", CommandMode, { this.span.style.display = (val ? "" : "none"); if (this.imgSpan) this.imgSpan.style.display = (val ? "" : "none"); - this.active = this.active; } }, @@ -92,8 +101,8 @@ var HintSession = Class("HintSession", CommandMode, { 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); @@ -155,6 +164,21 @@ var HintSession = Class("HintSession", CommandMode, { 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. @@ -255,6 +279,11 @@ var HintSession = Class("HintSession", CommandMode, { 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 }; @@ -263,11 +292,15 @@ var HintSession = Class("HintSession", CommandMode, { 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; @@ -281,17 +314,24 @@ var HintSession = Class("HintSession", CommandMode, { util.computedStyle(fragment).height; // Force application of binding. let container = doc.getAnonymousElementByAttribute(fragment, "anonid", "hints") || fragment; - let baseNodeAbsolute = util.xmlToDom(, doc); + let baseNodeAbsolute = util.xmlToDom(, 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]; @@ -302,17 +342,15 @@ var HintSession = Class("HintSession", CommandMode, { 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); @@ -358,7 +396,7 @@ var HintSession = Class("HintSession", CommandMode, { }, /** - * Handle a hint mode event. + * Handle a hints mode event. * * @param {Event} event The event to handle. */ @@ -399,12 +437,17 @@ var HintSession = Class("HintSession", CommandMode, { 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. * @@ -420,14 +463,8 @@ var HintSession = Class("HintSession", CommandMode, { // 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; @@ -457,7 +494,7 @@ var HintSession = Class("HintSession", CommandMode, { 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); @@ -469,13 +506,14 @@ var HintSession = Class("HintSession", CommandMode, { 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); @@ -491,10 +529,15 @@ var HintSession = Class("HintSession", CommandMode, { */ 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"); @@ -561,19 +604,25 @@ var HintSession = Class("HintSession", CommandMode, { 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)) { @@ -685,16 +734,15 @@ var Hints = Module("hints", { 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)); @@ -719,21 +767,35 @@ var Hints = Module("hints", { 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); }, /** @@ -762,7 +824,7 @@ var Hints = Module("hints", { 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"])) { @@ -776,15 +838,14 @@ var Hints = Module("hints", { 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]; } @@ -820,7 +881,7 @@ var Hints = Module("hints", { * 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. @@ -971,6 +1032,7 @@ var Hints = Module("hints", { 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))]; @@ -979,10 +1041,13 @@ var Hints = Module("hints", { 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(); - }, + } }); }, @@ -1112,14 +1177,14 @@ var Hints = Module("hints", { 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 @@ -1128,20 +1193,20 @@ var Hints = Module("hints", { 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 is pressed", + "Start an extended hints mode and stay there until is pressed", function ({ count }) { hints.open("g;", { continue: true, count: count }); }, { count: true }); @@ -1172,7 +1237,7 @@ var Hints = Module("hints", { "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"] @@ -1180,7 +1245,7 @@ var Hints = Module("hints", { { 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)) @@ -1191,10 +1256,10 @@ var Hints = Module("hints", { }); 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); @@ -1213,8 +1278,8 @@ var Hints = Module("hints", { }, 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")); } }); @@ -1224,15 +1289,12 @@ var Hints = Module("hints", { { 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 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 .", "1": "Follow the selected hint on .", - "2": "Follow the selected hint on only it's been -selected." } });