aboutsummaryrefslogtreecommitdiffstats
path: root/content_scripts/link_hints.js
diff options
context:
space:
mode:
Diffstat (limited to 'content_scripts/link_hints.js')
-rw-r--r--content_scripts/link_hints.js555
1 files changed, 0 insertions, 555 deletions
diff --git a/content_scripts/link_hints.js b/content_scripts/link_hints.js
deleted file mode 100644
index 5c0cd3bd..00000000
--- a/content_scripts/link_hints.js
+++ /dev/null
@@ -1,555 +0,0 @@
-/*
- * This implements link hinting. Typing "F" will enter link-hinting mode, where all clickable items on the
- * page have a hint marker displayed containing a sequence of letters. Typing those letters will select a link.
- *
- * In our 'default' mode, the characters we use to show link hints are a user-configurable option. By default
- * they're the home row. The CSS which is used on the link hints is also a configurable option.
- *
- * In 'filter' mode, our link hints are numbers, and the user can narrow down the range of possibilities by
- * typing the text of the link itself.
- */
-var linkHints = {
- hintMarkers: [],
- hintMarkerContainingDiv: null,
- shouldOpenInNewTab: false,
- shouldOpenWithQueue: false,
- // function that does the appropriate action on the selected link
- linkActivator: undefined,
- // While in delayMode, all keypresses have no effect.
- delayMode: false,
- // Handle the link hinting marker generation and matching. Must be initialized after settings have been
- // loaded, so that we can retrieve the option setting.
- markerMatcher: undefined,
-
- /*
- * To be called after linkHints has been generated from linkHintsBase.
- */
- init: function() {
- this.onKeyDownInMode = this.onKeyDownInMode.bind(this);
- this.markerMatcher = settings.get('filterLinkHints') ? filterHints : alphabetHints;
- },
-
- /*
- * Generate an XPath describing what a clickable element is.
- * The final expression will be something like "//button | //xhtml:button | ..."
- * We use translate() instead of lower-case() because Chrome only supports XPath 1.0.
- */
- clickableElementsXPath: DomUtils.makeXPath(["a", "area[@href]", "textarea", "button", "select",
- "input[not(@type='hidden' or @disabled or @readonly)]",
- "*[@onclick or @tabindex or @role='link' or @role='button' or contains(@class, 'button') or " +
- "@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"]),
-
- // We need this as a top-level function because our command system doesn't yet support arguments.
- activateModeToOpenInNewTab: function() { this.activateMode(true, false, false); },
-
- activateModeToCopyLinkUrl: function() { this.activateMode(null, false, true); },
-
- activateModeWithQueue: function() { this.activateMode(true, true, false); },
-
- activateMode: function(openInNewTab, withQueue, copyLinkUrl) {
- if (!document.getElementById('vimiumLinkHintCss'))
- // linkHintCss is declared by vimiumFrontend.js and contains the user supplied css overrides.
- addCssToPage(linkHintCss, 'vimiumLinkHintCss');
- this.setOpenLinkMode(openInNewTab, withQueue, copyLinkUrl);
- this.buildLinkHints();
- handlerStack.push({ // handlerStack is declared by vimiumFrontend.js
- keydown: this.onKeyDownInMode,
- // trap all key events
- keypress: function() { return false; },
- keyup: function() { return false; }
- });
- },
-
- setOpenLinkMode: function(openInNewTab, withQueue, copyLinkUrl) {
- this.shouldOpenInNewTab = openInNewTab;
- this.shouldOpenWithQueue = withQueue;
-
- if (openInNewTab || withQueue) {
- if (openInNewTab)
- HUD.show("Open link in new tab");
- else if (withQueue)
- HUD.show("Open multiple links in a new tab");
- this.linkActivator = function(link) {
- // When "clicking" on a link, dispatch the event with the appropriate meta key (CMD on Mac, CTRL on windows)
- // to open it in a new tab if necessary.
- DomUtils.simulateClick(link, {
- metaKey: KeyboardUtils.platform == "Mac",
- ctrlKey: KeyboardUtils.platform != "Mac" });
- }
- } else if (copyLinkUrl) {
- HUD.show("Copy link URL to Clipboard");
- this.linkActivator = function(link) {
- chrome.extension.sendRequest({handler: 'copyToClipboard', data: link.href});
- }
- } else {
- HUD.show("Open link in current tab");
- // When we're opening the link in the current tab, don't navigate to the selected link immediately;
- // we want to give the user some time to notice which link has received focus.
- this.linkActivator = function(link) {
- setTimeout(DomUtils.simulateClick.bind(DomUtils, link), 400);
- }
- }
- },
-
- /*
- * Builds and displays link hints for every visible clickable item on the page.
- */
- buildLinkHints: function() {
- var visibleElements = this.getVisibleClickableElements();
- this.hintMarkers = this.markerMatcher.getHintMarkers(visibleElements);
-
- // Note(philc): Append these markers as top level children instead of as child nodes to the link itself,
- // because some clickable elements cannot contain children, e.g. submit buttons. This has the caveat
- // that if you scroll the page and the link has position=fixed, the marker will not stay fixed.
- // Also note that adding these nodes to document.body all at once is significantly faster than one-by-one.
- this.hintMarkerContainingDiv = document.createElement("div");
- this.hintMarkerContainingDiv.id = "vimiumHintMarkerContainer";
- this.hintMarkerContainingDiv.className = "vimiumReset";
- for (var i = 0; i < this.hintMarkers.length; i++)
- this.hintMarkerContainingDiv.appendChild(this.hintMarkers[i]);
-
- // sometimes this is triggered before documentElement is created
- // TODO(int3): fail more gracefully?
- if (document.documentElement)
- document.documentElement.appendChild(this.hintMarkerContainingDiv);
- else
- this.deactivateMode();
- },
-
- /*
- * Returns all clickable elements that are not hidden and are in the current viewport.
- * We prune invisible elements partly for performance reasons, but moreso it's to decrease the number
- * of digits needed to enumerate all of the links on screen.
- */
- getVisibleClickableElements: function() {
- var resultSet = DomUtils.evaluateXPath(this.clickableElementsXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
-
- var visibleElements = [];
-
- // Find all visible clickable elements.
- for (var i = 0, count = resultSet.snapshotLength; i < count; i++) {
- var element = resultSet.snapshotItem(i);
- var clientRect = DomUtils.getVisibleClientRect(element, clientRect);
- if (clientRect !== null)
- visibleElements.push({element: element, rect: clientRect});
-
- if (element.localName === "area") {
- var map = element.parentElement;
- if (!map) continue;
- var img = document.querySelector("img[usemap='#" + map.getAttribute("name") + "']");
- if (!img) continue;
- var imgClientRects = img.getClientRects();
- if (imgClientRects.length == 0) continue;
- var c = element.coords.split(/,/);
- var coords = [parseInt(c[0], 10), parseInt(c[1], 10), parseInt(c[2], 10), parseInt(c[3], 10)];
- var rect = {
- top: imgClientRects[0].top + coords[1],
- left: imgClientRects[0].left + coords[0],
- right: imgClientRects[0].left + coords[2],
- bottom: imgClientRects[0].top + coords[3],
- width: coords[2] - coords[0],
- height: coords[3] - coords[1]
- };
-
- visibleElements.push({element: element, rect: rect});
- }
- }
- return visibleElements;
- },
-
- /*
- * Handles shift and esc keys. The other keys are passed to markerMatcher.matchHintsByKey.
- */
- onKeyDownInMode: function(event) {
- if (this.delayMode)
- return;
-
- if (event.keyCode == keyCodes.shiftKey && this.shouldOpenInNewTab !== null) {
- // Toggle whether to open link in a new or current tab.
- this.setOpenLinkMode(!this.shouldOpenInNewTab, this.shouldOpenWithQueue, false);
- handlerStack.push({
- keyup: function(event) {
- if (event.keyCode !== keyCodes.shiftKey) return;
- linkHints.setOpenLinkMode(!linkHints.shouldOpenInNewTab, linkHints.shouldOpenWithQueue, false);
- handlerStack.pop();
- }
- });
- }
-
- // TODO(philc): Ignore keys that have modifiers.
- if (KeyboardUtils.isEscape(event)) {
- this.deactivateMode();
- } else {
- var keyResult = this.markerMatcher.matchHintsByKey(event, this.hintMarkers);
- var linksMatched = keyResult.linksMatched;
- var delay = keyResult.delay !== undefined ? keyResult.delay : 0;
- if (linksMatched.length == 0) {
- this.deactivateMode();
- } else if (linksMatched.length == 1) {
- this.activateLink(linksMatched[0], delay);
- } else {
- for (var i in this.hintMarkers)
- this.hideMarker(this.hintMarkers[i]);
- for (var i in linksMatched)
- this.showMarker(linksMatched[i], this.markerMatcher.hintKeystrokeQueue.length);
- }
- }
- },
-
- /*
- * When only one link hint remains, this function activates it in the appropriate way.
- */
- activateLink: function(matchedLink, delay) {
- this.delayMode = true;
- var clickEl = matchedLink.clickableItem;
- if (DomUtils.isSelectable(clickEl)) {
- DomUtils.simulateSelect(clickEl);
- this.deactivateMode(delay, function() { linkHints.delayMode = false; });
- } else {
- // TODO figure out which other input elements should not receive focus
- if (clickEl.nodeName.toLowerCase() === 'input' && clickEl.type !== 'button')
- clickEl.focus();
- DomUtils.flashRect(matchedLink.rect);
- this.linkActivator(clickEl);
- if (this.shouldOpenWithQueue) {
- this.deactivateMode(delay, function() {
- linkHints.delayMode = false;
- linkHints.activateModeWithQueue();
- });
- } else {
- this.deactivateMode(delay, function() { linkHints.delayMode = false; });
- }
- }
- },
-
- /*
- * Shows the marker, highlighting matchingCharCount characters.
- */
- showMarker: function(linkMarker, matchingCharCount) {
- linkMarker.style.display = "";
- for (var j = 0, count = linkMarker.childNodes.length; j < count; j++)
- (j < matchingCharCount) ? linkMarker.childNodes[j].classList.add("matchingCharacter") :
- linkMarker.childNodes[j].classList.remove("matchingCharacter");
- },
-
- hideMarker: function(linkMarker) {
- linkMarker.style.display = "none";
- },
-
- /*
- * If called without arguments, it executes immediately. Othewise, it
- * executes after 'delay' and invokes 'callback' when it is finished.
- */
- deactivateMode: function(delay, callback) {
- function deactivate() {
- if (linkHints.markerMatcher.deactivate)
- linkHints.markerMatcher.deactivate();
- if (linkHints.hintMarkerContainingDiv)
- linkHints.hintMarkerContainingDiv.parentNode.removeChild(linkHints.hintMarkerContainingDiv);
- linkHints.hintMarkerContainingDiv = null;
- linkHints.hintMarkers = [];
- handlerStack.pop();
- HUD.hide();
- }
- // we invoke the deactivate() function directly instead of using setTimeout(callback, 0) so that
- // deactivateMode can be tested synchronously
- if (!delay) {
- deactivate();
- if (callback) callback();
- } else {
- setTimeout(function() { deactivate(); if (callback) callback(); }, delay);
- }
- },
-
-};
-
-var alphabetHints = {
- hintKeystrokeQueue: [],
- logXOfBase: function(x, base) { return Math.log(x) / Math.log(base); },
-
- getHintMarkers: function(visibleElements) {
- var hintStrings = this.hintStrings(visibleElements.length);
- var hintMarkers = [];
- for (var i = 0, count = visibleElements.length; i < count; i++) {
- var marker = hintUtils.createMarkerFor(visibleElements[i]);
- marker.hintString = hintStrings[i];
- marker.innerHTML = hintUtils.spanWrap(marker.hintString.toUpperCase());
- hintMarkers.push(marker);
- }
-
- return hintMarkers;
- },
-
- /*
- * Returns a list of hint strings which will uniquely identify the given number of links. The hint strings
- * may be of different lengths.
- */
- hintStrings: function(linkCount) {
- var linkHintCharacters = settings.get("linkHintCharacters");
- // Determine how many digits the link hints will require in the worst case. Usually we do not need
- // all of these digits for every link single hint, so we can show shorter hints for a few of the links.
- var digitsNeeded = Math.ceil(this.logXOfBase(linkCount, linkHintCharacters.length));
- // Short hints are the number of hints we can possibly show which are (digitsNeeded - 1) digits in length.
- var shortHintCount = Math.floor(
- (Math.pow(linkHintCharacters.length, digitsNeeded) - linkCount) /
- linkHintCharacters.length);
- var longHintCount = linkCount - shortHintCount;
-
- var hintStrings = [];
-
- if (digitsNeeded > 1)
- for (var i = 0; i < shortHintCount; i++)
- hintStrings.push(this.numberToHintString(i, digitsNeeded - 1, linkHintCharacters));
-
- var start = shortHintCount * linkHintCharacters.length;
- for (var i = start; i < start + longHintCount; i++)
- hintStrings.push(this.numberToHintString(i, digitsNeeded, linkHintCharacters));
-
- return this.shuffleHints(hintStrings, linkHintCharacters.length);
- },
-
- /*
- * This shuffles the given set of hints so that they're scattered -- hints starting with the same character
- * will be spread evenly throughout the array.
- */
- shuffleHints: function(hints, characterSetLength) {
- var buckets = [], i = 0;
- for (i = 0; i < characterSetLength; i++)
- buckets[i] = []
- for (i = 0; i < hints.length; i++)
- buckets[i % buckets.length].push(hints[i]);
- var result = [];
- for (i = 0; i < buckets.length; i++)
- result = result.concat(buckets[i]);
- return result;
- },
-
- /*
- * 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, characterSet) {
- var base = characterSet.length;
- var hintString = [];
- var remainder = 0;
- do {
- remainder = number % base;
- hintString.unshift(characterSet[remainder]);
- number -= remainder;
- number /= Math.floor(base);
- } while (number > 0);
-
- // Pad the hint string we're returning so that it matches numHintDigits.
- // Note: the loop body changes hintString.length, so the original length must be cached!
- var hintStringLength = hintString.length;
- for (var i = 0; i < numHintDigits - hintStringLength; i++)
- hintString.unshift(characterSet[0]);
-
- return hintString.join("");
- },
-
- matchHintsByKey: function(event, hintMarkers) {
- // If a shifted-character is typed, treat it as lowerase for the purposes of matching hints.
- var keyChar = KeyboardUtils.getKeyChar(event).toLowerCase();
-
- if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) {
- if (!this.hintKeystrokeQueue.pop())
- return { linksMatched: [] };
- } else if (keyChar && settings.get('linkHintCharacters').indexOf(keyChar) >= 0) {
- this.hintKeystrokeQueue.push(keyChar);
- }
-
- var matchString = this.hintKeystrokeQueue.join("");
- var linksMatched = hintMarkers.filter(function(linkMarker) {
- return linkMarker.hintString.indexOf(matchString) == 0;
- });
- return { linksMatched: linksMatched };
- },
-
- deactivate: function() {
- this.hintKeystrokeQueue = [];
- }
-
-};
-
-var filterHints = {
- hintKeystrokeQueue: [],
- linkTextKeystrokeQueue: [],
- labelMap: {},
-
- /*
- * Generate a map of input element => label
- */
- generateLabelMap: function() {
- var labels = document.querySelectorAll("label");
- for (var i = 0, count = labels.length; i < count; 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;
- }
- }
- },
-
- generateHintString: function(linkHintNumber) {
- return (linkHintNumber + 1).toString();
- },
-
- generateLinkText: function(element) {
- var linkText = "";
- var showLinkText = false;
- // 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;
- }
- return { text: linkText, show: showLinkText };
- },
-
- renderMarker: function(marker) {
- marker.innerHTML = hintUtils.spanWrap(marker.hintString +
- (marker.showLinkText ? ": " + marker.linkText : ""));
- },
-
- getHintMarkers: function(visibleElements) {
- this.generateLabelMap();
- var hintMarkers = [];
- for (var i = 0, count = visibleElements.length; i < count; i++) {
- var marker = hintUtils.createMarkerFor(visibleElements[i]);
- marker.hintString = this.generateHintString(i);
- var linkTextObject = this.generateLinkText(marker.clickableItem);
- marker.linkText = linkTextObject.text;
- marker.showLinkText = linkTextObject.show;
- this.renderMarker(marker);
- hintMarkers.push(marker);
- }
- return hintMarkers;
- },
-
- matchHintsByKey: function(event, hintMarkers) {
- var keyChar = KeyboardUtils.getKeyChar(event);
- var delay = 0;
- var userIsTypingLinkText = false;
-
- if (event.keyCode == keyCodes.enter) {
- // activate the lowest-numbered link hint that is visible
- for (var i = 0, count = hintMarkers.length; i < count; i++)
- if (hintMarkers[i].style.display != 'none') {
- return { linksMatched: [ hintMarkers[i] ] };
- }
- } else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) {
- // backspace clears hint key queue first, then acts on link text key queue.
- // if both queues are empty. exit hinting mode
- if (!this.hintKeystrokeQueue.pop() && !this.linkTextKeystrokeQueue.pop())
- return { linksMatched: [] };
- } else if (keyChar) {
- if (/[0-9]/.test(keyChar))
- this.hintKeystrokeQueue.push(keyChar);
- else {
- // since we might renumber the hints, the current hintKeyStrokeQueue
- // should be rendered invalid (i.e. reset).
- this.hintKeystrokeQueue = [];
- this.linkTextKeystrokeQueue.push(keyChar);
- userIsTypingLinkText = true;
- }
- }
-
- // at this point, linkTextKeystrokeQueue and hintKeystrokeQueue have been updated to reflect the latest
- // input. use them to filter the link hints accordingly.
- var linksMatched = this.filterLinkHints(hintMarkers);
- var matchString = this.hintKeystrokeQueue.join("");
- linksMatched = linksMatched.filter(function(linkMarker) {
- return !linkMarker.filtered && linkMarker.hintString.indexOf(matchString) == 0;
- });
-
- if (linksMatched.length == 1 && userIsTypingLinkText) {
- // 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.
- var delay = 200;
- }
-
- return { linksMatched: linksMatched, delay: delay };
- },
-
- /*
- * Marks the links that do not match the linkText search string with the 'filtered' DOM property. Renumbers
- * the remainder if necessary.
- */
- filterLinkHints: function(hintMarkers) {
- var linksMatched = [];
- var linkSearchString = this.linkTextKeystrokeQueue.join("");
-
- for (var i = 0; i < hintMarkers.length; i++) {
- var linkMarker = hintMarkers[i];
- var matchedLink = linkMarker.linkText.toLowerCase().indexOf(linkSearchString.toLowerCase()) >= 0;
-
- if (!matchedLink) {
- linkMarker.filtered = true;
- } else {
- linkMarker.filtered = false;
- var oldHintString = linkMarker.hintString;
- linkMarker.hintString = this.generateHintString(linksMatched.length);
- if (linkMarker.hintString != oldHintString)
- this.renderMarker(linkMarker);
- linksMatched.push(linkMarker);
- }
- }
- return linksMatched;
- },
-
- deactivate: function(delay, callback) {
- this.hintKeystrokeQueue = [];
- this.linkTextKeystrokeQueue = [];
- this.labelMap = {};
- }
-
-};
-
-var hintUtils = {
- /*
- * Make each hint character a span, so that we can highlight the typed characters as you type them.
- */
- spanWrap: function(hintString) {
- var innerHTML = [];
- for (var i = 0; i < hintString.length; i++)
- innerHTML.push("<span class='vimiumReset'>" + hintString[i] + "</span>");
- return innerHTML.join("");
- },
-
- /*
- * Creates a link marker for the given link.
- */
- createMarkerFor: function(link) {
- var marker = document.createElement("div");
- marker.className = "vimiumReset internalVimiumHintMarker vimiumHintMarker";
- marker.clickableItem = link.element;
-
- var clientRect = link.rect;
- marker.style.left = clientRect.left + window.scrollX + "px";
- marker.style.top = clientRect.top + window.scrollY + "px";
-
- marker.rect = link.rect;
-
- return marker;
- }
-};