diff options
Diffstat (limited to 'content_scripts/vomnibar.coffee')
| -rw-r--r-- | content_scripts/vomnibar.coffee | 212 |
1 files changed, 212 insertions, 0 deletions
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 |
