diff options
-rw-r--r-- | char-hints-mod.js | 1369 |
1 files changed, 754 insertions, 615 deletions
diff --git a/char-hints-mod.js b/char-hints-mod.js index 02a59b7..807153d 100644 --- a/char-hints-mod.js +++ b/char-hints-mod.js @@ -1,710 +1,849 @@ -// Vimperator plugin: 'Char Hints Mod' -// Last Change: 06-Apr-2008. Jan 2008 -// License: GPL -// Version: 0.3 -// Maintainer: Trapezoid <trapezoid.g@gmail.com> - -// This file is a tweak based on char-hints.js by: -// (c) 2008: marco candrian <mac@calmar.ws> -// This file is a tweak based on hints.js by: -// (c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net> - -// Tested with vimperator 0.6pre from 2008-03-07 -// (won't work with older versions) - -// INSTALL: put this file into ~/.vimperator/plugin/ (create folders if necessary) -// and restart firefox or :source that file - -// plugin-setup -liberator.plugins.charhints = {}; -var chh = liberator.plugins.charhints; - -//<<<<<<<<<<<<<<<< EDIT USER SETTINGS HERE - -//chh.hintchars = "asdfjkl"; // chars to use for generating hints -chh.hintchars = "hjklasdfgyuiopqwertnmzxcvb"; // chars to use for generating hints - -chh.showcapitals = true; // show capital letters, even with lowercase hintchars -chh.timeout = 500; // in 1/000sec; when set to 0, press <RET> to follow - -chh.fgcolor = "black"; // hints foreground color -chh.bgcolor = "yellow"; // hints background color -chh.selcolor = "#99FF00"; // selected/active hints background color - -chh.mapNormal = "f"; // trigger normal mode with... -chh.mapNormalNewTab = "F"; // trigger and open in new tab -chh.mapExtended = ";"; // open in extended mode (see notes below) - -chh.hinttags = "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " + -"//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " + -"//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " + -"//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | " + -"//xhtml:button | //xhtml:select"; - -//======================================== -// extended hints mode arguments -// -// ; to focus a link and hover it with the mouse -// a to save its destination (prompting for save location) -// s to save its destination -// o to open its location in the current tab -// t to open its location in a new tab -// O to open its location in an :open query -// T to open its location in a :tabopen query -// v to view its destination source -// w to open its destination in a new window -// W to open its location in a :winopen query -// y to yank its location -// Y to yank its text description - -// variables etc//{{{ - - -// ignorecase when showcapitals = true -// (input keys on onEvent gets lowercased too - -if (chh.showcapitals) - chh.hintchars = chh.hintchars.toLowerCase(); - - -chh.submode = ""; // used for extended mode, can be "o", "t", "y", etc. -chh.hintString = ""; // the typed string part of the hint is in this string -chh.hintNumber = 0; // only the numerical part of the hint -chh.usedTabKey = false; // when we used <Tab> to select an element - -chh.hints = []; -chh.validHints = []; // store the indices of the "hints" array with valid elements - -chh.activeTimeout = null; // needed for hinttimeout > 0 -chh.canUpdate = false; - -// used in number2hintchars -chh.transval = {"0":0, "1":1, "2":2, "3":3, "4":4, "5":5, "6":6, "7":7, "8":8, "9":9, "a":10, "b":11, - "c":12, "d":13,"e":14, "f":15, "g":16, "h":17, "i":18, "j":19, "k":20, "l":21, "m":22, "n":23, - "o":24, "p":25,"q":26, "r":27, "s":28, "t":29, "u":30, "v":31, "w":32, "x":33, "y":34, "z":35}; - -// used in hintchars2number -chh.conversion = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - -// keep track of the documents which we generated the hints for -// docs = { doc: document, start: start_index in hints[], end: end_index in hints[] } -chh.docs = []; -//}}} -// reset all important variables -chh.reset = function ()//{{{ +/***** BEGIN LICENSE BLOCK ***** {{{ +Version: MPL 1.1/GPL 2.0/LGPL 2.1 + +The contents of this file are subject to the Mozilla Public License Version +1.1 (the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at +http://www.mozilla.org/MPL/ + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +for the specific language governing rights and limitations under the +License. + +(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net> + +Alternatively, the contents of this file may be used under the terms of +either the GNU General Public License Version 2 or later (the "GPL"), or +the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +in which case the provisions of the GPL or the LGPL are applicable instead +of those above. If you wish to allow use of your version of this file only +under the terms of either the GPL or the LGPL, and not to allow others to +use your version of this file under the terms of the MPL, indicate your +decision by deleting the provisions above and replace them with the notice +and other provisions required by the GPL or the LGPL. If you do not delete +the provisions above, a recipient may use your version of this file under +the terms of any one of the MPL, the GPL or the LGPL. +}}} ***** END LICENSE BLOCK *****/ +/** + * ==VimperatorPlugin== + * @name char-hints-mod.js + * @description Character Hints mode + * ==/VimperatorPlugin== + * + * It's based on vimperator-2.0pre(2008/11/28) hints.js + * + **/ +(function () //{{{ { - liberator.statusline.updateInputBuffer(""); - chh.hintString = ""; - chh.hintNumber = 0; - chh.usedTabKey = false; - chh.hints = []; - chh.validHints = []; - chh.canUpdate = false; - chh.docs = []; - - if (chh.activeTimeout) - clearTimeout(chh.activeTimeout); - chh.activeTimeout = null; -} -//}}} -chh.updateStatusline = function ()//{{{ -{ - liberator.statusline.updateInputBuffer(("") + - (chh.hintString ? "\"" + chh.hintString + "\"" : "") + - (chh.hintNumber > 0 ? " <" + chh.hintNumber + ">" : "")); -} -//}}} -// this function 'click' an element, which also works -// for javascript links -chh.hintchars2number = function (hintstr)//{{{ -{ - // convert into 'normal number then make it decimal-based + //////////////////////////////////////////////////////////////////////////////// + ////////////////////// PRIVATE SECTION ///////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + + const ELEM = 0, TEXT = 1, SPAN = 2, IMGSPAN = 3; + + var myModes = config.browserModes; + + var hintMode; + var submode = ""; // used for extended mode, can be "o", "t", "y", etc. + var hintString = ""; // the typed string part of the hint is in this string + var hintNumber = 0; // only the numerical part of the hint + var usedTabKey = false; // when we used <Tab> to select an element + var prevInput = ""; // record previous user input type, "text" || "number" + + // hints[] = [elem, text, span, imgspan, elem.style.backgroundColor, elem.style.color] + var pageHints = []; + var validHints = []; // store the indices of the "hints" array with valid elements + + var escapeNumbers = false; // escape mode for numbers. true -> treated as hint-text + var activeTimeout = null; // needed for hinttimeout > 0 + var canUpdate = false; + + // keep track of the documents which we generated the hints for + // docs = { doc: document, start: start_index in hints[], end: end_index in hints[] } + var docs = []; + + const Mode = new Struct("prompt", "action", "tags"); + Mode.defaultValue("tags", function () function () options.hinttags); + function extended() options.extendedhinttags; + const hintModes = { + ";": Mode("Focus hint", function (elem) buffer.focusElement(elem), extended), + a: Mode("Save hint with prompt", function (elem) buffer.saveLink(elem, false)), + s: Mode("Save hint", function (elem) buffer.saveLink(elem, true)), + o: Mode("Follow hint", function (elem) buffer.followLink(elem, liberator.CURRENT_TAB)), + t: Mode("Follow hint in a new tab", function (elem) buffer.followLink(elem, liberator.NEW_TAB)), + b: Mode("Follow hint in a background tab", function (elem) buffer.followLink(elem, liberator.NEW_BACKGROUND_TAB)), + v: Mode("View hint source", function (elem, loc) buffer.viewSource(loc, false), extended), + V: Mode("View hint source", function (elem, loc) buffer.viewSource(loc, true), extended), + w: Mode("Follow hint in a new window", function (elem) buffer.followLink(elem, liberator.NEW_WINDOW), extended), + + "?": Mode("Show information for hint", function (elem) buffer.showElementInfo(elem), extended), + O: Mode("Open location based on hint", function (elem, loc) commandline.open(":", "open " + loc, modes.EX)), + T: Mode("Open new tab based on hint", function (elem, loc) commandline.open(":", "tabopen " + loc, modes.EX)), + W: Mode("Open new window based on hint", function (elem, loc) commandline.open(":", "winopen " + loc, modes.EX)), + y: Mode("Yank hint location", function (elem, loc) util.copyToClipboard(loc, true)), + Y: Mode("Yank hint description", function (elem) util.copyToClipboard(elem.textContent || "", true), extended), + }; + + // reset all important variables + function reset() + { + statusline.updateInputBuffer(""); + hintString = ""; + hintNumber = 0; + usedTabKey = false; + prevInput = ""; + pageHints = []; + validHints = []; + canUpdate = false; + docs = []; + escapeNumbers = false; + + if (activeTimeout) + clearTimeout(activeTimeout); + activeTimeout = null; + } + + function updateStatusline() + { + statusline.updateInputBuffer((escapeNumbers ? mappings.getMapLeader() : "") + (hintNumber || "")); + } - var converted = ""; + function generate(win) + { + if (!win) + win = window.content; - // translate users hintchars into a number (chh.conversion) 0 -> 0, 1 -> 1, ... - for (let i = 0, l = hintstr.length; i < l; i++) - converted += "" + chh.conversion[chh.hintchars.indexOf(hintstr[i])]; + var doc = win.document; + var height = win.innerHeight; + var width = win.innerWidth; + var scrollX = doc.defaultView.scrollX; + var scrollY = doc.defaultView.scrollY; - // add one, since hints begin with 0; + var baseNodeAbsolute = util.xmlToDom(<span highlight="Hint"/>, doc); - return parseInt(converted, chh.hintchars.length); // hintchars.length is the base/radix -} -//}}} -chh.number2hintchars = function (nr)//{{{ -{ - var oldnr = nr; - var converted = ""; - var tmp = ""; + var elem, tagname, text, span, rect; + var res = buffer.evaluateXPath(hintMode.tags(), doc, null, true); + + var fragment = util.xmlToDom(<div highlight="hints"/>, doc); + var start = pageHints.length; + for (let elem in res) + { + // TODO: for iframes, this calculation is wrong + rect = elem.getBoundingClientRect(); + if (!rect || rect.top > height || rect.bottom < 0 || rect.left > width || rect.right < 0) + continue; + + rect = elem.getClientRects()[0]; + if (!rect) + continue; + + var computedStyle = doc.defaultView.getComputedStyle(elem, null); + if (computedStyle.getPropertyValue("visibility") == "hidden" || computedStyle.getPropertyValue("display") == "none") + continue; + + // TODO: mozilla docs recommend localName instead of tagName + tagname = elem.tagName.toLowerCase(); + if (tagname == "input" || tagname == "textarea") + text = elem.value; + else if (tagname == "select") + { + if (elem.selectedIndex >= 0) + text = elem.item(elem.selectedIndex).text; + else + text = ""; + } + else + text = elem.textContent.toLowerCase(); - tmp = nr.toString(chh.hintchars.length); // hintchars.length is the base/radix) + span = baseNodeAbsolute.cloneNode(true); + span.style.left = (rect.left + scrollX) + "px"; + span.style.top = (rect.top + scrollY) + "px"; + fragment.appendChild(span); - // translate numbers into users hintchars - // tmp might be 2e -> (chh.transval) 2 and 14 -> (chh.hintchars) according hintchars + pageHints.push([elem, text, span, null, elem.style.backgroundColor, elem.style.color]); + } - for (let i = 0, l = tmp.length; i < l; i++) - converted += "" + chh.hintchars[chh.transval[tmp[i]]]; + if (doc.body) + { + doc.body.appendChild(fragment); + docs.push({ doc: doc, start: start, end: pageHints.length - 1 }); + } - return converted; -} -//}}} -chh.openHint = function (where)//{{{ -{ - if (chh.validHints.length < 1) - return false; - - var x = 1, y = 1; - var elem = chh.validHints[chh.hintNumber - 1] || chh.validHints[0]; - var elemTagName = elem.localName.toLowerCase(); - elem.focus(); - - liberator.buffer.followLink(elem, where); - return true; -} -//}}} -chh.focusHint = function ()//{{{ -{ - if (chh.validHints.length < 1) - return false; + // also generate hints for frames + Array.forEach(win.frames, function (frame) { generate(frame); }); - var elem = chh.validHints[chh.hintNumber - 1] || chh.validHints[0]; - var doc = window.content.document; - var elemTagName = elem.localName.toLowerCase(); - if (elemTagName == "frame" || elemTagName == "iframe") - { - elem.contentWindow.focus(); - return false; + return true; } - else + + // TODO: make it aware of imgspans + function showActiveHint(newID, oldID) { - elem.focus(); + var oldElem = validHints[oldID - 1]; + if (oldElem) + setClass(oldElem, false); + + var newElem = validHints[newID - 1]; + if (newElem) + setClass(newElem, true); } - var evt = doc.createEvent("MouseEvents"); - var x = 0; - var y = 0; - // for imagemap - if (elemTagName == "area") + function setClass(elem, active) { - [x, y] = elem.getAttribute("coords").split(","); - x = Number(x); - y = Number(y); + let prefix = (elem.getAttributeNS(NS.uri, "class") || "") + " "; + if (active) + elem.setAttributeNS(NS.uri, "highlight", prefix + "HintActive"); + else + elem.setAttributeNS(NS.uri, "highlight", prefix + "HintElem"); } - evt.initMouseEvent("mouseover", true, true, doc.defaultView, 1, x, y, 0, 0, 0, 0, 0, 0, 0, null); - elem.dispatchEvent(evt); -} -//}}} -chh.yankHint = function (text)//{{{ -{ - if (chh.validHints.length < 1) - return false; - - var elem = chh.validHints[chh.hintNumber - 1] || chh.validHints[0]; - var loc; - if (text) - loc = elem.textContent; - else - loc = elem.href; - - liberator.copyToClipboard(loc); - liberator.echo("Yanked " + loc, liberator.commandline.FORCE_SINGLELINE); -} -//}}} -chh.saveHint = function (skipPrompt)//{{{ -{ - if (chh.validHints.length < 1) - return false; + function showHints() + { - var elem = chh.validHints[chh.hintNumber - 1] || chh.validHints[0]; + let elem, tagname, text, rect, span, imgspan; + let hintnum = 1; + let validHint = hintMatcher(hintString.toLowerCase()); + let activeHint = hintNumber || 1; + validHints = []; - try - { - liberator.buffer.saveLink(elem,skipPrompt); - } - catch (e) - { - liberator.echoerr(e); - } -} -//}}} -chh.generate = function (win)//{{{ -{ - var startDate = Date.now(); - - if (!win) - win = window.content; - - var doc = win.document; - var height = win.innerHeight; - var width = win.innerWidth; - var scrollX = doc.defaultView.scrollX; - var scrollY = doc.defaultView.scrollY; - - var baseNodeAbsolute = doc.createElementNS("http://www.w3.org/1999/xhtml", "span"); - baseNodeAbsolute.style.backgroundColor = "red"; - baseNodeAbsolute.style.color = "white"; - baseNodeAbsolute.style.position = "absolute"; - baseNodeAbsolute.style.fontSize = "10px"; - baseNodeAbsolute.style.fontWeight = "bold"; - baseNodeAbsolute.style.lineHeight = "10px"; - baseNodeAbsolute.style.padding = "0px 1px 0px 0px"; - baseNodeAbsolute.style.zIndex = "10000001"; - baseNodeAbsolute.style.display = "none"; - baseNodeAbsolute.className = "vimperator-hint"; - - var elem, tagname, text, span, rect; - var res = liberator.buffer.evaluateXPath(chh.hinttags, doc, null, true); - liberator.log("shints: evaluated XPath after: " + (Date.now() - startDate) + "ms"); - - var fragment = doc.createDocumentFragment(); - var start = chh.hints.length; - while ((elem = res.iterateNext()) != null) - { - // TODO: for frames, this calculation is wrong - rect = elem.getBoundingClientRect(); - if (!rect || rect.top > height || rect.bottom < 0 || rect.left > width || rect.right < 0) - continue; - - rect = elem.getClientRects()[0]; - if (!rect) - continue; - - // TODO: mozilla docs recommend localName instead of tagName - tagname = elem.tagName.toLowerCase(); - text = ""; - span = baseNodeAbsolute.cloneNode(true); - span.style.left = (rect.left + scrollX) + "px"; - span.style.top = (rect.top + scrollY) + "px"; - fragment.appendChild(span); - - chh.hints.push([elem, text, span, null, elem.style.backgroundColor, elem.style.color]); - } + for (let [,{ doc: doc, start: start, end: end }] in Iterator(docs)) + { + let scrollX = doc.defaultView.scrollX; + let scrollY = doc.defaultView.scrollY; - doc.body.appendChild(fragment); - chh.docs.push({ doc: doc, start: start, end: chh.hints.length - 1 }); + inner: + for (let i in (util.interruptableRange(start, end + 1, 500))) + { + let hint = pageHints[i]; + [elem, text, span, imgspan] = hint; - // also generate hints for frames - for (let i = 0; i < win.frames.length; i++) - chh.generate(win.frames[i]); + // if (!validHint(text)) + // { + // span.style.display = "none"; + // if (imgspan) + // imgspan.style.display = "none"; - liberator.log("shints: generate() completed after: " + (Date.now() - startDate) + "ms"); - return true; -} -//}}} -// TODO: make it aware of imgspans -chh.showActiveHint = function (newID, oldID)//{{{ -{ - var oldElem = chh.validHints[oldID - 1]; - if (oldElem) - oldElem.style.backgroundColor = chh.bgcolor; - - var newElem = chh.validHints[newID - 1]; - if (newElem) - newElem.style.backgroundColor = chh.selcolor; -} -//}}} -chh.showHints = function ()//{{{ -{ - var startDate = Date.now(); - var win = window.content; - var height = win.innerHeight; - var width = win.innerWidth; + // elem.removeAttributeNS(NS.uri, "highlight"); + // continue inner; + // } + if (text == "" && elem.firstChild && elem.firstChild.tagName == "IMG") + { + if (!imgspan) + { + rect = elem.firstChild.getBoundingClientRect(); + if (!rect) + continue; + + imgspan = util.xmlToDom(<span highlight="Hint"/>, doc); + imgspan.setAttributeNS(NS.uri, "class", "HintImage"); + imgspan.style.left = (rect.left + scrollX) + "px"; + imgspan.style.top = (rect.top + scrollY) + "px"; + imgspan.style.width = (rect.right - rect.left) + "px"; + imgspan.style.height = (rect.bottom - rect.top) + "px"; + hint[IMGSPAN] = imgspan; + span.parentNode.appendChild(imgspan); + } + setClass(imgspan, activeHint == hintnum) + } - var elem, tagname, text, rect, span, imgspan; - var hintnum = 1; - //var findTokens = chh.hintString.split(/ +/); - var activeHint = chh.hintNumber || 1; - chh.validHints = []; + var chars = num2chars(hintnum++); + span.setAttribute("number", chars); + elem.setAttribute("number", chars); + ++hintnum; + if (imgspan) + imgspan.setAttribute("number", num2chars(hintnum)); + else + setClass(elem, hintString == chars); + //setClass(elem, activeHint == chars); + if(chars.indexOf(hintString)==0) { + validHints.push(elem); + } + } + } - for (let j = 0; j < chh.docs.length; j++) - { - let doc = chh.docs[j].doc; - let start = chh.docs[j].start; - let end = chh.docs[j].end; - let scrollX = doc.defaultView.scrollX; - let scrollY = doc.defaultView.scrollY; - -outer: - for (let i = start; i <= end; i++) + if (options.usermode) { - [elem, , span, imgspan] = chh.hints[i]; - text = ""; - - if (elem.firstChild && elem.firstChild.tagName == "IMG") + let css = []; + // FIXME: Broken for imgspans. + for (let [,{ doc: doc }] in Iterator(docs)) { - if (!imgspan) + for (let elem in buffer.evaluateXPath("//*[@liberator:highlight and @number]", doc)) { - rect = elem.firstChild.getBoundingClientRect(); - if (!rect) - continue; - - imgspan = doc.createElementNS("http://www.w3.org/1999/xhtml", "span"); - imgspan.style.position = "absolute"; - imgspan.style.opacity = 0.5; - imgspan.style.zIndex = "10000000"; - imgspan.style.left = (rect.left + scrollX) + "px"; - imgspan.style.top = (rect.top + scrollY) + "px"; - imgspan.style.width = (rect.right - rect.left) + "px"; - imgspan.style.height = (rect.bottom - rect.top) + "px"; - imgspan.className = "vimperator-hint"; - chh.hints[i][3] = imgspan; - doc.body.appendChild(imgspan); + let group = elem.getAttributeNS(NS.uri, "highlight"); + css.push(highlight.selector(group) + "[number='" + elem.getAttribute("number") + "'] { " + elem.style.cssText + " }"); } - imgspan.style.backgroundColor = (activeHint == hintnum) ? chh.selcolor : chh.bgcolor; - imgspan.style.display = "inline"; } - - if (!imgspan) - elem.style.backgroundColor = (activeHint == hintnum) ? chh.selcolor : chh.bgcolor; - elem.style.color = chh.fgcolor; - if (chh.showcapitals) - span.textContent = chh.number2hintchars(hintnum++).toUpperCase(); - else - span.textContent = chh.number2hintchars(hintnum++); - - span.style.display = "inline"; - chh.validHints.push(elem); + styles.addSheet("hint-positions", "*", css.join("\n"), true, true); } - } - liberator.log("shints: showHints() completed after: " + (Date.now() - startDate) + "ms"); - return true; -} -//}}} -chh.removeHints = function (timeout)//{{{ -{ - var firstElem = chh.validHints[0] || null; - var firstElemselcolor = ""; - var firstElemColor = ""; + return true; + } - for (let j = 0; j < chh.docs.length; j++) + function removeHints(timeout) { - let doc = chh.docs[j].doc; - let start = chh.docs[j].start; - let end = chh.docs[j].end; + var firstElem = validHints[0] || null; - for (let i = start; i <= end; i++) + for (let [,{ doc: doc, start: start, end: end }] in Iterator(docs)) { - // remove the span for the numeric display part - doc.body.removeChild(chh.hints[i][2]); - if (chh.hints[i][3]) // a transparent span for images - doc.body.removeChild(chh.hints[i][3]); - - if (timeout && firstElem == chh.hints[i][0]) + for (let elem in buffer.evaluateXPath("//*[@liberator:highlight='hints']", doc)) + elem.parentNode.removeChild(elem); + for (let i in util.range(start, end + 1)) { - firstElemselcolor = chh.hints[i][4]; - firstElemColor = chh.hints[i][5]; + let hint = pageHints[i]; + if (!timeout || hint[ELEM] != firstElem) + hint[ELEM].removeAttributeNS(NS.uri, "highlight"); } - else + + // animate the disappearance of the first hint + if (timeout && firstElem) { - // restore colors - let elem = chh.hints[i][0]; - elem.style.backgroundColor = chh.hints[i][4]; - elem.style.color = chh.hints[i][5]; + // USE THIS FOR MAKING THE SELECTED ELEM RED + // firstElem.style.backgroundColor = "red"; + // firstElem.style.color = "white"; + // setTimeout(function () { + // firstElem.style.backgroundColor = firstElemBgColor; + // firstElem.style.color = firstElemColor; + // }, 200); + // OR USE THIS FOR BLINKING: + // var counter = 0; + // var id = setInterval(function () { + // firstElem.style.backgroundColor = "red"; + // if (counter % 2 == 0) + // firstElem.style.backgroundColor = "yellow"; + // else + // firstElem.style.backgroundColor = "#88FF00"; + // + // if (counter++ >= 2) + // { + // firstElem.style.backgroundColor = firstElemBgColor; + // firstElem.style.color = firstElemColor; + // clearTimeout(id); + // } + // }, 100); + setTimeout(function () { firstElem.removeAttributeNS(NS.uri, "highlight") }, timeout); } } + styles.removeSheet("hint-positions", null, null, null, true); - // animate the disappearance of the first hint - if (timeout && firstElem) - { - setTimeout(function () { - firstElem.style.backgroundColor = firstElemselcolor; - firstElem.style.color = firstElemColor; - }, timeout); - } + reset(); } - liberator.log("shints: removeHints() done"); - chh.reset(); -} -//}}} -chh.processHints = function (followFirst)//{{{ -{ - if (chh.validHints.length == 0) + function processHints(followFirst) { - liberator.beep(); - return false; - } + if (validHints.length == 0) + { + liberator.beep(); + return false; + } - if (!followFirst) - { - let firstHref = chh.validHints[0].getAttribute("href") || null; - if (firstHref) + if (options["followhints"] > 0) + { + if (!followFirst) + return false; // 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 + if ((options["followhints"] == 2) && validHints.length > 1 && !hintNumber) + return liberator.beep(); + } + + if (!followFirst) { - if (chh.validHints.some(function (e) { return e.getAttribute("href") != firstHref; })) + var firstHref = validHints[0].getAttribute("href") || null; + if (firstHref) + { + if (validHints.some(function (e) e.getAttribute("href") != firstHref)) + return false; + } + else if (validHints.length > 1) + { return false; + } } - else if (chh.validHints.length > 1) - return false; - } - - var activeNum = chh.hintNumber || 1; - var loc = chh.validHints[activeNum - 1].href || ""; - switch (chh.submode) - { - case ";": chh.focusHint(); break; - case "a": chh.saveHint(false); break; - case "s": chh.saveHint(true); break; - case "o": chh.openHint(liberator.CURRENT_TAB); break; - case "O": liberator.commandline.open(":", "open " + loc, liberator.modes.EX); break; - case "t": chh.openHint(liberator.NEW_TAB); break; - case "T": liberator.commandline.open(":", "tabopen " + loc, liberator.modes.EX); break; - case "w": chh.openHint(liberator.NEW_WINDOW); break; - case "W": liberator.commandline.open(":", "winopen " + loc, liberator.modes.EX); break; - case "y": chh.yankHint(false); break; - case "Y": chh.yankHint(true); break; - default: - liberator.echoerr("INTERNAL ERROR: unknown submode: " + chh.submode); - } - var timeout = followFirst ? 0 : 500; - chh.removeHints(timeout); + var timeout = followFirst || events.feedingKeys ? 0 : 500; + var elems = []; + for (let [,{ doc: doc }] in Iterator(docs)) + { + for (let elem in buffer.evaluateXPath("//*[@number=\""+hintString+"\"]", doc)) + { + elems.push(elem); + } + } + var elem = elems[0]; + removeHints(timeout); - if (liberator.modes.extended & liberator.modes.ALWAYS_HINT) - { + if (timeout == 0) + // force a possible mode change, based on wheter an input field has focus + events.onFocusChange(); setTimeout(function () { - chh.canUpdate = true; - chh.hintString = ""; - chh.hintNumber = 0; - liberator.statusline.updateInputBuffer(""); - }, timeout); + if (modes.extended & modes.HINTS) + modes.reset(); + hintMode.action(elem, elem.href || ""); + }, timeout); + return true; } - else + + function onInput (event) { - if (timeout == 0 || liberator.modes.isReplaying) - { - // force a possible mode change, based on wheter an input field has focus - liberator.events.onFocusChange(); - if (liberator.mode == liberator.modes.HINTS) - liberator.modes.reset(false); - } - else + prevInput = "text"; + + // clear any timeout which might be active after pressing a number + if (activeTimeout) { - liberator.modes.add(liberator.modes.INACTIVE_HINT); - setTimeout(function () { - if (liberator.mode == liberator.modes.HINTS) - liberator.modes.reset(false); - }, timeout); + clearTimeout(activeTimeout); + activeTimeout = null; } + + hintNumber = 0; + hintString = commandline.getCommand().toUpperCase(); + updateStatusline(); + showHints(); + if (validHints.length == 1) + processHints(false); } - return true; -} -//}}} -// TODO: implement framesets -chh.show = function (mode, minor, filter)//{{{ -{ - if (mode == liberator.modes.EXTENDED_HINT && !/^[;asoOtTwWyY]$/.test(minor)) + function hintMatcher(hintString) //{{{ { - liberator.beep(); - return; - } + function containsMatcher(hintString) //{{{ + { + var tokens = hintString.split(/ +/); + return function (linkText) tokens.every(function (token) linkText.indexOf(token) >= 0); + } //}}} - liberator.modes.set(liberator.modes.HINTS, mode); - chh.submode = minor || "o"; // open is the default mode - chh.hintString = filter || ""; - chh.hintNumber = 0; - chh.canUpdate = false; + function wordStartsWithMatcher(hintString, allowWordOverleaping) //{{{ + { + var hintStrings = hintString.split(/ +/); + var wordSplitRegex = new RegExp(options["wordseparators"]); - chh.generate(); + // What the **** does this do? --Kris + function charsAtBeginningOfWords(chars, words, allowWordOverleaping) + { + var charIdx = 0; + var numMatchedWords = 0; + for (let [,word] in Iterator(words)) + { + if (word.length == 0) + continue; - // get all keys from the input queue - var mt = Components.classes["@mozilla.org/thread-manager;1"].getService().mainThread; - while (mt.hasPendingEvents()) - mt.processNextEvent(true); + let wcIdx = 0; + // Check if the current word matches same characters as the previous word. + // Each already matched word has matched at least one character. + if (charIdx > numMatchedWords) + { + let matchingStarted = false; + for (let i in util.range(numMatchedWords, charIdx)) + { + if (chars[i] == word[wcIdx]) + { + matchingStarted = true; + wcIdx++; + } + else if (matchingStarted) + { + wcIdx = 0; + break; + } + } + } + + // the current word matches same characters as the previous word + var prevCharIdx; + if (wcIdx > 0) + { + prevCharIdx = charIdx; + // now check if it matches additional characters + for (; wcIdx < word.length && charIdx < chars.length; wcIdx++, charIdx++) + { + if (word[wcIdx] != chars[charIdx]) + break; + } + + // the word doesn't match additional characters, now check if the + // already matched characters are equal to the next characters for matching, + // if yes, then consume them + if (prevCharIdx == charIdx) + { + for (let i = 0; i < wcIdx && charIdx < chars.length; i++, charIdx++) + { + if (word[i] != chars[charIdx]) + break; + } + } + + numMatchedWords++; + } + // the current word doesn't match same characters as the previous word, just + // try to match the next characters + else + { + prevCharIdx = charIdx; + for (let i = 0; i < word.length && charIdx < chars.length; i++, charIdx++) + { + if (word[i] != chars[charIdx]) + break; + } + + if (prevCharIdx == charIdx) + { + if (!allowWordOverleaping) + return false; + } + else + numMatchedWords++; + } + + if (charIdx == chars.length) + return true; + } - chh.canUpdate = true; - chh.showHints(); + return (charIdx == chars.length); + } - if (chh.validHints.length == 0) - { - liberator.beep(); - liberator.modes.reset(); - return false; - } - else if (chh.validHints.length == 1) - { - chh.processHints(true); - return false; - } - else // still hints visible - return true; -} -//}}} -chh.hide = function ()//{{{ -{ - chh.removeHints(0); -} -//}}} -chh.onEvent = function (event)//{{{ -{ - var key = liberator.events.toString(event); + function stringsAtBeginningOfWords(strings, words, allowWordOverleaping) + { + var strIdx = 0; + for (let [,word] in Iterator(words)) + { + if (word.length == 0) + continue; - if (chh.showcapitals && key.length == 1) - key = key.toLowerCase(); + let str = strings[strIdx]; + if (str.length == 0 || word.indexOf(str) == 0) + strIdx++; + else if (!allowWordOverleaping) + return false; - // clear any timeout which might be active after pressing a number - if (chh.activeTimeout) - { - clearTimeout(chh.activeTimeout); - chh.activeTimeout = null; - } + if (strIdx == strings.length) + return true; + } - switch (key) - { - case "<Return>": - chh.processHints(true); - break; - - case "<Tab>": - case "<S-Tab>": - chh.usedTabKey = true; - if (chh.hintNumber == 0) - chh.hintNumber = 1; - - let oldID = chh.hintNumber; - if (key == "<Tab>") - { - if (++chh.hintNumber > chh.validHints.length) - chh.hintNumber = 1; - } - else - { - if (--chh.hintNumber < 1) - chh.hintNumber = chh.validHints.length; + for (; strIdx < strings.length; strIdx++) + { + if (strings[strIdx].length != 0) + return false; + } + return true; } - chh.showActiveHint(chh.hintNumber, oldID); - return; - case "<BS>": //TODO: may tweak orig hints.js too (adding 2 lines ...) - let oldID = chh.hintNumber; - if (chh.hintNumber > 0) - { - chh.hintNumber = Math.floor(chh.hintNumber / chh.hintchars.length); - chh.hintString = chh.hintString.substr(0, chh.hintString.length - 1); - chh.usedTabKey = false; - } - else + function wordStartsWith(linkText) { - chh.usedTabKey = false; - chh.hintNumber = 0; - liberator.beep(); - return; + if (hintStrings.length == 1 && hintStrings[0].length == 0) + return true; + + let words = linkText.split(wordSplitRegex).map(String.toLowerCase); + if (hintStrings.length == 1) + return charsAtBeginningOfWords(hintStrings[0], words, allowWordOverleaping); + else + return stringsAtBeginningOfWords(hintStrings, words, allowWordOverleaping); } - chh.showActiveHint(chh.hintNumber, oldID); - break; - - case "<C-w>": - case "<C-u>": - chh.hintString = ""; - chh.hintNumber = 0; - break; - - default: - // pass any special or ctrl- etc. prefixed key back to the main vimperator loop - if (/^<./.test(key) || key == ":") - { - //FIXME: won't work probably - let map = null; - if ((map = liberator.mappings.get(liberator.modes.NORMAL, key)) || - (map = liberator.mappings.get(liberator.modes.HINTS, key))) //TODO - { - map.execute(null, -1); - return; - } - liberator.beep(); - return; - } + return wordStartsWith; + } //}}} + + var hintMatching = options["hintmatching"]; + switch (hintMatching) + { + case "contains" : return containsMatcher(hintString); + case "wordstartswith": return wordStartsWithMatcher(hintString, /*allowWordOverleaping=*/ true); + case "firstletters" : return wordStartsWithMatcher(hintString, /*allowWordOverleaping=*/ false); + case "custom" : return liberator.plugins.customHintMatcher(hintString); + default : liberator.echoerr("Invalid hintmatching type: " + hintMatching); + } + return null; + } //}}} - if (chh.hintchars.indexOf(key) >= 0) // TODO: check if in hintchars + function histchars() // {{{ + { + return options["hintchars"].toUpperCase(); + } // }}} + function chars2num(chars) //{{{ + { + var num = 0; + var hintchars = histchars(); + var base = hintchars.length; + for(var i=0;i<chars.length;++i) { + num = base*num + hintchars.indexOf(chars[i]); + } + return num; + } //}}} + function num2chars(num) //{{{ + { + var chars = ""; + var hintchars = histchars(); + var base = hintchars.length; + do { + chars = hintchars[((num % base))] + chars; + num = Math.floor(num/base); + } while(num>0); + + return chars; + } //}}} + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// OPTIONS ///////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + + const DEFAULT_HINTTAGS = "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " + + "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " + + "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " + + "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | //xhtml:button | //xhtml:select"; + + options.add(["extendedhinttags", "eht"], + "XPath string of hintable elements activated by ';'", + "string", DEFAULT_HINTTAGS); + + options.add(["hinttags", "ht"], + "XPath string of hintable elements activated by 'f' and 'F'", + "string", DEFAULT_HINTTAGS); + + options.add(["hinttimeout", "hto"], + "Automatically follow non unique numerical hint", + "number", 0, + { validator: function (value) value >= 0 }); + + options.add(["followhints", "fh"], + "Change the way when to automatically follow hints", + "number", 0, + { validator: function (value) value >= 0 && value < 3 }); + + options.add(["hintmatching", "hm"], + "How links are matched", + "string", "contains", + { + completer: function (filter) { - chh.hintString += key; - let oldHintNumber = chh.hintNumber; - if (chh.hintNumber == 0 || chh.usedTabKey) - { - chh.usedTabKey = false; - } + return [[m, ""] for each (m in ["contains", "wordstartswith", "firstletters", "custom"])]; + }, + validator: options.validateCompleter + }); - chh.hintNumber = chh.hintchars2number(chh.hintString); + options.add(["wordseparators", "wsp"], + "How words are split for hintmatching", + "string", '[.,!?:;/"^$%&?()[\\]{}<>#*+|=~ _-]'); - chh.updateStatusline(); + options.add(["hintchars", "hchar"], + "Hints character", + "string", "hjklasdfgyuiopqwertnmzxcvb"); - if (!chh.canUpdate) - return; + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// MAPPINGS //////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ - if (chh.docs.length == 0) - { - chh.generate(); - chh.showHints(); - } - chh.showActiveHint(chh.hintNumber, oldHintNumber || 1); + mappings.addUserMap(myModes, ["f"], + "Start QuickHint mode", + function () { chh.show("o"); }); - if (chh.hintNumber == 0 || chh.hintNumber > chh.validHints.length) - { - liberator.beep(); - return; - } + mappings.addUserMap(myModes, ["F"], + "Start QuickHint mode, but open link in a new tab", + function () { chh.show("t"); }); - // orig hints.js comment: if we write a numeric part like 3, but we have 45 hints, only follow - // the hint after a timeout, as the user might have wanted to follow link 34 - if (chh.hintNumber > 0 && chh.hintNumber * chh.hintchars.length <= chh.validHints.length) - { - if (chh.timeout > 0) - chh.activeTimeout = setTimeout(chh.processHints, chh.timeout, true); + mappings.addUserMap(myModes, [";"], + "Start an extended hint mode", + function (arg) { chh.show(arg); }, + { flags: Mappings.flags.ARGUMENT }); - return false; - } - // we have a unique hint - chh.processHints(true); - return; - } + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// PUBLIC SECTION ////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + + + var chh = liberator.plugins.charhints = { - if (chh.usedTabKey) + addMode: function (mode) + { + hintModes[mode] = Mode.apply(Mode, Array.slice(arguments, 1)); + }, + + show: function (minor, filter, win) + { + removeHints(); + hintMode = hintModes[minor]; + if (!hintMode) { - chh.usedTabKey = false; - chh.showActiveHint(1, chh.hintNumber); + liberator.beep(); + return; } - } + commandline.input(hintMode.prompt + ":", null, { onChange: onInput }); + modes.extended = modes.HINTS; - chh.updateStatusline(); -}//}}} + submode = minor; + hintString = filter || ""; + hintNumber = 0; + usedTab = false; + prevInput = ""; + canUpdate = false; + generate(win); -// <<<<<<<<<<<<<<< registering/setting up this plugin + // get all keys from the input queue + liberator.threadYield(true); -//liberator.modes.setCustomMode ("CHAR-HINTS", liberator.plugins.charhints.onEvent, -// liberator.plugins.charhints.hide); -liberator.hints = chh; -liberator.mappings.addUserMap([liberator.modes.NORMAL], [chh.mapNormal], - "Start Custum-QuickHint mode", - function () { liberator.plugins.charhints.show(liberator.modes.QUICK_HINT); }, - { noremap: true } -); + canUpdate = true; + showHints(); -liberator.mappings.addUserMap([liberator.modes.NORMAL], [chh.mapNormalNewTab], - "Start Custum-QuickHint mode, but open link in a new tab", - function () { liberator.plugins.charhints.show(liberator.modes.QUICK_HINT, "t"); }, - { noremap: true } -); + if (validHints.length == 0) + { + liberator.beep(); + modes.reset(); + return false; + } + else if (validHints.length == 1) + { + processHints(true); + return false; + } + else // still hints visible + return true; + }, -liberator.mappings.addUserMap([liberator.modes.NORMAL], [chh.mapExtended], - "Start an extended hint mode", - function (arg) + hide: function () { - if (arg == "f") - liberator.plugins.charhints.show(liberator.modes.ALWAYS_HINT, "o"); - else if (arg == "F") - liberator.plugins.charhints.show(liberator.modes.ALWAYS_HINT, "t"); - else - liberator.plugins.charhints.show(liberator.modes.EXTENDED_HINT, arg); + removeHints(0); }, + + onEvent: function (event) { - flags: liberator.Mappings.flags.ARGUMENT, - noremap: true - } -); + var key = events.toString(event); + var followFirst = false; + + // clear any timeout which might be active after pressing a number + if (activeTimeout) + { + clearTimeout(activeTimeout); + activeTimeout = null; + } + + switch (key) + { + case "<Return>": + followFirst = true; + break; + + case "<Tab>": + case "<S-Tab>": + usedTabKey = true; + if (hintNumber == 0) + hintNumber = 1; + + var oldID = hintNumber; + if (key == "<Tab>") + { + if (++hintNumber > validHints.length) + hintNumber = 1; + } + else + { + if (--hintNumber < 1) + hintNumber = validHints.length; + } + hintString = num2chars(hintNumber); + showActiveHint(hintNumber, oldID); + updateStatusline(); + return; + + case "<BS>": + if (hintNumber > 0 && !usedTabKey) + { + hintNumber = Math.floor(hintNumber / 10); + if (hintNumber == 0) + prevInput = "text"; + } + else + { + usedTabKey = false; + hintNumber = 0; + liberator.beep(); + return; + } + break; + + case mappings.getMapLeader(): + escapeNumbers = !escapeNumbers; + if (escapeNumbers && usedTabKey) // hintNumber not used normally, but someone may wants to toggle + hintNumber = 0; // <tab>s ? reset. Prevent to show numbers not entered. + + updateStatusline(); + return; + + default: + var hintchars = histchars(); + if (hintchars.indexOf(key)==0) + { + // FIXME: Kludge. + if (escapeNumbers) + { + let cmdline = document.getElementById("liberator-commandline-command"); + let start = cmdline.selectionStart; + let end = cmdline.selectionEnd; + cmdline.value = cmdline.value.substr(0, start) + key + cmdline.value.substr(start); + cmdline.selectionStart = start + 1; + cmdline.selectionEnd = end + 1; + return; + } + + prevInput = "number"; + + var oldHintNumber = hintNumber; + if (hintNumber == 0 || usedTabKey) + { + usedTabKey = false; + hintNumber = parseInt(key, 10); + } + else + hintNumber = (hintNumber * 10) + parseInt(key, 10); + + updateStatusline(); + + if (!canUpdate) + return; + + if (docs.length == 0) + { + generate(); + showHints(); + } + showActiveHint(hintNumber, oldHintNumber || 1); + + if (hintNumber == 0 || hintNumber > validHints.length) + { + liberator.beep(); + return; + } + + // if we write a numeric part like 3, but we have 45 hints, only follow + // the hint after a timeout, as the user might have wanted to follow link 34 + if (hintNumber > 0 && hintNumber * 10 <= validHints.length) + { + var timeout = options["hinttimeout"]; + if (timeout > 0) + activeTimeout = setTimeout(function () { processHints(true); }, timeout); + + return false; + } + // we have a unique hint + processHints(true); + return; + } + } + + updateStatusline(); + + if (canUpdate) + { + if (docs.length == 0 && hintString.length > 0) + generate(); + + showHints(); + processHints(followFirst); + } + }, + }; + + for(let name in chh) { + hints[name] = chh[name]; + } + //}}} +})(); //}}} // vim: set fdm=marker sw=4 ts=4 et: |