aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CREDITS1
-rw-r--r--background_page.html1
-rw-r--r--commands.js14
-rw-r--r--linkHints.js751
-rw-r--r--options.html68
-rw-r--r--vimiumFrontend.js74
6 files changed, 574 insertions, 335 deletions
diff --git a/CREDITS b/CREDITS
index d5f896b4..a83cb77f 100644
--- a/CREDITS
+++ b/CREDITS
@@ -19,5 +19,6 @@ Contributors:
tsigo
Werner Laurensse (github: ab3)
Svein-Erik Larsen <feinom@gmail.com> (github: feinom)
+ Bill Casarin <jb@jb55.com> (github: jb55)
Feel free to add real names in addition to GitHub usernames.
diff --git a/background_page.html b/background_page.html
index ac83a0c3..106db28d 100644
--- a/background_page.html
+++ b/background_page.html
@@ -26,6 +26,7 @@
scrollStepSize: 60,
defaultZoomLevel: 100,
linkHintCharacters: "sadfjklewcmpgh",
+ filterLinkHints: false,
userDefinedLinkHintCss:
".vimiumHintMarker {\n\n}\n" +
".vimiumHintMarker > .matchingCharacter {\n\n}",
diff --git a/commands.js b/commands.js
index 3b9203dc..ec35c569 100644
--- a/commands.js
+++ b/commands.js
@@ -110,9 +110,9 @@ function clearKeyMappingsAndSetDefaults() {
mapKeyToCommand('gi', 'focusInput');
- mapKeyToCommand('f', 'activateLinkHintsMode');
- mapKeyToCommand('F', 'activateLinkHintsModeToOpenInNewTab');
- mapKeyToCommand('<a-f>', 'activateLinkHintsModeWithQueue');
+ mapKeyToCommand('f', 'linkHints.activateMode');
+ mapKeyToCommand('F', 'linkHints.activateModeToOpenInNewTab');
+ mapKeyToCommand('<a-f>', 'linkHints.activateModeWithQueue');
mapKeyToCommand('/', 'enterFindMode');
mapKeyToCommand('n', 'performFind');
@@ -157,9 +157,9 @@ addCommand('enterInsertMode', 'Enter insert mode');
addCommand('focusInput', 'Focus the first (or n-th) text box on the page', false, true);
-addCommand('activateLinkHintsMode', 'Enter link hints mode to open links in current tab');
-addCommand('activateLinkHintsModeToOpenInNewTab', 'Enter link hints mode to open links in new tab');
-addCommand('activateLinkHintsModeWithQueue', 'Enter link hints mode to open multiple links in a new tab');
+addCommand('linkHints.activateMode', 'Enter link hints mode to open links in current tab');
+addCommand('linkHints.activateModeToOpenInNewTab', 'Enter link hints mode to open links in new tab');
+addCommand('linkHints.activateModeWithQueue', 'Enter link hints mode to open multiple links in a new tab');
addCommand('enterFindMode', 'Enter find mode');
addCommand('performFind', 'Cycle forward to the next find match');
@@ -191,7 +191,7 @@ var commandGroups = {
"scrollPageUp", "scrollFullPageUp", "scrollFullPageDown",
"reload", "toggleViewSource", "zoomIn", "zoomOut", "copyCurrentUrl", "goUp",
"enterInsertMode", "focusInput",
- "activateLinkHintsMode", "activateLinkHintsModeToOpenInNewTab", "activateLinkHintsModeWithQueue",
+ "linkHints.activateMode", "linkHints.activateModeToOpenInNewTab", "linkHints.activateModeWithQueue",
"enterFindMode", "performFind", "performBackwardsFind", "nextFrame"],
historyNavigation:
["goBack", "goForward"],
diff --git a/linkHints.js b/linkHints.js
index 58d6e979..6825bfd0 100644
--- a/linkHints.js
+++ b/linkHints.js
@@ -1,337 +1,494 @@
/*
- * 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.
+ * 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.
*
- * 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.
- */
-
-var hintMarkers = [];
-var hintMarkerContainingDiv = null;
-// The characters that were typed in while in "link hints" mode.
-var hintKeystrokeQueue = [];
-var linkHintsModeActivated = false;
-var shouldOpenLinkHintInNewTab = false;
-var shouldOpenLinkHintWithQueue = false;
-// Whether link hint's "open in current/new tab" setting is currently toggled
-var openLinkModeToggle = false;
-// Whether we have added to the page the CSS needed to display link hints.
-var linkHintsCssAdded = false;
-
-/*
- * Generate an XPath describing what a clickable element is.
- * The final expression will be something like "//button | //xhtml:button | ..."
- */
-var clickableElementsXPath = (function() {
- var clickableElements = ["a", "textarea", "button", "select", "input[not(@type='hidden')]",
- "*[@onclick or @tabindex or @role='link' or @role='button']"];
- var xpath = [];
- for (var i in clickableElements)
- xpath.push("//" + clickableElements[i], "//xhtml:" + clickableElements[i]);
- return xpath.join(" | ")
-})();
-
-// We need this as a top-level function because our command system doesn't yet support arguments.
-function activateLinkHintsModeToOpenInNewTab() { activateLinkHintsMode(true, false); }
-
-function activateLinkHintsModeWithQueue() { activateLinkHintsMode(true, true); }
-
-function activateLinkHintsMode(openInNewTab, withQueue) {
- if (!linkHintsCssAdded)
- addCssToPage(linkHintCss); // linkHintCss is declared by vimiumFrontend.js
- linkHintCssAdded = true;
- linkHintsModeActivated = true;
- setOpenLinkMode(openInNewTab, withQueue);
- buildLinkHints();
- document.addEventListener("keydown", onKeyDownInLinkHintsMode, true);
- document.addEventListener("keyup", onKeyUpInLinkHintsMode, true);
-}
-
-function setOpenLinkMode(openInNewTab, withQueue) {
- shouldOpenLinkHintInNewTab = openInNewTab;
- shouldOpenLinkHintWithQueue = withQueue;
- if (shouldOpenLinkHintWithQueue) {
- HUD.show("Open multiple links in a new tab");
- } else {
- if (shouldOpenLinkHintInNewTab)
- HUD.show("Open link in new tab");
- else
- HUD.show("Open link in current tab");
- }
-}
-
-/*
- * Builds and displays link hints for every visible clickable item on the page.
+ * 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.
*/
-function buildLinkHints() {
- var visibleElements = getVisibleClickableElements();
-
- // Initialize the number used to generate the character hints to be as many digits as we need to
- // highlight all the links on the page; we don't want some link hints to have more chars than others.
- var digitsNeeded = Math.ceil(logXOfBase(visibleElements.length, settings.linkHintCharacters.length));
- var linkHintNumber = 0;
- for (var i = 0; i < visibleElements.length; i++) {
- hintMarkers.push(createMarkerFor(visibleElements[i], linkHintNumber, digitsNeeded));
- linkHintNumber++;
- }
- // 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.
- hintMarkerContainingDiv = document.createElement("div");
- hintMarkerContainingDiv.className = "internalVimiumHintMarker";
- for (var i = 0; i < hintMarkers.length; i++)
- hintMarkerContainingDiv.appendChild(hintMarkers[i]);
- document.documentElement.appendChild(hintMarkerContainingDiv);
-}
-
-function logXOfBase(x, base) { return Math.log(x) / Math.log(base); }
/*
- * 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.
+ * A set of common operations shared by any link-hinting system. Some methods
+ * are stubbed.
*/
-function getVisibleClickableElements() {
- var resultSet = document.evaluate(clickableElementsXPath, document.body,
- function (namespace) {
- return namespace == "xhtml" ? "http://www.w3.org/1999/xhtml" : null;
- },
- XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
-
-
- var visibleElements = [];
-
- // Find all visible clickable elements.
- for (var i = 0; i < resultSet.snapshotLength; i++) {
- var element = resultSet.snapshotItem(i);
- var clientRect = element.getClientRects()[0];
-
- if (isVisible(element, clientRect))
- visibleElements.push({element: element, rect: clientRect});
-
- // If the link has zero dimensions, it may be wrapping visible
- // but floated elements. Check for this.
- if (clientRect && (clientRect.width == 0 || clientRect.height == 0)) {
- for (var j = 0; j < element.children.length; j++) {
- if (window.getComputedStyle(element.children[j], null).getPropertyValue('float') != 'none') {
- var childClientRect = element.children[j].getClientRects()[0];
- if (isVisible(element.children[j], childClientRect)) {
- visibleElements.push({element: element.children[j], rect: childClientRect});
- break;
+var linkHintsPrototype = {
+ hintMarkers: [],
+ hintMarkerContainingDiv: null,
+ // The characters that were typed in while in "link hints" mode.
+ hintKeystrokeQueue: [],
+ modeActivated: false,
+ shouldOpenInNewTab: false,
+ shouldOpenWithQueue: false,
+ // Whether link hint's "open in current/new tab" setting is currently toggled
+ openLinkModeToggle: false,
+ // Whether we have added to the page the CSS needed to display link hints.
+ cssAdded: false,
+
+ init: function() {
+ // bind the event handlers to the appropriate instance of the prototype
+ this.onKeyDownInMode = this.onKeyDownInMode.bind(this);
+ this.onKeyUpInMode = this.onKeyUpInMode.bind(this);
+ },
+
+ /*
+ * Generate an XPath describing what a clickable element is.
+ * The final expression will be something like "//button | //xhtml:button | ..."
+ */
+ clickableElementsXPath: (function() {
+ var clickableElements = ["a", "textarea", "button", "select", "input[not(@type='hidden')]",
+ "*[@onclick or @tabindex or @role='link' or @role='button']"];
+ var xpath = [];
+ for (var i in clickableElements)
+ xpath.push("//" + clickableElements[i], "//xhtml:" + clickableElements[i]);
+ return xpath.join(" | ")
+ })(),
+
+ // We need this as a top-level function because our command system doesn't yet support arguments.
+ activateModeToOpenInNewTab: function() { this.activateMode(true, false); },
+
+ activateModeWithQueue: function() { this.activateMode(true, true); },
+
+ activateMode: function (openInNewTab, withQueue) {
+ if (!this.cssAdded)
+ addCssToPage(linkHintCss); // linkHintCss is declared by vimiumFrontend.js
+ this.linkHintCssAdded = true;
+ this.modeActivated = true;
+ this.setOpenLinkMode(openInNewTab, withQueue);
+ this.buildLinkHints();
+ document.addEventListener("keydown", this.onKeyDownInMode, true);
+ document.addEventListener("keyup", this.onKeyUpInMode, true);
+ },
+
+ setOpenLinkMode: function(openInNewTab, withQueue) {
+ this.shouldOpenInNewTab = openInNewTab;
+ this.shouldOpenWithQueue = withQueue;
+ if (this.shouldOpenWithQueue) {
+ HUD.show("Open multiple links in a new tab");
+ } else {
+ if (this.shouldOpenInNewTab)
+ HUD.show("Open link in new tab");
+ else
+ HUD.show("Open link in current tab");
+ }
+ },
+
+ /*
+ * Builds and displays link hints for every visible clickable item on the page.
+ */
+ buildLinkHints: function() {
+ var visibleElements = this.getVisibleClickableElements();
+
+ // Initialize the number used to generate the character hints to be as many digits as we need to
+ // highlight all the links on the page; we don't want some link hints to have more chars than others.
+ var linkHintNumber = 0;
+ this.initHintStringGenerator(visibleElements);
+ for (var i = 0; i < visibleElements.length; i++) {
+ this.hintMarkers.push(this.createMarkerFor(
+ visibleElements[i], linkHintNumber, this.hintStringGenerator.bind(this)));
+ linkHintNumber++;
+ }
+ // 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.className = "internalVimiumHintMarker";
+ for (var i = 0; i < this.hintMarkers.length; i++)
+ this.hintMarkerContainingDiv.appendChild(this.hintMarkers[i]);
+ document.documentElement.appendChild(this.hintMarkerContainingDiv);
+ },
+
+ /*
+ * Takes a number and returns the string label for the hint.
+ */
+ hintStringGenerator: function(linkHintNumber) {},
+
+ /*
+ * A hook for any necessary initialization for hintStringGenerator. Takes an
+ * array of visible elements. Any return value is ignored.
+ */
+ initHintStringGenerator: function(visibleElements) {},
+
+ /*
+ * 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 = document.evaluate(this.clickableElementsXPath, document.body,
+ function (namespace) {
+ return namespace == "xhtml" ? "http://www.w3.org/1999/xhtml" : null;
+ },
+ XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+
+
+ var visibleElements = [];
+
+ // Find all visible clickable elements.
+ for (var i = 0; i < resultSet.snapshotLength; i++) {
+ var element = resultSet.snapshotItem(i);
+ var clientRect = element.getClientRects()[0];
+
+ if (this.isVisible(element, clientRect))
+ visibleElements.push({element: element, rect: clientRect});
+
+ // If the link has zero dimensions, it may be wrapping visible
+ // but floated elements. Check for this.
+ if (clientRect && (clientRect.width == 0 || clientRect.height == 0)) {
+ for (var j = 0; j < element.children.length; j++) {
+ if (window.getComputedStyle(element.children[j], null).getPropertyValue('float') != 'none') {
+ var childClientRect = element.children[j].getClientRects()[0];
+ if (this.isVisible(element.children[j], childClientRect)) {
+ visibleElements.push({element: element.children[j], rect: childClientRect});
+ break;
+ }
}
}
}
}
- }
- return visibleElements;
-}
-
-/*
- * Returns true if element is visible.
- */
-function isVisible(element, clientRect) {
- // Exclude links which have just a few pixels on screen, because the link hints won't show for them anyway.
- var zoomFactor = currentZoomLevel / 100.0;
- if (!clientRect || clientRect.top < 0 || clientRect.top * zoomFactor >= window.innerHeight - 4 ||
- clientRect.left < 0 || clientRect.left * zoomFactor >= window.innerWidth - 4)
- return false;
-
- if (clientRect.width < 3 || clientRect.height < 3)
- return false;
-
- // eliminate invisible elements (see test_harnesses/visibility_test.html)
- var computedStyle = window.getComputedStyle(element, null);
- if (computedStyle.getPropertyValue('visibility') != 'visible' ||
- computedStyle.getPropertyValue('display') == 'none')
- return false;
-
- return true;
-}
-
-function onKeyDownInLinkHintsMode(event) {
- console.log("Key Down");
- if (event.keyCode == keyCodes.shiftKey && !openLinkModeToggle) {
- // Toggle whether to open link in a new or current tab.
- setOpenLinkMode(!shouldOpenLinkHintInNewTab, shouldOpenLinkHintWithQueue);
- openLinkModeToggle = true;
- }
-
- var keyChar = getKeyChar(event);
- if (!keyChar)
- return;
+ return visibleElements;
+ },
+
+ /*
+ * Returns true if element is visible.
+ */
+ isVisible: function(element, clientRect) {
+ // Exclude links which have just a few pixels on screen, because the link hints won't show for them anyway.
+ var zoomFactor = currentZoomLevel / 100.0;
+ if (!clientRect || clientRect.top < 0 || clientRect.top * zoomFactor >= window.innerHeight - 4 ||
+ clientRect.left < 0 || clientRect.left * zoomFactor >= window.innerWidth - 4)
+ return false;
+
+ if (clientRect.width < 3 || clientRect.height < 3)
+ return false;
+
+ // eliminate invisible elements (see test_harnesses/visibility_test.html)
+ var computedStyle = window.getComputedStyle(element, null);
+ if (computedStyle.getPropertyValue('visibility') != 'visible' ||
+ computedStyle.getPropertyValue('display') == 'none')
+ return false;
+
+ return true;
+ },
+
+ /*
+ * Handles shift and esc keys. The other keys are passed to normalKeyDownHandler.
+ */
+ onKeyDownInMode: function(event) {
+ console.log("Key Down");
+ if (event.keyCode == keyCodes.shiftKey && !this.openLinkModeToggle) {
+ // Toggle whether to open link in a new or current tab.
+ this.setOpenLinkMode(!this.shouldOpenInNewTab, this.shouldOpenWithQueue);
+ this.openLinkModeToggle = true;
+ }
- // TODO(philc): Ignore keys that have modifiers.
- if (isEscape(event)) {
- deactivateLinkHintsMode();
- } else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) {
- if (hintKeystrokeQueue.length == 0) {
- deactivateLinkHintsMode();
+ // TODO(philc): Ignore keys that have modifiers.
+ if (isEscape(event)) {
+ this.deactivateMode();
} else {
- hintKeystrokeQueue.pop();
- updateLinkHints();
+ this.normalKeyDownHandler(event);
}
- } else if (settings.linkHintCharacters.indexOf(keyChar) >= 0) {
- hintKeystrokeQueue.push(keyChar);
- updateLinkHints();
- } else {
- return;
- }
- event.stopPropagation();
- event.preventDefault();
-}
+ event.stopPropagation();
+ event.preventDefault();
+ },
-function onKeyUpInLinkHintsMode(event) {
- if (event.keyCode == keyCodes.shiftKey && openLinkModeToggle) {
- // Revert toggle on whether to open link in new or current tab.
- setOpenLinkMode(!shouldOpenLinkHintInNewTab, shouldOpenLinkHintWithQueue);
- openLinkModeToggle = false;
- }
- event.stopPropagation();
- event.preventDefault();
-}
+ /*
+ * Handle all keys other than shift and esc. Return value is ignored.
+ */
+ normalKeyDownHandler: function(event) {},
-/*
- * Updates the visibility of link hints on screen based on the keystrokes typed thus far. If only one
- * link hint remains, click on that link and exit link hints mode.
- */
-function updateLinkHints() {
- var matchString = hintKeystrokeQueue.join("");
- var linksMatched = highlightLinkMatches(matchString);
- if (linksMatched.length == 0)
- deactivateLinkHintsMode();
- else if (linksMatched.length == 1) {
- var matchedLink = linksMatched[0];
- if (isSelectable(matchedLink)) {
+ onKeyUpInMode: function(event) {
+ if (event.keyCode == keyCodes.shiftKey && this.openLinkModeToggle) {
+ // Revert toggle on whether to open link in new or current tab.
+ this.setOpenLinkMode(!this.shouldOpenInNewTab, this.shouldOpenWithQueue);
+ this.openLinkModeToggle = false;
+ }
+ event.stopPropagation();
+ event.preventDefault();
+ },
+
+ /*
+ * When only one link hint remains, this function activates it in the appropriate way.
+ */
+ activateLink: function(matchedLink) {
+ if (this.isSelectable(matchedLink)) {
matchedLink.focus();
// When focusing a textbox, put the selection caret at the end of the textbox's contents.
matchedLink.setSelectionRange(matchedLink.value.length, matchedLink.value.length);
- deactivateLinkHintsMode();
+ this.deactivateMode();
} else {
// 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 feedback depicting which link they've selected by focusing it.
- if (shouldOpenLinkHintWithQueue) {
- simulateClick(matchedLink);
- resetLinkHintsMode();
- } else if (shouldOpenLinkHintInNewTab) {
- simulateClick(matchedLink);
+ if (this.shouldOpenWithQueue) {
+ this.simulateClick(matchedLink);
+ this.resetMode();
+ } else if (this.shouldOpenInNewTab) {
+ this.simulateClick(matchedLink);
matchedLink.focus();
- deactivateLinkHintsMode();
+ this.deactivateMode();
} else {
- setTimeout(function() { simulateClick(matchedLink); }, 400);
+ setTimeout(this.simulateClick.bind(this, matchedLink), 400);
matchedLink.focus();
- deactivateLinkHintsMode();
+ this.deactivateMode();
}
}
- }
-}
-
-/*
- * Selectable means the element has a text caret; this is not the same as "focusable".
- */
-function isSelectable(element) {
- var selectableTypes = ["search", "text", "password"];
- return (element.tagName == "INPUT" && selectableTypes.indexOf(element.type) >= 0) ||
- element.tagName == "TEXTAREA";
-}
-
-/*
- * Hides link hints which do not match the given search string. To allow the backspace key to work, this
- * will also show link hints which do match but were previously hidden.
- */
-function highlightLinkMatches(searchString) {
- var linksMatched = [];
- for (var i = 0; i < hintMarkers.length; i++) {
- var linkMarker = hintMarkers[i];
- if (linkMarker.getAttribute("hintString").indexOf(searchString) == 0) {
+ },
+
+ /*
+ * Selectable means the element has a text caret; this is not the same as "focusable".
+ */
+ isSelectable: function(element) {
+ var selectableTypes = ["search", "text", "password"];
+ return (element.tagName == "INPUT" && selectableTypes.indexOf(element.type) >= 0) ||
+ element.tagName == "TEXTAREA";
+ },
+
+ /*
+ * Hides linkMarker if it does not match testString, and shows linkMarker
+ * if it does match but was previously hidden. To be used with Array.filter().
+ */
+ toggleHighlights: function(testString, linkMarker) {
+ if (linkMarker.getAttribute("hintString").indexOf(testString) == 0) {
if (linkMarker.style.display == "none")
linkMarker.style.display = "";
for (var j = 0; j < linkMarker.childNodes.length; j++)
- linkMarker.childNodes[j].className = (j >= searchString.length) ? "" : "matchingCharacter";
- linksMatched.push(linkMarker.clickableItem);
+ linkMarker.childNodes[j].className = (j >= testString.length) ? "" : "matchingCharacter";
+ return true;
} else {
linkMarker.style.display = "none";
+ return false;
}
- }
- return linksMatched;
-}
-
+ },
+
+ simulateClick: function(link) {
+ var event = document.createEvent("MouseEvents");
+ // 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.
+ var metaKey = (platform == "Mac" && linkHints.shouldOpenInNewTab);
+ var ctrlKey = (platform != "Mac" && linkHints.shouldOpenInNewTab);
+ event.initMouseEvent("click", true, true, window, 1, 0, 0, 0, 0, ctrlKey, false, false, metaKey, 0, null);
+
+ // Debugging note: Firefox will not execute the link's default action if we dispatch this click event,
+ // but Webkit will. Dispatching a click on an input box does not seem to focus it; we do that separately
+ link.dispatchEvent(event);
+ },
+
+ deactivateMode: function() {
+ if (this.hintMarkerContainingDiv)
+ this.hintMarkerContainingDiv.parentNode.removeChild(this.hintMarkerContainingDiv);
+ this.hintMarkerContainingDiv = null;
+ this.hintMarkers = [];
+ this.hintKeystrokeQueue = [];
+ document.removeEventListener("keydown", this.onKeyDownInMode, true);
+ document.removeEventListener("keyup", this.onKeyUpInMode, true);
+ this.modeActivated = false;
+ HUD.hide();
+ },
+
+ resetMode: function() {
+ this.deactivateMode();
+ this.activateModeWithQueue();
+ },
+
+ /*
+ * Creates a link marker for the given link.
+ */
+ createMarkerFor: function(link, linkHintNumber, stringGenerator) {
+ var hintString = stringGenerator(linkHintNumber);
+ var linkText = link.element.innerHTML.toLowerCase();
+ if (linkText == undefined)
+ linkText = "";
+ var marker = document.createElement("div");
+ marker.className = "internalVimiumHintMarker vimiumHintMarker";
+ marker.innerHTML = this.spanWrap(hintString);
+ marker.setAttribute("hintString", hintString);
+ marker.setAttribute("linkText", linkText);
+
+ // Note: this call will be expensive if we modify the DOM in between calls.
+ var clientRect = link.rect;
+ // The coordinates given by the window do not have the zoom factor included since the zoom is set only on
+ // the document node.
+ var zoomFactor = currentZoomLevel / 100.0;
+ marker.style.left = clientRect.left + window.scrollX / zoomFactor + "px";
+ marker.style.top = clientRect.top + window.scrollY / zoomFactor + "px";
+
+ marker.clickableItem = link.element;
+ return marker;
+ },
+
+ /*
+ * 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>" + hintString[i].toUpperCase() + "</span>");
+ return innerHTML.join("");
+ },
+
+};
+
+var linkHints;
/*
- * 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.
+ * Create the instance of linkHints, specialized based on the user settings.
*/
-function numberToHintString(number, numHintDigits) {
- var base = settings.linkHintCharacters.length;
- var hintString = [];
- var remainder = 0;
- do {
- remainder = number % base;
- hintString.unshift(settings.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.linkHintCharacters[0]);
- return hintString.join("");
-}
+function initializeLinkHints() {
+ linkHints = Object.create(linkHintsPrototype);
+ linkHints.init();
+
+ if (settings.get('filterLinkHints') != "true") { // the default hinting system
+
+ linkHints['digitsNeeded'] = 1;
+
+ linkHints['logXOfBase'] = function(x, base) { return Math.log(x) / Math.log(base); };
+
+ linkHints['initHintStringGenerator'] = function(visibleElements) {
+ this.digitsNeeded = Math.ceil(this.logXOfBase(
+ visibleElements.length, settings.get('linkHintCharacters').length));
+ };
+
+ linkHints['hintStringGenerator'] = function(linkHintNumber) {
+ return this.numberToHintString(linkHintNumber, this.digitsNeeded);
+ };
+
+ /*
+ * 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.
+ */
+ linkHints['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("");
+ };
+
+ linkHints['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);
+ }
+ };
-function simulateClick(link) {
- var event = document.createEvent("MouseEvents");
- // 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.
- var metaKey = (platform == "Mac" && shouldOpenLinkHintInNewTab);
- var ctrlKey = (platform != "Mac" && shouldOpenLinkHintInNewTab);
- event.initMouseEvent("click", true, true, window, 1, 0, 0, 0, 0, ctrlKey, false, false, metaKey, 0, null);
-
- // Debugging note: Firefox will not execute the link's default action if we dispatch this click event,
- // but Webkit will. Dispatching a click on an input box does not seem to focus it; we do that separately
- link.dispatchEvent(event);
-}
+ } else {
-function deactivateLinkHintsMode() {
- if (hintMarkerContainingDiv)
- hintMarkerContainingDiv.parentNode.removeChild(hintMarkerContainingDiv);
- hintMarkerContainingDiv = null;
- hintMarkers = [];
- hintKeystrokeQueue = [];
- document.removeEventListener("keydown", onKeyDownInLinkHintsMode, true);
- document.removeEventListener("keyup", onKeyUpInLinkHintsMode, true);
- linkHintsModeActivated = false;
- HUD.hide();
-}
+ linkHints['linkTextKeystrokeQueue'] = [];
+
+ linkHints['hintStringGenerator'] = function(linkHintNumber) {
+ return (linkHintNumber + 1).toString();
+ };
+
+ linkHints['normalKeyDownHandler'] = function(event) {
+ 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 {
+ 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);
+ }
-function resetLinkHintsMode() {
- deactivateLinkHintsMode();
- activateLinkHintsModeWithQueue();
-}
+ if (linksMatched.length == 0)
+ this.deactivateMode();
+ else if (linksMatched.length == 1)
+ this.activateLink(linksMatched[0].clickableItem);
+ }
+ };
+
+ /*
+ * 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.
+ */
+ linkHints['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 = "";
+ var newHintText = (linksMatched.length+1).toString();
+ linkMarker.innerHTML = this.spanWrap(newHintText);
+ linkMarker.setAttribute("hintString", newHintText);
+ linkMarker.setAttribute("filtered", "false");
+ linksMatched.push(linkMarker);
+ }
+ }
+ return linksMatched;
+ };
-/*
- * Creates a link marker for the given link.
- */
-function createMarkerFor(link, linkHintNumber, linkHintDigits) {
- var hintString = numberToHintString(linkHintNumber, linkHintDigits);
- var marker = document.createElement("div");
- marker.className = "internalVimiumHintMarker vimiumHintMarker";
- var innerHTML = [];
- // Make each hint character a span, so that we can highlight the typed characters as you type them.
- for (var i = 0; i < hintString.length; i++)
- innerHTML.push("<span>" + hintString[i].toUpperCase() + "</span>");
- marker.innerHTML = innerHTML.join("");
- marker.setAttribute("hintString", hintString);
-
- // Note: this call will be expensive if we modify the DOM in between calls.
- var clientRect = link.rect;
- // The coordinates given by the window do not have the zoom factor included since the zoom is set only on
- // the document node.
- var zoomFactor = currentZoomLevel / 100.0;
- marker.style.left = clientRect.left + window.scrollX / zoomFactor + "px";
- marker.style.top = clientRect.top + window.scrollY / zoomFactor + "px";
-
- marker.clickableItem = link.element;
- return marker;
+ linkHints['deactivateMode'] = function() {
+ this.linkTextKeystrokeQueue = [];
+ // call(this) is necessary to make deactivateMode reset
+ // the variables in linkHints instead of linkHintsPrototype
+ Object.getPrototypeOf(this).deactivateMode.call(this);
+ };
+
+ }
}
diff --git a/options.html b/options.html
index 344fc01d..f752abcd 100644
--- a/options.html
+++ b/options.html
@@ -70,7 +70,10 @@
tr.advancedOption {
display:none;
}
-
+ input:read-only {
+ background-color: #eee;
+ color: #666;
+ }
</style>
<script type="text/javascript">
@@ -80,7 +83,7 @@
var defaultSettings = chrome.extension.getBackgroundPage().defaultSettings;
var editableFields = ["scrollStepSize", "defaultZoomLevel", "excludedUrls", "linkHintCharacters",
- "userDefinedLinkHintCss", "keyMappings"];
+ "userDefinedLinkHintCss", "keyMappings", "filterLinkHints"];
var postSaveHooks = {
"keyMappings": function (value) {
@@ -93,8 +96,13 @@
function initializeOptions() {
populateOptions();
- for (var i = 0; i < editableFields.length; i++)
+
+ for (var i = 0; i < editableFields.length; i++) {
$(editableFields[i]).addEventListener("keyup", onOptionKeyup, false);
+ $(editableFields[i]).addEventListener("change", enableSaveButton, false);
+ $(editableFields[i]).addEventListener("change", onDataLoaded, false);
+ }
+
$("advancedOptions").addEventListener("click", openAdvancedOptions, false);
$("showCommands").addEventListener("click", function () {
showDialog("vimiumCommandListingContainer",
@@ -107,6 +115,10 @@
enableSaveButton();
}
+ function onDataLoaded() {
+ $("linkHintCharacters").readOnly = $("filterLinkHints").checked;
+ }
+
function enableSaveButton() { $("saveOptions").removeAttribute("disabled"); }
// Saves options to localStorage.
@@ -115,15 +127,25 @@
// the freedom to change the defaults in the future.
for (var i = 0; i < editableFields.length; i++) {
var fieldName = editableFields[i];
- var fieldValue = $(fieldName).value.trim();
+ var field = $(fieldName);
+
+ var fieldValue;
+ if (field.getAttribute("type") == "checkbox") {
+ fieldValue = field.checked ? "true" : "false";
+ } else {
+ fieldValue = field.value.trim();
+ field.value = fieldValue;
+ }
+
var defaultFieldValue = (defaultSettings[fieldName] != null) ?
defaultSettings[fieldName].toString() : "";
+
if (fieldValue == defaultFieldValue)
delete localStorage[fieldName];
else
localStorage[fieldName] = fieldValue;
- $(fieldName).value = fieldValue;
- $(fieldName).setAttribute("savedValue", fieldValue);
+
+ field.setAttribute("savedValue", fieldValue);
if (postSaveHooks[fieldName]) { postSaveHooks[fieldName](fieldValue); }
}
@@ -133,17 +155,31 @@
// Restores select box state to saved value from localStorage.
function populateOptions() {
for (var i = 0; i < editableFields.length; i++) {
- $(editableFields[i]).value = localStorage[editableFields[i]] || defaultSettings[editableFields[i]] || "";
- $(editableFields[i]).setAttribute("savedValue", $(editableFields[i]).value);
+ var val = localStorage[editableFields[i]] || defaultSettings[editableFields[i]] || "";
+ var field = $(editableFields[i]);
+ setFieldValue($(editableFields[i]), val);
}
+ onDataLoaded();
}
function restoreToDefaults() {
- for (var i = 0; i < editableFields.length; i++)
- $(editableFields[i]).value = defaultSettings[editableFields[i]] || "";
+ for (var i = 0; i < editableFields.length; i++) {
+ var val = defaultSettings[editableFields[i]] || "";
+ setFieldValue($(editableFields[i]), val);
+ }
+ onDataLoaded();
enableSaveButton();
}
+ function setFieldValue(field, value) {
+ if (field.getAttribute('type') == 'checkbox')
+ field.checked = value == "true";
+ else
+ field.value = value;
+
+ field.setAttribute("savedValue", value);
+ }
+
function openAdvancedOptions(event) {
var elements = document.getElementsByClassName("advancedOption");
for (var i = 0; i < elements.length; i++)
@@ -255,6 +291,18 @@
<textarea id="userDefinedLinkHintCss" type="text"></textarea>
</td>
</tr>
+ <tr class="advancedOption">
+ <td class="caption">Filter link hints</td>
+ <td verticalAlign="top">
+ <div class="help">
+ <div class="example">
+ Typing in link hints mode will filter link hints by the link text.<br/><br/>
+ Note: You <em>must</em> use numeric link hint characters in this mode
+ </div>
+ </div>
+ <input id="filterLinkHints" type="checkbox"/>
+ </td>
+ </tr>
</table>
<div id="buttonsPanel">
diff --git a/vimiumFrontend.js b/vimiumFrontend.js
index eb3de996..e5e214f7 100644
--- a/vimiumFrontend.js
+++ b/vimiumFrontend.js
@@ -4,8 +4,6 @@
* the page's zoom level. We tell the background page that we're in domReady and ready to accept normal
* commands by connectiong to a port named "domReady".
*/
-var settings = {};
-var settingsToLoad = ["scrollStepSize", "linkHintCharacters"];
var getCurrentUrlHandlers = []; // function(url)
@@ -37,6 +35,35 @@ var textInputXPath = '//input[' +
textInputTypes.map(function (type) { return '@type="' + type + '"'; }).join(" or ") +
' or not(@type)]';
+var settings = {
+ values: {},
+ loadedValues: 0,
+ valuesToLoad: ["scrollStepSize", "linkHintCharacters", "filterLinkHints"],
+
+ get: function (key) { return this.values[key]; },
+
+ load: function() {
+ for (var i in this.valuesToLoad) { this.sendMessage(this.valuesToLoad[i]); }
+ },
+
+ sendMessage: function (key) {
+ if (!settingPort)
+ settingPort = chrome.extension.connect({ name: "getSetting" });
+ settingPort.postMessage({ key: key });
+ },
+
+ receiveMessage: function (args) {
+ // not using 'this' due to issues with binding on callback
+ settings.values[args.key] = args.value;
+ if (++settings.loadedValues == settings.valuesToLoad.length)
+ settings.initializeOnReady();
+ },
+
+ initializeOnReady: function () {
+ initializeLinkHints();
+ }
+};
+
/*
* Give this frame a unique id.
*/
@@ -44,19 +71,11 @@ frameId = Math.floor(Math.random()*999999999)
var hasModifiersRegex = /^<([amc]-)+.>/;
-function getSetting(key) {
- if (!settingPort)
- settingPort = chrome.extension.connect({ name: "getSetting" });
- settingPort.postMessage({ key: key });
-}
-
-function setSetting(args) { settings[args.key] = args.value; }
-
/*
* Complete initialization work that sould be done prior to DOMReady, like setting the page's zoom level.
*/
function initializePreDomReady() {
- for (var i in settingsToLoad) { getSetting(settingsToLoad[i]); }
+ settings.load();
checkIfEnabledForUrl();
@@ -90,14 +109,27 @@ function initializePreDomReady() {
sendResponse({}); // Free up the resources used by this open connection.
});
+ /*
+ * Takes a dot-notation object string and call the function
+ * that it points to with the correct value for 'this'.
+ */
+ function invokeCommandString(str, argArray) {
+ var components = str.split('.');
+ var obj = this;
+ for (var i = 0; i < components.length - 1; i++)
+ obj = obj[components[i]];
+ var func = obj[components.pop()];
+ return func.apply(obj, argArray);
+ }
+
chrome.extension.onConnect.addListener(function(port, name) {
if (port.name == "executePageCommand") {
port.onMessage.addListener(function(args) {
- if (this[args.command] && frameId == args.frameId) {
+ if (frameId == args.frameId) {
if (args.passCountToFunction) {
- this[args.command].call(null, args.count);
+ invokeCommandString(args.command, [args.count]);
} else {
- for (var i = 0; i < args.count; i++) { this[args.command].call(); }
+ for (var i = 0; i < args.count; i++) { invokeCommandString(args.command); }
}
}
@@ -128,7 +160,7 @@ function initializePreDomReady() {
setPageZoomLevel(currentZoomLevel);
});
} else if (port.name == "returnSetting") {
- port.onMessage.addListener(setSetting);
+ port.onMessage.addListener(settings.receiveMessage);
} else if (port.name == "refreshCompletionKeys") {
port.onMessage.addListener(function (args) {
refreshCompletionKeys(args.completionKeys);
@@ -233,14 +265,14 @@ function scrollToBottom() { window.scrollTo(window.pageXOffset, document.body.sc
function scrollToTop() { window.scrollTo(window.pageXOffset, 0); }
function scrollToLeft() { window.scrollTo(0, window.pageYOffset); }
function scrollToRight() { window.scrollTo(document.body.scrollWidth, window.pageYOffset); }
-function scrollUp() { window.scrollBy(0, -1 * settings["scrollStepSize"]); }
-function scrollDown() { window.scrollBy(0, settings["scrollStepSize"]); }
+function scrollUp() { window.scrollBy(0, -1 * settings.get("scrollStepSize")); }
+function scrollDown() { window.scrollBy(0, settings.get("scrollStepSize")); }
function scrollPageUp() { window.scrollBy(0, -1 * window.innerHeight / 2); }
function scrollPageDown() { window.scrollBy(0, window.innerHeight / 2); }
function scrollFullPageUp() { window.scrollBy(0, -window.innerHeight); }
function scrollFullPageDown() { window.scrollBy(0, window.innerHeight); }
-function scrollLeft() { window.scrollBy(-1 * settings["scrollStepSize"], 0); }
-function scrollRight() { window.scrollBy(settings["scrollStepSize"], 0); }
+function scrollLeft() { window.scrollBy(-1 * settings.get("scrollStepSize"), 0); }
+function scrollRight() { window.scrollBy(settings.get("scrollStepSize"), 0); }
function focusInput(count) {
var results = document.evaluate(textInputXPath,
@@ -317,7 +349,7 @@ function toggleViewSourceCallback(url) {
function onKeypress(event) {
var keyChar = "";
- if (linkHintsModeActivated)
+ if (linkHints.modeActivated)
return;
// Ignore modifier keys by themselves.
@@ -352,7 +384,7 @@ function onKeypress(event) {
function onKeydown(event) {
var keyChar = "";
- if (linkHintsModeActivated)
+ if (linkHints.modeActivated)
return;
// handle modifiers being pressed.don't handle shiftKey alone (to avoid / being interpreted as ?