aboutsummaryrefslogtreecommitdiffstats
path: root/content_scripts/vomnibar.js
diff options
context:
space:
mode:
Diffstat (limited to 'content_scripts/vomnibar.js')
-rw-r--r--content_scripts/vomnibar.js232
1 files changed, 232 insertions, 0 deletions
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); }
+ }
+})();