diff options
Diffstat (limited to 'content_scripts/vomnibar.js')
| -rw-r--r-- | content_scripts/vomnibar.js | 232 |
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); } + } +})(); |
