X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=common%2Fcontent%2Fhints.js;h=af09ad4dd499498ef16561971e67bb3a09af3c68;hb=354a049cce8415487552ce405cce167b7071fe1f;hp=bcf431ce06175ffa7664bae2304ad5f26d819f64;hpb=eeed0be1a8abf7e3c97f43b63c1d595e940fef21;p=dactyl.git diff --git a/common/content/hints.js b/common/content/hints.js index bcf431c..af09ad4 100644 --- a/common/content/hints.js +++ b/common/content/hints.js @@ -1,6 +1,6 @@ // Copyright (c) 2006-2008 by Martin Stubenschrott // Copyright (c) 2007-2011 by Doug Kearns -// Copyright (c) 2008-2011 by Kris Maglione +// Copyright (c) 2008-2013 Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. @@ -12,14 +12,11 @@ var HintSession = Class("HintSession", CommandMode, { get extendedMode() modes.HINTS, - init: function init(mode, opts) { + init: function init(mode, opts = {}) { init.supercall(this); - opts = opts || {}; - - // Hack. - if (!opts.window && modes.main == modes.OUTPUT_MULTILINE) - opts.window = commandline.widgets.multilineOutput.contentWindow; + if (!opts.window) + opts.window = modes.getStack(0).params.window; this.hintMode = hints.modes[mode]; dactyl.assert(this.hintMode); @@ -27,7 +24,7 @@ var HintSession = Class("HintSession", CommandMode, { this.activeTimeout = null; // needed for hinttimeout > 0 this.continue = Boolean(opts.continue); this.docs = []; - this.hintKeys = events.fromString(options["hintkeys"]).map(events.closure.toString); + this.hintKeys = DOM.Event.parse(options["hintkeys"]).map(DOM.Event.closure.stringify); this.hintNumber = 0; this.hintString = opts.filter || ""; this.pageHints = []; @@ -35,15 +32,17 @@ var HintSession = Class("HintSession", CommandMode, { this.usedTabKey = false; this.validHints = []; // store the indices of the "hints" array with valid elements + mappings.pushCommand(); 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 +50,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 +68,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 +84,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; } }, @@ -89,11 +96,13 @@ var HintSession = Class("HintSession", CommandMode, { leave.superapply(this, arguments); if (!stack.push) { + mappings.popCommand(); + 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. @@ -231,7 +255,7 @@ var HintSession = Class("HintSession", CommandMode, { getContainerOffsets: function _getContainerOffsets(doc) { let body = doc.body || doc.documentElement; // TODO: getComputedStyle returns null for Facebook channel_iframe doc - probable Gecko bug. - let style = util.computedStyle(body); + let style = DOM(body).style; if (style && /^(absolute|fixed|relative)$/.test(style.position)) { let rect = body.getClientRects()[0]; @@ -255,6 +279,11 @@ var HintSession = Class("HintSession", CommandMode, { let doc = win.document; + memoize(doc, "dactylLabels", () => + 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,18 @@ 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, elem => elem instanceof Element + && DOM(elem).style.float != "none" + && isVisible(elem))) + if (elem.textContent || !elem.name) + return false; + let computedStyle = doc.defaultView.getComputedStyle(elem, null); if (computedStyle.visibility != "visible" || computedStyle.display == "none") return false; @@ -276,43 +312,49 @@ var HintSession = Class("HintSession", CommandMode, { let body = doc.body || doc.querySelector("body"); if (body) { - let fragment = util.xmlToDom(
, doc); - body.appendChild(fragment); - util.computedStyle(fragment).height; // Force application of binding. - let container = doc.getAnonymousElementByAttribute(fragment, "anonid", "hints") || fragment; + let fragment = DOM(["div", { highlight: "hints" }], doc).appendTo(body); + fragment.style.height; // Force application of binding. + let container = doc.getAnonymousElementByAttribute(fragment[0], "anonid", "hints") || fragment[0]; - let baseNodeAbsolute = util.xmlToDom(, doc); + let baseNode = DOM(["span", { highlight: "Hint", style: "display: none;" }], doc)[0]; 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 if (isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement])) + else if (isinstance(elem, [Ci.nsIDOMHTMLInputElement, + Ci.nsIDOMHTMLSelectElement, + Ci.nsIDOMHTMLTextAreaElement])) [hint.text, hint.showText] = hints.getInputHint(elem, doc); - else if (elem.firstElementChild instanceof HTMLImageElement && /^\s*$/.test(elem.textContent)) + else if (elem.firstElementChild instanceof Ci.nsIDOMHTMLImageElement && /^\s*$/.test(elem.textContent)) [hint.text, hint.showText] = [elem.firstElementChild.alt || elem.firstElementChild.title, true]; else hint.text = elem.textContent.toLowerCase(); - hint.span = baseNodeAbsolute.cloneNode(true); + hint.span = baseNode.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) + if (elem instanceof Ci.nsIDOMHTMLAreaElement) [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,13 +400,13 @@ var HintSession = Class("HintSession", CommandMode, { }, /** - * Handle a hint mode event. + * Handle a hints mode event. * * @param {Event} event The event to handle. */ onKeyPress: function onKeyPress(eventList) { const KILL = false, PASS = true; - let key = events.toString(eventList[0]); + let key = DOM.Event.stringify(eventList[0]); this.clearTimeout(); @@ -399,12 +441,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,19 +467,13 @@ 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; if (firstHref) { - if (this.validHints.some(function (h) h.elem.getAttribute("href") != firstHref)) + if (this.validHints.some(h => h.elem.getAttribute("href") != firstHref)) return; } else if (this.validHints.length > 1) @@ -451,31 +492,38 @@ var HintSession = Class("HintSession", CommandMode, { let n = 5; (function next() { - let hinted = n || this.validHints.some(function (h) h.elem === elem); + if (Cu.isDeadWrapper && Cu.isDeadWrapper(elem)) + // Hint document has been unloaded. + return; + + let hinted = n || this.validHints.some(h => h.elem === elem); if (!hinted) hints.setClass(elem, 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); }).call(this); + mappings.pushCommand(); if (!this.continue) { modes.pop(); if (timeout) modes.push(modes.IGNORE, modes.HINTS); } + dactyl.trapErrors("action", this.hintMode, + elem, elem.href || elem.src || "", + this.extendedhintCount, top); + mappings.popCommand(); + 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 +539,16 @@ var HintSession = Class("HintSession", CommandMode, { */ removeHints: function _removeHints(timeout) { for (let { doc, start, end } in values(this.docs)) { - for (let elem in util.evaluateXPath("//*[@dactyl:highlight='hints']", doc)) + DOM(doc.documentElement).highlight.remove("Hinting"); + // Goddamn stupid fucking Gecko 1.x security manager bullshit. + try { delete doc.dactylLabels; } catch (e) { doc.dactylLabels = undefined; } + + for (let elem in DOM.XPath("//*[@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"); @@ -521,59 +575,71 @@ var HintSession = Class("HintSession", CommandMode, { /** * Display the hints in pageHints that are still valid. */ + showCount: 0, show: function _show() { + let count = ++this.showCount; let hintnum = 1; let validHint = hints.hintMatcher(this.hintString.toLowerCase()); let activeHint = this.hintNumber || 1; this.validHints = []; for (let { doc, start, end } in values(this.docs)) { + DOM(doc.documentElement).highlight.add("Hinting"); let [offsetX, offsetY] = this.getContainerOffsets(doc); inner: for (let i in (util.interruptibleRange(start, end + 1, 500))) { + if (this.showCount != count) + return; + let hint = this.pageHints[i]; hint.valid = validHint(hint.text); if (!hint.valid) continue inner; - if (hint.text == "" && hint.elem.firstChild && hint.elem.firstChild instanceof HTMLImageElement) { + if (hint.text == "" && hint.elem.firstChild && hint.elem.firstChild instanceof Ci.nsIDOMHTMLImageElement) { if (!hint.imgSpan) { let rect = hint.elem.firstChild.getBoundingClientRect(); if (!rect) continue; - hint.imgSpan = util.xmlToDom(, doc); - hint.imgSpan.style.display = "none"; - hint.imgSpan.style.left = (rect.left + offsetX) + "px"; - hint.imgSpan.style.top = (rect.top + offsetY) + "px"; - hint.imgSpan.style.width = (rect.right - rect.left) + "px"; - hint.imgSpan.style.height = (rect.bottom - rect.top) + "px"; - hint.span.parentNode.appendChild(hint.imgSpan); + hint.imgSpan = DOM(["span", { highlight: "Hint", "dactyl:hl": "HintImage" }], doc).css({ + display: "none", + left: (rect.left + offsetX) + "px", + top: (rect.top + offsetY) + "px", + width: (rect.right - rect.left) + "px", + height: (rect.bottom - rect.top) + "px" + }).appendTo(hint.span.parentNode)[0]; } } let str = this.getHintString(hintnum); let text = []; - if (hint.elem instanceof HTMLInputElement) + if (hint.elem instanceof Ci.nsIDOMHTMLInputElement) if (hint.elem.type === "radio") 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)) { @@ -668,7 +734,7 @@ var HintSession = Class("HintSession", CommandMode, { * Display the current status to the user. */ updateStatusline: function _updateStatusline() { - statusline.inputBuffer = (this.escapeNumbers ? options["mapleader"] : "") + + statusline.inputBuffer = (this.escapeNumbers ? "\\" : "") + (this.hintNumber ? this.getHintString(this.hintNumber) : ""); }, }); @@ -685,55 +751,75 @@ 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("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)); - this.addMode("w", "Follow hint in a new window", function (elem) buffer.followLink(elem, dactyl.NEW_WINDOW)); - this.addMode("O", "Generate an ‘:open URL’ prompt", function (elem, loc) CommandExMode().open("open " + loc)); - this.addMode("T", "Generate a ‘:tabopen URL’ prompt", function (elem, loc) CommandExMode().open("tabopen " + loc)); - this.addMode("W", "Generate a ‘:winopen URL’ prompt", function (elem, loc) CommandExMode().open("winopen " + loc)); - this.addMode("a", "Add a bookmark", function (elem) bookmarks.addSearchKeyword(elem)); - this.addMode("S", "Add a search keyword", function (elem) bookmarks.addSearchKeyword(elem)); - this.addMode("v", "View hint source", function (elem, loc) buffer.viewSource(loc, false)); - this.addMode("V", "View hint source in external editor", function (elem, loc) buffer.viewSource(loc, true)); - this.addMode("y", "Yank hint location", function (elem, loc) dactyl.clipboardWrite(loc, true)); - this.addMode("Y", "Yank hint description", function (elem) dactyl.clipboardWrite(elem.textContent || "", true)); - this.addMode("c", "Open context menu", function (elem) buffer.openContextMenu(elem)); - this.addMode("i", "Show image", function (elem) dactyl.open(elem.src)); - this.addMode("I", "Show image in a new tab", function (elem) dactyl.open(elem.src, dactyl.NEW_TAB)); - - function isScrollable(elem) isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]) || + this.addMode("?", "Show information for hint", elem => buffer.showElementInfo(elem)); + // TODO: allow for ! override to overwrite existing paths -- where? --djk + this.addMode("s", "Save hint", elem => buffer.saveLink(elem, false)); + this.addMode("f", "Focus frame", elem => dactyl.focus(elem.ownerDocument.defaultView)); + this.addMode("F", "Focus frame or pseudo-frame", buffer.closure.focusElement, isScrollable); + this.addMode("o", "Follow hint", elem => buffer.followLink(elem, dactyl.CURRENT_TAB)); + this.addMode("t", "Follow hint in a new tab", elem => buffer.followLink(elem, dactyl.NEW_TAB)); + this.addMode("b", "Follow hint in a background tab", elem => buffer.followLink(elem, dactyl.NEW_BACKGROUND_TAB)); + this.addMode("w", "Follow hint in a new window", elem => buffer.followLink(elem, dactyl.NEW_WINDOW)); + this.addMode("O", "Generate an ‘:open URL’ prompt", (elem, loc) => CommandExMode().open("open " + loc)); + this.addMode("T", "Generate a ‘:tabopen URL’ prompt", (elem, loc) => CommandExMode().open("tabopen " + loc)); + this.addMode("W", "Generate a ‘:winopen URL’ prompt", (elem, loc) => CommandExMode().open("winopen " + loc)); + this.addMode("a", "Add a bookmark", elem => bookmarks.addSearchKeyword(elem)); + this.addMode("S", "Add a search keyword", elem => bookmarks.addSearchKeyword(elem)); + this.addMode("v", "View hint source", (elem, loc) => buffer.viewSource(loc, false)); + this.addMode("V", "View hint source in external editor", (elem, loc) => buffer.viewSource(loc, true)); + this.addMode("y", "Yank hint location", (elem, loc) => editor.setRegister(null, loc, true)); + this.addMode("Y", "Yank hint description", elem => editor.setRegister(null, elem.textContent || "", true)); + this.addMode("A", "Yank hint anchor url", function (elem) { + let uri = elem.ownerDocument.documentURIObject.clone(); + uri.ref = elem.id || elem.name; + dactyl.clipboardWrite(uri.spec, true); + }); + this.addMode("c", "Open context menu", elem => DOM(elem).contextmenu()); + this.addMode("i", "Show image", elem => dactyl.open(elem.src)); + this.addMode("I", "Show image in a new tab", elem => dactyl.open(elem.src, dactyl.NEW_TAB)); + + function isScrollable(elem) isinstance(elem, [Ci.nsIDOMHTMLFrameElement, + Ci.nsIDOMHTMLIFrameElement]) || Buffer.isScrollable(elem, 0, true) || Buffer.isScrollable(elem, 0, false); }, 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(re => toString(re) != toString(value)) + .concat(value); + + if (update) + eht.reset(); + } + + this.modes[mode] = Hints.Mode(mode, UTF8(prompt), action, filter); }, /** @@ -762,12 +848,12 @@ var Hints = Module("hints", { let type = elem.type; - if (elem instanceof HTMLInputElement && set.has(util.editableInputs, elem.type)) + if (DOM(elem).isInput) return [elem.value, false]; else { for (let [, option] in Iterator(options["hintinputs"])) { if (option == "value") { - if (elem instanceof HTMLSelectElement) { + if (elem instanceof Ci.nsIDOMHTMLSelectElement) { if (elem.selectedIndex >= 0) return [elem.item(elem.selectedIndex).text.toLowerCase(), false]; } @@ -776,15 +862,15 @@ 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]; + // Urgh. if (label) return [label.textContent.toLowerCase(), true]; } @@ -820,7 +906,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. @@ -829,7 +915,7 @@ var Hints = Module("hints", { let tokens = tokenize(/\s+/, hintString); return function (linkText) { linkText = linkText.toLowerCase(); - return tokens.every(function (token) indexOf(linkText, token) >= 0); + return tokens.every(token => indexOf(linkText, token) >= 0); }; } //}}} @@ -956,7 +1042,7 @@ var Hints = Module("hints", { let indexOf = String.indexOf; if (options.get("hintmatching").has("transliterated")) - indexOf = Hints.indexOf; + indexOf = Hints.closure.indexOf; switch (options["hintmatching"][0]) { case "contains" : return containsMatcher(hintString); @@ -968,21 +1054,29 @@ var Hints = Module("hints", { return null; }, //}}} - open: function open(mode, opts) { + open: function open(mode, opts = {}) { this._extendedhintCount = opts.count; - commandline.input(["Normal", mode], "", { + + mappings.pushCommand(); + commandline.input(["Normal", mode], null, { + autocomplete: false, completer: function (context) { - context.compare = function () 0; + context.compare = () => 0; context.completions = [[k, v.prompt] for ([k, v] in Iterator(hints.modes))]; }, + onCancel: mappings.closure.popCommand, onSubmit: function (arg) { if (arg) hints.show(arg, opts); + mappings.popCommand(); }, - onChange: function () { + onChange: function (arg) { + if (Object.keys(hints.modes).some(m => m != arg && m.indexOf(arg) == 0)) + return; + this.accepted = true; modes.pop(); - }, + } }); }, @@ -1012,7 +1106,26 @@ var Hints = Module("hints", { this.hintSession = HintSession(mode, opts); } }, { - translitTable: Class.memoize(function () { + isVisible: function isVisible(elem, offScreen) { + let rect = elem.getBoundingClientRect(); + if (!rect.width || !rect.height) + if (!Array.some(elem.childNodes, elem => elem instanceof Element + && DOM(elem).style.float != "none" + && isVisible(elem))) + return false; + + let win = elem.ownerDocument.defaultView; + if (offScreen && (rect.top + win.scrollY < 0 || rect.left + win.scrollX < 0 || + rect.bottom + win.scrollY > win.scrolMaxY + win.innerHeight || + rect.right + win.scrollX > win.scrolMaxX + win.innerWidth)) + return false; + + if (!DOM(elem).isVisible) + return false; + return true; + }, + + translitTable: Class.Memoize(function () { const table = {}; [ [0x00c0, 0x00c6, ["A"]], [0x00c7, 0x00c7, ["C"]], @@ -1078,7 +1191,7 @@ var Hints = Module("hints", { [0x24d0, 0x24e9, "a"], [0xfb00, 0xfb06, ["ff", "fi", "fl", "ffi", "ffl", "st", "st"]], [0xff21, 0xff3a, "A"], [0xff41, 0xff5a, "a"] - ].forEach(function (start, stop, val) { + ].forEach(function ([start, stop, val]) { if (typeof val != "string") for (let i = start; i <= stop; i++) table[String.fromCharCode(i)] = val[(i - start) % val.length]; @@ -1096,7 +1209,7 @@ var Hints = Module("hints", { if (src.length == 0) return 0; outer: - for (var i = 0; i < end; i++) { + for (var i = 0; i <= end; i++) { var j = i; for (var k = 0; k < src.length;) { var s = dest[j++]; @@ -1112,67 +1225,74 @@ 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 }); }, mappings: function () { - var myModes = config.browserModes.concat(modes.OUTPUT_MULTILINE); - mappings.add(myModes, ["f"], - "Start QuickHint mode", + let bind = function bind(names, description, action, params) + mappings.add(config.browserModes, names, description, + action, params); + + bind(["f"], + "Start Hints mode", function () { hints.show("o"); }); - mappings.add(myModes, ["F"], - "Start QuickHint mode, but open link in a new tab", + bind(["F"], + "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", + bind([";"], + "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", + bind(["g;"], + "Start an extended hints mode and stay there until is pressed", function ({ count }) { hints.open("g;", { continue: true, count: count }); }, { count: true }); - mappings.add(modes.HINTS, [""], + let bind = function bind(names, description, action, params) + mappings.add([modes.HINTS], names, description, + action, params); + + bind([""], "Follow the selected hint", function ({ self }) { self.update(true); }); - mappings.add(modes.HINTS, [""], + bind([""], "Focus the next matching hint", function ({ self }) { self.tab(false); }); - mappings.add(modes.HINTS, [""], + bind([""], "Focus the previous matching hint", function ({ self }) { self.tab(true); }); - mappings.add(modes.HINTS, ["", ""], + bind(["", ""], "Delete the previous character", function ({ self }) self.backspace()); - mappings.add(modes.HINTS, [""], + bind(["\\"], "Toggle hint filtering", function ({ self }) { self.escapeNumbers = !self.escapeNumbers; }); }, options: function () { - function xpath(arg) util.makeXPath(arg); - options.add(["extendedhinttags", "eht"], "XPath or CSS selector strings of hintable elements for extended hint modes", "regexpmap", { + // Make sure to update the docs when you change this. "[iI]": "img", - "[asOTivVWy]": ["a[href]", "area[href]", "img[src]", "iframe[src]"], + "[asOTvVWy]": [":-moz-any-link", "area[href]", "img[src]", "iframe[src]"], + "[A]": ["[id]", "a[name]"], "[f]": "body", "[F]": ["body", "code", "div", "html", "p", "pre", "span"], "[S]": ["input:not([type=hidden])", "textarea", "button", "select"] @@ -1180,27 +1300,30 @@ 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, re => let (match = re.exec(val)) match && match[0] == val, 0)) res ? res.matcher : default_, - setter: function (vals) { + parse: function parse(val) { + let vals = parse.supercall(this, val); for (let value in values(vals)) - value.matcher = util.compileMatcher(Option.splitList(value.result)); + value.matcher = DOM.compileMatcher(Option.splitList(value.result)); return vals; }, - validator: util.validateMatcher + testValues: function testValues(vals, validator) vals.every(re => Option.splitList(re).every(validator)), + validator: DOM.validateMatcher }); 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", + // Make sure to update the docs when you change this. + "stringlist", ":-moz-any-link,area,button,iframe,input:not([type=hidden]),label[for],select,textarea," + "[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); + this.matcher = DOM.compileMatcher(values); return values; }, - validator: util.validateMatcher + validator: DOM.validateMatcher }); options.add(["hintkeys", "hk"], @@ -1212,9 +1335,9 @@ var Hints = Module("hints", { "asdfg;lkjh": "Home Row" }, 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"); + let values = DOM.Event.parse(value).map(DOM.Event.closure.stringify); + return Option.validIf(array.uniq(values).length === values.length && values.length > 1, + _("option.hintkeys.duplicate")); } }); @@ -1224,15 +1347,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." + "1": "Follow the selected hint on ." } }); @@ -1248,7 +1368,8 @@ var Hints = Module("hints", { "transliterated": UTF8("When true, special latin characters are translated to their ASCII equivalents (e.g., é ⇒ e)") }, validator: function (values) Option.validateCompleter.call(this, values) && - 1 === values.reduce(function (acc, v) acc + (["contains", "custom", "firstletters", "wordstartswith"].indexOf(v) >= 0), 0) + 1 === values.reduce((acc, v) => acc + (["contains", "custom", "firstletters", "wordstartswith"].indexOf(v) >= 0), + 0) }); options.add(["wordseparators", "wsp"], @@ -1269,4 +1390,4 @@ var Hints = Module("hints", { } }); -// vim: set fdm=marker sw=4 ts=4 et: +// vim: set fdm=marker sw=4 sts=4 ts=8 et: