diff options
Diffstat (limited to 'linkHints.js')
| -rw-r--r-- | linkHints.js | 520 |
1 files changed, 256 insertions, 264 deletions
diff --git a/linkHints.js b/linkHints.js index 03af18b0..f7f3e306 100644 --- a/linkHints.js +++ b/linkHints.js @@ -11,6 +11,18 @@ * the range of possibilities by typing the text of the link itself. */ +var linkHints; +/* + * Create the instance of linkHints, specialized based on the user settings. + */ +function initializeLinkHints() { + if (settings.get('filterLinkHints') != "true") // the default hinting system + linkHints = utils.extendWithSuper(linkHintsBase, alphabetHints); + else + linkHints = utils.extendWithSuper(linkHintsBase, filterHints); + linkHints.init(); +} + /* * A set of common operations shared by any link-hinting system. Some methods * are stubbed. @@ -339,293 +351,273 @@ var linkHintsBase = { }; -var linkHints; -/* - * Create the instance of linkHints, specialized based on the user settings. - */ -function initializeLinkHints() { +var alphabetHints = { + digitsNeeded: 1, + logXOfBase: function(x, base) { return Math.log(x) / Math.log(base); }, - if (settings.get('filterLinkHints') != "true") { // the default hinting system - - linkHints = utils.extendWithSuper(linkHintsBase, { - - digitsNeeded: 1, + initSetMarkerAttributes: function(visibleElements) { + this.digitsNeeded = Math.ceil(this.logXOfBase( + visibleElements.length, settings.get('linkHintCharacters').length)); + }, - logXOfBase: function(x, base) { return Math.log(x) / Math.log(base); }, + setMarkerAttributes: function(marker, linkHintNumber) { + var hintString = this.numberToHintString(linkHintNumber, this.digitsNeeded); + marker.innerHTML = this.spanWrap(hintString); + marker.setAttribute("hintString", hintString); + return marker; + }, - initSetMarkerAttributes: function(visibleElements) { - this.digitsNeeded = Math.ceil(this.logXOfBase( - visibleElements.length, settings.get('linkHintCharacters').length)); - }, + /* + * Converts a number like "8" into a hint string like "JK". This is used to sequentially generate all of + * the hint text. The hint string will be "padded with zeroes" to ensure its length is equal to numHintDigits. + */ + numberToHintString: function(number, numHintDigits) { + var base = settings.get('linkHintCharacters').length; + var hintString = []; + var remainder = 0; + do { + remainder = number % base; + hintString.unshift(settings.get('linkHintCharacters')[remainder]); + number -= remainder; + number /= Math.floor(base); + } while (number > 0); + + // Pad the hint string we're returning so that it matches numHintDigits. + var hintStringLength = hintString.length; + for (var i = 0; i < numHintDigits - hintStringLength; i++) + hintString.unshift(settings.get('linkHintCharacters')[0]); + return hintString.join(""); + }, - setMarkerAttributes: function(marker, linkHintNumber) { - var hintString = this.numberToHintString(linkHintNumber, this.digitsNeeded); - marker.innerHTML = this.spanWrap(hintString); - marker.setAttribute("hintString", hintString); - return marker; - }, + normalKeyDownHandler: function (event) { + var keyChar = getKeyChar(event); + if (!keyChar) + return; - /* - * Converts a number like "8" into a hint string like "JK". This is used to sequentially generate all of - * the hint text. The hint string will be "padded with zeroes" to ensure its length is equal to numHintDigits. - */ - numberToHintString: function(number, numHintDigits) { - var base = settings.get('linkHintCharacters').length; - var hintString = []; - var remainder = 0; - do { - remainder = number % base; - hintString.unshift(settings.get('linkHintCharacters')[remainder]); - number -= remainder; - number /= Math.floor(base); - } while (number > 0); - - // Pad the hint string we're returning so that it matches numHintDigits. - var hintStringLength = hintString.length; - for (var i = 0; i < numHintDigits - hintStringLength; i++) - hintString.unshift(settings.get('linkHintCharacters')[0]); - return hintString.join(""); - }, - - normalKeyDownHandler: function (event) { - var keyChar = getKeyChar(event); - if (!keyChar) - return; - - if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) { - if (this.hintKeystrokeQueue.length == 0) { - this.deactivateMode(); - } else { - this.hintKeystrokeQueue.pop(); - var matchString = this.hintKeystrokeQueue.join(""); - this.hintMarkers.filter(this.toggleHighlights.bind(this, matchString)); - } - } else if (settings.get('linkHintCharacters').indexOf(keyChar) >= 0) { - this.hintKeystrokeQueue.push(keyChar); - var matchString = this.hintKeystrokeQueue.join(""); - linksMatched = this.hintMarkers.filter(this.toggleHighlights.bind(this, matchString)); - if (linksMatched.length == 0) - this.deactivateMode(); - else if (linksMatched.length == 1) - this.activateLink(linksMatched[0].clickableItem); - } + if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) { + if (this.hintKeystrokeQueue.length == 0) { + this.deactivateMode(); + } else { + this.hintKeystrokeQueue.pop(); + var matchString = this.hintKeystrokeQueue.join(""); + this.hintMarkers.filter(this.toggleHighlights.bind(this, matchString)); } - }); - - } else { - - linkHints = utils.extendWithSuper(linkHintsBase, { + } else if (settings.get('linkHintCharacters').indexOf(keyChar) >= 0) { + this.hintKeystrokeQueue.push(keyChar); + var matchString = this.hintKeystrokeQueue.join(""); + linksMatched = this.hintMarkers.filter(this.toggleHighlights.bind(this, matchString)); + if (linksMatched.length == 0) + this.deactivateMode(); + else if (linksMatched.length == 1) + this.activateLink(linksMatched[0].clickableItem); + } + } +} - linkTextKeystrokeQueue: [], +filterHints = { + linkTextKeystrokeQueue: [], + labelMap: {}, + delayMode: false, - labelMap: {}, + /* + * Generate a map of input element => label + */ + initSetMarkerAttributes: function() { + var labels = document.querySelectorAll("label"); + for (var i = 0; i < labels.length; i++) { + var forElement = labels[i].getAttribute("for"); + if (forElement) { + var labelText = labels[i].textContent.trim(); + // remove trailing : commonly found in labels + if (labelText[labelText.length-1] == ":") + labelText = labelText.substr(0, labelText.length-1); + this.labelMap[forElement] = labelText; + } + } + }, - delayMode: false, + setMarkerAttributes: function(marker, linkHintNumber) { + var hintString = (linkHintNumber + 1).toString(); + var linkText = ""; + var showLinkText = false; + var element = marker.clickableItem; + // toLowerCase is necessary as html documents return 'IMG' + // and xhtml documents return 'img' + var nodeName = element.nodeName.toLowerCase(); + + if (nodeName == "input") { + if (this.labelMap[element.id]) { + linkText = this.labelMap[element.id]; + showLinkText = true; + } else if (element.type != "password") { + linkText = element.value; + } + // check if there is an image embedded in the <a> tag + } else if (nodeName == "a" && !element.textContent.trim() + && element.firstElementChild + && element.firstElementChild.nodeName.toLowerCase() == "img") { + linkText = element.firstElementChild.alt || element.firstElementChild.title; + if (linkText) + showLinkText = true; + } else { + linkText = element.textContent || element.innerHTML; + } + linkText = linkText.trim().toLowerCase(); + marker.setAttribute("hintString", hintString); + marker.innerHTML = this.spanWrap(hintString + (showLinkText ? ": " + linkText : "")); + marker.setAttribute("linkText", linkText); + }, - /* - * Generate a map of input element => label - */ - initSetMarkerAttributes: function() { - var labels = document.querySelectorAll("label"); - for (var i = 0; i < labels.length; i++) { - var forElement = labels[i].getAttribute("for"); - if (forElement) { - var labelText = labels[i].textContent.trim(); - // remove trailing : commonly found in labels - if (labelText[labelText.length-1] == ":") - labelText = labelText.substr(0, labelText.length-1); - this.labelMap[forElement] = labelText; - } + normalKeyDownHandler: function(event) { + if (this.delayMode) + return; + if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) { + if (this.linkTextKeystrokeQueue.length == 0 && this.hintKeystrokeQueue.length == 0) { + this.deactivateMode(); + } else { + // backspace clears hint key queue first, then acts on link text key queue + if (this.hintKeystrokeQueue.pop()) + this.filterLinkHints(); + else { + this.linkTextKeystrokeQueue.pop(); + this.filterLinkHints(); } - }, - - setMarkerAttributes: function(marker, linkHintNumber) { - var hintString = (linkHintNumber + 1).toString(); - var linkText = ""; - var showLinkText = false; - var element = marker.clickableItem; - // toLowerCase is necessary as html documents return 'IMG' - // and xhtml documents return 'img' - var nodeName = element.nodeName.toLowerCase(); - - if (nodeName == "input") { - if (this.labelMap[element.id]) { - linkText = this.labelMap[element.id]; - showLinkText = true; - } else if (element.type != "password") { - linkText = element.value; + } + } else if (event.keyCode == keyCodes.enter) { + // activate the lowest-numbered link hint that is visible + for (var i = 0; i < this.hintMarkers.length; i++) + if (this.hintMarkers[i].style.display != 'none') { + this.activateLink(this.hintMarkers[i].clickableItem); + break; } - // check if there is an image embedded in the <a> tag - } else if (nodeName == "a" && !element.textContent.trim() - && element.firstElementChild - && element.firstElementChild.nodeName.toLowerCase() == "img") { - linkText = element.firstElementChild.alt || element.firstElementChild.title; - if (linkText) - showLinkText = true; - } else { - linkText = element.textContent || element.innerHTML; - } - linkText = linkText.trim().toLowerCase(); - marker.setAttribute("hintString", hintString); - marker.innerHTML = this.spanWrap(hintString + (showLinkText ? ": " + linkText : "")); - marker.setAttribute("linkText", linkText); - }, + } else { + var keyChar = getKeyChar(event); + if (!keyChar) + return; + + var linksMatched, matchString; + if (/[0-9]/.test(keyChar)) { + this.hintKeystrokeQueue.push(keyChar); + matchString = this.hintKeystrokeQueue.join(""); + linksMatched = this.hintMarkers.filter((function(linkMarker) { + if (linkMarker.getAttribute('filtered') == 'true') + return false; + return this.toggleHighlights(matchString, linkMarker); + }).bind(this)); + } else { + // since we might renumber the hints, the current hintKeyStrokeQueue + // should be rendered invalid (i.e. reset). + this.hintKeystrokeQueue = []; + this.linkTextKeystrokeQueue.push(keyChar); + matchString = this.linkTextKeystrokeQueue.join(""); + linksMatched = this.filterLinkHints(matchString); + } - normalKeyDownHandler: function(event) { - if (this.delayMode) - return; - if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) { - if (this.linkTextKeystrokeQueue.length == 0 && this.hintKeystrokeQueue.length == 0) { - this.deactivateMode(); - } else { - // backspace clears hint key queue first, then acts on link text key queue - if (this.hintKeystrokeQueue.pop()) - this.filterLinkHints(); - else { - this.linkTextKeystrokeQueue.pop(); - this.filterLinkHints(); - } - } - } else if (event.keyCode == keyCodes.enter) { - // activate the lowest-numbered link hint that is visible - for (var i = 0; i < this.hintMarkers.length; i++) - if (this.hintMarkers[i].style.display != 'none') { - this.activateLink(this.hintMarkers[i].clickableItem); - break; - } + if (linksMatched.length == 0) + this.deactivateMode(); + else if (linksMatched.length == 1) { + if (/[0-9]/.test(keyChar)) { + this.activateLink(linksMatched[0].clickableItem); } else { - var keyChar = getKeyChar(event); - if (!keyChar) - return; - - var linksMatched, matchString; - if (/[0-9]/.test(keyChar)) { - this.hintKeystrokeQueue.push(keyChar); - matchString = this.hintKeystrokeQueue.join(""); - linksMatched = this.hintMarkers.filter((function(linkMarker) { - if (linkMarker.getAttribute('filtered') == 'true') - return false; - return this.toggleHighlights(matchString, linkMarker); - }).bind(this)); - } else { - // since we might renumber the hints, the current hintKeyStrokeQueue - // should be rendered invalid (i.e. reset). - this.hintKeystrokeQueue = []; - this.linkTextKeystrokeQueue.push(keyChar); - matchString = this.linkTextKeystrokeQueue.join(""); - linksMatched = this.filterLinkHints(matchString); - } - - if (linksMatched.length == 0) - this.deactivateMode(); - else if (linksMatched.length == 1) { - if (/[0-9]/.test(keyChar)) { - this.activateLink(linksMatched[0].clickableItem); - } else { - // In filter mode, people tend to type out words past the point - // needed for a unique match. Hence we should avoid passing - // control back to command mode immediately after a match is found. - this.activateLink(linksMatched[0].clickableItem, 200); - } - } + // In filter mode, people tend to type out words past the point + // needed for a unique match. Hence we should avoid passing + // control back to command mode immediately after a match is found. + this.activateLink(linksMatched[0].clickableItem, 200); } - }, + } + } + }, - /* - * If called without arguments, it executes immediately. Othewise, it - * executes after 'delay'. - */ - activateLink: function(matchedLink, delay) { - var that = this; - if (delay) { - that.delayMode = true; - if (that.isSelectable(matchedLink)) { - that.simulateSelect(matchedLink); - that.deactivateMode(delay, function() { that.delayMode = false; }); - } else { - if (that.shouldOpenWithQueue) { - that.simulateClick(matchedLink); - that.resetMode(delay); - } else if (that.shouldOpenInNewTab) { - that.simulateClick(matchedLink); - matchedLink.focus(); - that.deactivateMode(delay, function() { that.delayMode = false; }); - } else { - setTimeout(that.simulateClick.bind(that, matchedLink), 400); - matchedLink.focus(); - that.deactivateMode(delay, function() { that.delayMode = false; }); - } - } + /* + * If called without arguments, it executes immediately. Othewise, it + * executes after 'delay'. + */ + activateLink: function(matchedLink, delay) { + var that = this; + if (delay) { + that.delayMode = true; + if (that.isSelectable(matchedLink)) { + that.simulateSelect(matchedLink); + that.deactivateMode(delay, function() { that.delayMode = false; }); + } else { + if (that.shouldOpenWithQueue) { + that.simulateClick(matchedLink); + that.resetMode(delay); + } else if (that.shouldOpenInNewTab) { + that.simulateClick(matchedLink); + matchedLink.focus(); + that.deactivateMode(delay, function() { that.delayMode = false; }); } else { - that._super('activateLink')(matchedLink); - } - }, - - /* - * Hides the links that do not match the linkText search string and marks - * them with the 'filtered' DOM property. Renumbers the remainder. Should - * only be called when there is a change in linkTextKeystrokeQueue, to - * avoid undesired renumbering. - */ - filterLinkHints: function(searchString) { - var linksMatched = []; - var linkSearchString = this.linkTextKeystrokeQueue.join(""); - - for (var i = 0; i < this.hintMarkers.length; i++) { - var linkMarker = this.hintMarkers[i]; - var matchedLink = linkMarker.getAttribute("linkText").toLowerCase() - .indexOf(linkSearchString.toLowerCase()) >= 0; - - if (!matchedLink) { - linkMarker.style.display = "none"; - linkMarker.setAttribute("filtered", "true"); - } else { - if (linkMarker.style.display == "none") - linkMarker.style.display = ""; - - this.setMarkerAttributes(linkMarker, linksMatched.length); - linkMarker.setAttribute("filtered", "false"); - linksMatched.push(linkMarker); - } + setTimeout(that.simulateClick.bind(that, matchedLink), 400); + matchedLink.focus(); + that.deactivateMode(delay, function() { that.delayMode = false; }); } - return linksMatched; - }, + } + } else { + that._super('activateLink')(matchedLink); + } + }, - /* - * If called without arguments, it executes immediately. Othewise, it - * executes after 'delay' and invokes 'callback' when it is finished. - */ - deactivateMode: function(delay, callback) { - var that = this; - function deactivate() { - that.linkTextKeystrokeQueue = []; - that.labelMap = {}; - that._super('deactivateMode')(); - } - if (!delay) - deactivate(); - else - setTimeout(function() { deactivate(); if (callback) callback(); }, delay); - }, + /* + * Hides the links that do not match the linkText search string and marks + * them with the 'filtered' DOM property. Renumbers the remainder. Should + * only be called when there is a change in linkTextKeystrokeQueue, to + * avoid undesired renumbering. + */ + filterLinkHints: function(searchString) { + var linksMatched = []; + var linkSearchString = this.linkTextKeystrokeQueue.join(""); + + for (var i = 0; i < this.hintMarkers.length; i++) { + var linkMarker = this.hintMarkers[i]; + var matchedLink = linkMarker.getAttribute("linkText").toLowerCase() + .indexOf(linkSearchString.toLowerCase()) >= 0; + + if (!matchedLink) { + linkMarker.style.display = "none"; + linkMarker.setAttribute("filtered", "true"); + } else { + if (linkMarker.style.display == "none") + linkMarker.style.display = ""; - resetMode: function(delay, callback) { - var that = this; - if (!delay) { - that.deactivateMode(); - that.activateModeWithQueue(); - } else { - that.deactivateMode(delay, function() { - that.delayMode = false; - that.activateModeWithQueue(); - if (callback) - callback(); - }); - } + this.setMarkerAttributes(linkMarker, linksMatched.length); + linkMarker.setAttribute("filtered", "false"); + linksMatched.push(linkMarker); } + } + return linksMatched; + }, - }); + /* + * If called without arguments, it executes immediately. Othewise, it + * executes after 'delay' and invokes 'callback' when it is finished. + */ + deactivateMode: function(delay, callback) { + var that = this; + function deactivate() { + that.linkTextKeystrokeQueue = []; + that.labelMap = {}; + that._super('deactivateMode')(); + } + if (!delay) + deactivate(); + else + setTimeout(function() { deactivate(); if (callback) callback(); }, delay); + }, + resetMode: function(delay, callback) { + var that = this; + if (!delay) { + that.deactivateMode(); + that.activateModeWithQueue(); + } else { + that.deactivateMode(delay, function() { + that.delayMode = false; + that.activateModeWithQueue(); + if (callback) + callback(); + }); + } } - linkHints.init(); } |
