aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--linkHints.js520
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();
}