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; } -  } -})(); | 
