aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitmodules3
-rw-r--r--CREDITS5
-rw-r--r--README.markdown38
-rw-r--r--background_page.html137
-rw-r--r--bookmarks.js131
-rw-r--r--commands.js47
-rw-r--r--completionDialog.js180
-rw-r--r--helpDialog.html27
-rw-r--r--lib/keyboardUtils.js2
-rw-r--r--lib/utils.js33
-rw-r--r--linkHints.js892
-rw-r--r--manifest.json10
-rw-r--r--options.html87
-rw-r--r--test_harnesses/automated.html252
-rw-r--r--test_harnesses/iframe.html2
m---------test_harnesses/shoulda.js0
-rw-r--r--vimiumFrontend.js444
17 files changed, 1653 insertions, 637 deletions
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..d496d533
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "test_harnesses/shoulda.js"]
+ path = test_harnesses/shoulda.js
+ url = git://github.com/philc/shoulda.js.git
diff --git a/CREDITS b/CREDITS
index 494a5c1d..082e70d6 100644
--- a/CREDITS
+++ b/CREDITS
@@ -10,18 +10,23 @@ Contributors:
Christian Stefanescu (github: stchris)
ConradIrwin
drizzd
+ gpurkins
hogelog
int3
Johannes Emerich (github: knuton)
Julian Naydichev <rublind@gmail.com> (github: naydichev)
lack
markstos
+ Matthew Cline <matt@nightrealms.com>
+ Murph (github: pandeiro)
rodimius
Tim Morgan <tim@timmorgan.org> (github: seven1m)
tsigo
Werner Laurensse (github: ab3)
Svein-Erik Larsen <feinom@gmail.com> (github: feinom)
+ Bill Casarin <jb@jb55.com> (github: jb55)
R.T. Lechow <rtlechow@gmail.com> (github: rtlechow)
+ Justin Blake <justin@hentzia.com> (github: blaix)
Wang Ning <daning106@gmail.com> (github:daning)
Feel free to add real names in addition to GitHub usernames.
diff --git a/README.markdown b/README.markdown
index 6adb3714..1a9294f5 100644
--- a/README.markdown
+++ b/README.markdown
@@ -26,6 +26,7 @@ Modifier keys are specified as &lt;c-x&gt; &lt;m-x&gt;, &lt;a-x&gt; for ctrl+x,
respectively. See the next section for instructions on modifying these bindings.
Navigating the current page:
+
? show the help dialog for a list of all available keys
h scroll left
j scroll down
@@ -39,29 +40,34 @@ Navigating the current page:
F open a link in a new tab
r reload
gs view source
- zi zoom in
- zo zoom out
i enter insert mode -- all commands will be ignored until you hit esc to exit
yy copy the current url to the clipboard
+ yf copy a link url to the clipboard
gf cycle forward to the next frame
Using find:
+
/ enter find mode -- type your search query and hit enter to search or esc to cancel
n cycle forward to the next find match
N cycle backward to the previous find match
Navigating your history:
+
H go back in history
L go forward in history
Manipulating tabs:
+
J, gT go one tab left
K, gt go one tab right
+ g0 go to the first tab
+ g$ go to the last tab
t create tab
x close current tab
X restore closed tab (i.e. unwind the 'x' command)
Additional advanced browsing commands:
+
]] Follow the link labeled 'next' or '>'. Helpful for browsing paginated sites.
[[ Follow the link labeled 'previous' or '<'. Helpful for browsing paginated sites.
<a-f> open multiple links in a new tab
@@ -69,8 +75,6 @@ Additional advanced browsing commands:
gu go up one level in the URL hierarchy
zH scroll all the way left
zL scroll all the way right
- z0 reset zoom to default value
-
Vimium supports command repetition so, for example, hitting '5t' will open 5 tabs in rapid succession. ESC (or
&lt;c-[&gt;) will clear any partial commands in the queue and will also exit insert and find modes.
@@ -131,9 +135,30 @@ don't exceed 110 characters.
Release Notes
-------------
-1.27
+1.30 (12/04/2011)
+
+- Support for image maps in link hints.
+- Counts now work with forward & backward navigation.
+- Tab & shift-tab to navigate bookmarks dialog.
+- An alternate link hints mode: type the title of a link to select it. You can enable it in Vimium's Advanced Preferences.
+- Bug fixes.
+
+1.29 (07/30/2011)
+
+- `yf` to copy a link hint url to the clipboard.
+- Scatter link hints to prevent clustering on dense sites.
+- Don't show insert mode notification unless you specifically hit `i`.
+- Remove zooming functionality now that Chrome does it all natively.
+
+1.28 (06/29/2011)
+
+- Support for opening bookmarks (`b` and `B`).
+- Support for contenteditable text boxes.
+- Speed improvements and bugfixes.
+
+1.27 (03/24/2011)
-- Bugfixes.
+- Improvements and bugfixes.
1.26 (02/17/2011)
@@ -145,7 +170,6 @@ Release Notes
- Some sites are now excluded by default.
- View source (`gs`) now opens in a new tab.
- Support for browsing paginated sites using `]]` and `[[` to go forward and backward respectively.
-- `z0` will reset the zoom level for the current page.
- Many of the less-used commands are now marked as "advanced" and hidden in the help dialog by default, so
that the core command set is more focused and approachable.
- Improvements to link hinting.
diff --git a/background_page.html b/background_page.html
index 2be1e7b2..7d8f749d 100644
--- a/background_page.html
+++ b/background_page.html
@@ -25,15 +25,23 @@
var defaultSettings = {
scrollStepSize: 60,
- defaultZoomLevel: 100,
linkHintCharacters: "sadfjklewcmpgh",
+ filterLinkHints: false,
userDefinedLinkHintCss:
".vimiumHintMarker {\n\n}\n" +
".vimiumHintMarker > .matchingCharacter {\n\n}",
excludedUrls: "http*://mail.google.com/*\n" +
"http*://www.google.com/reader/*\n",
- previousPatterns: "\\bprev\\b,\\bprevious\\b,\\u00AB,<<,<",
- nextPatterns: "\\bnext\\b,\\u00BB,>>,\\bmore\\b,>"
+
+ // NOTE : If a page contains both a single angle-bracket link and a double angle-bracket link, then in
+ // most cases the single bracket link will be "prev/next page" and the double bracket link will be
+ // "first/last page", so we put the single bracket first in the pattern string so that it gets searched
+ // for first.
+
+ // "\bprev\b,\bprevious\b,\bback\b,<,←,«,≪,<<"
+ previousPatterns: "prev,previous,back,<,\u2190,\xab,\u226a,<<",
+ // "\bnext\b,\bmore\b,>,→,»,≫,>>"
+ nextPatterns: "next,more,>,\u2192,\xbb,\u226b,>>",
};
// This is the base internal link hints CSS. It's combined with the userDefinedLinkHintCss before
@@ -65,13 +73,12 @@
keyDown: handleKeyDown,
returnScrollPosition: handleReturnScrollPosition,
getCurrentTabUrl: getCurrentTabUrl,
- getZoomLevel: getZoomLevel,
- saveZoomLevel: saveZoomLevel,
- getSetting: getSetting
+ getSetting: getSetting,
+ getBookmarks: getBookmarks
};
var sendRequestHandlers = {
- getCompletionKeys: getCompletionKeys,
+ getCompletionKeys: getCompletionKeysRequest,
getLinkHintCss: getLinkHintCss,
openUrlInNewTab: openUrlInNewTab,
openUrlInCurrentTab: openUrlInCurrentTab,
@@ -81,6 +88,7 @@
upgradeNotificationClosed: upgradeNotificationClosed,
updateScrollPosition: handleUpdateScrollPosition,
copyToClipboard: copyToClipboard,
+ copyLinkUrl: copyLinkUrl,
isEnabledForUrl: isEnabledForUrl,
saveHelpDialogSettings: saveHelpDialogSettings
};
@@ -158,17 +166,6 @@
localStorage["helpDialog_showAdvancedCommands"] = request.showAdvancedCommands;
}
- /*
- * Returns the previously saved zoom level for the current tab, or the default zoom level
- */
- function getZoomLevel(args, port) {
- var returnPort = chrome.tabs.connect(port.tab.id, { name: "returnZoomLevel" });
- var localStorageKey = "zoom" + args.domain;
- var zoomLevelForDomain = (localStorage[localStorageKey] || "").split(",")[1];
- var zoomLevel = parseInt(zoomLevelForDomain || getSettingFromLocalStorage("defaultZoomLevel"));
- returnPort.postMessage({ zoomLevel: zoomLevel });
- }
-
function showHelp(callback, frameId) {
chrome.tabs.getSelected(null, function(tab) {
chrome.tabs.sendRequest(tab.id,
@@ -236,8 +233,11 @@
/**
* Returns the keys that can complete a valid command given the current key queue.
*/
- function getCompletionKeys(request) {
- return {completionKeys: generateCompletionKeys()};
+ function getCompletionKeysRequest(request) {
+ return { name: "refreshCompletionKeys",
+ completionKeys: generateCompletionKeys(),
+ validFirstKeys: validFirstKeys
+ };
}
/**
@@ -258,6 +258,14 @@
chrome.tabs.create({ url: request.url, index: tab.index + 1, selected: request.selected });
});
}
+
+ /**
+ * Copies url of selected link to the clipboard (wget ftw)
+ */
+ function copyLinkUrl(request) {
+ Clipboard.copy(request.data);
+ }
+
/*
* Returns the core CSS used for link hints, along with any user-provided overrides.
*/
@@ -290,6 +298,12 @@
returnPort.postMessage({ key: args.key, value: value });
}
+ function getBookmarks(args, port) {
+ chrome.bookmarks.search(args.query, function(bookmarks) {
+ port.postMessage({bookmarks:bookmarks})
+ })
+ }
+
/*
* Used by everyone to get settings from local storage.
*/
@@ -301,16 +315,6 @@
}
}
- /*
- * Persists the current zoom level for a given domain
- */
- function saveZoomLevel(args) {
- var localStorageKey = "zoom" + args.domain;
- // TODO(philc): We might want to consider expiring these entries after X months as NoSquint does.
- // Note(philc): We might also want to jsonify this hash instead of polluting our local storage keyspace.
- localStorage[localStorageKey] = [getCurrentTimeInSeconds(), args.zoomLevel].join(",");
- }
-
function getCurrentTimeInSeconds() { Math.floor((new Date()).getTime() / 1000); }
chrome.tabs.onSelectionChanged.addListener(function(tabId, selectionInfo) {
@@ -337,23 +341,34 @@
function nextTab(callback) { selectTab(callback, "next"); }
function previousTab(callback) { selectTab(callback, "previous"); }
+ function firstTab(callback) { selectTab(callback, "first"); }
+ function lastTab(callback) { selectTab(callback, "last"); }
/*
- * Selects a tab before or after the currently selected tab. Direction is either "next" or "previous".
+ * Selects a tab before or after the currently selected tab. Direction is either "next", "previous", "first" or "last".
*/
function selectTab(callback, direction) {
chrome.tabs.getAllInWindow(null, function(tabs) {
if (tabs.length <= 1)
return;
- for (var i = 0; i < tabs.length; i++) {
- if (tabs[i].selected) {
- var delta = (direction == "next") ? 1 : -1;
- var toSelect = tabs[(i + delta + tabs.length) % tabs.length];
+ chrome.tabs.getSelected(null, function(currentTab) {
+ switch (direction) {
+ case "next":
+ toSelect = tabs[(currentTab.index + 1 + tabs.length) % tabs.length];
+ break;
+ case "previous":
+ toSelect = tabs[(currentTab.index - 1 + tabs.length) % tabs.length];
+ break;
+ case "first":
+ toSelect = tabs[0];
+ break;
+ case "last":
+ toSelect = tabs[tabs.length - 1];
+ break;
+ }
selectionChangedHandlers.push(callback);
chrome.tabs.update(toSelect.id, { selected: true });
- break;
- }
- }
+ });
});
}
@@ -368,6 +383,8 @@
function updateOpenTabs(tab) {
openTabs[tab.id] = { url: tab.url, positionIndex: tab.index, windowId: tab.windowId };
+ // Frames are recreated on refresh
+ delete framesForTab[tab.id];
}
function handleUpdateScrollPosition(request, sender) {
@@ -502,7 +519,7 @@
populateValidFirstKeys();
populateSingleKeyCommands();
- sendRequestToAllTabs({ name: "refreshCompletionKeys", completionKeys: generateCompletionKeys() });
+ sendRequestToAllTabs(getCompletionKeysRequest());
}
/*
@@ -574,7 +591,11 @@
refreshedCompletionKeys = true;
} else {
- repeatFunction(this[registryEntry.command], count, 0, frameId);
+ if(registryEntry.passCountToFunction){
+ this[registryEntry.command](count);
+ } else {
+ repeatFunction(this[registryEntry.command], count, 0, frameId);
+ }
}
newKeyQueue = "";
@@ -592,10 +613,8 @@
// If we haven't sent the completion keys piggybacked on executePageCommand,
// send them by themselves.
- if (!refreshedCompletionKeys)
- {
- var port = chrome.tabs.connect(tabId, { name: "refreshCompletionKeys" });
- port.postMessage({ completionKeys: generateCompletionKeys(newKeyQueue) });
+ if (!refreshedCompletionKeys) {
+ chrome.tabs.sendRequest(tabId, getCompletionKeysRequest(), null);
}
return newKeyQueue;
@@ -648,7 +667,7 @@
if (!framesForTab[sender.tab.id])
framesForTab[sender.tab.id] = { frames: [] };
- if (request.top) {
+ if (request.is_top) {
focusedFrame = request.frameId;
framesForTab[sender.tab.id].total = request.total;
}
@@ -681,25 +700,29 @@
focusedFrame = request.frameId;
}
- function nextFrame(callback, frameId) {
+ function nextFrame(count) {
chrome.tabs.getSelected(null, function(tab) {
- var index;
var frames = framesForTab[tab.id].frames;
+ var curr_index = getCurrFrameIndex(frames);
- for (index=0; index < frames.length; index++) {
- if (frames[index].id == focusedFrame)
- break;
- }
-
- if (index >= frames.length-1)
- index = 0;
- else
- index++;
+ // TODO: Skip the "top" frame (which doesn't actually have a <frame> tag),
+ // since it exists only to contain the other frames.
+ var new_index = (curr_index + count) % frames.length;
- chrome.tabs.sendRequest(tab.id, { name: "focusFrame", frameId: frames[index].id, highlight: true });
+ chrome.tabs.sendRequest(tab.id, { name: "focusFrame", frameId: frames[new_index].id, highlight: true });
});
}
+ function getCurrFrameIndex(frames) {
+ var index;
+ for (index=0; index < frames.length; index++) {
+ if (frames[index].id == focusedFrame)
+ break;
+ }
+ return index;
+ }
+
+
function init() {
clearKeyMappingsAndSetDefaults();
diff --git a/bookmarks.js b/bookmarks.js
new file mode 100644
index 00000000..9056c731
--- /dev/null
+++ b/bookmarks.js
@@ -0,0 +1,131 @@
+function activateBookmarkFindModeToOpenInNewTab() {
+ BookmarkMode.openInNewTab(true);
+ BookmarkMode.enable();
+}
+
+function activateBookmarkFindMode() {
+ BookmarkMode.openInNewTab(false);
+ BookmarkMode.enable();
+}
+
+(function() {
+ // so when they let go of shift after hitting capital "B" it won't
+ // untoggle it
+ var shiftWasPressedWhileToggled = false;
+
+ var BookmarkMode = {
+ isEnabled: function() {
+ return this.enabled;
+ },
+ openInNewTab: function(newTab) {
+ this.newTab = newTab;
+ },
+ invertNewTabSetting: function() {
+ this.newTab = !this.newTab;
+ if(this.isEnabled()) {
+ this.renderHUD();
+ }
+ },
+ enable: function() {
+ this.enabled = true;
+
+ if(!this.initialized) {
+ initialize.call(this);
+ }
+
+ handlerStack.push({
+ keydown: this.onKeydown,
+ keyup: this.onKeyup
+ });
+
+ this.renderHUD();
+ this.completionDialog.show();
+ },
+ disable: function() {
+ this.enabled = false;
+ this.completionDialog.hide();
+ handlerStack.pop();
+ HUD.hide();
+ },
+ renderHUD: function() {
+ if (this.newTab)
+ HUD.show("Open bookmark in new tab");
+ else
+ HUD.show("Open bookmark in current tab");
+ }
+
+ }
+
+ // private method
+ var initialize = function() {
+ var self = this;
+ self.initialized = true;
+
+ self.completionDialog = new CompletionDialog({
+ source: findBookmarks,
+
+ onSelect: function(selection) {
+ var url = selection.url;
+ var isABookmarklet = function(url) { return url.indexOf("javascript:") === 0; }
+
+ if (!self.newTab || isABookmarklet(url))
+ window.location = url;
+ else
+ window.open(url);
+
+ self.disable();
+ },
+
+ renderOption: function(searchString, selection) {
+ var displaytext = selection.title + " (" + selection.url + ")"
+ if (displaytext.length > 70)
+ displaytext = displaytext.substr(0, 70) + "...";
+
+ return displaytext.split(new RegExp(searchString, "i")).join("<strong>"+searchString+"</strong>")
+ },
+
+ initialSearchText: "Type a bookmark name or URL"
+ })
+
+ self.onKeydown = function(event) {
+ // shift key will toggle between new tab/same tab
+ if (event.keyCode == keyCodes.shiftKey) {
+ self.invertNewTabSetting();
+ shiftWasPressedWhileToggled = true;
+ return;
+ }
+
+ var keyChar = getKeyChar(event);
+ if (!keyChar)
+ return;
+
+ // TODO(philc): Ignore keys that have modifiers.
+ if (isEscape(event))
+ self.disable();
+
+ event.stopPropagation();
+ event.preventDefault();
+ };
+
+ self.onKeyup = function(event) {
+ // shift key will toggle between new tab/same tab
+ if (event.keyCode == keyCodes.shiftKey && shiftWasPressedWhileToggled) {
+ self.invertNewTabSetting();
+ shiftWasPressedWhileToggled = false;
+ }
+ event.stopPropagation();
+ event.preventDefault();
+ };
+ }
+
+ var findBookmarks = function(searchString, callback) {
+ var port = chrome.extension.connect({ name: "getBookmarks" }) ;
+ port.onMessage.addListener(function(msg) {
+ callback(msg.bookmarks);
+ port = null;
+ })
+ port.postMessage({query:searchString});
+ };
+
+ window.BookmarkMode = BookmarkMode;
+}())
diff --git a/commands.js b/commands.js
index be01a180..d214d2bf 100644
--- a/commands.js
+++ b/commands.js
@@ -111,15 +111,11 @@ function clearKeyMappingsAndSetDefaults() {
"L": "goForward",
"gu": "goUp",
- "zi": "zoomIn",
- "zo": "zoomOut",
- "z0": "zoomReset",
-
"gi": "focusInput",
- "f": "activateLinkHintsMode",
- "F": "activateLinkHintsModeToOpenInNewTab",
- "<a-f>": "activateLinkHintsModeWithQueue",
+ "f": "linkHints.activateMode",
+ "F": "linkHints.activateModeToOpenInNewTab",
+ "<a-f>": "linkHints.activateModeWithQueue",
"/": "enterFindMode",
"n": "performFind",
@@ -129,16 +125,22 @@ function clearKeyMappingsAndSetDefaults() {
"]]": "goNext",
"yy": "copyCurrentUrl",
+ "yf": "linkHints.activateModeToCopyLinkUrl",
"K": "nextTab",
"J": "previousTab",
"gt": "nextTab",
"gT": "previousTab",
+ "g0": "firstTab",
+ "g$": "lastTab",
"t": "createTab",
"x": "removeTab",
"X": "restoreTab",
+ "b": "activateBookmarkFindMode",
+ "B": "activateBookmarkFindModeToOpenInNewTab",
+
"gf": "nextFrame"
};
@@ -166,18 +168,17 @@ var commandDescriptions = {
reload: ["Reload the page"],
toggleViewSource: ["View page source"],
- zoomIn: ["Zoom in"],
- zoomOut: ["Zoom out"],
- zoomReset: ["Reset zoom to default value"],
copyCurrentUrl: ["Copy the current URL to the clipboard"],
+ 'linkHints.activateModeToCopyLinkUrl': ["Copy a link URL to the clipboard"],
+
enterInsertMode: ["Enter insert mode"],
focusInput: ["Focus the first (or n-th) text box on the page", { passCountToFunction: true }],
- activateLinkHintsMode: ["Open a link in the current tab"],
- activateLinkHintsModeToOpenInNewTab: ["Open a link in a new tab"],
- activateLinkHintsModeWithQueue: ["Open multiple links in a new tab"],
+ 'linkHints.activateMode': ["Open a link in the current tab"],
+ 'linkHints.activateModeToOpenInNewTab': ["Open a link in a new tab"],
+ 'linkHints.activateModeWithQueue': ["Open multiple links in a new tab"],
enterFindMode: ["Enter find mode"],
performFind: ["Cycle forward to the next find match"],
@@ -187,8 +188,8 @@ var commandDescriptions = {
goNext: ["Follow the link labeled next or >"],
// Navigating your history
- goBack: ["Go back in history"],
- goForward: ["Go forward in history"],
+ goBack: ["Go back in history", { passCountToFunction: true }],
+ goForward: ["Go forward in history", { passCountToFunction: true }],
// Navigating the URL hierarchy
goUp: ["Go up the URL hierarchy", { passCountToFunction: true }],
@@ -196,11 +197,16 @@ var commandDescriptions = {
// Manipulating tabs
nextTab: ["Go one tab right", { background: true }],
previousTab: ["Go one tab left", { background: true }],
+ firstTab: ["Go to the first tab", { background: true }],
+ lastTab: ["Go to the last tab", { background: true }],
createTab: ["Create new tab", { background: true }],
removeTab: ["Close current tab", { background: true }],
restoreTab: ["Restore closed tab", { background: true }],
- nextFrame: ["Cycle forward to the next frame on the page", { background: true }]
+ activateBookmarkFindMode: ["Open a bookmark in the current tab"],
+ activateBookmarkFindModeToOpenInNewTab: ["Open a bookmark in a new tab"],
+
+ nextFrame: ["Cycle forward to the next frame on the page", { background: true, passCountToFunction: true }]
};
for (var command in commandDescriptions)
@@ -214,15 +220,16 @@ var commandGroups = {
["scrollDown", "scrollUp", "scrollLeft", "scrollRight",
"scrollToTop", "scrollToBottom", "scrollToLeft", "scrollToRight", "scrollPageDown",
"scrollPageUp", "scrollFullPageUp", "scrollFullPageDown",
- "reload", "toggleViewSource", "zoomIn", "zoomOut", "zoomReset", "copyCurrentUrl", "goUp",
+ "reload", "toggleViewSource", "copyCurrentUrl", "linkHints.activateModeToCopyLinkUrl", "goUp",
"enterInsertMode", "focusInput",
- "activateLinkHintsMode", "activateLinkHintsModeToOpenInNewTab", "activateLinkHintsModeWithQueue",
+ "linkHints.activateMode", "linkHints.activateModeToOpenInNewTab", "linkHints.activateModeWithQueue",
+ "activateBookmarkFindMode", "activateBookmarkFindModeToOpenInNewTab",
"goPrevious", "goNext", "nextFrame"],
findCommands: ["enterFindMode", "performFind", "performBackwardsFind"],
historyNavigation:
["goBack", "goForward"],
tabManipulation:
- ["nextTab", "previousTab", "createTab", "removeTab", "restoreTab"],
+ ["nextTab", "previousTab", "firstTab", "lastTab", "createTab", "removeTab", "restoreTab"],
misc:
["showHelp"]
};
@@ -232,5 +239,5 @@ var commandGroups = {
// from Vimium will uncover these gems.
var advancedCommands = [
"scrollToLeft", "scrollToRight",
- "zoomReset", "goUp", "focusInput", "activateLinkHintsModeWithQueue",
+ "goUp", "focusInput", "linkHints.activateModeWithQueue",
"goPrevious", "goNext"];
diff --git a/completionDialog.js b/completionDialog.js
new file mode 100644
index 00000000..fae034fd
--- /dev/null
+++ b/completionDialog.js
@@ -0,0 +1,180 @@
+(function(window, document) {
+
+ var CompletionDialog = function(options) { this.options = options; }
+
+ CompletionDialog.prototype = {
+ show: function() {
+ if (!this.isShown) {
+ this.isShown=true;
+ this.query = [];
+ if (!this.initialized) {
+ initialize.call(this);
+ this.initialized = true;
+ }
+ handlerStack.push({ keydown: this.onKeydown });
+ render.call(this);
+ clearInterval(this._tweenId);
+ this.container.style.display = "";
+ this._tweenId = Tween.fade(this.container, 1.0, 150);
+ }
+ },
+
+ hide: function() {
+ if (this.isShown) {
+ handlerStack.pop();
+ this.isShown = false;
+ this.currentSelection = 0;
+ clearInterval(this._tweenId);
+ var completionContainer = this.container;
+ var cssHide = function() { completionContainer.style.display = "none"; }
+ this._tweenId = Tween.fade(this.container, 0, 150, cssHide);
+ }
+ },
+
+ getDisplayElement: function() {
+ if (!this.container)
+ this.container = createDivInside(document.body);
+ return this.container;
+ },
+
+ getQueryString: function() { return this.query.join(""); }
+ }
+
+ var initialize = function() {
+ var self = this;
+ addCssToPage(completionCSS);
+
+ self.currentSelection = 0;
+
+ self.onKeydown = function(event) {
+ var keyChar = getKeyChar(event);
+ // change selection with up or Shift-Tab
+ if (keyChar==="up" || (event.keyCode == 9 && event.shiftKey)) {
+ if (self.currentSelection>0) {
+ self.currentSelection-=1;
+ }
+ render.call(self,self.getQueryString(), self.completions);
+ }
+ // change selection with down or Tab
+ else if (keyChar==="down" || (event.keyCode == 9 && !event.shiftKey)) {
+ if (self.currentSelection < self.completions.length - 1) {
+ self.currentSelection += 1;
+ }
+ render.call(self,self.getQueryString(), self.completions);
+ }
+ else if (event.keyCode == keyCodes.enter) {
+ self.options.onSelect(self.completions[self.currentSelection]);
+ }
+ else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) {
+ if (self.query.length > 0) {
+ self.query.pop();
+ self.options.source(self.getQueryString(), function(completions) {
+ render.call(self, self.getQueryString(), completions);
+ })
+ }
+ }
+ else if (keyChar!=="left" && keyChar!="right") {
+ self.query.push(keyChar);
+ self.options.source(self.getQueryString(), function(completions) {
+ render.call(self, self.getQueryString(), completions);
+ });
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+ return true;
+ }
+ }
+
+ var render = function(searchString, completions) {
+ if (this.isShown) {
+ this.searchString = searchString;
+ this.completions = completions;
+ var container = this.getDisplayElement();
+ clearChildren(container);
+
+ if (searchString === undefined) {
+ this.container.className = "vimium-dialog";
+ createDivInside(container).innerHTML = this.options.initialSearchText || "Begin typing";
+ }
+ else {
+ this.container.className = "vimium-dialog vimium-completions";
+ var searchBar = createDivInside(container);
+ searchBar.innerHTML=searchString;
+ searchBar.className="vimium-searchBar";
+
+ searchResults = createDivInside(container);
+ searchResults.className="vimium-searchResults";
+ if (completions.length<=0) {
+ var resultDiv = createDivInside(searchResults);
+ resultDiv.className="vimium-noResults";
+ resultDiv.innerHTML="No results found";
+ }
+ else {
+ for (var i = 0; i < completions.length; i++) {
+ var resultDiv = createDivInside(searchResults);
+ if (i === this.currentSelection) {
+ resultDiv.className="vimium-selected";
+ }
+ resultDiv.innerHTML=this.options.renderOption(searchString, completions[i]);
+ }
+ }
+ }
+
+ container.style.top = Math.max(0, (window.innerHeight/2-container.clientHeight/2)) + "px";
+ container.style.left = (window.innerWidth/2-container.clientWidth/2) + "px";
+ }
+ };
+ var createDivInside = function(parent) {
+ var element = document.createElement("div");
+ parent.appendChild(element);
+ return element;
+ }
+
+ var clearChildren = function(elem) {
+ if (elem.hasChildNodes()) {
+ while (elem.childNodes.length >= 1) {
+ elem.removeChild(elem.firstChild);
+ }
+ }
+ }
+
+ var completionCSS = ".vimium-dialog {"+
+ "position:fixed;"+
+ "background-color: #ebebeb;" +
+ "z-index: 99999998;" +
+ "border: 1px solid #b3b3b3;" +
+ "font-size: 12px;" +
+ "text-align:left;"+
+ "color: black;" +
+ "padding:10px;"+
+ "border-radius: 4px;" +
+ "font-family: Lucida Grande, Arial, Sans;" +
+ "}"+
+ ".vimium-completions {"+
+ "width:400px;"+
+ "}"+
+ ".vimium-completions .vimium-searchBar {"+
+ "height: 15px;"+
+ "border-bottom: 1px solid #b3b3b3;"+
+ "}"+
+ ".vimium-completions .vimium-searchResults {"+
+ "}"+
+ ".vimium-completions .vimium-searchResults .vimium-selected{"+
+ "background-color:#aaa;"+
+ "border-radius: 4px;" +
+ "}"+
+ ".vimium-completions div{"+
+ "padding:4px;"+
+ "}"+
+ ".vimium-completions div strong{"+
+ "color: black;" +
+ "font-weight:bold;"+
+ "}"+
+ ".vimium-completions .vimium-noResults{"+
+ "color:#555;"+
+ "}";
+
+ window.CompletionDialog = CompletionDialog;
+
+}(window, document))
diff --git a/helpDialog.html b/helpDialog.html
index 51ff1692..5ded51f2 100644
--- a/helpDialog.html
+++ b/helpDialog.html
@@ -28,6 +28,22 @@
top:50px;
-webkit-box-shadow: rgba(0, 0, 0, 0.4) 0px 0px 6px;
z-index:99999998;
+ overflow-y: scroll;
+ }
+ @media screen and (max-height: 600px) {
+ #vimiumHelpDialog {
+ height: 430px;
+ }
+ }
+ @media screen and (max-height: 500px) {
+ #vimiumHelpDialog {
+ height: 330px;
+ }
+ }
+ @media screen and (max-height: 400px) {
+ #vimiumHelpDialog {
+ height: 230px;
+ }
}
#vimiumHelpDialog a { color:blue; }
#vimiumTitle, #vimiumTitle * { font-size:20px; }
@@ -141,7 +157,6 @@
this.dialogElement.getElementsByClassName("toggleAdvancedCommands")[0].addEventListener("click",
VimiumHelpDialog.toggleAdvancedCommands, false);
this.showAdvancedCommands(this.advancedCommandsVisible);
- this.centerDialog();
},
/*
@@ -153,7 +168,6 @@
chrome.extension.sendRequest({ handler: "saveHelpDialogSettings",
showAdvancedCommands: VimiumHelpDialog.advancedCommandsVisible });
VimiumHelpDialog.showAdvancedCommands(VimiumHelpDialog.advancedCommandsVisible);
- VimiumHelpDialog.centerDialog();
},
showAdvancedCommands: function(visible) {
@@ -162,14 +176,7 @@
var advanced = VimiumHelpDialog.dialogElement.getElementsByClassName("advanced");
for (var i = 0; i < advanced.length; i++)
advanced[i].style.display = (visible ? "table-row" : "none");
- },
-
- centerDialog: function() {
- var zoomFactor = currentZoomLevel / 100.0;
- this.dialogElement.style.top = Math.max(
- (window.innerHeight - this.dialogElement.clientHeight * zoomFactor) / 2.0,
- 20) / zoomFactor + "px";
- }
+ }
};
VimiumHelpDialog.init();
diff --git a/lib/keyboardUtils.js b/lib/keyboardUtils.js
index fe3dcd59..98725d95 100644
--- a/lib/keyboardUtils.js
+++ b/lib/keyboardUtils.js
@@ -44,7 +44,7 @@ function getKeyChar(event) {
// https://bugs.webkit.org/show_bug.cgi?id=19906 for more details.
if ((platform == "Windows" || platform == "Linux") && keyIdentifierCorrectionMap[keyIdentifier]) {
correctedIdentifiers = keyIdentifierCorrectionMap[keyIdentifier];
- keyIdentifier = event.shiftKey ? correctedIdentifiers[0] : correctedIdentifiers[1];
+ keyIdentifier = event.shiftKey ? correctedIdentifiers[1] : correctedIdentifiers[0];
}
var unicodeKeyInHex = "0x" + keyIdentifier.substring(2);
return String.fromCharCode(parseInt(unicodeKeyInHex)).toLowerCase();
diff --git a/lib/utils.js b/lib/utils.js
new file mode 100644
index 00000000..ef961833
--- /dev/null
+++ b/lib/utils.js
@@ -0,0 +1,33 @@
+var utils = {
+ /*
+ * Takes a dot-notation object string and call the function
+ * that it points to with the correct value for 'this'.
+ */
+ invokeCommandString: function(str, argArray) {
+ var components = str.split('.');
+ var obj = window;
+ for (var i = 0; i < components.length - 1; i++)
+ obj = obj[components[i]];
+ var func = obj[components.pop()];
+ return func.apply(obj, argArray);
+ },
+
+ /*
+ * Takes an array of XPath selectors, adds the necessary namespaces (currently only XHTML), and applies them
+ * to the document root. The namespaceResolver in evaluateXPath should be kept in sync with the namespaces
+ * here.
+ */
+ makeXPath: function(elementArray) {
+ var xpath = [];
+ for (var i in elementArray)
+ xpath.push("//" + elementArray[i], "//xhtml:" + elementArray[i]);
+ return xpath.join(" | ");
+ },
+
+ evaluateXPath: function(xpath, resultType) {
+ function namespaceResolver(namespace) {
+ return namespace == "xhtml" ? "http://www.w3.org/1999/xhtml" : null;
+ }
+ return document.evaluate(xpath, document.documentElement, namespaceResolver, resultType, null);
+ },
+};
diff --git a/linkHints.js b/linkHints.js
index de476e36..d37d2d7c 100644
--- a/linkHints.js
+++ b/linkHints.js
@@ -1,337 +1,605 @@
/*
- * 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 | ..."
+ * 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 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");
+var linkHints = {
+ hintMarkers: [],
+ hintMarkerContainingDiv: null,
+ // The characters that were typed in while in "link hints" mode.
+ shouldOpenInNewTab: false,
+ shouldOpenWithQueue: false,
+ // flag for copying link instead of opening
+ shouldCopyLinkUrl: 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,
+ // 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.onKeyPressInMode = this.onKeyPressInMode.bind(this);
+ this.onKeyUpInMode = this.onKeyUpInMode.bind(this);
+ this.markerMatcher = settings.get('filterLinkHints') == "true" ? filterHints : alphabetHints;
+ },
+
+ /*
+ * Generate an XPath describing what a clickable element is.
+ * The final expression will be something like "//button | //xhtml:button | ..."
+ */
+ clickableElementsXPath: utils.makeXPath(["a", "area[@href]", "textarea", "button", "select","input[not(@type='hidden')]",
+ "*[@onclick or @tabindex or @role='link' or @role='button']"]),
+
+ // 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(false, false, true); },
+
+ activateModeWithQueue: function() { this.activateMode(true, true, false); },
+
+ activateMode: function(openInNewTab, withQueue, copyLinkUrl) {
+ if (!this.cssAdded)
+ addCssToPage(linkHintCss); // linkHintCss is declared by vimiumFrontend.js
+ this.linkHintCssAdded = true;
+ this.setOpenLinkMode(openInNewTab, withQueue, copyLinkUrl);
+ this.buildLinkHints();
+ handlerStack.push({ // modeKeyHandler is declared by vimiumFrontend.js
+ keydown: this.onKeyDownInMode,
+ keypress: this.onKeyPressInMode,
+ keyup: this.onKeyUpInMode
+ });
+
+ this.openLinkModeToggle = false;
+ },
+
+ setOpenLinkMode: function(openInNewTab, withQueue, copyLinkUrl) {
+ this.shouldOpenInNewTab = openInNewTab;
+ this.shouldOpenWithQueue = withQueue;
+ this.shouldCopyLinkUrl = copyLinkUrl;
+ if (this.shouldCopyLinkUrl) {
+ HUD.show("Copy link URL to Clipboard");
+ } else 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();
+ 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.className = "internalVimiumHintMarker";
+ 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
- HUD.show("Open link in current tab");
- }
-}
-
-/*
- * Builds and displays link hints for every visible clickable item on the page.
- */
-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, count = visibleElements.length; i < count; 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.
- */
-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, count = resultSet.snapshotLength; i < count; 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, childrenCount = element.children.length; j < childrenCount; 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;
- }
- }
+ 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 = utils.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 = this.getVisibleClientRect(element, clientRect);
+ if (clientRect !== null)
+ visibleElements.push({element: element, rect: clientRect});
+
+ if (element.localName === "area") {
+ var map = element.parentElement;
+ var img = document.querySelector("img[usemap='#" + map.getAttribute("name") + "']");
+ var clientRect = img.getClientRects()[0];
+ 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: clientRect.top + coords[1],
+ left: clientRect.left + coords[0],
+ right: clientRect.left + coords[2],
+ bottom: clientRect.top + coords[3],
+ width: coords[2] - coords[0],
+ height: coords[3] - coords[1]
+ };
+
+ visibleElements.push({element: element, rect: rect});
}
}
- }
- 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;
- }
+ return visibleElements;
+ },
+
+ /**
+ * Returns the first visible clientRect of an element if it exists. Otherwise it returns null.
+ */
+ getVisibleClientRect: function(element) {
+ // Note: this call will be expensive if we modify the DOM in between calls.
+ var clientRects = element.getClientRects();
+
+ for (var i = 0, len = clientRects.length; i < len; i++) {
+ // Exclude links which have just a few pixels on screen, because the link hints won't show for them
+ // anyway.
+ if (clientRects[i].top < 0 || clientRects[i].top >= window.innerHeight - 4 ||
+ clientRects[i].left < 0 || clientRects[i].left >= window.innerWidth - 4)
+ continue;
+
+ if (clientRects[i].width < 3 || clientRects[i].height < 3)
+ continue;
+
+ // eliminate invisible elements (see test_harnesses/visibility_test.html)
+ var computedStyle = window.getComputedStyle(element, null);
+ if (computedStyle.getPropertyValue('visibility') != 'visible' ||
+ computedStyle.getPropertyValue('display') == 'none')
+ continue;
+
+ // If the link has zero dimensions, it may be wrapping visible
+ // but floated elements. Check for this.
+ if (clientRects[i].width == 0 || clientRects[i].height == 0) {
+ for (var j = 0, childrenCount = element.children.length; j < childrenCount; j++) {
+ var computedStyle = window.getComputedStyle(element.children[j], null);
+ // Ignore child elements which are not floated and not absolutely positioned for parent elements with zero width/height
+ if (computedStyle.getPropertyValue('float') == 'none' && computedStyle.getPropertyValue('position') != 'absolute')
+ continue;
+ var childClientRect = this.getVisibleClientRect(element.children[j]);
+ if (childClientRect === null)
+ continue;
+ return childClientRect;
+ }
+ }
- var keyChar = getKeyChar(event);
- if (!keyChar)
- return;
+ return clientRects[i];
+ };
+ return null;
+ },
+
+ /*
+ * 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.openLinkModeToggle) {
+ // Toggle whether to open link in a new or current tab.
+ this.setOpenLinkMode(!this.shouldOpenInNewTab, this.shouldOpenWithQueue, false);
+ 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();
+ 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].clickableItem, 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);
+ }
}
- } 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();
-}
+ onKeyPressInMode: function(event) {
+ return !this.delayMode;
+ },
-/*
- * 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)) {
- 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();
+ onKeyUpInMode: function(event) {
+ if (this.delayMode)
+ return;
+
+ 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, false);
+ this.openLinkModeToggle = false;
+ }
+ event.stopPropagation();
+ event.preventDefault();
+ },
+
+ /*
+ * When only one link hint remains, this function activates it in the appropriate way.
+ */
+ activateLink: function(matchedLink, delay) {
+ var that = this;
+ this.delayMode = true;
+ if (this.isSelectable(matchedLink)) {
+ this.simulateSelect(matchedLink);
+ this.deactivateMode(delay, function() { that.delayMode = false; });
} 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.deactivateMode(delay, function() {
+ that.delayMode = false;
+ that.activateModeWithQueue();
+ });
+ } else if (this.shouldCopyLinkUrl) {
+ this.copyLinkUrl(matchedLink);
+ this.deactivateMode(delay, function() { that.delayMode = false; });
+ } else if (this.shouldOpenInNewTab) {
+ this.simulateClick(matchedLink);
matchedLink.focus();
- deactivateLinkHintsMode();
+ this.deactivateMode(delay, function() { that.delayMode = false; });
} else {
- setTimeout(function() { simulateClick(matchedLink); }, 400);
+ // 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.
+ setTimeout(this.simulateClick.bind(this, matchedLink), 400);
matchedLink.focus();
- deactivateLinkHintsMode();
+ this.deactivateMode(delay, function() { that.delayMode = false; });
}
}
- }
-}
+ },
+
+ /*
+ * 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.nodeName.toLowerCase() == "input" && selectableTypes.indexOf(element.type) >= 0) ||
+ element.nodeName.toLowerCase() == "textarea";
+ },
+
+ copyLinkUrl: function(link) {
+ chrome.extension.sendRequest({handler: 'copyLinkUrl', data: link.href});
+ },
+
+ simulateSelect: function(element) {
+ element.focus();
+ // When focusing a textbox, put the selection caret at the end of the textbox's contents.
+ element.setSelectionRange(element.value.length, element.value.length);
+ },
+
+ /*
+ * Shows the marker, highlighting matchingCharCount characters.
+ */
+ showMarker: function(linkMarker, matchingCharCount) {
+ linkMarker.style.display = "";
+ for (var j = 0, count = linkMarker.childNodes.length; j < count; j++)
+ linkMarker.childNodes[j].className = (j >= matchingCharCount) ? "" : "matchingCharacter";
+ },
+
+ hideMarker: function(linkMarker) {
+ linkMarker.style.display = "none";
+ },
+
+ 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);
+
+ // TODO(int3): do this for @role='link' and similar elements as well
+ var nodeName = link.nodeName.toLowerCase();
+ if (nodeName == 'a' || nodeName == 'button')
+ link.blur();
+ },
+
+ /*
+ * 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() {
+ if (that.markerMatcher.deactivate)
+ that.markerMatcher.deactivate();
+ if (that.hintMarkerContainingDiv)
+ that.hintMarkerContainingDiv.parentNode.removeChild(that.hintMarkerContainingDiv);
+ that.hintMarkerContainingDiv = null;
+ that.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) {
+ //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(this.logXOfBase(
+ visibleElements.length, settings.get('linkHintCharacters').length));
+ var hintMarkers = [];
+
+ for (var i = 0, count = visibleElements.length; i < count; i++) {
+ var hintString = this.numberToHintString(i, digitsNeeded);
+ var marker = hintUtils.createMarkerFor(visibleElements[i]);
+ marker.innerHTML = hintUtils.spanWrap(hintString);
+ marker.setAttribute("hintString", hintString);
+ hintMarkers.push(marker);
+ }
-/*
- * 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";
-}
+ return hintMarkers;
+ },
+ /*
+ * 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.
+ // 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(settings.get('linkHintCharacters')[0]);
+
+ // Reversing the hint string has the advantage of making the link hints
+ // appear to spread out after the first key is hit. This is helpful on a
+ // page that has http links that are close to each other where link hints
+ // of 2 characters or more occlude each other.
+ hintString.reverse();
+ return hintString.join("");
+ },
+
+ matchHintsByKey: function(event, hintMarkers) {
+ var linksMatched = hintMarkers;
+ var keyChar = getKeyChar(event);
+ if (!keyChar)
+ return { 'linksMatched': linksMatched };
+
+ if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) {
+ if (this.hintKeystrokeQueue.length == 0) {
+ var linksMatched = [];
+ } else {
+ this.hintKeystrokeQueue.pop();
+ var matchString = this.hintKeystrokeQueue.join("");
+ var linksMatched = linksMatched.filter(function(linkMarker) {
+ return linkMarker.getAttribute("hintString").indexOf(matchString) == 0;
+ });
+ }
+ } else if (settings.get('linkHintCharacters').indexOf(keyChar) >= 0) {
+ this.hintKeystrokeQueue.push(keyChar);
+ var matchString = this.hintKeystrokeQueue.join("");
+ var linksMatched = linksMatched.filter(function(linkMarker) {
+ return linkMarker.getAttribute("hintString").indexOf(matchString) == 0;
+ });
+ }
+ return { 'linksMatched': linksMatched };
+ },
-/*
- * 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) {
- if (linkMarker.style.display == "none")
- linkMarker.style.display = "";
- var childNodes = linkMarker.childNodes;
- for (var j = 0, childNodesCount = childNodes.length; j < childNodesCount; j++)
- childNodes[j].className = (j >= searchString.length) ? "" : "matchingCharacter";
- linksMatched.push(linkMarker.clickableItem);
+ 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;
+ }
+ }
+ },
+
+ 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 {
- linkMarker.style.display = "none";
+ linkText = element.textContent || element.innerHTML;
}
- }
- return linksMatched;
-}
+ linkText = linkText.trim().toLowerCase();
+ marker.setAttribute("hintString", hintString);
+ marker.innerHTML = hintUtils.spanWrap(hintString + (showLinkText ? ": " + linkText : ""));
+ marker.setAttribute("linkText", linkText);
+ },
+
+ getHintMarkers: function(visibleElements) {
+ this.generateLabelMap();
+ var hintMarkers = [];
+ for (var i = 0, count = visibleElements.length; i < count; i++) {
+ var marker = hintUtils.createMarkerFor(visibleElements[i]);
+ this.setMarkerAttributes(marker, i);
+ hintMarkers.push(marker);
+ }
+ return hintMarkers;
+ },
+
+ matchHintsByKey: function(event, hintMarkers) {
+ var linksMatched = hintMarkers;
+ var delay = 0;
+ var keyChar = getKeyChar(event);
+
+ if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) {
+ // backspace clears hint key queue first, then acts on link text key queue
+ if (this.hintKeystrokeQueue.pop())
+ linksMatched = this.filterLinkHints(linksMatched);
+ else if (this.linkTextKeystrokeQueue.pop())
+ linksMatched = this.filterLinkHints(linksMatched);
+ else // both queues are empty. exit hinting mode
+ linksMatched = [];
+ } else if (event.keyCode == keyCodes.enter) {
+ // activate the lowest-numbered link hint that is visible
+ for (var i = 0, count = linksMatched.length; i < count; i++)
+ if (linksMatched[i].style.display != 'none') {
+ linksMatched = [ linksMatched[i] ];
+ break;
+ }
+ } else if (keyChar) {
+ var matchString;
+ if (/[0-9]/.test(keyChar)) {
+ this.hintKeystrokeQueue.push(keyChar);
+ matchString = this.hintKeystrokeQueue.join("");
+ linksMatched = linksMatched.filter(function(linkMarker) {
+ return linkMarker.getAttribute('filtered') != 'true'
+ && linkMarker.getAttribute("hintString").indexOf(matchString) == 0;
+ });
+ } else {
+ // since we might renumber the hints, the current hintKeyStrokeQueue
+ // should be rendered invalid (i.e. reset).
+ this.hintKeystrokeQueue = [];
+ this.linkTextKeystrokeQueue.push(keyChar);
+ linksMatched = this.filterLinkHints(linksMatched);
+ }
-/*
- * 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.
- */
-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.
- for (var i = 0, count = numHintDigits - hintString.length; i < count; i++)
- hintString.unshift(settings.linkHintCharacters[0]);
- return hintString.join("");
-}
-
-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);
-}
-
-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();
-}
-
-function resetLinkHintsMode() {
- deactivateLinkHintsMode();
- activateLinkHintsModeWithQueue();
-}
+ if (linksMatched.length == 1 && !/[0-9]/.test(keyChar)) {
+ // 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 };
+ },
+
+ /*
+ * 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(hintMarkers) {
+ var linksMatched = [];
+ var linkSearchString = this.linkTextKeystrokeQueue.join("");
+
+ for (var i = 0; i < hintMarkers.length; i++) {
+ var linkMarker = hintMarkers[i];
+ var matchedLink = linkMarker.getAttribute("linkText").toLowerCase()
+ .indexOf(linkSearchString.toLowerCase()) >= 0;
+
+ if (!matchedLink) {
+ linkMarker.setAttribute("filtered", "true");
+ } else {
+ this.setMarkerAttributes(linkMarker, linksMatched.length);
+ 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;
-}
+ 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>" + hintString[i].toUpperCase() + "</span>");
+ return innerHTML.join("");
+ },
+
+ /*
+ * Creates a link marker for the given link.
+ */
+ createMarkerFor: function(link) {
+ var marker = document.createElement("div");
+ marker.className = "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";
+
+ return marker;
+ }
+};
diff --git a/manifest.json b/manifest.json
index 38c21b2d..a79fd90c 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,6 +1,6 @@
{
"name": "Vimium",
- "version": "1.26",
+ "version": "1.30",
"description": "The Hacker's Browser. Vimium provides keyboard shortcuts for navigation and control in the spirit of Vim.",
"icons": { "16": "icons/icon16.png",
"48": "icons/icon48.png",
@@ -9,16 +9,20 @@
"options_page": "options.html",
"permissions": [
"tabs",
+ "bookmarks",
"http://*/*",
"https://*/*"
],
"content_scripts": [
{
"matches": ["<all_urls>"],
- "js": ["lib/keyboardUtils.js",
+ "js": ["lib/utils.js",
+ "lib/keyboardUtils.js",
"lib/clipboard.js",
"linkHints.js",
- "vimiumFrontend.js"
+ "vimiumFrontend.js",
+ "completionDialog.js",
+ "bookmarks.js"
],
"run_at": "document_start",
"all_frames": true
diff --git a/options.html b/options.html
index 627560f8..8dfd23e8 100644
--- a/options.html
+++ b/options.html
@@ -1,6 +1,7 @@
<html>
<head>
<title>Vimium Options</title>
+ <script src="lib/utils.js"></script>
<script src="lib/keyboardUtils.js"></script>
<script src="linkHints.js"></script>
<script src="lib/clipboard.js"></script>
@@ -17,12 +18,13 @@
border:1px solid red;
}
.example {
- font-size:80%;
+ font-size: 12px;
color:#555;
margin-left:20px;
}
.caption {
margin-right:10px;
+ min-width: 130px;
}
td {
padding:5px 0;
@@ -70,15 +72,20 @@
tr.advancedOption {
display:none;
}
-
+ input:read-only {
+ background-color: #eee;
+ color: #666;
+ }
+ /* Boolean options have a tighter form representation than text options. */
+ td.booleanOption { font-size: 12px; }
</style>
<script type="text/javascript">
$ = function(id) { return document.getElementById(id); };
var defaultSettings = chrome.extension.getBackgroundPage().defaultSettings;
- var editableFields = ["scrollStepSize", "defaultZoomLevel", "excludedUrls", "linkHintCharacters",
- "userDefinedLinkHintCss", "keyMappings", "previousPatterns", "nextPatterns"];
+ var editableFields = ["scrollStepSize", "excludedUrls", "linkHintCharacters", "userDefinedLinkHintCss",
+ "keyMappings", "filterLinkHints", "previousPatterns", "nextPatterns"];
var canBeEmptyFields = ["excludedUrls", "keyMappings", "userDefinedLinkHintCss"];
@@ -93,8 +100,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 () {
showHelpDialog(
@@ -107,6 +119,10 @@
enableSaveButton();
}
+ function onDataLoaded() {
+ $("linkHintCharacters").readOnly = $("filterLinkHints").checked;
+ }
+
function enableSaveButton() { $("saveOptions").removeAttribute("disabled"); }
// Saves options to localStorage.
@@ -115,7 +131,16 @@
// 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() : "";
@@ -142,20 +167,33 @@
for (var i = 0; i < editableFields.length; i++) {
// If it's null or undefined, let's go to the default. We want to allow empty strings in certain cases.
if (localStorage[editableFields[i]] != "" && !localStorage[editableFields[i]]) {
- $(editableFields[i]).value = defaultSettings[editableFields[i]] || "";
+ var val = defaultSettings[editableFields[i]] || "";
} else {
- $(editableFields[i]).value = localStorage[editableFields[i]];
+ var val = localStorage[editableFields[i]];
}
- $(editableFields[i]).setAttribute("savedValue", $(editableFields[i]).value);
+ 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++)
@@ -176,12 +214,6 @@
</td>
</tr>
<tr>
- <td><span class="caption">Default zoom level</span></td>
- <td>
- <input id="defaultZoomLevel" type="text" value="100" style="width:50px" />%
- </td>
- </tr>
- <tr>
<td colspan="3">
Excluded URLs<br/>
<div class="help">
@@ -200,7 +232,7 @@
</td>
</tr>
<tr class="advancedOption">
- <td class="caption">Key mappings</td>
+ <td class="caption">Custom key<br/>mappings</td>
<td id="mappingsHelp" verticalAlign="top">
<div class="help">
<div class="example">
@@ -244,11 +276,26 @@
</td>
</tr>
<tr class="advancedOption">
+ <td class="caption"></td>
+ <td verticalAlign="top" class="booleanOption">
+ <div class="help">
+ <div class="example">
+ After typing "F" to enter link hinting mode, this option lets you type the text of a link
+ to select it.
+ </div>
+ </div>
+ <label>
+ <input id="filterLinkHints" type="checkbox"/>
+ Use the link's name and numbers for link hint filtering
+ </label>
+ </td>
+ </tr>
+ <tr class="advancedOption">
<td class="caption">Previous Patterns</td>
<td verticalAlign="top">
<div class="help">
<div class="example">
- The Patterns split by ','.
+ Vimium will match against these patterns to navigate to a 'previous' page.
</div>
</div>
<input id="previousPatterns" type="text" style="width:320px" />
@@ -259,7 +306,7 @@
<td verticalAlign="top">
<div class="help">
<div class="example">
- The Patterns split by ','.
+ Vimium will match against these patterns to navigate to a 'next' page.
</div>
</div>
<input id="nextPatterns" type="text" style="width:320px" />
diff --git a/test_harnesses/automated.html b/test_harnesses/automated.html
new file mode 100644
index 00000000..9f1b8007
--- /dev/null
+++ b/test_harnesses/automated.html
@@ -0,0 +1,252 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+ "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+ <head>
+ <style type="text/css">
+ body {
+ font-family:"helvetica neue", "helvetica", "arial", "sans";
+ width: 800px;
+ margin: 0px auto;
+ }
+ #output-div {
+ white-space: pre-wrap;
+ background-color: #eee;
+ font-family: monospace;
+ margin: 0 0 50px 0;
+ border-style: dashed;
+ border-width: 1px 1px 0 1px;
+ border-color: #999;
+ }
+ .errorPosition {
+ color: #f33;
+ font-weight: bold;
+ }
+ .output-section {
+ padding: 10px 15px 10px 15px;
+ border-bottom: dashed 1px #999;
+ }
+ </style>
+ <script type="text/javascript" src="../lib/utils.js"></script>
+ <script type="text/javascript" src="../lib/keyboardUtils.js"></script>
+ <script type="text/javascript" src="../linkHints.js"></script>
+ <script type="text/javascript" src="../lib/clipboard.js"></script>
+ <script type="text/javascript" src="../vimiumFrontend.js"></script>
+ <script type="text/javascript" src="shoulda.js/shoulda.js"></script>
+ <script type="text/javascript">
+ /*
+ * Dispatching keyboard events via the DOM would require async tests,
+ * which tend to be more complicated. Here we create mock events and
+ * invoke the handlers directly.
+ */
+ function mockKeyboardEvent(keyChar) {
+ var event = {};
+ event.charCode = keyCodes[keyChar] !== undefined ? keyCodes[keyChar] : keyChar.charCodeAt(0);
+ event.keyIdentifier = "U+00" + event.charCode.toString(16);
+ event.keyCode = event.charCode;
+ event.stopPropagation = function(){};
+ event.preventDefault = function(){};
+ return event;
+ }
+
+ /*
+ * Generate tests that are common to both default and filtered
+ * link hinting modes.
+ */
+ function createGeneralHintTests(isFilteredMode) {
+ context("Link hints",
+
+ setup(function() {
+ var testContent =
+ "<a>test</a>" +
+ "<a>tress</a>";
+ document.getElementById("test-div").innerHTML = testContent;
+ linkHints.markerMatcher = alphabetHints;
+ }),
+
+ tearDown(function() {
+ document.getElementById("test-div").innerHTML = "";
+ }),
+
+ should("create hints when activated, discard them when deactivated", function() {
+ linkHints.activateMode();
+ assert.isFalse(linkHints.hintMarkerContainingDiv == null);
+ linkHints.deactivateMode();
+ assert.isTrue(linkHints.hintMarkerContainingDiv == null);
+ }),
+
+ should("position items correctly", function() {
+ function assertStartPosition(element1, element2) {
+ assert.equal(element1.getClientRects()[0].left, element2.getClientRects()[0].left);
+ assert.equal(element1.getClientRects()[0].top, element2.getClientRects()[0].top);
+ }
+ stub(document.body, "style", "static");
+ linkHints.activateMode();
+ assertStartPosition(document.getElementsByTagName("a")[0], linkHints.hintMarkers[0]);
+ assertStartPosition(document.getElementsByTagName("a")[1], linkHints.hintMarkers[1]);
+ linkHints.deactivateMode();
+
+ stub(document.body.style, "position", "relative");
+ linkHints.activateMode();
+ assertStartPosition(document.getElementsByTagName("a")[0], linkHints.hintMarkers[0]);
+ assertStartPosition(document.getElementsByTagName("a")[1], linkHints.hintMarkers[1]);
+ linkHints.deactivateMode();
+ })
+
+ );
+ }
+ createGeneralHintTests(false);
+ createGeneralHintTests(true);
+
+ context("Alphabetical link hints",
+
+ setup(function() {
+ stub(settings.values, "filterLinkHints", "false");
+ linkHints.markerMatcher = alphabetHints;
+ // we need at least 16 elements to have double-character link hints
+ for (var i = 0; i < 16; i++) {
+ var link = document.createElement("a");
+ link.textContent = "test";
+ document.getElementById("test-div").appendChild(link);
+ }
+ linkHints.activateMode();
+ }),
+
+ tearDown(function() {
+ linkHints.deactivateMode();
+ document.getElementById("test-div").innerHTML = "";
+ }),
+
+ should("label the hints correctly", function() {
+ var hintStrings = ["ss", "sa", "sd"];
+ for (var i = 0; i < 3; i++)
+ assert.equal(hintStrings[i], linkHints.hintMarkers[i].getAttribute("hintString"));
+ }),
+
+ should("narrow the hints", function() {
+ linkHints.onKeyDownInMode(mockKeyboardEvent("A"));
+ assert.equal("none", linkHints.hintMarkers[0].style.display);
+ assert.equal("", linkHints.hintMarkers[15].style.display);
+ })
+
+ );
+
+ context("Filtered link hints",
+
+ setup(function() {
+ stub(settings.values, "filterLinkHints", "true");
+ linkHints.markerMatcher = filterHints;
+ }),
+
+ context("Text hints",
+
+ setup(function() {
+ var testContent =
+ "<a>test</a>" +
+ "<a>tress</a>" +
+ "<a>trait</a>" +
+ "<a>track<img alt='alt text'/></a>";
+ document.getElementById("test-div").innerHTML = testContent;
+ linkHints.activateMode();
+ }),
+
+ tearDown(function() {
+ document.getElementById("test-div").innerHTML = "";
+ linkHints.deactivateMode();
+ }),
+
+ should("label the hints", function() {
+ for (var i = 0; i < 4; i++)
+ assert.equal((i + 1).toString(), linkHints.hintMarkers[i].textContent.toLowerCase());
+ }),
+
+ should("narrow the hints", function() {
+ linkHints.onKeyDownInMode(mockKeyboardEvent("T"));
+ linkHints.onKeyDownInMode(mockKeyboardEvent("R"));
+ assert.equal("none", linkHints.hintMarkers[0].style.display);
+ assert.equal("1", linkHints.hintMarkers[1].getAttribute("hintString"));
+ assert.equal("", linkHints.hintMarkers[1].style.display);
+ linkHints.onKeyDownInMode(mockKeyboardEvent("A"));
+ assert.equal("2", linkHints.hintMarkers[3].getAttribute("hintString"));
+ })
+
+ ),
+
+ context("Image hints",
+
+ setup(function() {
+ var testContent =
+ "<a><img alt='alt text'/></a>" +
+ "<a><img alt='alt text' title='some title'/></a>" +
+ "<a><img title='some title'/></a>" +
+ "<a><img src='blah' width='320px' height='100px'/></a>";
+ document.getElementById("test-div").innerHTML = testContent;
+ linkHints.activateMode();
+ }),
+
+ tearDown(function() {
+ document.getElementById("test-div").innerHTML = "";
+ linkHints.deactivateMode();
+ }),
+
+ should("label the images", function() {
+ assert.equal("1: alt text", linkHints.hintMarkers[0].textContent.toLowerCase());
+ assert.equal("2: alt text", linkHints.hintMarkers[1].textContent.toLowerCase());
+ assert.equal("3: some title", linkHints.hintMarkers[2].textContent.toLowerCase());
+ assert.equal("4", linkHints.hintMarkers[3].textContent.toLowerCase());
+ })
+
+ ),
+
+ context("Input hints",
+
+ setup(function() {
+ var testContent =
+ "<input type='text' value='some value'/>" +
+ "<input type='password' value='some value'/>" +
+ "<textarea>some text</textarea>" +
+ "<label for='test-input'/>a label</label><input type='text' id='test-input' value='some value'/>" +
+ "<label for='test-input-2'/>a label: </label><input type='text' id='test-input-2' value='some value'/>";
+ document.getElementById("test-div").innerHTML = testContent;
+ linkHints.activateMode();
+ }),
+
+ tearDown(function() {
+ document.getElementById("test-div").innerHTML = "";
+ linkHints.deactivateMode();
+ }),
+
+ should("label the input elements", function() {
+ assert.equal("1", linkHints.hintMarkers[0].textContent.toLowerCase());
+ assert.equal("2", linkHints.hintMarkers[1].textContent.toLowerCase());
+ assert.equal("3", linkHints.hintMarkers[2].textContent.toLowerCase());
+ assert.equal("4: a label", linkHints.hintMarkers[3].textContent.toLowerCase());
+ assert.equal("5: a label", linkHints.hintMarkers[4].textContent.toLowerCase());
+ })
+
+ )
+ );
+
+ Tests.outputMethod = function(output) {
+ var newOutput = Array.prototype.join.call(arguments, "\n");
+ newOutput = newOutput.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;"); // escape html
+ // highlight the source of the error
+ newOutput = newOutput.replace(/\/([^:/]+):([0-9]+):([0-9]+)/, "/<span class='errorPosition'>$1:$2</span>:$3");
+ document.getElementById("output-div").innerHTML += "<div class='output-section'>" + newOutput + "</div>";
+ console.log.apply(console, arguments);
+ }
+ // ensure the extension has time to load before commencing the tests
+ document.addEventListener("DOMContentLoaded", function(){
+ setTimeout(Tests.run, 200);
+ });
+ </script>
+ </head>
+ <body>
+ <!-- should always be the first element on the page -->
+ <div id="test-div"></div>
+
+ <h1>Vimium Tests</h1>
+
+ <div id="output-div"></div>
+
+ </body>
+</html>
diff --git a/test_harnesses/iframe.html b/test_harnesses/iframe.html
index 1de9b75f..750ad32d 100644
--- a/test_harnesses/iframe.html
+++ b/test_harnesses/iframe.html
@@ -21,6 +21,6 @@
</head>
<body>
<h2>IFrame test page</h2>
- <iframe src="http://www.google.com"></iframe>
+ <iframe src="http://www.bing.com"></iframe>
</body>
</html> \ No newline at end of file
diff --git a/test_harnesses/shoulda.js b/test_harnesses/shoulda.js
new file mode 160000
+Subproject 695d0eb2084de5380dccac8c9b188ce91d838dc
diff --git a/vimiumFrontend.js b/vimiumFrontend.js
index 0e8bdbb5..a9dc1d37 100644
--- a/vimiumFrontend.js
+++ b/vimiumFrontend.js
@@ -1,41 +1,69 @@
/*
* This content script takes input from its webpage and executes commands locally on behalf of the background
- * page. It must be run prior to domReady so that we perform some operations very early, like setting
- * 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".
+ * page. It must be run prior to domReady so that we perform some operations very early. 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", "previousPatterns", "nextPatterns"];
-
var getCurrentUrlHandlers = []; // function(url)
-var insertMode = false;
+var insertModeLock = null;
var findMode = false;
var findModeQuery = "";
var findModeQueryHasResults = false;
var isShowingHelpDialog = false;
+var handlerStack = [];
var keyPort;
var settingPort;
-var saveZoomLevelPort;
// Users can disable Vimium on URL patterns via the settings page.
var isEnabledForUrl = true;
// The user's operating system.
var currentCompletionKeys;
+var validFirstKeys;
var linkHintCss;
-// TODO(philc): This should be pulled from the extension's storage when the page loads.
-var currentZoomLevel = 100;
-
// The types in <input type="..."> that we consider for focusInput command. Right now this is recalculated in
// each content script. Alternatively we could calculate it once in the background page and use a request to
// fetch it each time.
//
// Should we include the HTML5 date pickers here?
-var textInputTypes = ["text", "search", "email", "url", "number"];
+
// The corresponding XPath for such elements.
-var textInputXPath = '//input[' +
- textInputTypes.map(function (type) { return '@type="' + type + '"'; }).join(" or ") +
- ' or not(@type)]';
+var textInputXPath = (function() {
+ var textInputTypes = ["text", "search", "email", "url", "number"];
+ var inputElements = ["input[" +
+ textInputTypes.map(function (type) { return '@type="' + type + '"'; }).join(" or ") + "or not(@type)]",
+ "textarea"];
+ return utils.makeXPath(inputElements);
+})();
+
+var settings = {
+ values: {},
+ loadedValues: 0,
+ valuesToLoad: ["scrollStepSize", "linkHintCharacters", "filterLinkHints", "previousPatterns", "nextPatterns"],
+
+ 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 () {
+ linkHints.init();
+ }
+};
/*
* Give this frame a unique id.
@@ -43,27 +71,16 @@ var textInputXPath = '//input[' +
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; }
+var googleRegex = /:\/\/[^/]*google[^/]+/;
/*
- * Complete initialization work that sould be done prior to DOMReady, like setting the page's zoom level.
+ * Complete initialization work that sould be done prior to DOMReady.
*/
function initializePreDomReady() {
- for (var i in settingsToLoad) { getSetting(settingsToLoad[i]); }
+ settings.load();
checkIfEnabledForUrl();
- var getZoomLevelPort = chrome.extension.connect({ name: "getZoomLevel" });
- if (window.self == window.parent)
- getZoomLevelPort.postMessage({ domain: window.location.host });
-
chrome.extension.sendRequest({handler: "getLinkHintCss"}, function (response) {
linkHintCss = response.linkHintCss;
});
@@ -74,35 +91,36 @@ function initializePreDomReady() {
keyPort = chrome.extension.connect({ name: "keyDown" });
chrome.extension.onRequest.addListener(function(request, sender, sendResponse) {
- if (request.name == "hideUpgradeNotification")
+ if (request.name == "hideUpgradeNotification") {
HUD.hideUpgradeNotification();
- else if (request.name == "showUpgradeNotification" && isEnabledForUrl)
+ } else if (request.name == "showUpgradeNotification" && isEnabledForUrl) {
HUD.showUpgradeNotification(request.version);
- else if (request.name == "showHelpDialog")
+ } else if (request.name == "showHelpDialog") {
if (isShowingHelpDialog)
hideHelpDialog();
else
showHelpDialog(request.dialogHtml, request.frameId);
- else if (request.name == "focusFrame")
- if(frameId == request.frameId)
+ } else if (request.name == "focusFrame") {
+ if (frameId == request.frameId)
focusThisFrame(request.highlight);
- else if (request.name == "refreshCompletionKeys")
- refreshCompletionKeys(request.completionKeys);
+ } else if (request.name == "refreshCompletionKeys") {
+ refreshCompletionKeys(request);
+ }
sendResponse({}); // Free up the resources used by this open connection.
});
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);
+ utils.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++) { utils.invokeCommandString(args.command); }
}
}
- refreshCompletionKeys(args.completionKeys);
+ refreshCompletionKeys(args);
});
}
else if (port.name == "getScrollPosition") {
@@ -122,14 +140,8 @@ function initializePreDomReady() {
port.onMessage.addListener(function(args) {
if (getCurrentUrlHandlers.length > 0) { getCurrentUrlHandlers.pop()(args.url); }
});
- } else if (port.name == "returnZoomLevel") {
- port.onMessage.addListener(function(args) {
- currentZoomLevel = args.zoomLevel;
- if (isEnabledForUrl)
- 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);
@@ -144,6 +156,7 @@ function initializePreDomReady() {
function initializeWhenEnabled() {
document.addEventListener("keydown", onKeydown, true);
document.addEventListener("keypress", onKeypress, true);
+ document.addEventListener("keyup", onKeyup, true);
document.addEventListener("focus", onFocusCapturePhase, true);
document.addEventListener("blur", onBlurCapturePhase, true);
enterInsertModeIfElementIsFocused();
@@ -183,100 +196,59 @@ function initializeOnDomReady() {
};
// This is a little hacky but sometimes the size wasn't available on domReady?
-function registerFrameIfSizeAvailable (top) {
+function registerFrameIfSizeAvailable (is_top) {
if (innerWidth != undefined && innerWidth != 0 && innerHeight != undefined && innerHeight != 0)
chrome.extension.sendRequest({ handler: "registerFrame", frameId: frameId,
- area: innerWidth * innerHeight, top: top, total: frames.length + 1 });
+ area: innerWidth * innerHeight, is_top: is_top, total: frames.length + 1 });
else
- setTimeout(function () { registerFrameIfSizeAvailable(top); }, 100);
+ setTimeout(function () { registerFrameIfSizeAvailable(is_top); }, 100);
}
/*
- * Checks the currently focused element of the document and will enter insert mode if that element is focusable.
+ * Enters insert mode if the currently focused element in the DOM is focusable.
*/
function enterInsertModeIfElementIsFocused() {
- // Enter insert mode automatically if there's already a text box focused.
if (document.activeElement && isEditable(document.activeElement))
- enterInsertMode();
-}
-
-/*
- * Asks the background page to persist the zoom level for the given domain to localStorage.
- */
-function saveZoomLevel(domain, zoomLevel) {
- if (!saveZoomLevelPort)
- saveZoomLevelPort = chrome.extension.connect({ name: "saveZoomLevel" });
- saveZoomLevelPort.postMessage({ domain: domain, zoomLevel: zoomLevel });
-}
-
-/*
- * Zoom in increments of 20%; this matches chrome's CMD+ and CMD- keystrokes.
- * Set the zoom style on documentElement because document.body does not exist pre-page load.
- */
-function setPageZoomLevel(zoomLevel, showUINotification) {
- document.documentElement.style.zoom = zoomLevel + "%";
- if (document.body)
- HUD.updatePageZoomLevel(zoomLevel);
- if (showUINotification)
- HUD.showForDuration("Zoom: " + currentZoomLevel + "%", 1000);
-}
-
-function zoomIn() {
- currentZoomLevel += 20;
- setAndSaveZoom();
-}
-
-function zoomOut() {
- currentZoomLevel -= 20;
- setAndSaveZoom();
-}
-
-function zoomReset() {
- currentZoomLevel = 100;
- setAndSaveZoom();
-}
-
-function setAndSaveZoom() {
- setPageZoomLevel(currentZoomLevel, true);
- saveZoomLevel(window.location.host, currentZoomLevel);
+ enterInsertModeWithoutShowingIndicator(document.activeElement);
}
function scrollToBottom() { window.scrollTo(window.pageXOffset, document.body.scrollHeight); }
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,
- document.documentElement, null,
- XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
+ var results = utils.evaluateXPath(textInputXPath, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
var lastInputBox;
var i = 0;
while (i < count) {
- i += 1;
-
var currentInputBox = results.iterateNext();
if (!currentInputBox) { break; }
+ if (linkHints.getVisibleClientRect(currentInputBox) === null)
+ continue;
+
lastInputBox = currentInputBox;
+
+ i += 1;
}
if (lastInputBox) { lastInputBox.focus(); }
}
function reload() { window.location.reload(); }
-function goBack() { history.back(); }
-function goForward() { history.forward(); }
+function goBack(count) { history.go(-count); }
+function goForward(count) { history.go(count); }
function goUp(count) {
var url = window.location.href;
@@ -308,6 +280,8 @@ function copyCurrentUrl() {
// TODO(ilya): Convert to sendRequest.
var getCurrentUrlPort = chrome.extension.connect({ name: "getCurrentTabUrl" });
getCurrentUrlPort.postMessage({});
+
+ HUD.showForDuration("Yanked URL", 1000);
}
function toggleViewSourceCallback(url) {
@@ -327,18 +301,18 @@ function toggleViewSourceCallback(url) {
* Note that some keys will only register keydown events and not keystroke events, e.g. ESC.
*/
function onKeypress(event) {
- var keyChar = "";
-
- if (linkHintsModeActivated)
+ if (!bubbleEvent('keypress', event))
return;
+ var keyChar = "";
+
// Ignore modifier keys by themselves.
if (event.keyCode > 31) {
keyChar = String.fromCharCode(event.charCode);
// Enter insert mode when the user enables the native find interface.
if (keyChar == "f" && isPrimaryModifierKey(event)) {
- enterInsertMode();
+ enterInsertModeWithoutShowingIndicator();
return;
}
@@ -349,7 +323,7 @@ function onKeypress(event) {
// Don't let the space scroll us if we're searching.
if (event.keyCode == keyCodes.space)
event.preventDefault();
- } else if (!insertMode && !findMode) {
+ } else if (!isInsertMode() && !findMode) {
if (currentCompletionKeys.indexOf(keyChar) != -1) {
event.preventDefault();
event.stopPropagation();
@@ -361,18 +335,32 @@ function onKeypress(event) {
}
}
-function onKeydown(event) {
- var keyChar = "";
+/**
+ * Called whenever we receive a key event. Each individual handler has the option to stop the event's
+ * propagation by returning a falsy value.
+ */
+function bubbleEvent(type, event) {
+ for (var i = handlerStack.length-1; i >= 0; i--) {
+ // We need to check for existence of handler because the last function call may have caused the release of
+ // more than one handler.
+ if (handlerStack[i] && handlerStack[i][type] && !handlerStack[i][type](event))
+ return false;
+ }
+ return true;
+}
- if (linkHintsModeActivated)
+function onKeydown(event) {
+ if (!bubbleEvent('keydown', event))
return;
+ var keyChar = "";
+
// handle modifiers being pressed.don't handle shiftKey alone (to avoid / being interpreted as ?
- if (event.metaKey && event.keyCode > 31 || event.ctrlKey && event.keyCode > 31 || event.altKey && event.keyCode > 31) {
+ if (event.metaKey && event.keyCode > 31 || event.ctrlKey && event.keyCode > 31 ||
+ event.altKey && event.keyCode > 31) {
keyChar = getKeyChar(event);
- if (keyChar != "") // Again, ignore just modifiers. Maybe this should replace the keyCode > 31 condition.
- {
+ if (keyChar != "") { // Again, ignore just modifiers. Maybe this should replace the keyCode>31 condition.
var modifiers = [];
if (event.shiftKey)
@@ -392,44 +380,45 @@ function onKeydown(event) {
}
}
- if (insertMode && isEscape(event))
- {
+ if (isInsertMode() && isEscape(event)) {
// Note that we can't programmatically blur out of Flash embeds from Javascript.
if (!isEmbed(event.srcElement)) {
- // Remove focus so the user can't just get himself back into insert mode by typing in the same input box.
- if (isEditable(event.srcElement)) { event.srcElement.blur(); }
+ // Remove focus so the user can't just get himself back into insert mode by typing in the same input
+ // box.
+ if (isEditable(event.srcElement))
+ event.srcElement.blur();
exitInsertMode();
- // Added to prevent Google Instant from reclaiming the keystroke and putting us back into the search box.
- // TOOD(ilya): Revisit this. Not sure it's the absolute best approach.
- event.stopPropagation();
+ // Added to prevent Google Instant from reclaiming the keystroke and putting us back into the search
+ // box.
+ if (isGoogleSearch())
+ event.stopPropagation();
}
}
- else if (findMode)
- {
- if (isEscape(event))
+ else if (findMode) {
+ if (isEscape(event)) {
exitFindMode();
// Don't let backspace take us back in history.
- else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey)
- {
+ }
+ else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) {
handleDeleteForFindMode();
event.preventDefault();
}
- else if (event.keyCode == keyCodes.enter)
+ else if (event.keyCode == keyCodes.enter) {
handleEnterForFindMode();
+ }
}
- else if (isShowingHelpDialog && isEscape(event))
- {
+ else if (isShowingHelpDialog && isEscape(event)) {
hideHelpDialog();
}
- else if (!insertMode && !findMode) {
+ else if (!isInsertMode() && !findMode) {
if (keyChar) {
- if (currentCompletionKeys.indexOf(keyChar) != -1) {
- event.preventDefault();
- event.stopPropagation();
- }
+ if (currentCompletionKeys.indexOf(keyChar) != -1) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
- keyPort.postMessage({keyChar:keyChar, frameId:frameId});
+ keyPort.postMessage({keyChar:keyChar, frameId:frameId});
}
else if (isEscape(event)) {
keyPort.postMessage({keyChar:"<ESC>", frameId:frameId});
@@ -443,40 +432,55 @@ function onKeydown(event) {
// Subject to internationalization issues since we're using keyIdentifier instead of charCode (in keypress).
//
// TOOD(ilya): Revisit this. Not sure it's the absolute best approach.
- if (keyChar == "" && !insertMode && currentCompletionKeys.indexOf(getKeyChar(event)) != -1)
+ if (keyChar == "" && !isInsertMode() && (currentCompletionKeys.indexOf(getKeyChar(event)) != -1 ||
+ validFirstKeys[getKeyChar(event)]))
event.stopPropagation();
}
+function onKeyup() {
+ if (!bubbleEvent('keyup', event))
+ return;
+}
+
function checkIfEnabledForUrl() {
- var url = window.location.toString();
-
- chrome.extension.sendRequest({ handler: "isEnabledForUrl", url: url }, function (response) {
- isEnabledForUrl = response.isEnabledForUrl;
- if (isEnabledForUrl)
- initializeWhenEnabled();
- else if (HUD.isReady())
- // Quickly hide any HUD we might already be showing, e.g. if we entered insertMode on page load.
- HUD.hide();
- });
+ var url = window.location.toString();
+
+ chrome.extension.sendRequest({ handler: "isEnabledForUrl", url: url }, function (response) {
+ isEnabledForUrl = response.isEnabledForUrl;
+ if (isEnabledForUrl)
+ initializeWhenEnabled();
+ else if (HUD.isReady())
+ // Quickly hide any HUD we might already be showing, e.g. if we entered insert mode on page load.
+ HUD.hide();
+ });
}
-function refreshCompletionKeys(completionKeys) {
- if (completionKeys)
- currentCompletionKeys = completionKeys;
- else
- chrome.extension.sendRequest({handler: "getCompletionKeys"}, function (response) {
- currentCompletionKeys = response.completionKeys;
- });
+// TODO(ilya): This just checks if "google" is in the domain name. Probably should be more targeted.
+function isGoogleSearch() {
+ var url = window.location.toString();
+ return !!url.match(googleRegex);
+}
+
+function refreshCompletionKeys(response) {
+ if (response) {
+ currentCompletionKeys = response.completionKeys;
+
+ if (response.validFirstKeys)
+ validFirstKeys = response.validFirstKeys;
+ }
+ else {
+ chrome.extension.sendRequest({ handler: "getCompletionKeys" }, refreshCompletionKeys);
+ }
}
function onFocusCapturePhase(event) {
if (isFocusable(event.target))
- enterInsertMode();
+ enterInsertModeWithoutShowingIndicator(event.target);
}
function onBlurCapturePhase(event) {
if (isFocusable(event.target))
- exitInsertMode();
+ exitInsertMode(event.target);
}
/*
@@ -488,32 +492,52 @@ function isFocusable(element) { return isEditable(element) || isEmbed(element);
* Embedded elements like Flash and quicktime players can obtain focus but cannot be programmatically
* unfocused.
*/
-function isEmbed(element) { return ["EMBED", "OBJECT"].indexOf(element.tagName) > 0; }
+function isEmbed(element) { return ["embed", "object"].indexOf(element.nodeName.toLowerCase()) > 0; }
/*
* Input or text elements are considered focusable and able to receieve their own keyboard events,
* and will enter enter mode if focused. Also note that the "contentEditable" attribute can be set on
* any element which makes it a rich text editor, like the notes on jjot.com.
- * Note: we used to discriminate for text-only inputs, but this is not accurate since all input fields
- * can be controlled via the keyboard, particuarlly SELECT combo boxes.
*/
function isEditable(target) {
- if (target.getAttribute("contentEditable") == "true")
+ if (target.isContentEditable)
+ return true;
+ var nodeName = target.nodeName.toLowerCase();
+ // use a blacklist instead of a whitelist because new form controls are still being implemented for html5
+ var noFocus = ["radio", "checkbox"];
+ if (nodeName == "input" && noFocus.indexOf(target.type) == -1)
return true;
- var focusableInputs = ["input", "textarea", "select", "button"];
- return focusableInputs.indexOf(target.tagName.toLowerCase()) >= 0;
+ var focusableElements = ["textarea", "select"];
+ return focusableElements.indexOf(nodeName) >= 0;
}
-function enterInsertMode() {
- insertMode = true;
+/*
+ * Enters insert mode and show an "Insert mode" message. Showing the UI is only useful when entering insert
+ * mode manually by pressing "i". In most cases we do not show any UI (enterInsertModeWithoutShowingIndicator)
+ */
+function enterInsertMode(target) {
+ enterInsertModeWithoutShowingIndicator(target);
HUD.show("Insert mode");
}
-function exitInsertMode() {
- insertMode = false;
- HUD.hide();
+/*
+ * We cannot count on 'focus' and 'blur' events to happen sequentially. For example, if blurring element A
+ * causes element B to come into focus, we may get "B focus" before "A blur". Thus we only leave insert mode
+ * when the last editable element that came into focus -- which insertModeLock points to -- has been blurred.
+ * If insert mode is entered manually (via pressing 'i'), then we set insertModeLock to 'undefined', and only
+ * leave insert mode when the user presses <ESC>.
+ */
+function enterInsertModeWithoutShowingIndicator(target) { insertModeLock = target; }
+
+function exitInsertMode(target) {
+ if (target === undefined || insertModeLock === target) {
+ insertModeLock = null;
+ HUD.hide();
+ }
}
+function isInsertMode() { return insertModeLock !== null; }
+
function handleKeyCharForFindMode(keyChar) {
findModeQuery = findModeQuery + keyChar;
performFindInPlace();
@@ -521,13 +545,11 @@ function handleKeyCharForFindMode(keyChar) {
}
function handleDeleteForFindMode() {
- if (findModeQuery.length == 0)
- {
+ if (findModeQuery.length == 0) {
exitFindMode();
performFindInPlace();
}
- else
- {
+ else {
findModeQuery = findModeQuery.substring(0, findModeQuery.length - 1);
performFindInPlace();
showFindModeHUDForQuery();
@@ -551,31 +573,51 @@ function performFindInPlace() {
// backwards.
window.scrollTo(cachedScrollX, cachedScrollY);
- performFind();
+ executeFind();
}
-function performFind() {
- findModeQueryHasResults = window.find(findModeQuery, false, false, true, false, true, false);
+function executeFind(backwards) {
+ findModeQueryHasResults = window.find(findModeQuery, false, backwards, true, false, true, false);
}
-function performBackwardsFind() {
- findModeQueryHasResults = window.find(findModeQuery, false, true, true, false, true, false);
+function focusFoundLink() {
+ if (findModeQueryHasResults) {
+ var link = getLinkFromSelection();
+ if (link)
+ link.focus();
+ }
+}
+
+function findAndFocus(backwards) {
+ executeFind(backwards);
+ focusFoundLink();
+}
+
+function performFind() { findAndFocus(); }
+
+function performBackwardsFind() { findAndFocus(true); }
+
+function getLinkFromSelection() {
+ var node = window.getSelection().anchorNode;
+ while (node.nodeName.toLowerCase() !== 'body') {
+ if (node.nodeName.toLowerCase() === 'a') return node;
+ node = node.parentNode;
+ }
+ return null;
}
function findAndFollowLink(linkStrings) {
for (i = 0; i < linkStrings.length; i++) {
- var findModeQueryHasResults = window.find(linkStrings[i], false, true, true, false, true, false);
- if (findModeQueryHasResults) {
- var node = window.getSelection().anchorNode;
- while (node.nodeName != 'BODY') {
- if (node.nodeName == 'A') {
- window.location = node.href;
- return true;
- }
- node = node.parentNode;
+ var hasResults = window.find(linkStrings[i], false, true, true, false, true, false);
+ if (hasResults) {
+ var link = getLinkFromSelection();
+ if (link) {
+ window.location = link.href;
+ return true;
}
}
}
+ return false;
}
function findAndFollowRel(value) {
@@ -592,13 +634,13 @@ function findAndFollowRel(value) {
}
function goPrevious() {
- var previousPatterns = settings["previousPatterns"] || "";
+ var previousPatterns = settings.get("previousPatterns") || "";
var previousStrings = previousPatterns.split(",");
findAndFollowRel('prev') || findAndFollowLink(previousStrings);
}
function goNext() {
- var nextPatterns = settings["nextPatterns"] || "";
+ var nextPatterns = settings.get("nextPatterns") || "";
var nextStrings = nextPatterns.split(",");
findAndFollowRel('next') || findAndFollowLink(nextStrings);
}
@@ -616,12 +658,11 @@ function showFindModeHUDForQuery() {
function insertSpaces(query) {
var newQuery = "";
- for (var i = 0; i < query.length; i++)
- {
+ for (var i = 0; i < query.length; i++) {
if (query[i] == " " || (i + 1 < query.length && query[i + 1] == " "))
newQuery = newQuery + query[i];
- else
- newQuery = newQuery + query[i] + "<span style=\"font-size: 0px;\"> </span>";
+ else // &#8203; is a zero-width space
+ newQuery = newQuery + query[i] + "<span>&#8203;</span>";
}
return newQuery;
@@ -635,6 +676,7 @@ function enterFindMode() {
function exitFindMode() {
findMode = false;
+ focusFoundLink();
HUD.hide();
}
@@ -648,13 +690,14 @@ function showHelpDialog(html, fid) {
document.body.appendChild(container);
container.innerHTML = html;
+ container.getElementsByClassName("closeButton")[0].addEventListener("click", hideHelpDialog, false);
+ container.getElementsByClassName("optionsPage")[0].addEventListener("click",
+ function() { chrome.extension.sendRequest({ handler: "openOptionsPageInNewTab" }); }, false);
+
// This is necessary because innerHTML does not evaluate javascript embedded in <script> tags.
var scripts = Array.prototype.slice.call(container.getElementsByTagName("script"));
scripts.forEach(function(script) { eval(script.text); });
- container.getElementsByClassName("closeButton")[0].addEventListener("click", hideHelpDialog, false);
- container.getElementsByClassName("optionsPage")[0].addEventListener("click",
- function() { chrome.extension.sendRequest({ handler: "openOptionsPageInNewTab" }); }, false);
}
function hideHelpDialog(clickEvent) {
@@ -763,15 +806,6 @@ HUD = {
function() { HUD.upgradeNotificationElement().style.display = "none"; });
},
- updatePageZoomLevel: function(pageZoomLevel) {
- // Since the chrome HUD does not scale with the page's zoom level, neither will this HUD.
- var inverseZoomLevel = (100.0 / pageZoomLevel) * 100;
- if (HUD._displayElement)
- HUD.displayElement().style.zoom = inverseZoomLevel + "%";
- if (HUD._upgradeNotificationElement)
- HUD.upgradeNotificationElement().style.zoom = inverseZoomLevel + "%";
- },
-
/*
* Retrieves the HUD HTML element.
*/
@@ -780,7 +814,6 @@ HUD = {
HUD._displayElement = HUD.createHudElement();
// Keep this far enough to the right so that it doesn't collide with the "popups blocked" chrome HUD.
HUD._displayElement.style.right = "150px";
- HUD.updatePageZoomLevel(currentZoomLevel);
}
return HUD._displayElement;
},
@@ -790,7 +823,6 @@ HUD = {
HUD._upgradeNotificationElement = HUD.createHudElement();
// Position this just to the left of our normal HUD.
HUD._upgradeNotificationElement.style.right = "315px";
- HUD.updatePageZoomLevel(currentZoomLevel);
}
return HUD._upgradeNotificationElement;
},