diff options
| -rw-r--r-- | background_scripts/exclusions.coffee | 35 | ||||
| -rw-r--r-- | background_scripts/main.coffee | 19 | ||||
| -rw-r--r-- | lib/utils.coffee | 6 | ||||
| -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 |
10 files changed, 542 insertions, 564 deletions
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 647923c0..cebb38ca 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -75,26 +75,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) @@ -353,7 +337,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 1bedb3d1..8f6dca2a 100644 --- a/lib/utils.coffee +++ b/lib/utils.coffee @@ -116,6 +116,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' - |
