aboutsummaryrefslogtreecommitdiffstats
path: root/content_scripts
diff options
context:
space:
mode:
Diffstat (limited to 'content_scripts')
-rw-r--r--content_scripts/link_hints.js552
-rw-r--r--content_scripts/vimium_frontend.js1182
-rw-r--r--content_scripts/vomnibar.js232
3 files changed, 1966 insertions, 0 deletions
diff --git a/content_scripts/link_hints.js b/content_scripts/link_hints.js
new file mode 100644
index 00000000..7d6b431f
--- /dev/null
+++ b/content_scripts/link_hints.js
@@ -0,0 +1,552 @@
+/*
+ * This implements link hinting. Typing "F" will enter link-hinting mode, where all clickable items on the
+ * page have a hint marker displayed containing a sequence of letters. Typing those letters will select a link.
+ *
+ * In our 'default' mode, the characters we use to show link hints are a user-configurable option. By default
+ * they're the home row. The CSS which is used on the link hints is also a configurable option.
+ *
+ * In 'filter' mode, our link hints are numbers, and the user can narrow down the range of possibilities by
+ * typing the text of the link itself.
+ */
+var linkHints = {
+ hintMarkers: [],
+ hintMarkerContainingDiv: null,
+ shouldOpenInNewTab: false,
+ shouldOpenWithQueue: false,
+ // function that does the appropriate action on the selected link
+ linkActivator: undefined,
+ // While in delayMode, all keypresses have no effect.
+ delayMode: false,
+ // Handle the link hinting marker generation and matching. Must be initialized after settings have been
+ // loaded, so that we can retrieve the option setting.
+ markerMatcher: undefined,
+
+ /*
+ * To be called after linkHints has been generated from linkHintsBase.
+ */
+ init: function() {
+ this.onKeyDownInMode = this.onKeyDownInMode.bind(this);
+ this.markerMatcher = settings.get('filterLinkHints') ? filterHints : alphabetHints;
+ },
+
+ /*
+ * Generate an XPath describing what a clickable element is.
+ * The final expression will be something like "//button | //xhtml:button | ..."
+ * We use translate() instead of lower-case() because Chrome only supports XPath 1.0.
+ */
+ clickableElementsXPath: domUtils.makeXPath(["a", "area[@href]", "textarea", "button", "select",
+ "input[not(@type='hidden' or @disabled or @readonly)]",
+ "*[@onclick or @tabindex or @role='link' or @role='button' or contains(@class, 'button') or " +
+ "@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"]),
+
+ // We need this as a top-level function because our command system doesn't yet support arguments.
+ activateModeToOpenInNewTab: function() { this.activateMode(true, false, false); },
+
+ activateModeToCopyLinkUrl: function() { this.activateMode(null, false, true); },
+
+ activateModeWithQueue: function() { this.activateMode(true, true, false); },
+
+ activateMode: function(openInNewTab, withQueue, copyLinkUrl) {
+ if (!document.getElementById('vimiumLinkHintCss'))
+ // linkHintCss is declared by vimiumFrontend.js and contains the user supplied css overrides.
+ addCssToPage(linkHintCss, 'vimiumLinkHintCss');
+ this.setOpenLinkMode(openInNewTab, withQueue, copyLinkUrl);
+ this.buildLinkHints();
+ handlerStack.push({ // handlerStack is declared by vimiumFrontend.js
+ keydown: this.onKeyDownInMode,
+ // trap all key events
+ keypress: function() { return false; },
+ keyup: function() { return false; }
+ });
+ },
+
+ setOpenLinkMode: function(openInNewTab, withQueue, copyLinkUrl) {
+ this.shouldOpenInNewTab = openInNewTab;
+ this.shouldOpenWithQueue = withQueue;
+
+ if (openInNewTab || withQueue) {
+ if (openInNewTab)
+ HUD.show("Open link in new tab");
+ else if (withQueue)
+ HUD.show("Open multiple links in a new tab");
+ this.linkActivator = function(link) {
+ // When "clicking" on a link, dispatch the event with the appropriate meta key (CMD on Mac, CTRL on windows)
+ // to open it in a new tab if necessary.
+ domUtils.simulateClick(link, { metaKey: platform == "Mac", ctrlKey: platform != "Mac" });
+ }
+ } else if (copyLinkUrl) {
+ HUD.show("Copy link URL to Clipboard");
+ this.linkActivator = function(link) {
+ chrome.extension.sendRequest({handler: 'copyToClipboard', data: link.href});
+ }
+ } else {
+ HUD.show("Open link in current tab");
+ // When we're opening the link in the current tab, don't navigate to the selected link immediately;
+ // we want to give the user some time to notice which link has received focus.
+ this.linkActivator = function(link) {
+ setTimeout(domUtils.simulateClick.bind(domUtils, link), 400);
+ }
+ }
+ },
+
+ /*
+ * Builds and displays link hints for every visible clickable item on the page.
+ */
+ buildLinkHints: function() {
+ var visibleElements = this.getVisibleClickableElements();
+ this.hintMarkers = this.markerMatcher.getHintMarkers(visibleElements);
+
+ // Note(philc): Append these markers as top level children instead of as child nodes to the link itself,
+ // because some clickable elements cannot contain children, e.g. submit buttons. This has the caveat
+ // that if you scroll the page and the link has position=fixed, the marker will not stay fixed.
+ // Also note that adding these nodes to document.body all at once is significantly faster than one-by-one.
+ this.hintMarkerContainingDiv = document.createElement("div");
+ this.hintMarkerContainingDiv.id = "vimiumHintMarkerContainer";
+ this.hintMarkerContainingDiv.className = "vimiumReset";
+ for (var i = 0; i < this.hintMarkers.length; i++)
+ this.hintMarkerContainingDiv.appendChild(this.hintMarkers[i]);
+
+ // sometimes this is triggered before documentElement is created
+ // TODO(int3): fail more gracefully?
+ if (document.documentElement)
+ document.documentElement.appendChild(this.hintMarkerContainingDiv);
+ else
+ this.deactivateMode();
+ },
+
+ /*
+ * Returns all clickable elements that are not hidden and are in the current viewport.
+ * We prune invisible elements partly for performance reasons, but moreso it's to decrease the number
+ * of digits needed to enumerate all of the links on screen.
+ */
+ getVisibleClickableElements: function() {
+ var resultSet = domUtils.evaluateXPath(this.clickableElementsXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
+
+ var visibleElements = [];
+
+ // Find all visible clickable elements.
+ for (var i = 0, count = resultSet.snapshotLength; i < count; i++) {
+ var element = resultSet.snapshotItem(i);
+ var clientRect = domUtils.getVisibleClientRect(element, clientRect);
+ if (clientRect !== null)
+ visibleElements.push({element: element, rect: clientRect});
+
+ if (element.localName === "area") {
+ var map = element.parentElement;
+ if (!map) continue;
+ var img = document.querySelector("img[usemap='#" + map.getAttribute("name") + "']");
+ if (!img) continue;
+ var imgClientRects = img.getClientRects();
+ if (imgClientRects.length == 0) continue;
+ var c = element.coords.split(/,/);
+ var coords = [parseInt(c[0], 10), parseInt(c[1], 10), parseInt(c[2], 10), parseInt(c[3], 10)];
+ var rect = {
+ top: imgClientRects[0].top + coords[1],
+ left: imgClientRects[0].left + coords[0],
+ right: imgClientRects[0].left + coords[2],
+ bottom: imgClientRects[0].top + coords[3],
+ width: coords[2] - coords[0],
+ height: coords[3] - coords[1]
+ };
+
+ visibleElements.push({element: element, rect: rect});
+ }
+ }
+ return visibleElements;
+ },
+
+ /*
+ * Handles shift and esc keys. The other keys are passed to markerMatcher.matchHintsByKey.
+ */
+ onKeyDownInMode: function(event) {
+ if (this.delayMode)
+ return;
+
+ if (event.keyCode == keyCodes.shiftKey && this.shouldOpenInNewTab !== null) {
+ // Toggle whether to open link in a new or current tab.
+ this.setOpenLinkMode(!this.shouldOpenInNewTab, this.shouldOpenWithQueue, false);
+ handlerStack.push({
+ keyup: function(event) {
+ if (event.keyCode !== keyCodes.shiftKey) return;
+ linkHints.setOpenLinkMode(!linkHints.shouldOpenInNewTab, linkHints.shouldOpenWithQueue, false);
+ handlerStack.pop();
+ }
+ });
+ }
+
+ // TODO(philc): Ignore keys that have modifiers.
+ if (isEscape(event)) {
+ this.deactivateMode();
+ } else {
+ var keyResult = this.markerMatcher.matchHintsByKey(event, this.hintMarkers);
+ var linksMatched = keyResult.linksMatched;
+ var delay = keyResult.delay !== undefined ? keyResult.delay : 0;
+ if (linksMatched.length == 0) {
+ this.deactivateMode();
+ } else if (linksMatched.length == 1) {
+ this.activateLink(linksMatched[0], delay);
+ } else {
+ for (var i in this.hintMarkers)
+ this.hideMarker(this.hintMarkers[i]);
+ for (var i in linksMatched)
+ this.showMarker(linksMatched[i], this.markerMatcher.hintKeystrokeQueue.length);
+ }
+ }
+ },
+
+ /*
+ * When only one link hint remains, this function activates it in the appropriate way.
+ */
+ activateLink: function(matchedLink, delay) {
+ this.delayMode = true;
+ var clickEl = matchedLink.clickableItem;
+ if (domUtils.isSelectable(clickEl)) {
+ domUtils.simulateSelect(clickEl);
+ this.deactivateMode(delay, function() { linkHints.delayMode = false; });
+ } else {
+ // TODO figure out which other input elements should not receive focus
+ if (clickEl.nodeName.toLowerCase() === 'input' && clickEl.type !== 'button')
+ clickEl.focus();
+ domUtils.flashRect(matchedLink.rect);
+ this.linkActivator(clickEl);
+ if (this.shouldOpenWithQueue) {
+ this.deactivateMode(delay, function() {
+ linkHints.delayMode = false;
+ linkHints.activateModeWithQueue();
+ });
+ } else {
+ this.deactivateMode(delay, function() { linkHints.delayMode = false; });
+ }
+ }
+ },
+
+ /*
+ * Shows the marker, highlighting matchingCharCount characters.
+ */
+ showMarker: function(linkMarker, matchingCharCount) {
+ linkMarker.style.display = "";
+ for (var j = 0, count = linkMarker.childNodes.length; j < count; j++)
+ (j < matchingCharCount) ? linkMarker.childNodes[j].classList.add("matchingCharacter") :
+ linkMarker.childNodes[j].classList.remove("matchingCharacter");
+ },
+
+ hideMarker: function(linkMarker) {
+ linkMarker.style.display = "none";
+ },
+
+ /*
+ * If called without arguments, it executes immediately. Othewise, it
+ * executes after 'delay' and invokes 'callback' when it is finished.
+ */
+ deactivateMode: function(delay, callback) {
+ function deactivate() {
+ if (linkHints.markerMatcher.deactivate)
+ linkHints.markerMatcher.deactivate();
+ if (linkHints.hintMarkerContainingDiv)
+ linkHints.hintMarkerContainingDiv.parentNode.removeChild(linkHints.hintMarkerContainingDiv);
+ linkHints.hintMarkerContainingDiv = null;
+ linkHints.hintMarkers = [];
+ handlerStack.pop();
+ HUD.hide();
+ }
+ // we invoke the deactivate() function directly instead of using setTimeout(callback, 0) so that
+ // deactivateMode can be tested synchronously
+ if (!delay) {
+ deactivate();
+ if (callback) callback();
+ } else {
+ setTimeout(function() { deactivate(); if (callback) callback(); }, delay);
+ }
+ },
+
+};
+
+var alphabetHints = {
+ hintKeystrokeQueue: [],
+ logXOfBase: function(x, base) { return Math.log(x) / Math.log(base); },
+
+ getHintMarkers: function(visibleElements) {
+ var hintStrings = this.hintStrings(visibleElements.length);
+ var hintMarkers = [];
+ for (var i = 0, count = visibleElements.length; i < count; i++) {
+ var marker = hintUtils.createMarkerFor(visibleElements[i]);
+ marker.hintString = hintStrings[i];
+ marker.innerHTML = hintUtils.spanWrap(marker.hintString.toUpperCase());
+ hintMarkers.push(marker);
+ }
+
+ return hintMarkers;
+ },
+
+ /*
+ * Returns a list of hint strings which will uniquely identify the given number of links. The hint strings
+ * may be of different lengths.
+ */
+ hintStrings: function(linkCount) {
+ var linkHintCharacters = settings.get("linkHintCharacters");
+ // Determine how many digits the link hints will require in the worst case. Usually we do not need
+ // all of these digits for every link single hint, so we can show shorter hints for a few of the links.
+ var digitsNeeded = Math.ceil(this.logXOfBase(linkCount, linkHintCharacters.length));
+ // Short hints are the number of hints we can possibly show which are (digitsNeeded - 1) digits in length.
+ var shortHintCount = Math.floor(
+ (Math.pow(linkHintCharacters.length, digitsNeeded) - linkCount) /
+ linkHintCharacters.length);
+ var longHintCount = linkCount - shortHintCount;
+
+ var hintStrings = [];
+
+ if (digitsNeeded > 1)
+ for (var i = 0; i < shortHintCount; i++)
+ hintStrings.push(this.numberToHintString(i, digitsNeeded - 1, linkHintCharacters));
+
+ var start = shortHintCount * linkHintCharacters.length;
+ for (var i = start; i < start + longHintCount; i++)
+ hintStrings.push(this.numberToHintString(i, digitsNeeded, linkHintCharacters));
+
+ return this.shuffleHints(hintStrings, linkHintCharacters.length);
+ },
+
+ /*
+ * This shuffles the given set of hints so that they're scattered -- hints starting with the same character
+ * will be spread evenly throughout the array.
+ */
+ shuffleHints: function(hints, characterSetLength) {
+ var buckets = [], i = 0;
+ for (i = 0; i < characterSetLength; i++)
+ buckets[i] = []
+ for (i = 0; i < hints.length; i++)
+ buckets[i % buckets.length].push(hints[i]);
+ var result = [];
+ for (i = 0; i < buckets.length; i++)
+ result = result.concat(buckets[i]);
+ return result;
+ },
+
+ /*
+ * Converts a number like "8" into a hint string like "JK". This is used to sequentially generate all of
+ * the hint text. The hint string will be "padded with zeroes" to ensure its length is equal to numHintDigits.
+ */
+ numberToHintString: function(number, numHintDigits, characterSet) {
+ var base = characterSet.length;
+ var hintString = [];
+ var remainder = 0;
+ do {
+ remainder = number % base;
+ hintString.unshift(characterSet[remainder]);
+ number -= remainder;
+ number /= Math.floor(base);
+ } while (number > 0);
+
+ // Pad the hint string we're returning so that it matches numHintDigits.
+ // Note: the loop body changes hintString.length, so the original length must be cached!
+ var hintStringLength = hintString.length;
+ for (var i = 0; i < numHintDigits - hintStringLength; i++)
+ hintString.unshift(characterSet[0]);
+
+ return hintString.join("");
+ },
+
+ matchHintsByKey: function(event, hintMarkers) {
+ var keyChar = getKeyChar(event);
+
+ if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) {
+ if (!this.hintKeystrokeQueue.pop())
+ return { linksMatched: [] };
+ } else if (keyChar && settings.get('linkHintCharacters').indexOf(keyChar) >= 0) {
+ this.hintKeystrokeQueue.push(keyChar);
+ }
+
+ var matchString = this.hintKeystrokeQueue.join("");
+ var linksMatched = hintMarkers.filter(function(linkMarker) {
+ return linkMarker.hintString.indexOf(matchString) == 0;
+ });
+ return { linksMatched: linksMatched };
+ },
+
+ deactivate: function() {
+ this.hintKeystrokeQueue = [];
+ }
+
+};
+
+var filterHints = {
+ hintKeystrokeQueue: [],
+ linkTextKeystrokeQueue: [],
+ labelMap: {},
+
+ /*
+ * Generate a map of input element => label
+ */
+ generateLabelMap: function() {
+ var labels = document.querySelectorAll("label");
+ for (var i = 0, count = labels.length; i < count; i++) {
+ var forElement = labels[i].getAttribute("for");
+ if (forElement) {
+ var labelText = labels[i].textContent.trim();
+ // remove trailing : commonly found in labels
+ if (labelText[labelText.length-1] == ":")
+ labelText = labelText.substr(0, labelText.length-1);
+ this.labelMap[forElement] = labelText;
+ }
+ }
+ },
+
+ generateHintString: function(linkHintNumber) {
+ return (linkHintNumber + 1).toString();
+ },
+
+ generateLinkText: function(element) {
+ var linkText = "";
+ var showLinkText = false;
+ // toLowerCase is necessary as html documents return 'IMG'
+ // and xhtml documents return 'img'
+ var nodeName = element.nodeName.toLowerCase();
+
+ if (nodeName == "input") {
+ if (this.labelMap[element.id]) {
+ linkText = this.labelMap[element.id];
+ showLinkText = true;
+ } else if (element.type != "password") {
+ linkText = element.value;
+ }
+ // check if there is an image embedded in the <a> tag
+ } else if (nodeName == "a" && !element.textContent.trim()
+ && element.firstElementChild
+ && element.firstElementChild.nodeName.toLowerCase() == "img") {
+ linkText = element.firstElementChild.alt || element.firstElementChild.title;
+ if (linkText)
+ showLinkText = true;
+ } else {
+ linkText = element.textContent || element.innerHTML;
+ }
+ return { text: linkText, show: showLinkText };
+ },
+
+ renderMarker: function(marker) {
+ marker.innerHTML = hintUtils.spanWrap(marker.hintString +
+ (marker.showLinkText ? ": " + marker.linkText : ""));
+ },
+
+ getHintMarkers: function(visibleElements) {
+ this.generateLabelMap();
+ var hintMarkers = [];
+ for (var i = 0, count = visibleElements.length; i < count; i++) {
+ var marker = hintUtils.createMarkerFor(visibleElements[i]);
+ marker.hintString = this.generateHintString(i);
+ var linkTextObject = this.generateLinkText(marker.clickableItem);
+ marker.linkText = linkTextObject.text;
+ marker.showLinkText = linkTextObject.show;
+ this.renderMarker(marker);
+ hintMarkers.push(marker);
+ }
+ return hintMarkers;
+ },
+
+ matchHintsByKey: function(event, hintMarkers) {
+ var keyChar = getKeyChar(event);
+ var delay = 0;
+ var userIsTypingLinkText = false;
+
+ if (event.keyCode == keyCodes.enter) {
+ // activate the lowest-numbered link hint that is visible
+ for (var i = 0, count = hintMarkers.length; i < count; i++)
+ if (hintMarkers[i].style.display != 'none') {
+ return { linksMatched: [ hintMarkers[i] ] };
+ }
+ } else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) {
+ // backspace clears hint key queue first, then acts on link text key queue.
+ // if both queues are empty. exit hinting mode
+ if (!this.hintKeystrokeQueue.pop() && !this.linkTextKeystrokeQueue.pop())
+ return { linksMatched: [] };
+ } else if (keyChar) {
+ if (/[0-9]/.test(keyChar))
+ this.hintKeystrokeQueue.push(keyChar);
+ else {
+ // since we might renumber the hints, the current hintKeyStrokeQueue
+ // should be rendered invalid (i.e. reset).
+ this.hintKeystrokeQueue = [];
+ this.linkTextKeystrokeQueue.push(keyChar);
+ userIsTypingLinkText = true;
+ }
+ }
+
+ // at this point, linkTextKeystrokeQueue and hintKeystrokeQueue have been updated to reflect the latest
+ // input. use them to filter the link hints accordingly.
+ var linksMatched = this.filterLinkHints(hintMarkers);
+ var matchString = this.hintKeystrokeQueue.join("");
+ linksMatched = linksMatched.filter(function(linkMarker) {
+ return !linkMarker.filtered && linkMarker.hintString.indexOf(matchString) == 0;
+ });
+
+ if (linksMatched.length == 1 && userIsTypingLinkText) {
+ // In filter mode, people tend to type out words past the point
+ // needed for a unique match. Hence we should avoid passing
+ // control back to command mode immediately after a match is found.
+ var delay = 200;
+ }
+
+ return { linksMatched: linksMatched, delay: delay };
+ },
+
+ /*
+ * Marks the links that do not match the linkText search string with the 'filtered' DOM property. Renumbers
+ * the remainder if necessary.
+ */
+ filterLinkHints: function(hintMarkers) {
+ var linksMatched = [];
+ var linkSearchString = this.linkTextKeystrokeQueue.join("");
+
+ for (var i = 0; i < hintMarkers.length; i++) {
+ var linkMarker = hintMarkers[i];
+ var matchedLink = linkMarker.linkText.toLowerCase().indexOf(linkSearchString.toLowerCase()) >= 0;
+
+ if (!matchedLink) {
+ linkMarker.filtered = true;
+ } else {
+ linkMarker.filtered = false;
+ var oldHintString = linkMarker.hintString;
+ linkMarker.hintString = this.generateHintString(linksMatched.length);
+ if (linkMarker.hintString != oldHintString)
+ this.renderMarker(linkMarker);
+ linksMatched.push(linkMarker);
+ }
+ }
+ return linksMatched;
+ },
+
+ deactivate: function(delay, callback) {
+ this.hintKeystrokeQueue = [];
+ this.linkTextKeystrokeQueue = [];
+ this.labelMap = {};
+ }
+
+};
+
+var hintUtils = {
+ /*
+ * Make each hint character a span, so that we can highlight the typed characters as you type them.
+ */
+ spanWrap: function(hintString) {
+ var innerHTML = [];
+ for (var i = 0; i < hintString.length; i++)
+ innerHTML.push("<span class='vimiumReset'>" + hintString[i] + "</span>");
+ return innerHTML.join("");
+ },
+
+ /*
+ * Creates a link marker for the given link.
+ */
+ createMarkerFor: function(link) {
+ var marker = document.createElement("div");
+ marker.className = "vimiumReset internalVimiumHintMarker vimiumHintMarker";
+ marker.clickableItem = link.element;
+
+ var clientRect = link.rect;
+ marker.style.left = clientRect.left + window.scrollX + "px";
+ marker.style.top = clientRect.top + window.scrollY + "px";
+
+ marker.rect = link.rect;
+
+ return marker;
+ }
+};
diff --git a/content_scripts/vimium_frontend.js b/content_scripts/vimium_frontend.js
new file mode 100644
index 00000000..31bb2b44
--- /dev/null
+++ b/content_scripts/vimium_frontend.js
@@ -0,0 +1,1182 @@
+/*
+ * 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. We tell the
+ * background page that we're in domReady and ready to accept normal commands by connectiong to a port named
+ * "domReady".
+ */
+var getCurrentUrlHandlers = []; // function(url)
+
+var insertModeLock = null;
+var findMode = false;
+var findModeQuery = { rawQuery: "" };
+var findModeQueryHasResults = false;
+var findModeAnchorNode = null;
+var isShowingHelpDialog = false;
+var handlerStack = [];
+var keyPort;
+// 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;
+var activatedElement;
+
+// 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?
+
+// The corresponding XPath for such elements.
+var textInputXPath = (function() {
+ var textInputTypes = ["text", "search", "email", "url", "number", "password"];
+ var inputElements = ["input[" +
+ "(" + textInputTypes.map(function(type) {return '@type="' + type + '"'}).join(" or ") + "or not(@type))" +
+ " and not(@disabled or @readonly)]",
+ "textarea", "*[@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"];
+ return domUtils.makeXPath(inputElements);
+})();
+
+/**
+ * settings provides a browser-global localStorage-backed dict. get() and set() are synchronous, but load()
+ * must be called beforehand to ensure get() will return up-to-date values.
+ */
+var settings = {
+ port: null,
+ values: {},
+ loadedValues: 0,
+ valuesToLoad: ["scrollStepSize", "linkHintCharacters", "filterLinkHints", "hideHud", "previousPatterns",
+ "nextPatterns", "findModeRawQuery"],
+ isLoaded: false,
+ eventListeners: {},
+
+ init: function () {
+ this.port = chrome.extension.connect({ name: "settings" });
+ this.port.onMessage.addListener(this.receiveMessage);
+ },
+
+ get: function (key) { return this.values[key]; },
+
+ set: function (key, value) {
+ if (!this.port)
+ this.init();
+
+ this.values[key] = value;
+ this.port.postMessage({ operation: "set", key: key, value: value });
+ },
+
+ load: function() {
+ if (!this.port)
+ this.init();
+
+ for (var i in this.valuesToLoad) {
+ this.port.postMessage({ operation: "get", key: this.valuesToLoad[i] });
+ }
+ },
+
+ receiveMessage: function (args) {
+ // not using 'this' due to issues with binding on callback
+ settings.values[args.key] = args.value;
+ // since load() can be called more than once, loadedValues can be greater than valuesToLoad, but we test
+ // for equality so initializeOnReady only runs once
+ if (++settings.loadedValues == settings.valuesToLoad.length) {
+ settings.isLoaded = true;
+ var listener;
+ while (listener = settings.eventListeners["load"].pop())
+ listener();
+ }
+ },
+
+ addEventListener: function(eventName, callback) {
+ if (!(eventName in this.eventListeners))
+ this.eventListeners[eventName] = [];
+ this.eventListeners[eventName].push(callback);
+ },
+
+};
+
+/*
+ * Give this frame a unique id.
+ */
+frameId = Math.floor(Math.random()*999999999)
+
+var hasModifiersRegex = /^<([amc]-)+.>/;
+
+/*
+ * Complete initialization work that sould be done prior to DOMReady.
+ */
+function initializePreDomReady() {
+ settings.addEventListener("load", linkHints.init.bind(linkHints));
+ settings.load();
+
+ checkIfEnabledForUrl();
+
+ chrome.extension.sendRequest({handler: "getLinkHintCss"}, function (response) {
+ linkHintCss = response.linkHintCss;
+ });
+
+ refreshCompletionKeys();
+
+ // Send the key to the key handler in the background page.
+ keyPort = chrome.extension.connect({ name: "keyDown" });
+
+ chrome.extension.onRequest.addListener(function(request, sender, sendResponse) {
+ if (request.name == "hideUpgradeNotification") {
+ HUD.hideUpgradeNotification();
+ } else if (request.name == "showUpgradeNotification" && isEnabledForUrl) {
+ HUD.showUpgradeNotification(request.version);
+ } else if (request.name == "showHelpDialog") {
+ if (isShowingHelpDialog)
+ hideHelpDialog();
+ else
+ showHelpDialog(request.dialogHtml, request.frameId);
+ } else if (request.name == "focusFrame") {
+ if (frameId == request.frameId)
+ focusThisFrame(request.highlight);
+ } 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 (frameId == args.frameId) {
+ if (args.passCountToFunction) {
+ utils.invokeCommandString(args.command, [args.count]);
+ } else {
+ for (var i = 0; i < args.count; i++) { utils.invokeCommandString(args.command); }
+ }
+ }
+
+ refreshCompletionKeys(args);
+ });
+ }
+ else if (port.name == "getScrollPosition") {
+ port.onMessage.addListener(function(args) {
+ var scrollPort = chrome.extension.connect({ name: "returnScrollPosition" });
+ scrollPort.postMessage({
+ scrollX: window.scrollX,
+ scrollY: window.scrollY,
+ currentTab: args.currentTab
+ });
+ });
+ } else if (port.name == "setScrollPosition") {
+ port.onMessage.addListener(function(args) {
+ if (args.scrollX > 0 || args.scrollY > 0) {
+ domUtils.documentReady(function() { window.scrollBy(args.scrollX, args.scrollY); });
+ }
+ });
+ } else if (port.name == "returnCurrentTabUrl") {
+ port.onMessage.addListener(function(args) {
+ if (getCurrentUrlHandlers.length > 0) { getCurrentUrlHandlers.pop()(args.url); }
+ });
+ } else if (port.name == "refreshCompletionKeys") {
+ port.onMessage.addListener(function (args) {
+ refreshCompletionKeys(args.completionKeys);
+ });
+ } else if (port.name == "getActiveState") {
+ port.onMessage.addListener(function(args) {
+ port.postMessage({ enabled: isEnabledForUrl });
+ });
+ } else if (port.name == "disableVimium") {
+ port.onMessage.addListener(function(args) { disableVimium(); });
+ }
+ });
+}
+
+/*
+ * This is called once the background page has told us that Vimium should be enabled for the current URL.
+ */
+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);
+ document.addEventListener("DOMActivate", onDOMActivate, true);
+ enterInsertModeIfElementIsFocused();
+}
+
+/*
+ * Used to disable Vimium without needing to reload the page.
+ * This is called if the current page's url is blacklisted using the popup UI.
+ */
+function disableVimium() {
+ document.removeEventListener("keydown", onKeydown, true);
+ document.removeEventListener("keypress", onKeypress, true);
+ document.removeEventListener("keyup", onKeyup, true);
+ document.removeEventListener("focus", onFocusCapturePhase, true);
+ document.removeEventListener("blur", onBlurCapturePhase, true);
+ document.removeEventListener("DOMActivate", onDOMActivate, true);
+ isEnabledForUrl = false;
+}
+
+/*
+ * The backend needs to know which frame has focus.
+ */
+window.addEventListener("focus", function(e) {
+ // settings may have changed since the frame last had focus
+ settings.load();
+ chrome.extension.sendRequest({ handler: "frameFocused", frameId: frameId });
+});
+
+/*
+ * Called from the backend in order to change frame focus.
+ */
+function focusThisFrame(shouldHighlight) {
+ window.focus();
+ if (document.body && shouldHighlight) {
+ var borderWas = document.body.style.border;
+ document.body.style.border = '5px solid yellow';
+ setTimeout(function(){document.body.style.border = borderWas}, 200);
+ }
+}
+
+/*
+ * Initialization tasks that must wait for the document to be ready.
+ */
+function initializeOnDomReady() {
+ registerFrameIfSizeAvailable(window.top == window.self);
+
+ if (isEnabledForUrl)
+ enterInsertModeIfElementIsFocused();
+
+ // Tell the background page we're in the dom ready state.
+ chrome.extension.connect({ name: "domReady" });
+};
+
+// This is a little hacky but sometimes the size wasn't available on domReady?
+function registerFrameIfSizeAvailable (is_top) {
+ if (innerWidth != undefined && innerWidth != 0 && innerHeight != undefined && innerHeight != 0)
+ chrome.extension.sendRequest({ handler: "registerFrame", frameId: frameId,
+ area: innerWidth * innerHeight, is_top: is_top, total: frames.length + 1 });
+ else
+ setTimeout(function () { registerFrameIfSizeAvailable(is_top); }, 100);
+}
+
+/*
+ * Enters insert mode if the currently focused element in the DOM is focusable.
+ */
+function enterInsertModeIfElementIsFocused() {
+ if (document.activeElement && isEditable(document.activeElement) && !findMode)
+ enterInsertModeWithoutShowingIndicator(document.activeElement);
+}
+
+function onDOMActivate(event) {
+ activatedElement = event.target;
+}
+
+/**
+ * activatedElement is different from document.activeElement -- the latter seems to be reserved mostly for
+ * input elements. This mechanism allows us to decide whether to scroll a div or to scroll the whole document.
+ */
+function scrollActivatedElementBy(direction, amount) {
+ // if this is called before domReady, just use the window scroll function
+ if (!document.body) {
+ if (direction === "x")
+ window.scrollBy(amount, 0);
+ else // "y"
+ window.scrollBy(0, amount);
+ return;
+ }
+
+ // TODO refactor and put this together with the code in getVisibleClientRect
+ function isRendered(element) {
+ var computedStyle = window.getComputedStyle(element, null);
+ return !(computedStyle.getPropertyValue('visibility') != 'visible' ||
+ computedStyle.getPropertyValue('display') == 'none');
+ }
+
+ if (!activatedElement || !isRendered(activatedElement))
+ activatedElement = document.body;
+
+ scrollName = direction === "x" ? "scrollLeft" : "scrollTop";
+
+ // Chrome does not report scrollHeight accurately for nodes with pseudo-elements of height 0 (bug 110149).
+ // Therefore we just try to increase scrollTop blindly -- if it fails we know we have reached the end of the
+ // content.
+ if (amount !== 0) {
+ var element = activatedElement;
+ do {
+ var oldScrollValue = element[scrollName];
+ element[scrollName] += amount;
+ var lastElement = element;
+ // we may have an orphaned element. if so, just scroll the body element.
+ element = element.parentElement || document.body;
+ } while(lastElement[scrollName] == oldScrollValue && lastElement != document.body);
+ }
+
+ // if the activated element has been scrolled completely offscreen, subsequent changes in its scroll
+ // position will not provide any more visual feedback to the user. therefore we deactivate it so that
+ // subsequent scrolls only move the parent element.
+ var rect = activatedElement.getBoundingClientRect();
+ if (rect.bottom < 0 || rect.top > window.innerHeight ||
+ rect.right < 0 || rect.left > window.innerWidth)
+ activatedElement = lastElement;
+}
+
+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() { scrollActivatedElementBy("y", -1 * settings.get("scrollStepSize")); }
+function scrollDown() { scrollActivatedElementBy("y", parseFloat(settings.get("scrollStepSize"))); }
+function scrollPageUp() { scrollActivatedElementBy("y", -1 * window.innerHeight / 2); }
+function scrollPageDown() { scrollActivatedElementBy("y", window.innerHeight / 2); }
+function scrollFullPageUp() { scrollActivatedElementBy("y", -window.innerHeight); }
+function scrollFullPageDown() { scrollActivatedElementBy("y", window.innerHeight); }
+function scrollLeft() { scrollActivatedElementBy("x", -1 * settings.get("scrollStepSize")); }
+function scrollRight() { scrollActivatedElementBy("x", parseFloat(settings.get("scrollStepSize"))); }
+
+function focusInput(count) {
+ var results = domUtils.evaluateXPath(textInputXPath, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
+
+ var lastInputBox;
+ var i = 0;
+
+ while (i < count) {
+ var currentInputBox = results.iterateNext();
+ if (!currentInputBox) { break; }
+
+ if (domUtils.getVisibleClientRect(currentInputBox) === null)
+ continue;
+
+ lastInputBox = currentInputBox;
+
+ i += 1;
+ }
+
+ if (lastInputBox) { lastInputBox.focus(); }
+}
+
+function reload() { window.location.reload(); }
+function goBack(count) { history.go(-count); }
+function goForward(count) { history.go(count); }
+
+function goUp(count) {
+ var url = window.location.href;
+ if (url[url.length-1] == '/')
+ url = url.substring(0, url.length - 1);
+
+ var urlsplit = url.split('/');
+ // make sure we haven't hit the base domain yet
+ if (urlsplit.length > 3) {
+ urlsplit = urlsplit.slice(0, Math.max(3, urlsplit.length - count));
+ window.location.href = urlsplit.join('/');
+ }
+}
+
+function toggleViewSource() {
+ getCurrentUrlHandlers.push(toggleViewSourceCallback);
+
+ var getCurrentUrlPort = chrome.extension.connect({ name: "getCurrentTabUrl" });
+ getCurrentUrlPort.postMessage({});
+}
+
+function copyCurrentUrl() {
+ // TODO(ilya): When the following bug is fixed, revisit this approach of sending back to the background page
+ // to copy.
+ // http://code.google.com/p/chromium/issues/detail?id=55188
+ //getCurrentUrlHandlers.push(function (url) { Clipboard.copy(url); });
+ getCurrentUrlHandlers.push(function (url) { chrome.extension.sendRequest({ handler: "copyToClipboard", data: url }); });
+
+ // TODO(ilya): Convert to sendRequest.
+ var getCurrentUrlPort = chrome.extension.connect({ name: "getCurrentTabUrl" });
+ getCurrentUrlPort.postMessage({});
+
+ HUD.showForDuration("Yanked URL", 1000);
+}
+
+function toggleViewSourceCallback(url) {
+ if (url.substr(0, 12) == "view-source:")
+ {
+ url = url.substr(12, url.length - 12);
+ }
+ else { url = "view-source:" + url; }
+ chrome.extension.sendRequest({handler: "openUrlInNewTab", url: url, selected: true});
+}
+
+/**
+ * Sends everything except i & ESC to the handler in background_page. i & ESC are special because they control
+ * insert mode which is local state to the page. The key will be are either a single ascii letter or a
+ * key-modifier pair, e.g. <c-a> for control a.
+ *
+ * Note that some keys will only register keydown events and not keystroke events, e.g. ESC.
+ */
+function onKeypress(event) {
+ 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)) {
+ enterInsertModeWithoutShowingIndicator();
+ return;
+ }
+
+ if (keyChar) {
+ if (findMode) {
+ handleKeyCharForFindMode(keyChar);
+ suppressEvent(event);
+ } else if (!isInsertMode() && !findMode) {
+ if (currentCompletionKeys.indexOf(keyChar) != -1)
+ suppressEvent(event);
+
+ keyPort.postMessage({keyChar:keyChar, frameId:frameId});
+ }
+ }
+ }
+}
+
+/**
+ * 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)) {
+ suppressEvent(event);
+ return false;
+ }
+ }
+ return true;
+}
+
+function suppressEvent(event) {
+ event.preventDefault();
+ event.stopPropagation();
+}
+
+function onKeydown(event) {
+ if (!bubbleEvent('keydown', event))
+ return;
+
+ var keyChar = "";
+
+ // handle special keys, and normal input keys with modifiers being pressed. don't handle shiftKey alone (to
+ // avoid / being interpreted as ?
+ if (((event.metaKey || event.ctrlKey || event.altKey) && event.keyCode > 31)
+ || event.keyIdentifier.slice(0, 2) != "U+") {
+ keyChar = getKeyChar(event);
+
+ if (keyChar != "") { // Again, ignore just modifiers. Maybe this should replace the keyCode>31 condition.
+ var modifiers = [];
+
+ if (event.shiftKey)
+ keyChar = keyChar.toUpperCase();
+ if (event.metaKey)
+ modifiers.push("m");
+ if (event.ctrlKey)
+ modifiers.push("c");
+ if (event.altKey)
+ modifiers.push("a");
+
+ for (var i in modifiers)
+ keyChar = modifiers[i] + "-" + keyChar;
+
+ if (modifiers.length > 0 || keyChar.length > 1)
+ keyChar = "<" + keyChar + ">";
+ }
+ }
+
+ 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();
+ exitInsertMode();
+ suppressEvent(event);
+ }
+ }
+ else if (findMode) {
+ if (isEscape(event)) {
+ handleEscapeForFindMode();
+ suppressEvent(event);
+ }
+ else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) {
+ handleDeleteForFindMode();
+ suppressEvent(event);
+ }
+ else if (event.keyCode == keyCodes.enter) {
+ handleEnterForFindMode();
+ suppressEvent(event);
+ }
+ else if (!modifiers) {
+ event.stopPropagation();
+ }
+ }
+ else if (isShowingHelpDialog && isEscape(event)) {
+ hideHelpDialog();
+ }
+ else if (!isInsertMode() && !findMode) {
+ if (keyChar) {
+ if (currentCompletionKeys.indexOf(keyChar) != -1)
+ suppressEvent(event);
+
+ keyPort.postMessage({keyChar:keyChar, frameId:frameId});
+ }
+ else if (isEscape(event)) {
+ keyPort.postMessage({keyChar:"<ESC>", frameId:frameId});
+ }
+ }
+
+ // Added to prevent propagating this event to other listeners if it's one that'll trigger a Vimium command.
+ // The goal is to avoid the scenario where Google Instant Search uses every keydown event to dump us
+ // back into the search box. As a side effect, this should also prevent overriding by other sites.
+ //
+ // 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 == "" && !isInsertMode() && (currentCompletionKeys.indexOf(getKeyChar(event)) != -1 ||
+ isValidFirstKey(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 insert mode on page load.
+ HUD.hide();
+ });
+}
+
+function refreshCompletionKeys(response) {
+ if (response) {
+ currentCompletionKeys = response.completionKeys;
+
+ if (response.validFirstKeys)
+ validFirstKeys = response.validFirstKeys;
+ }
+ else {
+ chrome.extension.sendRequest({ handler: "getCompletionKeys" }, refreshCompletionKeys);
+ }
+}
+
+function isValidFirstKey(keyChar) {
+ return validFirstKeys[keyChar] || /[1-9]/.test(keyChar);
+}
+
+function onFocusCapturePhase(event) {
+ if (isFocusable(event.target) && !findMode)
+ enterInsertModeWithoutShowingIndicator(event.target);
+}
+
+function onBlurCapturePhase(event) {
+ if (isFocusable(event.target))
+ exitInsertMode(event.target);
+}
+
+/*
+ * Returns true if the element is focusable. This includes embeds like Flash, which steal the keybaord focus.
+ */
+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.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.
+ */
+function isEditable(target) {
+ 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 focusableElements = ["textarea", "select"];
+ return focusableElements.indexOf(nodeName) >= 0;
+}
+
+/*
+ * 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");
+}
+
+/*
+ * 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; }
+
+// should be called whenever rawQuery is modified.
+function updateFindModeQuery() {
+ // the query can be treated differently (e.g. as a plain string versus regex depending on the presence of
+ // escape sequences. '\' is the escape character and needs to be escaped itself to be used as a normal
+ // character. here we grep for the relevant escape sequences.
+ findModeQuery.isRegex = false;
+ var hasNoIgnoreCaseFlag = false;
+ findModeQuery.parsedQuery = findModeQuery.rawQuery.replace(/\\./g, function(match) {
+ switch (match) {
+ case "\\r":
+ findModeQuery.isRegex = true;
+ return '';
+ case "\\I":
+ hasNoIgnoreCaseFlag = true;
+ return '';
+ case "\\\\":
+ return "\\";
+ default:
+ return match;
+ }
+ });
+
+ // default to 'smartcase' mode, unless noIgnoreCase is explicitly specified
+ findModeQuery.ignoreCase = !hasNoIgnoreCaseFlag && !/[A-Z]/.test(findModeQuery.parsedQuery);
+
+ // if we are dealing with a regex, grep for all matches in the text, and then call window.find() on them
+ // sequentially so the browser handles the scrolling / text selection.
+ if (findModeQuery.isRegex) {
+ try {
+ var pattern = new RegExp(findModeQuery.parsedQuery, "g" + (findModeQuery.ignoreCase ? "i" : ""));
+ }
+ catch (e) {
+ // if we catch a SyntaxError, assume the user is not done typing yet and return quietly
+ return;
+ }
+ // innerText will not return the text of hidden elements, and strip out tags while preserving newlines
+ var text = document.body.innerText;
+ findModeQuery.regexMatches = text.match(pattern);
+ findModeQuery.activeRegexIndex = 0;
+ }
+}
+
+function handleKeyCharForFindMode(keyChar) {
+ findModeQuery.rawQuery += keyChar;
+ updateFindModeQuery();
+ performFindInPlace();
+ showFindModeHUDForQuery();
+}
+
+function handleEscapeForFindMode() {
+ exitFindMode();
+ document.body.classList.remove("vimiumFindMode");
+ // removing the class does not re-color existing selections. we recreate the current selection so it reverts
+ // back to the default color.
+ var selection = window.getSelection();
+ if (!selection.isCollapsed) {
+ var range = window.getSelection().getRangeAt(0);
+ window.getSelection().removeAllRanges();
+ window.getSelection().addRange(range);
+ }
+ focusFoundLink() || selectFoundInputElement();
+}
+
+function handleDeleteForFindMode() {
+ if (findModeQuery.rawQuery.length == 0) {
+ exitFindMode();
+ performFindInPlace();
+ }
+ else {
+ findModeQuery.rawQuery = findModeQuery.rawQuery.substring(0, findModeQuery.rawQuery.length - 1);
+ updateFindModeQuery();
+ performFindInPlace();
+ showFindModeHUDForQuery();
+ }
+}
+
+// <esc> sends us into insert mode if possible, but <cr> does not.
+// <esc> corresponds approximately to 'nevermind, I have found it already' while <cr> means 'I want to save
+// this query and do more searches with it'
+function handleEnterForFindMode() {
+ exitFindMode();
+ focusFoundLink();
+ document.body.classList.add("vimiumFindMode");
+ settings.set("findModeRawQuery", findModeQuery.rawQuery);
+}
+
+function performFindInPlace() {
+ var cachedScrollX = window.scrollX;
+ var cachedScrollY = window.scrollY;
+
+ var query = findModeQuery.isRegex ? getNextQueryFromRegexMatches(0) : findModeQuery.parsedQuery;
+
+ // Search backwards first to "free up" the current word as eligible for the real forward search. This allows
+ // us to search in place without jumping around between matches as the query grows.
+ executeFind(query, { backwards: true, caseSensitive: !findModeQuery.ignoreCase });
+
+ // We need to restore the scroll position because we might've lost the right position by searching
+ // backwards.
+ window.scrollTo(cachedScrollX, cachedScrollY);
+
+ findModeQueryHasResults = executeFind(query, { caseSensitive: !findModeQuery.ignoreCase });
+}
+
+// :options is an optional dict. valid parameters are 'caseSensitive' and 'backwards'.
+function executeFind(query, options) {
+ options = options || {};
+
+ // rather hacky, but this is our way of signalling to the insertMode listener not to react to the focus
+ // changes that find() induces.
+ var oldFindMode = findMode;
+ findMode = true;
+
+ document.body.classList.add("vimiumFindMode");
+
+ // prevent find from matching its own search query in the HUD
+ HUD.hide(true);
+ // ignore the selectionchange event generated by find()
+ document.removeEventListener("selectionchange",restoreDefaultSelectionHighlight, true);
+ var rv = window.find(query, options.caseSensitive, options.backwards, true, false, true, false);
+ setTimeout(function() {
+ document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true);
+ }, 0);
+
+ findMode = oldFindMode;
+ // we need to save the anchor node here because <esc> seems to nullify it, regardless of whether we do
+ // preventDefault()
+ findModeAnchorNode = document.getSelection().anchorNode;
+ return rv;
+}
+
+function restoreDefaultSelectionHighlight() {
+ document.body.classList.remove("vimiumFindMode");
+}
+
+function focusFoundLink() {
+ if (findModeQueryHasResults) {
+ var link = getLinkFromSelection();
+ if (link)
+ link.focus();
+ }
+}
+
+function isDOMDescendant(parent, child) {
+ var node = child;
+ while (node !== null) {
+ if (node === parent)
+ return true;
+ node = node.parentNode;
+ }
+ return false;
+}
+
+function selectFoundInputElement() {
+ // if the found text is in an input element, getSelection().anchorNode will be null, so we use activeElement
+ // instead. however, since the last focused element might not be the one currently pointed to by find (e.g.
+ // the current one might be disabled and therefore unable to receive focus), we use the approximate
+ // heuristic of checking that the last anchor node is an ancestor of our element.
+ if (findModeQueryHasResults && domUtils.isSelectable(document.activeElement) &&
+ isDOMDescendant(findModeAnchorNode, document.activeElement)) {
+ domUtils.simulateSelect(document.activeElement);
+ // the element has already received focus via find(), so invoke insert mode manually
+ enterInsertModeWithoutShowingIndicator(document.activeElement);
+ }
+}
+
+function getNextQueryFromRegexMatches(stepSize) {
+ if (!findModeQuery.regexMatches)
+ return ""; // find()ing an empty query always returns false
+
+ var totalMatches = findModeQuery.regexMatches.length;
+ findModeQuery.activeRegexIndex += stepSize + totalMatches;
+ findModeQuery.activeRegexIndex %= totalMatches;
+
+ return findModeQuery.regexMatches[findModeQuery.activeRegexIndex];
+}
+
+function findAndFocus(backwards) {
+ // check if the query has been changed by a script in another frame
+ var mostRecentQuery = settings.get("findModeRawQuery") || "";
+ if (mostRecentQuery !== findModeQuery.rawQuery) {
+ findModeQuery.rawQuery = mostRecentQuery;
+ updateFindModeQuery();
+ }
+
+ var query = findModeQuery.isRegex ? getNextQueryFromRegexMatches(backwards ? -1 : 1) :
+ findModeQuery.parsedQuery;
+
+ findModeQueryHasResults = executeFind(query, { backwards: backwards, caseSensitive: !findModeQuery.ignoreCase });
+
+ if (!findModeQueryHasResults) {
+ HUD.showForDuration("No matches for '" + findModeQuery.rawQuery + "'", 1000);
+ return;
+ }
+
+ // if we have found an input element via 'n', pressing <esc> immediately afterwards sends us into insert
+ // mode
+ var elementCanTakeInput = domUtils.isSelectable(document.activeElement) &&
+ isDOMDescendant(findModeAnchorNode, document.activeElement);
+ if (elementCanTakeInput) {
+ handlerStack.push({
+ keydown: function(event) {
+ handlerStack.pop();
+ if (isEscape(event)) {
+ domUtils.simulateSelect(document.activeElement);
+ enterInsertModeWithoutShowingIndicator(document.activeElement);
+ return false; // we have 'consumed' this event, so do not propagate
+ }
+ return true;
+ }
+ });
+ }
+
+ focusFoundLink();
+}
+
+function performFind() { findAndFocus(); }
+
+function performBackwardsFind() { findAndFocus(true); }
+
+function getLinkFromSelection() {
+ var node = window.getSelection().anchorNode;
+ while (node && node !== document.body) {
+ if (node.nodeName.toLowerCase() === 'a') return node;
+ node = node.parentNode;
+ }
+ return null;
+}
+
+// used by the findAndFollow* functions.
+function followLink(linkElement) {
+ if (linkElement.nodeName.toLowerCase() === 'link')
+ window.location.href = linkElement.href;
+ else {
+ // if we can click on it, don't simply set location.href: some next/prev links are meant to trigger AJAX
+ // calls, like the 'more' button on GitHub's newsfeed.
+ linkElement.scrollIntoView();
+ linkElement.focus();
+ domUtils.simulateClick(linkElement);
+ }
+}
+
+/**
+ * Find and follow a link which matches any one of a list of strings. If there are multiple such links, they
+ * are prioritized for shortness, by their position in :linkStrings, how far down the page they are located,
+ * and finally by whether the match is exact. Practically speaking, this means we favor 'next page' over 'the
+ * next big thing', and 'more' over 'nextcompany', even if 'next' occurs before 'more' in :linkStrings.
+ */
+function findAndFollowLink(linkStrings) {
+ var linksXPath = domUtils.makeXPath(["a", "*[@onclick or @role='link' or contains(@class, 'button')]"]);
+ var links = domUtils.evaluateXPath(linksXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
+ var candidateLinks = [];
+
+ // at the end of this loop, candidateLinks will contain all visible links that match our patterns
+ // links lower in the page are more likely to be the ones we want, so we loop through the snapshot backwards
+ for (var i = links.snapshotLength - 1; i >= 0; i--) {
+ var link = links.snapshotItem(i);
+
+ // ensure link is visible (we don't mind if it is scrolled offscreen)
+ var boundingClientRect = link.getBoundingClientRect();
+ if (boundingClientRect.width == 0 || boundingClientRect.height == 0)
+ continue;
+ var computedStyle = window.getComputedStyle(link, null);
+ if (computedStyle.getPropertyValue('visibility') != 'visible' ||
+ computedStyle.getPropertyValue('display') == 'none')
+ continue;
+
+ var linkMatches = false;
+ for (var j = 0; j < linkStrings.length; j++) {
+ if (link.innerText.toLowerCase().indexOf(linkStrings[j]) !== -1) {
+ linkMatches = true;
+ break;
+ }
+ }
+ if (!linkMatches) continue;
+
+ candidateLinks.push(link);
+ }
+
+ if (candidateLinks.length === 0) return;
+
+ function wordCount(link) { return link.innerText.trim().split(/\s+/).length; }
+
+ // We can use this trick to ensure that Array.sort is stable. We need this property to retain the reverse
+ // in-page order of the links.
+ candidateLinks.forEach(function(a,i){ a.originalIndex = i; });
+
+ // favor shorter links, and ignore those that are more than one word longer than the shortest link
+ candidateLinks =
+ candidateLinks
+ .sort(function(a,b) {
+ var wcA = wordCount(a), wcB = wordCount(b);
+ return wcA === wcB ? a.originalIndex - b.originalIndex : wcA - wcB;
+ })
+ .filter(function(a){return wordCount(a) <= wordCount(candidateLinks[0]) + 1});
+
+ // try to get exact word matches first
+ for (var i = 0; i < linkStrings.length; i++)
+ for (var j = 0; j < candidateLinks.length; j++) {
+ var exactWordRegex = new RegExp("\\b" + linkStrings[i] + "\\b", "i");
+ if (exactWordRegex.test(candidateLinks[j].innerText)) {
+ followLink(candidateLinks[j]);
+ return true;
+ }
+ }
+
+ for (var i = 0; i < linkStrings.length; i++)
+ for (var j = 0; j < candidateLinks.length; j++) {
+ if (candidateLinks[j].innerText.toLowerCase().indexOf(linkStrings[i]) !== -1) {
+ followLink(candidateLinks[j]);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function findAndFollowRel(value) {
+ var relTags = ['link', 'a', 'area'];
+ for (i = 0; i < relTags.length; i++) {
+ var elements = document.getElementsByTagName(relTags[i]);
+ for (j = 0; j < elements.length; j++) {
+ if (elements[j].hasAttribute('rel') && elements[j].rel == value) {
+ followLink(elements[j]);
+ return true;
+ }
+ }
+ }
+}
+
+function goPrevious() {
+ var previousPatterns = settings.get("previousPatterns") || "";
+ var previousStrings = previousPatterns.split(",");
+ findAndFollowRel('prev') || findAndFollowLink(previousStrings);
+}
+
+function goNext() {
+ var nextPatterns = settings.get("nextPatterns") || "";
+ var nextStrings = nextPatterns.split(",");
+ findAndFollowRel('next') || findAndFollowLink(nextStrings);
+}
+
+function showFindModeHUDForQuery() {
+ if (findModeQueryHasResults || findModeQuery.parsedQuery.length == 0)
+ HUD.show("/" + findModeQuery.rawQuery);
+ else
+ HUD.show("/" + findModeQuery.rawQuery + " (No Matches)");
+}
+
+function enterFindMode() {
+ findModeQuery = { rawQuery: "" };
+ findMode = true;
+ HUD.show("/");
+}
+
+function exitFindMode() {
+ findMode = false;
+ HUD.hide();
+}
+
+function showHelpDialog(html, fid) {
+ if (isShowingHelpDialog || !document.body || fid != frameId)
+ return;
+ isShowingHelpDialog = true;
+ var container = document.createElement("div");
+ container.id = "vimiumHelpDialogContainer";
+ container.className = "vimiumReset";
+
+ 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); });
+
+}
+
+function hideHelpDialog(clickEvent) {
+ isShowingHelpDialog = false;
+ var helpDialog = document.getElementById("vimiumHelpDialogContainer");
+ if (helpDialog)
+ helpDialog.parentNode.removeChild(helpDialog);
+ if (clickEvent)
+ clickEvent.preventDefault();
+}
+
+/*
+ * A heads-up-display (HUD) for showing Vimium page operations.
+ * Note: you cannot interact with the HUD until document.body is available.
+ */
+HUD = {
+ _tweenId: -1,
+ _displayElement: null,
+ _upgradeNotificationElement: null,
+
+ // This HUD is styled to precisely mimick the chrome HUD on Mac. Use the "has_popup_and_link_hud.html"
+ // test harness to tweak these styles to match Chrome's. One limitation of our HUD display is that
+ // it doesn't sit on top of horizontal scrollbars like Chrome's HUD does.
+
+ showForDuration: function(text, duration) {
+ HUD.show(text);
+ HUD._showForDurationTimerId = setTimeout(function() { HUD.hide(); }, duration);
+ },
+
+ show: function(text) {
+ if (!HUD.enabled()) return;
+ clearTimeout(HUD._showForDurationTimerId);
+ HUD.displayElement().innerHTML = text;
+ clearInterval(HUD._tweenId);
+ HUD._tweenId = Tween.fade(HUD.displayElement(), 1.0, 150);
+ HUD.displayElement().style.display = "";
+ },
+
+ showUpgradeNotification: function(version) {
+ HUD.upgradeNotificationElement().innerHTML = "Vimium has been updated to " +
+ "<a class='vimiumReset' href='https://chrome.google.com/extensions/detail/dbepggeogbaibhgnhhndojpepiihcmeb'>" +
+ version + "</a>.<a class='vimiumReset close-button' href='#'>x</a>";
+ var links = HUD.upgradeNotificationElement().getElementsByTagName("a");
+ links[0].addEventListener("click", HUD.onUpdateLinkClicked, false);
+ links[1].addEventListener("click", function(event) {
+ event.preventDefault();
+ HUD.onUpdateLinkClicked();
+ });
+ Tween.fade(HUD.upgradeNotificationElement(), 1.0, 150);
+ },
+
+ onUpdateLinkClicked: function(event) {
+ HUD.hideUpgradeNotification();
+ chrome.extension.sendRequest({ handler: "upgradeNotificationClosed" });
+ },
+
+ hideUpgradeNotification: function(clickEvent) {
+ Tween.fade(HUD.upgradeNotificationElement(), 0, 150,
+ function() { HUD.upgradeNotificationElement().style.display = "none"; });
+ },
+
+ /*
+ * Retrieves the HUD HTML element.
+ */
+ displayElement: function() {
+ if (!HUD._displayElement) {
+ 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";
+ }
+ return HUD._displayElement;
+ },
+
+ upgradeNotificationElement: function() {
+ if (!HUD._upgradeNotificationElement) {
+ HUD._upgradeNotificationElement = HUD.createHudElement();
+ // Position this just to the left of our normal HUD.
+ HUD._upgradeNotificationElement.style.right = "315px";
+ }
+ return HUD._upgradeNotificationElement;
+ },
+
+ createHudElement: function() {
+ var element = document.createElement("div");
+ element.className = "vimiumReset vimiumHUD";
+ document.body.appendChild(element);
+ return element;
+ },
+
+ hide: function(immediate) {
+ clearInterval(HUD._tweenId);
+ if (immediate)
+ HUD.displayElement().style.display = "none";
+ else
+ HUD._tweenId = Tween.fade(HUD.displayElement(), 0, 150,
+ function() { HUD.displayElement().style.display = "none"; });
+ },
+
+ isReady: function() { return document.body != null; },
+
+ /* A preference which can be toggled in the Options page. */
+ enabled: function() { return !settings.get("hideHud"); }
+
+};
+
+Tween = {
+ /*
+ * Fades an element's alpha. Returns a timer ID which can be used to stop the tween via clearInterval.
+ */
+ fade: function(element, toAlpha, duration, onComplete) {
+ var state = {};
+ state.duration = duration;
+ state.startTime = (new Date()).getTime();
+ state.from = parseInt(element.style.opacity) || 0;
+ state.to = toAlpha;
+ state.onUpdate = function(value) {
+ element.style.opacity = value;
+ if (value == state.to && onComplete)
+ onComplete();
+ };
+ state.timerId = setInterval(function() { Tween.performTweenStep(state); }, 50);
+ return state.timerId;
+ },
+
+ performTweenStep: function(state) {
+ var elapsed = (new Date()).getTime() - state.startTime;
+ if (elapsed >= state.duration) {
+ clearInterval(state.timerId);
+ state.onUpdate(state.to)
+ } else {
+ var value = (elapsed / state.duration) * (state.to - state.from) + state.from;
+ state.onUpdate(value);
+ }
+ }
+};
+
+/*
+ * Adds the given CSS to the page.
+ */
+function addCssToPage(css, id) {
+ var head = document.getElementsByTagName("head")[0];
+ if (!head) {
+ head = document.createElement("head");
+ document.documentElement.appendChild(head);
+ }
+ var style = document.createElement("style");
+ style.id = id;
+ style.type = "text/css";
+ style.appendChild(document.createTextNode(css));
+ head.appendChild(style);
+}
+
+initializePreDomReady();
+window.addEventListener("DOMContentLoaded", initializeOnDomReady);
+
+window.onbeforeunload = function() {
+ chrome.extension.sendRequest({ handler: "updateScrollPosition",
+ scrollX: window.scrollX, scrollY: window.scrollY });
+}
diff --git a/content_scripts/vomnibar.js b/content_scripts/vomnibar.js
new file mode 100644
index 00000000..4f136635
--- /dev/null
+++ b/content_scripts/vomnibar.js
@@ -0,0 +1,232 @@
+var vomnibar = (function() {
+ var vomnibarUI = null; // the dialog instance for this window
+ var completers = { };
+
+ function getCompleter(name) {
+ if (!(name in completers))
+ completers[name] = new BackgroundCompleter(name);
+ return completers[name];
+ }
+
+ /*
+ * Activate the Vomnibox.
+ */
+ function activate(completerName, refreshInterval, initialQueryValue) {
+ var completer = getCompleter(completerName);
+ if (!vomnibarUI)
+ vomnibarUI = new VomnibarUI(10);
+ completer.refresh();
+ vomnibarUI.setCompleter(completer);
+ vomnibarUI.setRefreshInterval(refreshInterval);
+ if (initialQueryValue)
+ vomnibarUI.setQuery(initialQueryValue);
+ vomnibarUI.show();
+ }
+
+ /** User interface for fuzzy completion */
+ var VomnibarUI = Class.extend({
+ init: function(maxResults) {
+ this.prompt = '>';
+ this.maxResults = maxResults;
+ this.refreshInterval = 0;
+ this.initDom();
+ },
+
+ setQuery: function(query) { this.input.value = query; },
+
+ setCompleter: function(completer) {
+ this.completer = completer;
+ this.reset();
+ },
+
+ setRefreshInterval: function(refreshInterval) { this.refreshInterval = refreshInterval; },
+
+ show: function() {
+ this.box.style.display = "block";
+ this.input.focus();
+ handlerStack.push({ keydown: this.onKeydown.bind(this) });
+ },
+
+ hide: function() {
+ this.box.style.display = "none";
+ this.completionList.style.display = "none";
+ this.input.blur();
+ handlerStack.pop();
+ },
+
+ reset: function() {
+ this.input.value = "";
+ this.updateTimer = null;
+ this.completions = [];
+ this.selection = 0;
+ this.update(true);
+ },
+
+ updateSelection: function() {
+ if (this.completions.length > 0)
+ this.selection = Math.min(this.selection, this.completions.length - 1);
+ for (var i = 0; i < this.completionList.children.length; ++i)
+ this.completionList.children[i].className = (i == this.selection) ? "selected" : "";
+ },
+
+ /*
+ * Returns the user's action ("up", "down", "enter", "dismiss" or null) based on their keypress.
+ * We support the arrow keys and other shortcuts for moving, so this method hides that complexity.
+ */
+ actionFromKeyEvent: function(event) {
+ var key = getKeyChar(event);
+ if (isEscape(event))
+ return "dismiss";
+ else if (key == "up" ||
+ (event.shiftKey && event.keyCode == keyCodes.tab) ||
+ (event.ctrlKey && (key == "k" || key == "p")))
+ return "up";
+ else if (key == "down" ||
+ (event.keyCode == keyCodes.tab && !event.shiftKey) ||
+ (event.ctrlKey && (key == "j" || key == "n")))
+ return "down";
+ else if (event.keyCode == keyCodes.enter)
+ return "enter";
+ },
+
+ onKeydown: function(event) {
+ var action = this.actionFromKeyEvent(event);
+ if (!action) return true; // pass through
+
+ if (action == "dismiss") {
+ this.hide();
+ }
+ else if (action == "up") {
+ if (this.selection > 0)
+ this.selection -= 1;
+ this.updateSelection();
+ }
+ else if (action == "down") {
+ if (this.selection < this.completions.length - 1)
+ this.selection += 1;
+ this.updateSelection();
+ }
+ else if (action == "enter") {
+ this.update(true, function() {
+ // Shift+Enter will open the result in a new tab instead of the current tab.
+ var openInNewTab = (event.shiftKey || isPrimaryModifierKey(event));
+ this.completions[this.selection].performAction(openInNewTab);
+ this.hide();
+ }.proxy(this));
+ }
+
+ // It seems like we have to manually supress the event here and still return true.
+ event.stopPropagation();
+ event.preventDefault();
+ return true;
+ },
+
+ updateCompletions: function(callback) {
+ query = this.input.value.replace(/^\s*/, "");
+
+ this.completer.filter(query, this.maxResults, function(completions) {
+ this.completions = completions;
+
+ // update completion list with the new data
+ this.completionList.innerHTML = completions.map(function(completion) {
+ return "<li>" + completion.html + "</li>";
+ }).join('');
+
+ this.completionList.style.display = this.completions.length > 0 ? "block" : "none";
+ this.updateSelection();
+ if (callback) callback();
+ }.proxy(this));
+ },
+
+ update: function(force, callback) {
+ force = force || false; // explicitely default to asynchronous updating
+
+ if (force) {
+ // cancel scheduled update
+ if (this.updateTimer !== null)
+ window.clearTimeout(this.updateTimer);
+ this.updateCompletions(callback);
+ } else if (this.updateTimer !== null) {
+ // an update is already scheduled, don't do anything
+ return;
+ } else {
+ // always update asynchronously for better user experience and to take some load off the CPU
+ // (not every keystroke will cause a dedicated update)
+ this.updateTimer = setTimeout(function() {
+ this.updateCompletions(callback);
+ this.updateTimer = null;
+ }.proxy(this), this.refreshInterval);
+ }
+ },
+
+ initDom: function() {
+ this.box = utils.createElementFromHtml(
+ '<div id="vomnibar" class="vimiumReset">'+
+ '<div class="input">'+
+ '<span class="prompt">' + utils.escapeHtml(this.prompt) + '</span> '+
+ '<input type="text" class="query"></span></div>'+
+ '<ul></ul></div>');
+ this.box.style.display = 'none';
+ document.body.appendChild(this.box);
+
+ this.input = document.querySelector("#vomnibar .query");
+ this.input.addEventListener("input", function() { this.update(); }.bind(this));
+ this.completionList = document.querySelector("#vomnibar ul");
+ this.completionList.style.display = "none";
+ }
+ });
+
+ /*
+ * Sends filter and refresh requests to a Vomnibox completer on the background page.
+ */
+ var BackgroundCompleter = Class.extend({
+ /* - name: The background page completer that you want to interface with. Either "omni" or "tabs". */
+ init: function(name) {
+ this.name = name;
+ this.filterPort = chrome.extension.connect({ name: "filterCompleter" });
+ },
+
+ refresh: function() { chrome.extension.sendRequest({ handler: "refreshCompleter", name: this.name }); },
+
+ filter: function(query, maxResults, callback) {
+ var id = utils.createUniqueId();
+ this.filterPort.onMessage.addListener(function(msg) {
+ if (msg.id != id) return;
+ callback(msg.results.map(function(result) {
+ // functionName will be either "navigateToUrl" or "switchToTab". args will be a URL or a tab ID.
+ var functionToCall = completionActions[result.action.functionName];
+ result.performAction = functionToCall.curry(result.action.args);
+ return result;
+ }));
+ });
+ this.filterPort.postMessage({ id: id, name: this.name, query: query, maxResults: maxResults });
+ }
+ });
+
+ /*
+ * These are the actions we can perform when the user selects a result in the Vomnibox.
+ */
+ var completionActions = {
+ navigateToUrl: function(url, openInNewTab) {
+ // If the URL is a bookmarklet prefixed with javascript:, we shouldn't open that in a new tab.
+ if (url.indexOf("javascript:") == 0)
+ openInNewTab = false;
+ chrome.extension.sendRequest({
+ handler: openInNewTab ? "openUrlInNewTab" : "openUrlInCurrentTab",
+ url: url,
+ selected: openInNewTab
+ });
+ },
+
+ switchToTab: function(tabId) {
+ chrome.extension.sendRequest({ handler: "selectSpecificTab", id: tabId });
+ }
+ };
+
+ // public interface
+ return {
+ activate: function() { activate("omni", 100); },
+ activateWithCurrentUrl: function() { activate("omni", 100, window.location.toString()); },
+ activateTabSelection: function() { activate("tabs", 0); }
+ }
+})();