aboutsummaryrefslogtreecommitdiffstats
path: root/content_scripts
diff options
context:
space:
mode:
authorPhil Crosby2012-05-05 20:15:27 -0700
committerPhil Crosby2012-05-05 20:24:33 -0700
commit083ed4dc8282de961e1733e1d98a792d79befc5f (patch)
tree30eb6fef963d3a70204c53c6a4c22c2893326a6c /content_scripts
parent6cec158a79263067e14ba9a8efef7bd2626203ad (diff)
downloadvimium-083ed4dc8282de961e1733e1d98a792d79befc5f.tar.bz2
Put content scripts and background scripts in separate directories, so the purpose and execution mode are more clear.
Sorry if you had patches in your local copies and this breaks them -- these renames were a long time coming, and now is better than later.
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); }
+ }
+})();