aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTING.md48
-rw-r--r--README.md12
-rw-r--r--background_scripts/commands.coffee2
-rw-r--r--background_scripts/exclusions.coffee7
-rw-r--r--background_scripts/settings.coffee1
-rw-r--r--content_scripts/scroller.coffee255
-rw-r--r--content_scripts/vimium.css17
-rw-r--r--content_scripts/vimium_frontend.coffee6
-rw-r--r--lib/dom_utils.coffee14
-rw-r--r--pages/options.coffee39
-rw-r--r--pages/options.html178
-rw-r--r--pages/popup.html2
12 files changed, 391 insertions, 190 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9382a020..a417caf5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -74,6 +74,34 @@ reports:
When you're done with your changes, send us a pull request on Github. Feel free to include a change to the
CREDITS file with your patch.
+Vimium design goals
+-------------------
+
+When improving Vimium it's helpful to know what design goals we're optimizing for.
+
+The core goal is to make it easy to navigate the web using just the keyboard. When people first start using
+Vimium, it provides an incredibly powerful workflow improvement and it makes them feel awesome. And it turns
+out that Vimium is applicable to a huge, broad population of people, not just users of Vim, which is great.
+
+A secondary goal is to make Vimium approachable, or in other words, to minimize the barriers which will
+prevent a new user from feeling awesome. Many of Vimium's users haven't used Vim before (about 1 in 5 app
+store reviews say this), and most people have strong web browsing habits forged from years of browsing that
+they rely on. Given that, it's a great experience when Vimium feels like a natural addition to Chrome which
+augments but doesn't break their current browsing habits.
+
+In some ways, making software approachable is even harder than just enabling the core use case. But in this
+area, Vimium really shines. It's approachable today because:
+
+1. It's simple to understand (even if you're not very familiar with Vim). The Vimium video shows you all you
+ need to know to start using Vimium and feel awesome.
+2. The core feature set works in almost all cases on all sites, so Vimium feels reliable.
+3. Requires no configuration or doc-reading before it's useful. Just watch the video or hit `?`.
+4. Doesn't drastically change the way Chrome looks or behaves. You can transition into using Vimium piecemeal;
+ you don't need to jump in whole-hog from the start.
+5. The core feature set isn't overwhelming. This is easy to degrade as we evolve Vimium, so it requires active
+ effort to maintain this feel.
+6. Developers find the code is relatively simple and easy to jump into, so we have an active dev community.
+
## What makes for a good feature request/contribution to Vimium?
Good features:
@@ -96,23 +124,3 @@ We use these guidelines, in addition to the code complexity, when deciding wheth
If you're worried that a feature you plan to build won't be a good fit for core Vimium, just open a github
issue for discussion or send an email to the Vimium mailing list.
-
-## How to release Vimium to the Chrome Store
-
-This process is currently only done by Phil or Ilya.
-
-1. Increment the version number in manifest.json
-2. Update the Changelog in README.md
-
- You can see a summary of commits since the last version: `git log --oneline v1.45..`
-
-3. Push your commits
-4. Create a git tag for this newly released version
-
- git tag -a v1.45 -m "v1.45 release"
-
-5. Run `cake package`
-6. Take the distributable found in `dist` and upload it
- [here](https://chrome.google.com/webstore/developer/dashboard)
-7. Update the description in the Chrome store to include the latest version's release notes
-8. Celebrate
diff --git a/README.md b/README.md
index fc38c2b8..20331785 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,8 @@ Vimium - The Hacker's Browser
[![Build Status](https://secure.travis-ci.org/philc/vimium.png?branch=master)](https://travis-ci.org/philc/vimium)
-Vimium is a Chrome extension that provides keyboard-based navigation and control in the spirit of the Vim
-editor.
+Vimium is a Chrome extension that provides keyboard-based navigation and control of the web in the spirit of
+the Vim editor.
__Installation instructions:__
@@ -15,14 +15,16 @@ Please see
[CONTRIBUTING.md](https://github.com/philc/vimium/blob/master/CONTRIBUTING.md#installing-from-source)
for instructions on how you can install Vimium from source.
-The Options page can be reached via a link on the help dialog (hit `?`) or via the button next to Vimium on
+The Options page can be reached via a link on the help dialog (type `?`) or via the button next to Vimium on
the Chrome Extensions page (`chrome://extensions`).
Keyboard Bindings
-----------------
Modifier keys are specified as `<c-x>`, `<m-x>`, and `<a-x>` for ctrl+x, meta+x, and alt+x
-respectively. See the next section for instructions on customizing these bindings.
+respectively. See the next section for how to customize these bindings.
+
+Once you have Vimium installed, you can see this list of key bindings at any time by typing `?`.
Navigating the current page:
@@ -90,7 +92,7 @@ Additional advanced browsing commands:
Vimium supports command repetition so, for example, hitting '5t' will open 5 tabs in rapid succession. `<ESC>` (or
`<c-[>`) will clear any partial commands in the queue and will also exit insert and find modes.
-There are some advanced commands which aren't documented here. Refer to the help dialog (type `?`) for a full
+There are some advanced commands which aren't documented here; refer to the help dialog (type `?`) for a full
list.
Custom Key Mappings
diff --git a/background_scripts/commands.coffee b/background_scripts/commands.coffee
index 10fa323b..585ef572 100644
--- a/background_scripts/commands.coffee
+++ b/background_scripts/commands.coffee
@@ -207,8 +207,6 @@ defaultKeyMappings =
"F": "LinkHints.activateModeToOpenInNewTab"
"<a-f>": "LinkHints.activateModeWithQueue"
- "af": "LinkHints.activateModeToDownloadLink"
-
"/": "enterFindMode"
"n": "performFind"
"N": "performBackwardsFind"
diff --git a/background_scripts/exclusions.coffee b/background_scripts/exclusions.coffee
index 3a8ef1e7..2b34238b 100644
--- a/background_scripts/exclusions.coffee
+++ b/background_scripts/exclusions.coffee
@@ -6,7 +6,12 @@ RegexpCache =
if regexp = @cache[pattern]
regexp
else
- @cache[pattern] = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$")
+ @cache[pattern] =
+ # We use try/catch to ensure that a broken regexp doesn't wholly cripple Vimium.
+ try
+ new RegExp("^" + pattern.replace(/\*/g, ".*") + "$")
+ catch
+ /^$/ # Match the empty string.
# The Exclusions class manages the exclusion rule setting.
# An exclusion is an object with two attributes: pattern and passKeys.
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee
index d6e8fcde..4a63a5fb 100644
--- a/background_scripts/settings.coffee
+++ b/background_scripts/settings.coffee
@@ -61,6 +61,7 @@ root.Settings = Settings =
# or strings
defaults:
scrollStepSize: 60
+ smoothScroll: true
keyMappings: "# Insert your prefered key mappings here."
linkHintCharacters: "sadfjklewcmpgh"
linkHintNumbers: "0123456789"
diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee
index b3a14c78..5eb1c5e2 100644
--- a/content_scripts/scroller.coffee
+++ b/content_scripts/scroller.coffee
@@ -1,94 +1,217 @@
-window.Scroller = root = {}
-
#
# activatedElement is different from document.activeElement -- the latter seems to be reserved mostly for
# input elements. This mechanism allows us to decide whether to scroll a div or to scroll the whole document.
#
activatedElement = null
-root.init = ->
- handlerStack.push DOMActivate: -> activatedElement = event.target
-
scrollProperties =
x: {
axisName: 'scrollLeft'
max: 'scrollWidth'
- viewSize: 'clientHeight'
+ viewSize: 'clientWidth'
}
y: {
axisName: 'scrollTop'
max: 'scrollHeight'
- viewSize: 'clientWidth'
+ viewSize: 'clientHeight'
}
-getDimension = (el, direction, name) ->
- # the clientSizes of the body are the dimensions of the entire page, but the viewport should only be the
- # part visible through the window
- if name is 'viewSize' and el is document.body
- if direction is 'x' then window.innerWidth else window.innerHeight
+# Translate a scroll request into a number (which will be interpreted by `scrollBy` as a relative amount, or
+# by `scrollTo` as an absolute amount). :direction must be "x" or "y". :amount may be either a number (in
+# which case it is simply returned) or a string. If :amount is a string, then it is either "max" (meaning the
+# height or width of element), or "viewSize". In both cases, we look up and return the requested amount,
+# either in `element` or in `window`, as appropriate.
+getDimension = (el, direction, amount) ->
+ if Utils.isString amount
+ name = amount
+ # the clientSizes of the body are the dimensions of the entire page, but the viewport should only be the
+ # part visible through the window
+ if name is 'viewSize' and el is document.body
+ # TODO(smblott) Should we not be returning the width/height of element, here?
+ if direction is 'x' then window.innerWidth else window.innerHeight
+ else
+ el[scrollProperties[direction][name]]
else
- el[scrollProperties[direction][name]]
+ amount
-# Chrome does not report scrollHeight accurately for nodes with pseudo-elements of height 0 (bug 110149).
-# Therefore we cannot figure out if we have scrolled to the bottom of an element by testing if scrollTop +
-# clientHeight == scrollHeight. So just try to increase scrollTop blindly -- if it fails we know we have
-# reached the end of the content.
-ensureScrollChange = (direction, changeFn) ->
+# Perform a scroll. Return true if we successfully scrolled by the requested amount, and false otherwise.
+performScroll = (element, direction, amount) ->
axisName = scrollProperties[direction].axisName
- element = activatedElement
- loop
- oldScrollValue = element[axisName]
- # Elements with `overflow: hidden` should not be scrolled.
- overflow = window.getComputedStyle(element).getPropertyValue("overflow-#{direction}")
- changeFn(element, axisName) unless overflow == "hidden"
- break unless element[axisName] == oldScrollValue && element != document.body
- # we may have an orphaned element. if so, just scroll the body element.
- element = element.parentElement || document.body
-
- # if the activated element has been scrolled completely offscreen, subsequent changes in its scroll
- # position will not provide any more visual feedback to the user. therefore we deactivate it so that
- # subsequent scrolls only move the parent element.
+ before = element[axisName]
+ element[axisName] += amount
+ element[axisName] == amount + before
+
+# Test whether `element` should be scrolled. E.g. hidden elements should not be scrolled.
+shouldScroll = (element, direction) ->
+ computedStyle = window.getComputedStyle(element)
+ # Elements with `overflow: hidden` must not be scrolled.
+ return false if computedStyle.getPropertyValue("overflow-#{direction}") == "hidden"
+ # Elements which are not visible should not be scrolled.
+ return false if computedStyle.getPropertyValue("visibility") in ["hidden", "collapse"]
+ return false if computedStyle.getPropertyValue("display") == "none"
+ true
+
+# Test whether element does actually scroll in the direction required when asked to do so. Due to chrome bug
+# 110149, scrollHeight and clientHeight cannot be used to reliably determine whether an element will scroll.
+# Instead, we scroll the element by 1 or -1 and see if it moved (then put it back). :factor is the factor by
+# which :scrollBy and :scrollTo will later scale the scroll amount. :factor can be negative, so we need it
+# here in order to decide whether we should test a forward scroll or a backward scroll.
+# Bug verified in Chrome 38.0.2125.104.
+doesScroll = (element, direction, amount, factor) ->
+ # amount is treated as a relative amount, which is correct for relative scrolls. For absolute scrolls (only
+ # gg, G, and friends), amount can be either a string ("max" or "viewSize") or zero. In the former case,
+ # we're definitely scrolling forwards, so any positive value will do for delta. In the latter, we're
+ # definitely scrolling backwards, so a delta of -1 will do. For absolute scrolls, factor is always 1.
+ delta = factor * getDimension(element, direction, amount) || -1
+ delta = Math.sign delta # 1 or -1
+ performScroll(element, direction, delta) and performScroll(element, direction, -delta)
+
+# From element and its parents, find the first which we should scroll and which does scroll.
+findScrollableElement = (element, direction, amount, factor) ->
+ while element != document.body and
+ not (doesScroll(element, direction, amount, factor) and shouldScroll(element, direction))
+ element = element.parentElement || document.body
+ element
+
+checkVisibility = (element) ->
+ # If the activated element has been scrolled completely offscreen, then subsequent changes in its scroll
+ # position will not provide any more visual feedback to the user. Therefore, we deactivate it so that
+ # subsequent scrolls affect the parent element.
rect = activatedElement.getBoundingClientRect()
if (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth)
activatedElement = element
-# scroll the active element in :direction by :amount * :factor.
-# :factor is needed because :amount can take on string values, which scrollBy converts to element dimensions.
-root.scrollBy = (direction, amount, factor = 1) ->
- # if this is called before domReady, just use the window scroll function
- if (!document.body and amount instanceof Number)
- if (direction == "x")
- window.scrollBy(amount, 0)
- else
- window.scrollBy(0, amount)
- return
+# How scrolling is handled by CoreScroller.
+# - For jump scrolling, the entire scroll happens immediately.
+# - For smooth scrolling with distinct key presses, a separate animator is initiated for each key press.
+# Therefore, several animators may be active at the same time. This ensures that two quick taps on `j`
+# scroll to the same position as two slower taps.
+# - For smooth scrolling with keyboard repeat (continuous scrolling), the most recently-activated animator
+# continues scrolling at least until its keyup event is received. We never initiate a new animator on
+# keyboard repeat.
- if (!activatedElement || !isRendered(activatedElement))
- activatedElement = document.body
+# CoreScroller contains the core function (scroll) and logic for relative scrolls. All scrolls are ultimately
+# translated to relative scrolls. CoreScroller is not exported.
+CoreScroller =
+ init: (frontendSettings) ->
+ @settings = frontendSettings
+ @time = 0
+ @lastEvent = null
+ @keyIsDown = false
- ensureScrollChange direction, (element, axisName) ->
- if Utils.isString amount
- elementAmount = getDimension element, direction, amount
- else
- elementAmount = amount
- elementAmount *= factor
- element[axisName] += elementAmount
+ handlerStack.push
+ keydown: (event) =>
+ @keyIsDown = true
+ @lastEvent = event
+ keyup: =>
+ @keyIsDown = false
+ @time += 1
-root.scrollTo = (direction, pos) ->
- return unless document.body
+ # Return true if CoreScroller would not initiate a new scroll right now.
+ wouldNotInitiateScroll: -> @lastEvent?.repeat and @settings.get "smoothScroll"
- if (!activatedElement || !isRendered(activatedElement))
- activatedElement = document.body
+ # Calibration fudge factors for continuous scrolling. The calibration value starts at 1.0. We then
+ # increase it (until it exceeds @maxCalibration) if we guess that the scroll is too slow, or decrease it
+ # (until it is less than @minCalibration) if we guess that the scroll is too fast. The cutoff point for
+ # which guess we make is @calibrationBoundary. We require: 0 < @minCalibration <= 1 <= @maxCalibration.
+ minCalibration: 0.5 # Controls how much we're willing to slow scrolls down; smaller means more slow down.
+ maxCalibration: 1.6 # Controls how much we're willing to speed scrolls up; bigger means more speed up.
+ calibrationBoundary: 150 # Boundary between scrolls which are considered too slow, or too fast.
- ensureScrollChange direction, (element, axisName) ->
- if Utils.isString pos
- elementPos = getDimension element, direction, pos
- else
- elementPos = pos
- element[axisName] = elementPos
-
-# TODO refactor and put this together with the code in getVisibleClientRect
-isRendered = (element) ->
- computedStyle = window.getComputedStyle(element, null)
- return !(computedStyle.getPropertyValue("visibility") != "visible" ||
- computedStyle.getPropertyValue("display") == "none")
+ # Scroll element by a relative amount (a number) in some direction.
+ scroll: (element, direction, amount) ->
+ return unless amount
+
+ unless @settings.get "smoothScroll"
+ # Jump scrolling.
+ performScroll element, direction, amount
+ checkVisibility element
+ return
+
+ # We don't activate new animators on keyboard repeats; rather, the most-recently activated animator
+ # continues scrolling.
+ return if @lastEvent?.repeat
+
+ activationTime = ++@time
+ myKeyIsStillDown = => @time == activationTime and @keyIsDown
+
+ # Store amount's sign and make amount positive; the arithmetic is clearer when amount is positive.
+ sign = Math.sign amount
+ amount = Math.abs amount
+
+ # Initial intended scroll duration (in ms). We allow a bit longer for longer scrolls.
+ duration = Math.max 100, 20 * Math.log amount
+
+ totalDelta = 0
+ totalElapsed = 0.0
+ calibration = 1.0
+ previousTimestamp = null
+
+ animate = (timestamp) =>
+ previousTimestamp ?= timestamp
+ return requestAnimationFrame(animate) if timestamp == previousTimestamp
+
+ # The elapsed time is typically about 16ms.
+ elapsed = timestamp - previousTimestamp
+ totalElapsed += elapsed
+ previousTimestamp = timestamp
+
+ # The constants in the duration calculation, above, are chosen to provide reasonable scroll speeds for
+ # distinct keypresses. For continuous scrolls, some scrolls are too slow, and others too fast. Here, we
+ # speed up the slower scrolls, and slow down the faster scrolls.
+ if myKeyIsStillDown() and 75 <= totalElapsed and @minCalibration <= calibration <= @maxCalibration
+ calibration *= 1.05 if 1.05 * calibration * amount < @calibrationBoundary # Speed up slow scrolls.
+ calibration *= 0.95 if @calibrationBoundary < 0.95 * calibration * amount # Slow down fast scrolls.
+
+ # Calculate the initial delta, rounding up to ensure progress. Then, adjust delta to account for the
+ # current scroll state.
+ delta = Math.ceil amount * (elapsed / duration) * calibration
+ delta = if myKeyIsStillDown() then delta else Math.max 0, Math.min delta, amount - totalDelta
+
+ if delta and performScroll element, direction, sign * delta
+ totalDelta += delta
+ requestAnimationFrame animate
+ else
+ # We're done.
+ checkVisibility element
+
+ # Launch animator.
+ requestAnimationFrame animate
+
+# Scroller contains the two main scroll functions (scrollBy and scrollTo) which are exported to clients.
+Scroller =
+ init: (frontendSettings) ->
+ handlerStack.push DOMActivate: -> activatedElement = event.target
+ CoreScroller.init frontendSettings
+
+ # scroll the active element in :direction by :amount * :factor.
+ # :factor is needed because :amount can take on string values, which scrollBy converts to element dimensions.
+ scrollBy: (direction, amount, factor = 1) ->
+ # if this is called before domReady, just use the window scroll function
+ if (!document.body and amount instanceof Number)
+ if (direction == "x")
+ window.scrollBy(amount, 0)
+ else
+ window.scrollBy(0, amount)
+ return
+
+ activatedElement ||= document.body
+ return unless activatedElement
+
+ # Avoid the expensive scroll calculation if it will not be used. This reduces costs during smooth,
+ # continuous scrolls, and is just an optimization.
+ unless CoreScroller.wouldNotInitiateScroll()
+ element = findScrollableElement activatedElement, direction, amount, factor
+ elementAmount = factor * getDimension element, direction, amount
+ CoreScroller.scroll element, direction, elementAmount
+
+ scrollTo: (direction, pos) ->
+ return unless document.body or activatedElement
+ activatedElement ||= document.body
+
+ element = findScrollableElement activatedElement, direction, pos, 1
+ amount = getDimension(element,direction,pos) - element[scrollProperties[direction].axisName]
+ CoreScroller.scroll element, direction, amount
+
+root = exports ? window
+root.Scroller = Scroller
diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css
index 7998fe5c..a8ae5583 100644
--- a/content_scripts/vimium.css
+++ b/content_scripts/vimium.css
@@ -2,6 +2,10 @@
* Many CSS class names in this file use the verbose "vimiumXYZ" as the class name. This is so we don't
* use the same CSS class names that the page is using, so the page's CSS doesn't mess with the style of our
* Vimium dialogs.
+ *
+ * The z-indexes of Vimium elements are very large, because we always want them to show on top. Chrome may
+ * support up to Number.MAX_VALUE, which is approximately 1.7976e+308. We're using 2**31, which is the max value of a singed 32 bit int.
+ * Let's use try larger valeus if 2**31 empirically isn't large enough.
*/
/*
@@ -54,7 +58,7 @@ tr.vimiumReset {
vertical-align: baseline;
white-space: normal;
width: auto;
- z-index: 99999999;
+ z-index: 2147483648;
}
/* Linkhints CSS */
@@ -122,7 +126,8 @@ div#vimiumHelpDialog {
top:50px;
-webkit-box-shadow: rgba(0, 0, 0, 0.4) 0px 0px 6px;
overflow-y: auto;
- z-index:99999998;
+ /* One less than vimiumReset */
+ z-index: 2147483647;
}
div#vimiumHelpDialog a { color:blue; }
@@ -233,8 +238,8 @@ div.vimiumHUD {
border-radius: 4px 4px 0 0;
font-family: "Lucida Grande", "Arial", "Sans";
font-size: 12px;
- /* One less than vimium's hint markers, so link hints can be shown e.g. for the panel's close button. */
- z-index: 99999997;
+ /* One less than vimium's hint markers, so link hints can be shown e.g. for the HUD panel's close button. */
+ z-index: 2147483646;
text-shadow: 0px 1px 2px #FFF;
line-height: 1.0;
opacity: 0;
@@ -291,7 +296,7 @@ body.vimiumFindMode ::selection {
box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.8);
border: 1px solid #aaa;
/* One less than hint markers and the help dialog. */
- z-index: 99999996;
+ z-index: 2147483645;
}
#vomnibar input {
@@ -401,5 +406,5 @@ div#vimiumFlash {
padding: 1px;
background-color: transparent;
position: absolute;
- z-index: 99999;
+ z-index: 2147483648;
}
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index 44093cc3..165b3329 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -49,7 +49,7 @@ settings =
loadedValues: 0
valuesToLoad: ["scrollStepSize", "linkHintCharacters", "linkHintNumbers", "filterLinkHints", "hideHud",
"previousPatterns", "nextPatterns", "findModeRawQuery", "regexFindMode", "userDefinedLinkHintCss",
- "helpDialog_showAdvancedCommands"]
+ "helpDialog_showAdvancedCommands", "smoothScroll"]
isLoaded: false
eventListeners: {}
@@ -101,7 +101,7 @@ initializePreDomReady = ->
settings.addEventListener("load", LinkHints.init.bind(LinkHints))
settings.load()
- Scroller.init()
+ Scroller.init settings
checkIfEnabledForUrl()
@@ -186,7 +186,7 @@ initializeOnDomReady = ->
registerFrame = ->
# Don't register frameset containers; focusing them is no use.
- if document.body.tagName != "FRAMESET"
+ if document.body.tagName.toLowerCase() != "frameset"
chrome.runtime.sendMessage
handler: "registerFrame"
frameId: frameId
diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee
index 21018049..dfaa5d5f 100644
--- a/lib/dom_utils.coffee
+++ b/lib/dom_utils.coffee
@@ -91,17 +91,23 @@ DomUtils =
null
#
- # Selectable means the element has a text caret; this is not the same as "focusable".
+ # Selectable means that we should use the simulateSelect method to activate the element instead of a click.
+ #
+ # The html5 input types that should use simulateSelect are:
+ # ["date", "datetime", "datetime-local", "email", "month", "number", "password", "range", "search",
+ # "submit", "tel", "text", "time", "url", "week"]
+ # An unknown type will be treated the same as "text", in the same way that the browser does.
#
isSelectable: (element) ->
- selectableTypes = ["search", "text", "password"]
- (element.nodeName.toLowerCase() == "input" && selectableTypes.indexOf(element.type) >= 0) ||
+ unselectableTypes = ["button", "checkbox", "color", "file", "hidden", "image", "radio", "reset"]
+ (element.nodeName.toLowerCase() == "input" && unselectableTypes.indexOf(element.type) == -1) ||
element.nodeName.toLowerCase() == "textarea"
simulateSelect: (element) ->
element.focus()
# When focusing a textbox, put the selection caret at the end of the textbox's contents.
- element.setSelectionRange(element.value.length, element.value.length)
+ # For some HTML5 input types (eg. date) we can't position the caret, so we wrap this with a try.
+ try element.setSelectionRange(element.value.length, element.value.length)
simulateClick: (element, modifiers) ->
modifiers ||= {}
diff --git a/pages/options.coffee b/pages/options.coffee
index f5968eb9..cd19fa37 100644
--- a/pages/options.coffee
+++ b/pages/options.coffee
@@ -42,13 +42,7 @@ class Option
@saveOptions: ->
Option.all.map (option) -> option.save()
$("saveOptions").disabled = true
-
- # Used by text options. <ctrl-Enter> saves all options.
- activateCtrlEnterListener: (element) ->
- element.addEventListener "keyup", (event) ->
- if event.ctrlKey and event.keyCode == 13
- element.blur()
- Option.saveOptions()
+ $("saveOptions").innerHTML = "No Changes"
# Abstract method; only implemented in sub-classes.
# Populate the option's DOM element (@element) with the setting's current value.
@@ -66,7 +60,6 @@ class TextOption extends Option
constructor: (field,enableSaveButton) ->
super(field,enableSaveButton)
@element.addEventListener "input", enableSaveButton
- @activateCtrlEnterListener @element
populateElement: (value) -> @element.value = value
readValueFromElement: -> @element.value.trim()
@@ -74,7 +67,6 @@ class NonEmptyTextOption extends Option
constructor: (field,enableSaveButton) ->
super(field,enableSaveButton)
@element.addEventListener "input", enableSaveButton
- @activateCtrlEnterListener @element
populateElement: (value) -> @element.value = value
# If the new value is not empty, then return it. Otherwise, restore the default value.
@@ -89,7 +81,6 @@ class ExclusionRulesOption extends Option
super(args...)
$("exclusionAddButton").addEventListener "click", (event) =>
@appendRule { pattern: "", passKeys: "" }
- @maintainExclusionMargin()
# 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.
@@ -97,11 +88,8 @@ class ExclusionRulesOption extends Option
exclusionScrollBox.scrollTop = exclusionScrollBox.scrollHeight
populateElement: (rules) ->
- while @element.firstChild
- @element.removeChild @element.firstChild
for rule in rules
@appendRule rule
- @maintainExclusionMargin()
# Append a row for a new rule.
appendRule: (rule) ->
@@ -111,7 +99,6 @@ class ExclusionRulesOption extends Option
for field in ["pattern", "passKeys"]
element = row.querySelector ".#{field}"
element.value = rule[field]
- @activateCtrlEnterListener element
for event in [ "input", "change" ]
element.addEventListener event, enableSaveButton
@@ -120,13 +107,12 @@ class ExclusionRulesOption extends Option
row = event.target.parentNode.parentNode
row.parentNode.removeChild row
enableSaveButton()
- @maintainExclusionMargin()
@element.appendChild row
readValueFromElement: ->
rules =
- for element in @element.children
+ for element in @element.getElementsByClassName "exclusionRuleTemplateInstance"
pattern = element.children[0].firstChild.value.trim()
passKeys = element.children[1].firstChild.value.trim()
{ pattern: pattern, passKeys: passKeys }
@@ -138,20 +124,11 @@ 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")
- # Hack. There has to be a better way than...
- # The y-axis scrollbar for "exclusionRules" is only displayed if it is needed. When visible, it appears on
- # top of the enclosed content (partially obscuring it). Here, we adjust the margin of the "Remove" button to
- # compensate.
- maintainExclusionMargin: ->
- scrollBox = $("exclusionScrollBox")
- margin = if scrollBox.clientHeight < scrollBox.scrollHeight then "16px" else "0px"
- for element in scrollBox.getElementsByClassName "exclusionRemoveButton"
- element.style["margin-right"] = margin
-
#
# Operations for page elements.
enableSaveButton = ->
$("saveOptions").removeAttribute "disabled"
+ $("saveOptions").innerHTML = "Save Changes"
# Display either "linkHintNumbers" or "linkHintCharacters", depending upon "filterLinkHints".
maintainLinkHintsView = ->
@@ -175,9 +152,13 @@ toggleAdvancedOptions =
$("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()
#
# Initialization.
@@ -196,6 +177,7 @@ document.addEventListener "DOMContentLoaded", ->
previousPatterns: NonEmptyTextOption
regexFindMode: CheckBoxOption
scrollStepSize: NumberOption
+ smoothScroll: CheckBoxOption
searchEngines: TextOption
searchUrl: NonEmptyTextOption
userDefinedLinkHintCss: TextOption
@@ -213,3 +195,8 @@ document.addEventListener "DOMContentLoaded", ->
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
+ Option.saveOptions()
+
diff --git a/pages/options.html b/pages/options.html
index 4f037ba5..b52974d6 100644
--- a/pages/options.html
+++ b/pages/options.html
@@ -6,12 +6,14 @@
body {
font: 14px "DejaVu Sans", "Arial", sans-serif;
color: #303942;
- width: 680px;
margin: 0 auto;
}
a, a:visited { color: #15c; }
a:active { color: #052577; }
- div#wrapper { width: 500px; }
+ div#wrapper, #footerWrapper {
+ width: 540px;
+ margin-left: 35px;
+ }
header {
font-size: 18px;
font-weight: normal;
@@ -112,21 +114,11 @@
width: 40px;
margin-right: 3px;
}
- textarea#userDefinedLinkHintCss {
+ textarea#userDefinedLinkHintCss, textarea#keyMappings, textarea#searchEngines {
width: 100%;;
min-height: 130px;
white-space: nowrap;
}
- textarea#keyMappings {
- width: 100%;
- min-height: 135px;
- white-space: nowrap;
- }
- textarea#searchEngines {
- width: 100%;
- min-height: 135px;
- white-space: nowrap;
- }
input#previousPatterns, input#nextPatterns {
width: 100%;
}
@@ -141,20 +133,13 @@
font-size: 80%;
}
/* Make the caption in the settings table as small as possible, to pull the other fields to the right. */
- td:first-child {
+ .caption {
width: 1px;
white-space: nowrap;
}
#buttonsPanel { width: 100%; }
#advancedOptions { display: none; }
#advancedOptionsLink { line-height: 24px; }
- #saveOptions { float: right; }
- #saveOptions { margin-right: 0; }
- #showHelpDialogMessage {
- width: 100%;
- color: #979ca0;
- font-size: 12px;
- }
.help {
position: absolute;
right: -320px;
@@ -180,34 +165,84 @@
}
/* Boolean options have a tighter form representation than text options. */
td.booleanOption { font-size: 12px; }
- footer {
- padding: 15px 0;
- border-top: 1px solid #eee;
- }
/* Ids and classes for rendering exclusionRules */
#exclusionScrollBox {
overflow: scroll;
overflow-x: hidden;
overflow-y: auto;
- height: 170px;
- border: 1px solid #bfbfbf;
+ max-height: 135px;
+ min-height: 75px;
border-radius: 2px;
color: #444;
+ width: 100%
+ }
+ #exclusionScrollBox::-webkit-scrollbar {
+ width: 12px;
}
- input.pattern, input.passKeys {
+ #exclusionScrollBox::-webkit-scrollbar-track {
+ -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
+ border-radius: 2px;
+ }
+
+ #exclusionScrollBox::-webkit-scrollbar-thumb {
+ border-radius: 2px;
+ -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.5);
+ }
+ #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;
}
- .pattern {
- width: 250px;
- }
- .passKeys {
- width: 120px;
+ .exclusionHeaderText {
+ padding-left: 3px;
+ color: #979ca0;
}
#exclusionAddButton {
float: right;
- margin-top: 5px;
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>
@@ -224,24 +259,28 @@
<td>
<div class="help">
<div class="example">
- The left column contains URL patterns. Vimium will be wholly or partially disabled for URLs
- matching these patterns. Patterns are Javascript regular expressions. Additionally, the
- character "*" matches any zero or more characters.
+ Wholly or partially disable Vimium. "Patterns" are URL regular expressions;
+ additionally, "*" matches any zero or more characters.
<br/><br/>
- The right column contains keys which Vimium would would normally handle, but which should
- instead be passed through to the underlying web page (for pages matching the
- pattern). If left empty, then Vimium will be wholly disabled.
+ If "Keys" is left empty, then vimium is wholly disabled.
+ Otherwise, just the listed keys are disabled (they are passed through).
</div>
</div>
<div>
<div id="exclusionScrollBox">
- <table id="exclusionRules"></table>
+ <table id="exclusionRules">
+ <tr>
+ <td><span class="exclusionHeaderText">Patterns</span></td>
+ <td><span class="exclusionHeaderText">Keys</span></td>
+ </tr>
+ </table>
<template id="exclusionRuleTemplate">
- <tr>
- <td><input/ type="text" class="pattern" placeholder="URL pattern"></td>
- <td><input/ type="text" class="passKeys" placeholder="Exclude keys"></td>
- <td><input/ type="button" class="exclusionRemoveButton" tabindex = "-1" value="&#x2716;"></td>
- </tr>
+ <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="&#x2716;"></td>
+ </tr>
</template>
</div>
<button id="exclusionAddButton">Add Rule</button>
@@ -283,13 +322,17 @@ unmapAll
</td>
</tr>
<tr>
- <td><a href="#" id="advancedOptionsLink">Show advanced options&hellip;</a></td>
- <td><button id="saveOptions" disabled="true">Save Options</button></td>
+ <td colspan="2"><a href="#" id="advancedOptionsLink">Show advanced options&hellip;</a></td>
</tr>
<tbody id='advancedOptions'>
<tr>
<td class="caption">Scroll step size</td>
<td>
+ <div class="help">
+ <div class="example">
+ The size for basic movements (usually j/k/h/l).
+ </div>
+ </div>
<input id="scrollStepSize" type="number" />px
</td>
</tr>
@@ -306,7 +349,7 @@ unmapAll
</td>
</tr>
<tr>
- <td class="caption">Numbers used<br/> for filtered link hints</td>
+ <td class="caption">Numbers used<br/> for link hints</td>
<td verticalAlign="top">
<div class="help">
<div class="example">
@@ -318,6 +361,15 @@ unmapAll
</td>
</tr>
<tr>
+ <td class="caption" verticalAlign="top">Miscellaneous<br/>options</td>
+ <td verticalAlign="top" class="booleanOption">
+ <label>
+ <input id="smoothScroll" type="checkbox"/>
+ Use smooth scrolling
+ </label>
+ </td>
+ </tr>
+ <tr>
<td class="caption"></td>
<td verticalAlign="top" class="booleanOption">
<div class="help">
@@ -355,7 +407,7 @@ unmapAll
</div>
<label>
<input id="regexFindMode" type="checkbox"/>
- Treat find queries as regular expressions.
+ Treat find queries as regular expressions
</label>
</td>
</tr>
@@ -424,14 +476,28 @@ unmapAll
</tr>
</tbody>
</table>
+ </div>
- <br/>
+ <!-- Some extra space which is hidden underneath the footer. -->
+ <div id="endSpace"/>
- <footer id="showHelpDialogMessage">
- Type <strong>?</strong> to show the Vimium help dialog.
- <br/>
- Type <strong>Ctrl-Enter</strong> in text inputs to save all options.
- </footer>
+ <div id="footer">
+ <div id="footerWrapper">
+ <table id="footerTable">
+ <tr>
+ <td id="footerTableData">
+ <span id="helpText">
+ Type <strong>?</strong> to show the Vimium help dialog.
+ <br/>
+ Type <strong>Ctrl-Enter</strong> to save <i>all</i> options.
+ </span>
+ </td>
+ <td id="saveOptionsTableData">
+ <button id="saveOptions" disabled="true">No Changes</button>
+ </td>
+ </tr>
+ </table>
+ </div>
</div>
</body>
</html>
diff --git a/pages/popup.html b/pages/popup.html
index 691414f2..775d6c07 100644
--- a/pages/popup.html
+++ b/pages/popup.html
@@ -76,7 +76,7 @@
<div id="popupMenu">
<ul>
<li>
- <span id="helpText">Type <strong>Ctrl-ENTER</strong> to save and close.</span>
+ <span id="helpText">Type <strong>Ctrl-Enter</strong> to save and close.</span>
<a id="optionsLink" target="_blank">Options</a>
</li>
</ul>