diff options
| author | Phil Crosby | 2012-06-12 21:59:20 -0700 |
|---|---|---|
| committer | Phil Crosby | 2012-06-12 22:00:35 -0700 |
| commit | f69f952d9332dcc6e4831ec52982f0012d4aed9f (patch) | |
| tree | af7c89b737509a09124762e45f4c718c8bd0cd66 | |
| parent | 38bb8e5850e2352266c84b2ed9db39e04ca1e694 (diff) | |
| download | vimium-f69f952d9332dcc6e4831ec52982f0012d4aed9f.tar.bz2 | |
Port vomnibar.js to coffeescript
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | content_scripts/vomnibar.coffee | 212 | ||||
| -rw-r--r-- | content_scripts/vomnibar.js | 253 |
3 files changed, 213 insertions, 253 deletions
@@ -3,6 +3,7 @@ background_scripts/commands.js background_scripts/settings.js content_scripts/link_hints.js content_scripts/vimium_frontend.js +content_scripts/vomnibar.js tests/completion_test.js tests/test_helper.js tests/utils_test.js diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee new file mode 100644 index 00000000..a9a5af26 --- /dev/null +++ b/content_scripts/vomnibar.coffee @@ -0,0 +1,212 @@ +Vomnibar = + vomnibarUI: null # the dialog instance for this window + completers: {} + + getCompleter: (name) -> + if (!(name of @completers)) + @completers[name] = new BackgroundCompleter(name) + @completers[name] + + # + # Activate the Vomnibox. + # + activateWithCompleter: (completerName, refreshInterval, initialQueryValue) -> + completer = @getCompleter(completerName) + @vomnibarUI = new VomnibarUI() unless @vomnibarUI + completer.refresh() + @vomnibarUI.setCompleter(completer) + @vomnibarUI.setRefreshInterval(refreshInterval) + @vomnibarUI.show() + if (initialQueryValue) + @vomnibarUI.setQuery(initialQueryValue) + @vomnibarUI.update() + + activate: -> @activateWithCompleter("omni", 100) + activateWithCurrentUrl: -> @activateWithCompleter("omni", 100, window.location.toString()) + activateTabSelection: -> @activateWithCompleter("tabs", 0) + getUI: -> @vomnibarUI + + +class VomnibarUI + constructor: -> + @refreshInterval = 0 + @initDom() + + setQuery: (query) -> @input.value = query + + setCompleter: (completer) -> + @completer = completer + @reset() + + setRefreshInterval: (refreshInterval) -> @refreshInterval = refreshInterval + + show: -> + @box.style.display = "block" + @input.focus() + handlerStack.push({ keydown: @onKeydown.bind(this) }) + + hide: -> + @box.style.display = "none" + @completionList.style.display = "none" + @input.blur() + handlerStack.pop() + + reset: -> + @input.value = "" + @updateTimer = null + @completions = [] + @selection = -1 + @update(true) + + updateSelection: -> + if (@completions.length > 0) + @selection = Math.min(@selection, @completions.length - 1) + for i in [0...@completionList.children.length] + @completionList.children[i].className = (if i == @selection then "selected" else "") + + # + # 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: (event) -> + key = KeyboardUtils.getKeyChar(event) + if (KeyboardUtils.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: (event) -> + action = @actionFromKeyEvent(event) + return true unless action # pass through + + openInNewTab = (event.shiftKey || KeyboardUtils.isPrimaryModifierKey(event)) + if (action == "dismiss") + @hide() + else if (action == "up") + @selection -=1 if @selection >= 0 + @updateSelection() + else if (action == "down") + @selection += 1 if (@selection < @completions.length - 1) + @updateSelection() + else if (action == "enter") + # If they type something and hit enter without selecting a completion from our list of suggestions, + # try to open their query as a URL directly. If it doesn't look like a URL, we will search using + # google. + if (@selection == -1) + query = @input.value.trim() + chrome.extension.sendRequest({ + handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab" + url: query }) + else + @update true, => + # Shift+Enter will open the result in a new tab instead of the current tab. + @completions[@selection].performAction(openInNewTab) + @hide() + + # It seems like we have to manually supress the event here and still return true. + event.stopPropagation() + event.preventDefault() + true + + updateCompletions: (callback) -> + query = @input.value.trim() + + @completer.filter query, (completions) => + @completions = completions + @populateUiWithCompletions(completions) + callback() if callback + + populateUiWithCompletions: (completions) -> + # update completion list with the new data + @completionList.innerHTML = completions.map((completion) -> "<li>" + completion.html + "</li>").join("") + @completionList.style.display = if completions.length > 0 then "block" else "none" + @updateSelection() + + update: (updateSynchronously, callback) -> + if (updateSynchronously) + # cancel scheduled update + if (@updateTimer != null) + window.clearTimeout(@updateTimer) + @updateCompletions(callback) + else if (@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) + @updateTimer = setTimeout(=> + @updateCompletions(callback) + @updateTimer = null + @refreshInterval) + + initDom: -> + @box = Utils.createElementFromHtml( + '<div id="vomnibar" class="vimiumReset">' + + '<div class="searchArea">' + + '<input type="text" />' + + '</div>' + + '<ul></ul>' + + '</div>') + @box.style.display = "none" + document.body.appendChild(@box) + + @input = document.querySelector("#vomnibar input") + @input.addEventListener "input", => @update() + console.log("@input:", @input); + @completionList = document.querySelector("#vomnibar ul") + @completionList.style.display = "none" + +# +# Sends filter and refresh requests to a Vomnibox completer on the background page. +# +class BackgroundCompleter + # - name: The background page completer that you want to interface with. Either "omni" or "tabs". */ + constructor: (@name) -> + @filterPort = chrome.extension.connect({ name: "filterCompleter" }) + + refresh: -> chrome.extension.sendRequest({ handler: "refreshCompleter", name: @name }) + + filter: (query, callback) -> + id = Utils.createUniqueId() + @filterPort.onMessage.addListener (msg) -> + return if (msg.id != id) + # The result objects coming from the background page will be of the form: + # { html: "", type: "", url: "" } + # type will be one of [tab, bookmark, history, domain]. + results = msg.results.map (result) -> + functionToCall = if (result.type == "tab") + BackgroundCompleter.completionActions.switchToTab.curry(result.tabId) + else + BackgroundCompleter.completionActions.navigateToUrl.curry(result.url) + result.performAction = functionToCall + result + callback(results) + + @filterPort.postMessage({ id: id, name: @name, query: query }) + +extend BackgroundCompleter, + # + # These are the actions we can perform when the user selects a result in the Vomnibox. + # + completionActions: + navigateToUrl: (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: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab" + url: url, + selected: openInNewTab) + + switchToTab: (tabId) -> chrome.extension.sendRequest({ handler: "selectSpecificTab", id: tabId }) + +root = exports ? window +root.Vomnibar = Vomnibar
\ No newline at end of file diff --git a/content_scripts/vomnibar.js b/content_scripts/vomnibar.js deleted file mode 100644 index c1deeb3c..00000000 --- a/content_scripts/vomnibar.js +++ /dev/null @@ -1,253 +0,0 @@ -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(); - completer.refresh(); - vomnibarUI.setCompleter(completer); - vomnibarUI.setRefreshInterval(refreshInterval); - vomnibarUI.show(); - if (initialQueryValue) { - vomnibarUI.setQuery(initialQueryValue); - vomnibarUI.update(); - } - } - - /** User interface for fuzzy completion */ - var VomnibarUI = Class.extend({ - init: function() { - 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 = -1; - 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 = KeyboardUtils.getKeyChar(event); - if (KeyboardUtils.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 - - var openInNewTab = (event.shiftKey || KeyboardUtils.isPrimaryModifierKey(event)); - 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") { - // If they type something and hit enter without selecting a completion from our list of suggestions, - // try to open their query as a URL directly. If it doesn't look like a URL, we will search using - // google. - if (this.selection == -1) { - var query = this.input.value.trim(); - chrome.extension.sendRequest({ - handler: openInNewTab ? "openUrlInNewTab" : "openUrlInCurrentTab", - url: query }); - } else { - this.update(true, function() { - // Shift+Enter will open the result in a new tab instead of the current tab. - 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.trim(); - - this.completer.filter(query, function(completions) { - this.completions = completions; - this.populateUiWithCompletions(completions); - if (callback) callback(); - }.proxy(this)); - }, - - populateUiWithCompletions: function(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 = (completions.length > 0) ? "block" : "none"; - this.updateSelection(); - }, - - update: function(updateSynchronously, callback) { - if (updateSynchronously) { - // 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="searchArea">' + - '<input type="text" />' + - '</div>' + - '<ul></ul>' + - '</div>'); - this.box.style.display = 'none'; - document.body.appendChild(this.box); - - this.input = document.querySelector("#vomnibar input"); - 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, callback) { - var id = Utils.createUniqueId(); - this.filterPort.onMessage.addListener(function(msg) { - if (msg.id != id) return; - // The result objects coming from the background page will be of the form: - // { html: "", type: "", url: "" } - // type will be one of [tab, bookmark, history, domain]. - var results = msg.results.map(function(result) { - var functionToCall; - if (result.type == "tab") - functionToCall = completionActions.switchToTab.curry(result.tabId); - else - functionToCall = completionActions.navigateToUrl.curry(result.url); - result.performAction = functionToCall; - return result; - }); - callback(results); - }); - this.filterPort.postMessage({ id: id, name: this.name, query: query }); - } - }); - - /* - * 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); }, - /* Used by our vomnibar dev harness. */ - getUI: function() { return vomnibarUI; } - } -})(); |
