diff options
| -rw-r--r-- | background_scripts/completion.coffee | 2 | ||||
| -rw-r--r-- | background_scripts/exclusions.coffee | 35 | ||||
| -rw-r--r-- | background_scripts/main.coffee | 19 | ||||
| -rw-r--r-- | lib/utils.coffee | 18 | ||||
| -rw-r--r-- | pages/exclusions.html | 13 | ||||
| -rw-r--r-- | pages/options.coffee | 276 | ||||
| -rw-r--r-- | pages/options.css | 232 | ||||
| -rw-r--r-- | pages/options.html | 252 | ||||
| -rw-r--r-- | pages/popup.coffee | 110 | ||||
| -rw-r--r-- | pages/popup.html | 113 | ||||
| -rw-r--r-- | tests/unit_tests/exclusion_test.coffee | 50 | ||||
| -rw-r--r-- | tests/unit_tests/utils_test.coffee | 3 | 
12 files changed, 554 insertions, 569 deletions
| diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee index dc5519d5..d62f82fe 100644 --- a/background_scripts/completion.coffee +++ b/background_scripts/completion.coffee @@ -327,7 +327,7 @@ class SearchEngineCompleter      searchEngineMatch = this.getSearchEngineMatches(queryTerms[0])      suggestions = []      if searchEngineMatch -      searchEngineMatch = searchEngineMatch.replace(/%s/g, queryTerms[1..].join(" ")) +      searchEngineMatch = searchEngineMatch.replace(/%s/g, Utils.createSearchQuery queryTerms[1..])        suggestion = new Suggestion(queryTerms, "search", searchEngineMatch, queryTerms[0] + ": " + queryTerms[1..].join(" "), @computeRelevancy)        suggestions.push(suggestion)      onComplete(suggestions) diff --git a/background_scripts/exclusions.coffee b/background_scripts/exclusions.coffee index 2b34238b..55ced3ef 100644 --- a/background_scripts/exclusions.coffee +++ b/background_scripts/exclusions.coffee @@ -15,17 +15,27 @@ RegexpCache =  # The Exclusions class manages the exclusion rule setting.  # An exclusion is an object with two attributes: pattern and passKeys. -# The exclusions are an array of such objects (because the order matters). +# The exclusions are an array of such objects.  root.Exclusions = Exclusions = +  # Make RegexpCache, which is required on the page popup, accessible via the Exclusions object. +  RegexpCache: RegexpCache    rules: Settings.get("exclusionRules") -  # Return the first exclusion rule matching the URL, or null. -  getRule: (url) -> -    for rule in @rules -      return rule if url.match(RegexpCache.get(rule.pattern)) -    return null +  # Merge the matching rules for URL, or null.  In the normal case, we use the configured @rules; hence, this +  # is the default.  However, when called from the page popup, we are testing what effect candidate new rules +  # would have on the current tab.  In this case, the candidate rules are provided by the caller. +  getRule: (url, rules=@rules) -> +    matches = (rule for rule in rules when rule.pattern and 0 <= url.search(RegexpCache.get(rule.pattern))) +    # An absolute exclusion rule (with no passKeys) takes priority. +    for rule in matches +      return rule unless rule.passKeys +    if 0 < matches.length +      pattern: (rule.pattern for rule in matches).join " | " # Not used; for debugging only. +      passKeys: Utils.distinctCharacters (rule.passKeys for rule in matches).join "" +    else +      null    setRules: (rules) ->      # Callers map a rule to null to have it deleted, and rules without a pattern are useless. @@ -35,19 +45,6 @@ root.Exclusions = Exclusions =    postUpdateHook: (rules) ->      @rules = rules -  # Update an existing rule or add a new rule. -  updateOrAdd: (newRule) -> -    seen = false -    @rules.push(newRule) -    @setRules @rules.map (rule) -> -      if rule.pattern == newRule.pattern -        if seen then null else seen = newRule -      else -        rule - -  remove: (pattern) -> -    @setRules(@rules.filter((rule) -> rule and rule.pattern != pattern)) -  # Development and debug only.  # Enable this (temporarily) to restore legacy exclusion rules from backup.  if false and Settings.has("excludedUrlsBackup") diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index 44ab5bac..4c1b9ae7 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -80,26 +80,10 @@ getCurrentTabUrl = (request, sender) -> sender.tab.url  root.isEnabledForUrl = isEnabledForUrl = (request) ->    rule = Exclusions.getRule(request.url)    { -    rule: rule      isEnabledForUrl: not rule or rule.passKeys      passKeys: rule?.passKeys or ""    } -# Called by the popup UI. -# If the URL pattern matches an existing rule, then the existing rule is updated. Otherwise, a new rule is created. -root.addExclusionRule = (pattern,passKeys) -> -  if pattern = pattern.trim() -    Exclusions.updateOrAdd({ pattern: pattern, passKeys: passKeys }) -    chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT, active: true }, -      (tabs) -> updateActiveState(tabs[0].id)) - -# Called by the popup UI.  Remove all existing exclusion rules with this pattern. -root.removeExclusionRule = (pattern) -> -  if pattern = pattern.trim() -    Exclusions.remove(pattern) -    chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT, active: true }, -      (tabs) -> updateActiveState(tabs[0].id)) -  saveHelpDialogSettings = (request) ->    Settings.set("helpDialog_showAdvancedCommands", request.showAdvancedCommands) @@ -358,7 +342,8 @@ setBrowserActionIcon = (tabId,path) ->  # Updates the browserAction icon to indicate whether Vimium is enabled or disabled on the current page.  # Also propagates new enabled/disabled/passkeys state to active window, if necessary.  # This lets you disable Vimium on a page without needing to reload. -updateActiveState = (tabId) -> +# Exported via root because it's called from the page popup. +root.updateActiveState = updateActiveState = (tabId) ->    enabledIcon = "icons/browser_action_enabled.png"    disabledIcon = "icons/browser_action_disabled.png"    partialIcon = "icons/browser_action_partial.png" diff --git a/lib/utils.coffee b/lib/utils.coffee index 2efb4716..661f7e84 100644 --- a/lib/utils.coffee +++ b/lib/utils.coffee @@ -87,11 +87,17 @@ Utils =      # Fallback: no URL      return false +  # Map a search query to its URL encoded form. The query may be either a string or an array of strings. +  # E.g. "BBC Sport" -> "BBC+Sport". +  createSearchQuery: (query) -> +    query = query.split(/\s+/) if typeof(query) == "string" +    query.map(encodeURIComponent).join "+" +    # Creates a search URL from the given :query.    createSearchUrl: (query) -> -    # it would be better to pull the default search engine from chrome itself, -    # but it is not clear if/how that is possible -    Settings.get("searchUrl") + encodeURIComponent(query) +    # It would be better to pull the default search engine from chrome itself.  However, unfortunately chrome +    # does not provide an API for doing so. +    Settings.get("searchUrl") + @createSearchQuery query    # Converts :string into a Google search if it's not already a URL. We don't bother with escaping characters    # as Chrome will do that for us. @@ -109,6 +115,12 @@ Utils =    # detects both literals and dynamically created strings    isString: (obj) -> typeof obj == 'string' or obj instanceof String +  # Transform "zjkjkabz" into "abjkz". +  distinctCharacters: (str) -> +    unique = "" +    for char in str.split("").sort() +      unique += char unless 0 <= unique.indexOf char +    unique    # Compares two version strings (e.g. "1.1" and "1.5") and returns    # -1 if versionA is < versionB, 0 if they're equal, and 1 if versionA is > versionB. diff --git a/pages/exclusions.html b/pages/exclusions.html new file mode 100644 index 00000000..b09f2895 --- /dev/null +++ b/pages/exclusions.html @@ -0,0 +1,13 @@ +<table id="exclusionRules"> +   <tr> +      <td><span class="exclusionHeaderText">Patterns</span></td> +      <td><span class="exclusionHeaderText">Keys</span></td> +   </tr> +</table> +<template id="exclusionRuleTemplate"> +   <tr class="exclusionRuleTemplateInstance"> +      <td><input/ type="text" class="pattern" spellcheck="false" placeholder="URL pattern"></td> +      <td class="exclusionRulePassKeys"><input/ type="text" class="passKeys" spellcheck="false" placeholder="Exclude keys"></td> +      <td class="exclusionRemoveButton"><input/ type="button" class="exclusionRemoveButtonButton" value="✖"></td> +   </tr> +</template> diff --git a/pages/options.coffee b/pages/options.coffee index cd19fa37..93c9b503 100644 --- a/pages/options.coffee +++ b/pages/options.coffee @@ -1,6 +1,8 @@  $ = (id) -> document.getElementById id +bgUtils = chrome.extension.getBackgroundPage().Utils  bgSettings = chrome.extension.getBackgroundPage().Settings +bgExclusions = chrome.extension.getBackgroundPage().Exclusions  #  # Class hierarchy for various types of option. @@ -11,10 +13,9 @@ class Option    # Static. Array of all options.    @all = [] -  constructor: (field,enableSaveButton) -> -    @field = field +  constructor: (@field,@onUpdated) ->      @element = $(@field) -    @element.addEventListener "change", enableSaveButton +    @element.addEventListener "change", @onUpdated      @fetch()      Option.all.push @ @@ -41,8 +42,6 @@ class Option    # Static method.    @saveOptions: ->      Option.all.map (option) -> option.save() -    $("saveOptions").disabled = true -    $("saveOptions").innerHTML = "No Changes"    # Abstract method; only implemented in sub-classes.    # Populate the option's DOM element (@element) with the setting's current value. @@ -57,16 +56,16 @@ class NumberOption extends Option    readValueFromElement: -> parseFloat @element.value  class TextOption extends Option -  constructor: (field,enableSaveButton) -> -    super(field,enableSaveButton) -    @element.addEventListener "input", enableSaveButton +  constructor: (args...) -> +    super(args...) +    @element.addEventListener "input", @onUpdated    populateElement: (value) -> @element.value = value    readValueFromElement: -> @element.value.trim()  class NonEmptyTextOption extends Option -  constructor: (field,enableSaveButton) -> -    super(field,enableSaveButton) -    @element.addEventListener "input", enableSaveButton +  constructor: (args...) -> +    super(args...) +    @element.addEventListener "input", @onUpdated    populateElement: (value) -> @element.value = value    # If the new value is not empty, then return it. Otherwise, restore the default value. @@ -80,18 +79,24 @@ class ExclusionRulesOption extends Option    constructor: (args...) ->      super(args...)      $("exclusionAddButton").addEventListener "click", (event) => -      @appendRule { pattern: "", passKeys: "" } -      # Focus the pattern element in the new rule. -      @element.children[@element.children.length-1].children[0].children[0].focus() -      # Scroll the new rule into view. +      @addRule() + +  # Add a new rule, focus its pattern, scroll it into view, and return the newly-added element.  On the +  # options page, there is no current URL, so there is no initial pattern.  This is the default.  On the popup +  # page (see ExclusionRulesOnPopupOption), the pattern is pre-populated based on the current tab's URL. +  addRule: (pattern="") -> +      element = @appendRule { pattern: pattern, passKeys: "" } +      @getPattern(element).focus()        exclusionScrollBox = $("exclusionScrollBox")        exclusionScrollBox.scrollTop = exclusionScrollBox.scrollHeight +      @onUpdated() +      element    populateElement: (rules) ->      for rule in rules        @appendRule rule -  # Append a row for a new rule. +  # Append a row for a new rule.  Return the newly-added element.    appendRule: (rule) ->      content = document.querySelector('#exclusionRuleTemplate').content      row = document.importNode content, true @@ -100,22 +105,21 @@ class ExclusionRulesOption extends Option        element = row.querySelector ".#{field}"        element.value = rule[field]        for event in [ "input", "change" ] -        element.addEventListener event, enableSaveButton +        element.addEventListener event, @onUpdated -    remove = row.querySelector ".exclusionRemoveButton" -    remove.addEventListener "click", (event) => -      row = event.target.parentNode.parentNode -      row.parentNode.removeChild row -      enableSaveButton() +    @getRemoveButton(row).addEventListener "click", (event) => +      rule = event.target.parentNode.parentNode +      rule.parentNode.removeChild rule +      @onUpdated()      @element.appendChild row +    @element.children[@element.children.length-1]    readValueFromElement: ->      rules =        for element in @element.getElementsByClassName "exclusionRuleTemplateInstance" -        pattern = element.children[0].firstChild.value.trim() -        passKeys = element.children[1].firstChild.value.trim() -        { pattern: pattern, passKeys: passKeys } +        pattern: @getPattern(element).value.split(/\s+/).join "" +        passKeys: @getPassKeys(element).value.split(/\s+/).join ""      rules.filter (rule) -> rule.pattern    areEqual: (a,b) -> @@ -124,48 +128,123 @@ class ExclusionRulesOption extends Option      flatten = (rule) -> if rule and rule.pattern then rule.pattern + "\n" + rule.passKeys else ""      a.map(flatten).join("\n") == b.map(flatten).join("\n") -# -# Operations for page elements. -enableSaveButton = -> -  $("saveOptions").removeAttribute "disabled" -  $("saveOptions").innerHTML = "Save Changes" - -# Display either "linkHintNumbers" or "linkHintCharacters", depending upon "filterLinkHints". -maintainLinkHintsView = -> -  hide = (el) -> el.parentNode.parentNode.style.display = "none" -  show = (el) -> el.parentNode.parentNode.style.display = "table-row" -  if $("filterLinkHints").checked -    hide $("linkHintCharacters") -    show $("linkHintNumbers") -  else -    show $("linkHintCharacters") -    hide $("linkHintNumbers") - -toggleAdvancedOptions = -  do (advancedMode=false) -> -    (event) -> -      if advancedMode -        $("advancedOptions").style.display = "none" -        $("advancedOptionsLink").innerHTML = "Show advanced options…" +  # Accessors for the three main sub-elements of an "exclusionRuleTemplateInstance". +  getPattern: (element) -> element.querySelector(".pattern") +  getPassKeys: (element) -> element.querySelector(".passKeys") +  getRemoveButton: (element) -> element.querySelector(".exclusionRemoveButtonButton") + +# ExclusionRulesOnPopupOption is ExclusionRulesOption, extended with some UI tweeks suitable for use in the +# page popup.  This also differs from ExclusionRulesOption in that, on the page popup, there is always a URL +# (@url) associated with the current tab. +class ExclusionRulesOnPopupOption extends ExclusionRulesOption +  constructor: (@url, args...) -> +    super(args...) + +  addRule: -> +    element = super @generateDefaultPattern() +    @activatePatternWatcher element +    # ExclusionRulesOption.addRule()/super() has focused the pattern.  Here, focus the passKeys instead; +    # because, in the popup, we already have a pattern, so the user is more likely to edit the passKeys. +    @getPassKeys(element).focus() +    # Return element (for consistency with ExclusionRulesOption.addRule()). +    element + +  populateElement: (rules) -> +    super(rules) +    elements = @element.getElementsByClassName "exclusionRuleTemplateInstance" +    @activatePatternWatcher element for element in elements + +    haveMatch = false +    for element in elements +      pattern = @getPattern(element).value.trim() +      if 0 <= @url.search bgExclusions.RegexpCache.get pattern +        haveMatch = true +        @getPassKeys(element).focus() +      else +        element.style.display = 'none' +    @addRule() unless haveMatch + +  # Provide visual feedback (make it red) when a pattern does not match the current tab's URL. +  activatePatternWatcher: (element) -> +    patternElement = element.children[0].firstChild +    patternElement.addEventListener "keyup", => +      if @url.match bgExclusions.RegexpCache.get patternElement.value +        patternElement.title = patternElement.style.color = ""        else -        $("advancedOptions").style.display = "table-row-group" -        $("advancedOptionsLink").innerHTML = "Hide advanced options" -      advancedMode = !advancedMode -      event.preventDefault() -      # Prevent the "advanced options" link from retaining the focus. -      document.activeElement.blur() - -activateHelpDialog = -> -  showHelpDialog chrome.extension.getBackgroundPage().helpDialogHtml(true, true, "Command Listing"), frameId -  # Prevent the "show help" link from retaining the focus. -  document.activeElement.blur() +        patternElement.style.color = "red" +        patternElement.title = "Red text means that the pattern does not\nmatch the current URL." + +  # Generate a default exclusion-rule pattern from a URL.  This is then used to pre-populate the pattern on +  # the page popup. +  generateDefaultPattern: -> +    if /^https?:\/\/./.test @url +      # The common use case is to disable Vimium at the domain level. +      # Generate "https?://www.example.com/*" from "http://www.example.com/path/to/page.html". +      "https?:/" + @url.split("/",3)[1..].join("/") + "/*" +    else if /^[a-z]{3,}:\/\/./.test @url +      # Anything else which seems to be a URL. +      @url.split("/",3).join("/") + "/*" +    else +      @url + "*" + +initOptionsPage = -> +  onUpdated = -> +    $("saveOptions").removeAttribute "disabled" +    $("saveOptions").innerHTML = "Save Changes" + +  # Display either "linkHintNumbers" or "linkHintCharacters", depending upon "filterLinkHints". +  maintainLinkHintsView = -> +    hide = (el) -> el.parentNode.parentNode.style.display = "none" +    show = (el) -> el.parentNode.parentNode.style.display = "table-row" +    if $("filterLinkHints").checked +      hide $("linkHintCharacters") +      show $("linkHintNumbers") +    else +      show $("linkHintCharacters") +      hide $("linkHintNumbers") + +  toggleAdvancedOptions = +    do (advancedMode=false) -> +      (event) -> +        if advancedMode +          $("advancedOptions").style.display = "none" +          $("advancedOptionsLink").innerHTML = "Show advanced options…" +        else +          $("advancedOptions").style.display = "table-row-group" +          $("advancedOptionsLink").innerHTML = "Hide advanced options" +        advancedMode = !advancedMode +        event.preventDefault() +        # Prevent the "advanced options" link from retaining the focus. +        document.activeElement.blur() + +  activateHelpDialog = -> +    showHelpDialog chrome.extension.getBackgroundPage().helpDialogHtml(true, true, "Command Listing"), frameId +    # Prevent the "show help" link from retaining the focus when clicked. +    document.activeElement.blur() + +  saveOptions = -> +    Option.saveOptions() +    $("saveOptions").disabled = true +    $("saveOptions").innerHTML = "No Changes" -# -# Initialization. -document.addEventListener "DOMContentLoaded", -> +  $("saveOptions").addEventListener "click", saveOptions +  $("advancedOptionsLink").addEventListener "click", toggleAdvancedOptions +  $("showCommands").addEventListener "click", activateHelpDialog +  $("filterLinkHints").addEventListener "click", maintainLinkHintsView + +  for element in document.getElementsByClassName "nonEmptyTextOption" +    element.className = element.className + " example info" +    element.innerHTML = "Leave empty to reset this option." -  # Populate options.  The constructor adds each new object to "Option.all". -  new type(name,enableSaveButton) for name, type of { +  maintainLinkHintsView() +  window.onbeforeunload = -> "You have unsaved changes to options." unless $("saveOptions").disabled + +  document.addEventListener "keyup", (event) -> +    if event.ctrlKey and event.keyCode == 13 +      document.activeElement.blur() if document?.activeElement?.blur +      saveOptions() + +  options =      exclusionRules: ExclusionRulesOption      filterLinkHints: CheckBoxOption      hideHud: CheckBoxOption @@ -181,22 +260,63 @@ document.addEventListener "DOMContentLoaded", ->      searchEngines: TextOption      searchUrl: NonEmptyTextOption      userDefinedLinkHintCss: TextOption -  } -  $("saveOptions").addEventListener "click", Option.saveOptions -  $("advancedOptionsLink").addEventListener "click", toggleAdvancedOptions -  $("showCommands").addEventListener "click", activateHelpDialog -  $("filterLinkHints").addEventListener "click", maintainLinkHintsView +  # Populate options. The constructor adds each new object to "Option.all". +  for name, type of options +    new type(name,onUpdated) + +initPopupPage = -> +  chrome.tabs.getSelected null, (tab) -> +    exclusions = null +    document.getElementById("optionsLink").setAttribute "href", chrome.runtime.getURL("pages/options.html") + +    updateState = -> +      rule = bgExclusions.getRule tab.url, exclusions.readValueFromElement() +      $("state").innerHTML = "Vimium will " + +        if rule and rule.passKeys +          "exclude <span class='code'>#{rule.passKeys}</span>" +        else if rule +          "be disabled" +        else +          "be enabled" + +    onUpdated = -> +      $("helpText").innerHTML = "Type <strong>Ctrl-Enter</strong> to save and close." +      $("saveOptions").removeAttribute "disabled" +      $("saveOptions").innerHTML = "Save Changes" +      updateState() if exclusions + +    saveOptions = -> +      Option.saveOptions() +      $("saveOptions").innerHTML = "Saved" +      $("saveOptions").disabled = true +      chrome.tabs.query { windowId: chrome.windows.WINDOW_ID_CURRENT, active: true }, (tabs) -> +        chrome.extension.getBackgroundPage().updateActiveState(tabs[0].id) -  for element in document.getElementsByClassName "nonEmptyTextOption" -    element.className = element.className + " example info" -    element.innerHTML = "Leave empty to reset this option." +    $("saveOptions").addEventListener "click", saveOptions -  maintainLinkHintsView() -  window.onbeforeunload = -> "You have unsaved changes to options." unless $("saveOptions").disabled +    document.addEventListener "keyup", (event) -> +      if event.ctrlKey and event.keyCode == 13 +        saveOptions() +        window.close() -  document.addEventListener "keyup", (event) -> -    if event.ctrlKey and event.keyCode == 13 -      document.activeElement.blur() if document?.activeElement?.blur -      Option.saveOptions() +    # Populate options. Just one, here. +    exclusions = new ExclusionRulesOnPopupOption(tab.url, "exclusionRules", onUpdated) + +    updateState() +    document.addEventListener "keyup", updateState + +# +# Initialization. +document.addEventListener "DOMContentLoaded", -> +  xhr = new XMLHttpRequest() +  xhr.open 'GET', chrome.extension.getURL('pages/exclusions.html'), true +  xhr.onreadystatechange = -> +    if xhr.readyState == 4 +      $("exclusionScrollBox").innerHTML = xhr.responseText +      switch location.pathname +        when "/pages/options.html" then initOptionsPage() +        when "/pages/popup.html" then initPopupPage() + +  xhr.send() diff --git a/pages/options.css b/pages/options.css new file mode 100644 index 00000000..5b098c8f --- /dev/null +++ b/pages/options.css @@ -0,0 +1,232 @@ +/* NOTE: This stylesheet is included in both options.html and popup.html.  So changes here affect +   both of these. */ +body { +  font: 14px "DejaVu Sans", "Arial", sans-serif; +  color: #303942; +  margin: 0 auto; +} +a, a:visited { color: #15c; } +a:active { color: #052577; } +div#wrapper, #footerWrapper { +  width: 540px; +  margin-left: 35px; +} +header { +  font-size: 18px; +  font-weight: normal; +  border-bottom: 1px solid #eee; +  padding: 20px 0 15px 0; +  width: 100%; +} +button { +  -webkit-user-select: none; +  -webkit-appearance: none; +  background-image: -webkit-linear-gradient(#ededed, #ededed 38%, #dedede); +  border: 1px solid rgba(0, 0, 0, 0.25); +  border-radius: 2px; +  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75); +  color: #444; +  font: inherit; +  text-shadow: 0 1px 0 #f0f0f0; +  height: 24px; +  font-size: 12px; +  padding: 0 10px; +} +button:hover { +  background-image: -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); +  border-color: rgba(0, 0, 0, 0.3); +  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12), inset 0 1px 2px rgba(255, 255, 255, 0.95); +  color: black; +} +button:active { +  background-image: -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); +  box-shadow: none; +  text-shadow: none; +} +button[disabled], button[disabled]:hover, button[disabled]:active { +  background-image: -webkit-linear-gradient(#ededed, #ededed 38%, #dedede); +  border: 1px solid rgba(0, 0, 0, 0.25); +  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75); +  text-shadow: 0 1px 0 #f0f0f0; +  color: #888; +} +input[type="checkbox"] { +  -webkit-user-select: none; +} +label:hover { +  color: black; +} +pre, code, .code { +  font-family: Consolas, "Liberation Mono", Courier, monospace; +} +pre { +  margin: 5px; +  border-left: 1px solid #eee; +  padding-left: 5px; + +} +input, textarea { +  box-sizing: border-box; +} +textarea { +  /* Horizontal resizing is pretty screwy-looking. */ +  resize: vertical; +} +table#options{ +  width: 100%; +  font-size: 14px; +  position: relative; +  border-spacing: 0 23px; +} +.example { +  font-size: 12px; +  line-height: 16px; +  color: #979ca0; +  margin-left: 20px; +} +.info { +  margin-left: 0px; +} +.caption { +  margin-right: 10px; +  min-width: 130px; +  padding-top: 3px; +  vertical-align: top; +} +td { padding: 0; } +div#exampleKeyMapping { +  margin-left: 10px; +  margin-top: 5px; +} +input#linkHintCharacters { +  width: 100%; +} +input#linkHintNumbers { +  width: 100%; +} +input#linkHintCharacters { +  width: 100%; +} +input#scrollStepSize { +  width: 40px; +  margin-right: 3px; +} +textarea#userDefinedLinkHintCss, textarea#keyMappings, textarea#searchEngines { +  width: 100%;; +  min-height: 130px; +  white-space: nowrap; +} +input#previousPatterns, input#nextPatterns { +  width: 100%; +} +input#newTabUrl { +  width: 100%; +} +input#searchUrl { +  width: 100%; +} +#status { +  margin-left: 10px; +  font-size: 80%; +} +/* Make the caption in the settings table as small as possible, to pull the other fields to the right. */ +.caption { +  width: 1px; +  white-space: nowrap; +} +#buttonsPanel { width: 100%; } +#advancedOptions { display: none; } +#advancedOptionsLink { line-height: 24px; } +.help { +  position: absolute; +  right: -320px; +  width: 320px; +} +input:read-only { +  background-color: #eee; +  color: #666; +  pointer-events: none; +  -webkit-user-select: none; +} +input[type="text"], textarea { +  border: 1px solid #bfbfbf; +  border-radius: 2px; +  color: #444; +  font: inherit; +  padding: 3px; +} +button:focus, input[type="text"]:focus, textarea:focus { +  -webkit-transition: border-color 200ms; +  border-color: #4d90fe; +  outline: none; +} +/* Boolean options have a tighter form representation than text options. */ +td.booleanOption { font-size: 12px; } +/* Ids and classes for rendering exclusionRules */ +#exclusionScrollBox { +  overflow: scroll; +  overflow-x: hidden; +  overflow-y: auto; +  /* Each exclusion rule is about 30px, so this allows 7 before scrolling */ +  max-height: 215px; +  min-height: 75px; +  border-radius: 2px; +  color: #444; +  width: 100% +} +#exclusionRules { +  width: 100%; +} +.exclusionRulePassKeys { +  width: 33%; +} +.exclusionRemoveButton { +  width: 1px; /* 1px; smaller than the button itself. */ +} +.exclusionRemoveButtonButton { +  border: none; +  background-color: #fff; +  color: #979ca0; +} +.exclusionRemoveButtonButton:hover { +  color: #444; +} +input.pattern, input.passKeys, .exclusionHeaderText { +  width: 100%; +  font-family: Consolas, "Liberation Mono", Courier, monospace; +  font-size: 14px; +} +.exclusionHeaderText { +  padding-left: 3px; +  color: #979ca0; +} +#exclusionAddButton { +  float: right; +  margin-right: 0px; +  margin-top: 5px; +} +#footer { +  background: #f5f5f5; +  border-top: 1px solid #979ca0; +  position: fixed; +  bottom: 0px; +  z-index: 10; +} +#footer, #footerTable, #footerTableData { +  width: 100%; +} +#endSpace { +  /* Leave space for the fixed footer. */ +  min-height: 30px; +  max-height: 30px; +} +#helpText { +  font-size: 12px; +} +#saveOptionsTableData { +  float: right; +} +#saveOptions { +  white-space: nowrap; +  width: 110px; +} diff --git a/pages/options.html b/pages/options.html index 8e685304..ae45537a 100644 --- a/pages/options.html +++ b/pages/options.html @@ -1,242 +1,9 @@  <html>    <head>      <title>Vimium Options</title> +    <link rel="stylesheet" type="text/css" href="options.css">      <script src="content_script_loader.js"></script> -    <style type="text/css" media="screen"> -      body { -        font: 14px "DejaVu Sans", "Arial", sans-serif; -        color: #303942; -        margin: 0 auto; -      } -      a, a:visited { color: #15c; } -      a:active { color: #052577; } -      div#wrapper, #footerWrapper { -        width: 540px; -        margin-left: 35px; -      } -      header { -        font-size: 18px; -        font-weight: normal; -        border-bottom: 1px solid #eee; -        padding: 20px 0 15px 0; -        width: 100%; -      } -      button { -        -webkit-user-select: none; -        -webkit-appearance: none; -        background-image: -webkit-linear-gradient(#ededed, #ededed 38%, #dedede); -        border: 1px solid rgba(0, 0, 0, 0.25); -        border-radius: 2px; -        box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75); -        color: #444; -        font: inherit; -        text-shadow: 0 1px 0 #f0f0f0; -        height: 24px; -        font-size: 12px; -        padding: 0 10px; -      } -      button:hover { -        background-image: -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); -        border-color: rgba(0, 0, 0, 0.3); -        box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12), inset 0 1px 2px rgba(255, 255, 255, 0.95); -        color: black; -      } -      button:active { -        background-image: -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); -        box-shadow: none; -        text-shadow: none; -      } -      button[disabled], button[disabled]:hover, button[disabled]:active { -        background-image: -webkit-linear-gradient(#ededed, #ededed 38%, #dedede); -        border: 1px solid rgba(0, 0, 0, 0.25); -        box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75); -        text-shadow: 0 1px 0 #f0f0f0; -        color: #888; -      } -      input[type="checkbox"] { -        -webkit-user-select: none; -      } -      label:hover { -        color: black; -      } -      pre, code, .code { -        font-family: Consolas, "Liberation Mono", Courier, monospace; -      } -      pre { -        margin: 5px; -        border-left: 1px solid #eee; -        padding-left: 5px; - -      } -      input, textarea { -        box-sizing: border-box; -      } -      textarea { -        /* Horizontal resizing is pretty screwy-looking. */ -        resize: vertical; -      } -      table#options{ -        width: 100%; -        font-size: 14px; -        position: relative; -        border-spacing: 0 23px; -      } -      .example { -        font-size: 12px; -        line-height: 16px; -        color: #979ca0; -        margin-left: 20px; -      } -      .info { -        margin-left: 0px; -      } -      .caption { -        margin-right: 10px; -        min-width: 130px; -        padding-top: 3px; -        vertical-align: top; -      } -      td { padding: 0; } -      div#exampleKeyMapping { -        margin-left: 10px; -        margin-top: 5px; -      } -      input#linkHintCharacters { -        width: 100%; -      } -      input#linkHintNumbers { -        width: 100%; -      } -      input#linkHintCharacters { -        width: 100%; -      } -      input#scrollStepSize { -        width: 40px; -        margin-right: 3px; -      } -      textarea#userDefinedLinkHintCss, textarea#keyMappings, textarea#searchEngines { -        width: 100%;; -        min-height: 130px; -        white-space: nowrap; -      } -      input#previousPatterns, input#nextPatterns { -        width: 100%; -      } -      input#newTabUrl { -        width: 100%; -      } -      input#searchUrl { -        width: 100%; -      } -      #status { -        margin-left: 10px; -        font-size: 80%; -      } -      /* Make the caption in the settings table as small as possible, to pull the other fields to the right. */ -      .caption { -        width: 1px; -        white-space: nowrap; -      } -      #buttonsPanel { width: 100%; } -      #advancedOptions { display: none; } -      #advancedOptionsLink { line-height: 24px; } -      .help { -        position: absolute; -        right: -320px; -        width: 320px; -      } -      input:read-only { -        background-color: #eee; -        color: #666; -        pointer-events: none; -        -webkit-user-select: none; -      } -      input[type="text"], textarea { -        border: 1px solid #bfbfbf; -        border-radius: 2px; -        color: #444; -        font: inherit; -        padding: 3px; -      } -      button:focus, input[type="text"]:focus, textarea:focus { -        -webkit-transition: border-color 200ms; -        border-color: #4d90fe; -        outline: none; -      } -      /* Boolean options have a tighter form representation than text options. */ -      td.booleanOption { font-size: 12px; } -      /* Ids and classes for rendering exclusionRules */ -      #exclusionScrollBox { -        overflow: scroll; -        overflow-x: hidden; -        overflow-y: auto; -        /* Each exclusion rule is about 30px, so this allows 7 before scrolling */ -        max-height: 215px; -        min-height: 75px; -        border-radius: 2px; -        color: #444; -        width: 100% -      } -      #exclusionRules { -        width: 100%; -      } -      .exclusionRulePassKeys { -        width: 33%; -      } -      .exclusionRemoveButton { -        width: 1px; /* 1px; smaller than the button itself. */ -      } -      .exclusionRemoveButtonButton { -        border: none; -        background-color: #fff; -        color: #979ca0; -      } -      .exclusionRemoveButtonButton:hover { -        color: #444; -      } -      input.pattern, input.passKeys, .exclusionHeaderText { -        width: 100%; -        font-family: Consolas, "Liberation Mono", Courier, monospace; -        font-size: 14px; -      } -      .exclusionHeaderText { -        padding-left: 3px; -        color: #979ca0; -      } -      #exclusionAddButton { -        float: right; -        margin-right: 0px; -        margin-top: 5px; -      } -      #footer { -        background: #f5f5f5; -        border-top: 1px solid #979ca0; -        position: fixed; -        bottom: 0px; -        z-index: 10; -      } -      #footer, #footerTable, #footerTableData { -        width: 100%; -      } -      #endSpace { -        /* Leave space for the fixed footer. */ -        min-height: 30px; -        max-height: 30px; -      } -      #helpText { -        font-size: 12px; -      } -      #saveOptionsTableData { -        float: right; -      } -      #saveOptions { -        white-space: nowrap; -        width: 110px; -      } -    </style> - -  <script type="text/javascript" src="options.js"></script> - +    <script type="text/javascript" src="options.js"></script>    </head>    <body> @@ -257,20 +24,7 @@              </div>              <div>                 <div id="exclusionScrollBox"> -                  <table id="exclusionRules"> -                     <tr> -                        <td><span class="exclusionHeaderText">Patterns</span></td> -                        <td><span class="exclusionHeaderText">Keys</span></td> -                     </tr> -                  </table> -                  <template id="exclusionRuleTemplate"> -                     <tr class="exclusionRuleTemplateInstance"> -                        <td><input/ type="text" class="pattern" placeholder="URL pattern"></td> -                        <td class="exclusionRulePassKeys"><input/ type="text" class="passKeys" placeholder="Exclude keys"></td> -                        <td class="exclusionRemoveButton"> -                          <input/ type="button" tabindex = "-1" class="exclusionRemoveButtonButton" value="✖"></td> -                     </tr> -                  </template> +                  <!-- Populated from exclusions.html by options.coffee. -->                 </div>                 <button id="exclusionAddButton">Add Rule</button>              </div> diff --git a/pages/popup.coffee b/pages/popup.coffee deleted file mode 100644 index 99a4eb87..00000000 --- a/pages/popup.coffee +++ /dev/null @@ -1,110 +0,0 @@ - -originalRule = undefined -originalPattern = undefined -originalPassKeys = undefined - -generateDefaultPattern = (url) -> -  if /^https?:\/\/./.test url -    # The common use case is to disable Vimium at the domain level. -    # Generate "https?://www.example.com/*" from "http://www.example.com/path/to/page.html". -    "https?:/" + url.split("/",3)[1..].join("/") + "/*" -  else if /^[a-z]{3,}:\/\/./.test url -    # Anything else which seems to be a URL. -    url.split("/",3).join("/") + "/*" -  else -    url + "*" - -reset = (initialize=false) -> -  document.getElementById("optionsLink").setAttribute "href", chrome.runtime.getURL("pages/options.html") -  chrome.tabs.getSelected null, (tab) -> -    isEnabled = chrome.extension.getBackgroundPage().isEnabledForUrl(url: tab.url) -    # Check if we have an existing exclusing rule for this page. -    if isEnabled.rule -      originalRule = isEnabled.rule -      originalPattern = originalRule.pattern -      originalPassKeys = originalRule.passKeys -    else -      originalRule = null -      originalPattern = generateDefaultPattern tab.url -      originalPassKeys = "" -    patternElement = document.getElementById("popupPattern") -    passKeysElement = document.getElementById("popupPassKeys") -    patternElement.value  = originalPattern -    passKeysElement.value = originalPassKeys -    if initialize -      # Activate <Ctrl-Enter> to save. -      for element in [ patternElement, passKeysElement ] -        element.addEventListener "keyup", (event) -> -          if event.ctrlKey and event.keyCode == 13 -            addExclusionRule() -            window.close() -        element.addEventListener "focus", -> document.getElementById("helpText").style.display = "block" -        element.addEventListener "blur", -> document.getElementById("helpText").style.display = "none" -      # Focus passkeys with cursor at the end (but only when creating popup). -      passKeysElement.focus() -      passKeysElement.setSelectionRange(passKeysElement.value.length, passKeysElement.value.length) -    onChange() - -onChange = -> -  # As the text in the popup's input elements is changed, update the the popup's buttons accordingly. -  # Aditionally, enable and disable those buttons as appropriate. -  pattern = document.getElementById("popupPattern").value.trim() -  passKeys = document.getElementById("popupPassKeys").value.trim() -  popupExclude = document.getElementById("popupExclude") - -  document.getElementById("popupRemove").disabled = -    not (originalRule and pattern == originalPattern) - -  if originalRule and pattern == originalPattern and passKeys == originalPassKeys -    popupExclude.disabled = true -    popupExclude.value = "Update Rule" - -  else if originalRule and pattern == originalPattern -    popupExclude.disabled = false -    popupExclude.value = "Update Rule" - -  else if originalRule -    popupExclude.disabled = false -    popupExclude.value = "Add Rule" - -  else if pattern -    popupExclude.disabled = false -    popupExclude.value = "Add Rule" - -  else -    popupExclude.disabled = true -    popupExclude.value = "Add Rule" - -showMessage = do -> -  timer = null - -  hideConfirmationMessage = -> -    document.getElementById("confirmationMessage").setAttribute "style", "display: none" -    timer = null - -  (message) -> -    document.getElementById("confirmationMessage").setAttribute "style", "display: inline-block" -    document.getElementById("confirmationMessage").innerHTML = message -    clearTimeout(timer) if timer -    timer = setTimeout(hideConfirmationMessage,2000) - -addExclusionRule = -> -  pattern = document.getElementById("popupPattern").value.trim() -  passKeys = document.getElementById("popupPassKeys").value.trim() -  chrome.extension.getBackgroundPage().addExclusionRule pattern, passKeys -  showMessage("Updated.") -  reset() - -removeExclusionRule = -> -  pattern = document.getElementById("popupPattern").value.trim() -  chrome.extension.getBackgroundPage().removeExclusionRule pattern -  showMessage("Removed.") -  reset() - -document.addEventListener "DOMContentLoaded", -> -  document.getElementById("popupExclude").addEventListener "click", addExclusionRule, false -  document.getElementById("popupRemove").addEventListener "click", removeExclusionRule, false -  for field in ["popupPattern", "popupPassKeys"] -    for event in ["input", "change"] -      document.getElementById(field).addEventListener event, onChange, false -  reset true diff --git a/pages/popup.html b/pages/popup.html index 775d6c07..c7e2fd6f 100644 --- a/pages/popup.html +++ b/pages/popup.html @@ -1,85 +1,82 @@  <html>    <head> +    <link rel="stylesheet" type="text/css" href="options.css">      <style>        * {          margin: 0px;          padding: 0px;        } -      #vimiumPopup { width: 400px; } - -      #excludeControls { -        padding: 10px; +      #helpText, #optionsLink, #state { +        font-family : "Helvetica Neue", "Helvetica", "Arial", sans-serif; +        font-size: 12px;        } -      #popupPattern, #popupPassKeys { -        margin: 5px; -        width: 330px; -        /* Match the corresponding font and font size used on the options page. */ -        /* TODO (smblott): Match other styles from the options page. */ -        font-family: Consolas, "Liberation Mono", Courier, monospace; -        font-size: 14px; +      #helpText, #stateLine, #state { color: #979ca0; } +      #exclusionAddButton { width: 80px; } + +      #saveOptions { +        margin-top: 5px; /* Match #exclusionAddButton */ +        margin-left: 5px; +        float: right;        } -      #confirmationMessage { -        display: inline-block; -        width: 18px; -        height: 13px; -        background: url(icons/check.png) 3px 2px no-repeat; -        display: none; +      #state { +        padding-left: 5px; +        background: #f5f5f5; +        width: 100%; +        border-bottom: 1px solid #979ca0; +        margin: 0px;        } -      #popupRemove { margin: 5px; } -      #popupExclude { margin: 5px; } +      /* These are overridden from options.css. */ -      #popupMenu ul { -        list-style: none; -      } +      #endSpace, #footerWrapper { width: 500px; } +      #footerWrapper { margin-left: 0px; } -      #popupMenu li, #popupMenu a:active, #popupMenu a:visited { -        color: #3F6EC2; -        display: block; -        border-top: 1px solid #DDDDDD; -        padding: 3px; -        padding-left: 10px; +      /* Make exclusionScrollBox smaller than on the options page, because there are likely to be fewer +         matching rules, and the popup obscures the underlying page. +       */ +      #exclusionScrollBox { +        max-height: 124px; +        min-height: 124px;        } -      #popupMenu a:hover { -        background: #EEEEEE; +      #endSpace { /* Leave space for the fixed footer. */ +        min-height: 40px; +        max-height: 40px;        } -      #optionsLink { -        font-family : "Helvetica Neue", "Helvetica", "Arial", sans-serif; -        font-size: 12px; -        float: right; -      } -      #helpText { -        color: #979ca0; -        font-family : "Helvetica Neue", "Helvetica", "Arial", sans-serif; -        font-size: 12px; -        float: left; -        display: none; -      }      </style> -    <script src="popup.js"></script> +    <script src="options.js"></script>    </head>    <body> -    <div id="vimiumPopup"> -      <div id="excludeControls"> -        <input id="popupPattern" placeholder="Pattern against which to match URLs..." type="text" /><br/> -        <input id="popupPassKeys" placeholder="Only exclude these keys..." type="text" /><br/> -        <input id="popupRemove" type="button" value="Remove Rule" /> -        <input id="popupExclude" type="button" value="Add or Update Rule" /> -        <span id="confirmationMessage">Text is added in popup.coffee.</span> -      </div> +    <div id="state"></div> + +    <div id="exclusionScrollBox"> +      <!-- Populated from exclusions.html by options.coffee. --> +    </div> + +    <!-- Some extra space which is hidden underneath the footer. --> +    <div id="endSpace"/> -      <div id="popupMenu"> -        <ul> -          <li> -             <span id="helpText">Type <strong>Ctrl-Enter</strong> to save and close.</span> -             <a id="optionsLink" target="_blank">Options</a> -          </li> -        </ul> +    <div id="footer"> +      <div id="footerWrapper"> +        <table> +          <tr> +            <td id="stateLine" style="width: 99%"> +              <span id="helpText">These are the rules matching this page.</span> +              <br/> +              <a id="optionsLink" target="_blank" tabindex="99999">Options</a> +            </td> +            <td valign="top"> +              <button id="exclusionAddButton">Add Rule</button> +            </td> +            <td valign="top"> +              <button id="saveOptions" disabled="true">No Changes</button> +            </td> +          </tr> +        </table>        </div>      </div>    </body> diff --git a/tests/unit_tests/exclusion_test.coffee b/tests/unit_tests/exclusion_test.coffee index 25bd8125..b3ed7194 100644 --- a/tests/unit_tests/exclusion_test.coffee +++ b/tests/unit_tests/exclusion_test.coffee @@ -25,49 +25,33 @@ extend(global, require "../../background_scripts/main.js")  #  context "Excluded URLs and pass keys", -  # These tests have no setup, they use the default values from settings.coffee. +  setup -> +    Exclusions.postUpdateHook( +      [ +        { pattern: "http*://mail.google.com/*", passKeys: "" } +        { pattern: "http*://www.facebook.com/*", passKeys: "abab" } +        { pattern: "http*://www.facebook.com/*", passKeys: "cdcd" } +        { pattern: "http*://www.bbc.com/*", passKeys: "" } +        { pattern: "http*://www.bbc.com/*", passKeys: "ab" } +      ])    should "be disabled for excluded sites", -> -    rule = isEnabledForUrl({ url: 'http://www.google.com/calendar/page' }) -    assert.isFalse rule.isEnableForUrl +    rule = isEnabledForUrl({ url: 'http://mail.google.com/calendar/page' }) +    assert.isFalse rule.isEnabledForUrl      assert.isFalse rule.passKeys -  should "be enabled, but with pass keys", -> -    rule = isEnabledForUrl({ url: 'https://www.facebook.com/something' }) -    assert.isTrue rule.isEnabledForUrl +  should "be disabled for excluded sites, one exclusion", -> +    rule = isEnabledForUrl({ url: 'http://www.bbc.com/calendar/page' }) +    assert.isFalse rule.isEnabledForUrl      assert.isFalse rule.passKeys -    addExclusionRule("http*://www.facebook.com/*","oO") + +  should "be enabled, but with pass keys", ->      rule = isEnabledForUrl({ url: 'https://www.facebook.com/something' })      assert.isTrue rule.isEnabledForUrl -    assert.equal rule.passKeys, 'oO' +    assert.equal rule.passKeys, 'abcd'    should "be enabled", ->      rule = isEnabledForUrl({ url: 'http://www.twitter.com/pages' })      assert.isTrue rule.isEnabledForUrl      assert.isFalse rule.passKeys -  should "add a new excluded URL", -> -    rule = isEnabledForUrl({ url: 'http://www.example.com/page' }) -    assert.isTrue rule.isEnabledForUrl -    addExclusionRule("http://www.example.com*") -    rule = isEnabledForUrl({ url: 'http://www.example.com/page' }) -    assert.isFalse rule.isEnabledForUrl -    assert.isFalse rule.passKeys - -  should "add a new excluded URL with passkeys", -> -    rule = isEnabledForUrl({ url: 'http://www.anotherexample.com/page' }) -    assert.isTrue rule.isEnabledForUrl -    addExclusionRule("http://www.anotherexample.com/*","jk") -    rule = isEnabledForUrl({ url: 'http://www.anotherexample.com/page' }) -    assert.isTrue rule.isEnabledForUrl -    assert.equal rule.passKeys, 'jk' - -  should "update an existing excluded URL with passkeys", -> -    rule = isEnabledForUrl({ url: 'http://mail.google.com/page' }) -    assert.isFalse rule.isEnabledForUrl -    assert.isFalse rule.passKeys -    addExclusionRule("http*://mail.google.com/*","jknp") -    rule = isEnabledForUrl({ url: 'http://mail.google.com/page' }) -    assert.isTrue rule.isEnabledForUrl -    assert.equal rule.passKeys, 'jknp' - diff --git a/tests/unit_tests/utils_test.coffee b/tests/unit_tests/utils_test.coffee index 556f5b7a..88e9a15b 100644 --- a/tests/unit_tests/utils_test.coffee +++ b/tests/unit_tests/utils_test.coffee @@ -45,7 +45,8 @@ context "convertToUrl",    should "convert non-URL terms into search queries", ->      assert.equal "http://www.google.com/search?q=google", Utils.convertToUrl("google") -    assert.equal "http://www.google.com/search?q=go%20ogle.com", Utils.convertToUrl("go ogle.com") +    assert.equal "http://www.google.com/search?q=go+ogle.com", Utils.convertToUrl("go ogle.com") +    assert.equal "http://www.google.com/search?q=%40twitter", Utils.convertToUrl("@twitter")  context "hasChromePrefix",    should "detect chrome prefixes of URLs", -> | 
