aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitmodules2
-rw-r--r--.travis.yml14
-rw-r--r--CONTRIBUTING.md15
-rw-r--r--Cakefile10
-rw-r--r--README.md85
-rw-r--r--background_scripts/bg_utils.coffee2
-rw-r--r--background_scripts/commands.coffee9
-rw-r--r--background_scripts/completion.coffee4
-rw-r--r--background_scripts/completion_engines.coffee48
-rw-r--r--background_scripts/completion_search.coffee2
-rw-r--r--background_scripts/exclusions.coffee9
-rw-r--r--background_scripts/main.coffee99
-rw-r--r--background_scripts/marks.coffee12
-rw-r--r--content_scripts/hud.coffee44
-rw-r--r--content_scripts/link_hints.coffee199
-rw-r--r--content_scripts/marks.coffee79
-rw-r--r--content_scripts/mode.coffee38
-rw-r--r--content_scripts/mode_find.coffee79
-rw-r--r--content_scripts/mode_insert.coffee94
-rw-r--r--content_scripts/mode_key_handler.coffee56
-rw-r--r--content_scripts/mode_normal.coffee369
-rw-r--r--content_scripts/mode_visual.coffee23
-rw-r--r--content_scripts/scroller.coffee28
-rw-r--r--content_scripts/ui_component.coffee3
-rw-r--r--content_scripts/vimium.css2
-rw-r--r--content_scripts/vimium_frontend.coffee442
-rw-r--r--content_scripts/vomnibar.coffee11
-rw-r--r--lib/clipboard.coffee12
-rw-r--r--lib/dom_utils.coffee116
-rw-r--r--lib/find_mode_history.coffee3
-rw-r--r--lib/handler_stack.coffee14
-rw-r--r--lib/keyboard_utils.coffee190
-rw-r--r--lib/rect.coffee20
-rw-r--r--lib/settings.coffee82
-rw-r--r--lib/utils.coffee38
-rw-r--r--manifest.json17
-rw-r--r--pages/blank.html23
-rw-r--r--pages/completion_engines.html23
-rw-r--r--pages/help_dialog.coffee9
-rw-r--r--pages/help_dialog.html25
-rw-r--r--pages/hud.coffee32
-rw-r--r--pages/hud.html3
-rw-r--r--pages/logging.coffee1
-rw-r--r--pages/logging.html23
-rw-r--r--pages/options.coffee99
-rw-r--r--pages/options.css7
-rw-r--r--pages/options.html82
-rw-r--r--pages/popup.html2
-rw-r--r--pages/vimium_resources.html24
-rw-r--r--pages/vomnibar.coffee14
-rw-r--r--pages/vomnibar.html1
-rw-r--r--tests/dom_tests/chrome.coffee2
-rw-r--r--tests/dom_tests/dom_tests.coffee185
-rw-r--r--tests/dom_tests/dom_tests.html1
-rw-r--r--tests/dom_tests/phantom_runner.coffee24
-rw-r--r--tests/unit_tests/commands_test.coffee22
-rw-r--r--tests/unit_tests/exclusion_test.coffee5
-rw-r--r--tests/unit_tests/handler_stack_test.coffee1
-rw-r--r--tests/unit_tests/rect_test.coffee28
-rw-r--r--tests/unit_tests/test_chrome_stubs.coffee6
-rw-r--r--tests/unit_tests/utils_test.coffee23
61 files changed, 1696 insertions, 1239 deletions
diff --git a/.gitmodules b/.gitmodules
index 3f5b74a2..24458233 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
[submodule "tests/shoulda.js"]
path = tests/shoulda.js
- url = git://github.com/philc/shoulda.js.git
+ url = https://github.com/philc/shoulda.js.git
diff --git a/.travis.yml b/.travis.yml
index 4f9de03b..81d73211 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,11 +1,13 @@
+sudo: false
language: node_js
-node_js: 0.12
+node_js: "8"
before_install:
- - "npm install -g coffee-script"
- - "npm install path@0.11"
- - "npm install util"
- - "cake build"
-script: "cake test"
+ - npm install -g coffee-script
+ - npm install path@0.11
+ - npm install util
+ - npm install -g phantomjs@1.9.20
+ - cake build
+script: cake test
notifications:
email: false
branches:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 27db315c..e4ed8b8b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -3,7 +3,7 @@
You'd like to fix a bug or implement a feature? Great! Check out the bugs on our issues tracker, or implement
one of the suggestions there that have been tagged "help wanted". If you have a suggestion of your own, start
a discussion on the issues tracker or on the
-[mailing list](http://groups.google.com/group/vimium-dev?hl=en). If it mirrors a similar feature in another
+[mailing list](https://groups.google.com/group/vimium-dev?hl=en). If it mirrors a similar feature in another
browser or in Vim itself, let us know. Once you've picked something to work on, add a comment to the
respective issue so others don't duplicate your effort.
@@ -11,9 +11,16 @@ respective issue so others don't duplicate your effort.
Please include the following when reporting an issue:
- 1. Chrome and OS Version: `chrome://version`
+### Chrome/Chromium
+
+ 1. Chrome/Chromium and OS Version: `chrome://version`
1. Vimium Version: `chrome://extensions`
+### Firefox
+
+ 1. Firefox and OS Version: `about:`
+ 1. Vimium Version: `about:addons`, then click on `More` below Vimium
+
## Installing From Source
Vimium is written in Coffeescript, which compiles to Javascript. To
@@ -37,7 +44,7 @@ Our tests use [shoulda.js](https://github.com/philc/shoulda.js) and [PhantomJS](
1. `git submodule update --init --recursive` -- this pulls in shoulda.js.
1. Install [PhantomJS](http://phantomjs.org/download.html).
- 1. `npm install path@0.11` to install the [Node.js Path module](http://nodejs.org/api/path.html), used by the test runner.
+ 1. `npm install path@0.11` to install the [Node.js Path module](https://nodejs.org/api/path.html), used by the test runner.
1. `npm install util` to install the [util module](https://www.npmjs.com/package/util), used by the tests.
1. `cake build` to compile `*.coffee` to `*.js`
1. `cake test` to run the tests.
@@ -47,7 +54,7 @@ Our tests use [shoulda.js](https://github.com/philc/shoulda.js) and [PhantomJS](
You can find out which portions of code need them by looking at our coverage reports. To generate these
reports:
- 1. Download [JSCoverage](http://siliconforks.com/jscoverage/download.html) or `brew install jscoverage`
+ 1. Download [JSCoverage](https://siliconforks.com/jscoverage/download.html) or `brew install jscoverage`
1. `npm install temp`
1. `cake coverage` will generate a coverage report in the form of a JSON file (`jscoverage.json`), which can
then be viewed using [jscoverage-report](https://github.com/int3/jscoverage-report). See
diff --git a/Cakefile b/Cakefile
index 88e92546..2b5a4fb5 100644
--- a/Cakefile
+++ b/Cakefile
@@ -75,8 +75,12 @@ task "package", "Builds a zip file for submission to the Chrome store. The outpu
blacklist.map((item) -> ["--exclude", "#{item}"]))
spawn "rsync", rsyncOptions, false, true
+ spawn "sed", "-i /clipboardWrite/d dist/vimium/manifest.json".split /\s+/
spawn "zip", ["-r", "dist/vimium-#{vimium_version}.zip", "dist/vimium"], false, true
+ spawn "zip", "-r -FS dist/vimium-ff-#{vimium_version}.zip background_scripts Cakefile content_scripts CONTRIBUTING.md CREDITS icons lib
+ manifest.json MIT-LICENSE.txt pages README.md -x *.coffee -x Cakefile -x CREDITS -x *.md".split(/\s+/), false, true
+
# This builds a CRX that's distributable outside of the Chrome web store. Is this used by folks who fork
# Vimium and want to distribute their fork?
task "package-custom-crx", "build .crx file", ->
@@ -86,7 +90,11 @@ task "package-custom-crx", "build .crx file", ->
# ugly hack to modify our manifest file on-the-fly
origManifestText = fs.readFileSync "manifest.json"
manifest = JSON.parse origManifestText
- manifest.update_url = "http://philc.github.com/vimium/updates.xml"
+ # Update manifest fields that you would like to override here. If
+ # distributing your CRX outside the Chrome webstore in a fork, please follow
+ # the instructions available at
+ # https://developer.chrome.com/extensions/autoupdate.
+ # manifest.update_url = "http://philc.github.com/vimium/updates.xml"
fs.writeFileSync "manifest.json", JSON.stringify manifest
pem = process.env.VIMIUM_CRX_PEM ? "vimium.pem"
diff --git a/README.md b/README.md
index 7fcafdfe..d282532d 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
Vimium - The Hacker's Browser
=============================
-[![Build Status](https://secure.travis-ci.org/philc/vimium.png?branch=master)](https://travis-ci.org/philc/vimium)
+[![Build Status](https://travis-ci.org/philc/vimium.svg?branch=master)](https://travis-ci.org/philc/vimium)
Vimium is a Chrome extension that provides keyboard-based navigation and control of the web in the spirit of
the Vim editor.
@@ -12,7 +12,7 @@ You can install the stable version of Vimium from the
[Chrome Extensions Gallery](https://chrome.google.com/extensions/detail/dbepggeogbaibhgnhhndojpepiihcmeb).
Please see
-[CONTRIBUTING.md](https://github.com/philc/vimium/blob/master/CONTRIBUTING.md#installing-from-source)
+[CONTRIBUTING.md](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 (type `?`) or via the button next to Vimium on
@@ -22,7 +22,7 @@ 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 how to customize these bindings.
+respectively. For shift+x and ctrl-shift-x, just type `X` and `<c-X>`. 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 `?`.
@@ -80,6 +80,7 @@ Manipulating tabs:
x close current tab
X restore closed tab (i.e. unwind the 'x' command)
T search through your open tabs
+ W move current tab to new window
<a-p> pin/unpin current tab
Using marks:
@@ -140,29 +141,89 @@ The following special keys are available for mapping:
- `<c-*>`, `<a-*>`, `<m-*>` for ctrl, alt, and meta (command on Mac) respectively with any key. Replace `*`
with the key of choice.
-- `<left>`, `<right>`, `<up>`, `<down>` for the arrow keys
-- `<space>` and `<backspace>` for the space and backspace keys
-- `<f1>` through `<f12>` for the function keys
+- `<left>`, `<right>`, `<up>`, `<down>` for the arrow keys.
+- `<f1>` through `<f12>` for the function keys.
+- `<space>` for the space key.
+- `<tab>`, `<enter>`, `<delete>`, `<backspace>`, `<insert>`, `<home>` and `<end>` for the corresponding non-printable keys (version 1.62 onwards).
Shifts are automatically detected so, for example, `<c-&>` corresponds to ctrl+shift+7 on an English keyboard.
More documentation
------------------
Many of the more advanced or involved features are documented on
-[Vimium's github wiki](https://github.com/philc/vimium/wiki). Also
+[Vimium's GitHub wiki](https://github.com/philc/vimium/wiki). Also
see the [FAQ](https://github.com/philc/vimium/wiki/FAQ).
Contributing
------------
-Please see [CONTRIBUTING.md](https://github.com/philc/vimium/blob/master/CONTRIBUTING.md) for details.
+Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
+
+Firefox Support
+---------------
+
+There is an *experimental* port of Vimium on Firefox [here](https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/).
+This is very much experimental: most features work, although some bugs and issues remain.
+
+PRs are welcome.
Release Notes
-------------
-Changes since the previous release (not yet in the Chrome Store version)
+1.62 (2017-12-09)
+
+- Backup and restore Vimium options (see the very bottom of the options page, below *Advanced Options*).
+- It is now possible to map `<tab>`, `<enter>`, `<delete>`, `<insert>`, `<home>` and `<end>`.
+- New command options for `createTab` to create new normal and incognito windows
+ ([examples](https://github.com/philc/vimium/wiki/Tips-and-Tricks#creating-tabs-with-urls-and-windows)).
+- Firefox only:
+ - Fix copy and paste commands.
+ - When upgrading, you will be asked to re-validate permissions. The only
+ new permission is "copy and paste to/from clipboard" (the
+ `clipboardWrite` permission). This is necessary to support copy/paste on
+ Firefox.
+- Various bug fixes.
+
+1.61 (2017-10-27)
+
+- For *filtered hints*, you can now use alphabetical hint characters
+ instead of digits; use `<Shift>` for hint characters.
+- With `map R reload hard`, the reload command now asks Chrome to bypass its cache.
+- You can now map `<c-[>` to a command (in which case it will not be treated as `Escape`).
+- Various bug fixes, particularly for Firefox.
+- Minor versions:
+ - 1.61.1: Fix `map R reload hard`.
+
+1.60 (2017-09-14)
+
+- Features:
+ - There's a new (advanced) option to ignore the keyboard layout; this can
+ be helpful for users of non-Latin keyboards.
+ - Firefox support. This is a work in progress; please report any issues
+ [here](https://github.com/philc/vimium/issues?q=is%3Aopen+sort%3Aupdated-desc);
+ see the [add
+ on](https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/).
+
+- Bug fixes:
+ - Fixed issue affecting hint placement when the display is zoomed.
+ - Fixed search completion for Firefox (released as 1.59.1, Firefox only).
+
+- Minor versions:
+ - 1.60.1: fix [#2642](https://github.com/philc/vimium/issues/2642).
+ - 1.60.2: revert previous fix for HiDPI screens. This was breaking link-hint positioning for some users.
+ - 1.60.3: [fix](https://github.com/philc/vimium/pull/2649) link-hint positioning.
+ - 1.60.4: [fix](https://github.com/philc/vimium/pull/2602) hints opening in new tab (Firefox only).
+
+1.59 (2017-04-07)
+
+- Features:
+ - Some commands now work on PDF tabs (`J`, `K`, `o`, `b`, etc.). Scrolling and other content-related commands still do not work.
+
+1.58 (2017-03-08)
- Features:
- The `createTab` command can now open specific URLs (e.g, `map X createTab http://www.bbc.com/news`).
+ - With pass keys defined for a site (such as GMail), you can now use Vimium's bindings again with, for example, `map \ passNextKey normal`;
+ this reactivates normal mode temporarily, but *without any pass keys*.
- You can now map multi-modifier keys, for example: `<c-a-X>`.
- Vimium can now do simple key mapping in some modes; see
[here](https://github.com/philc/vimium/wiki/Tips-and-Tricks#key-mapping).
@@ -172,6 +233,10 @@ Changes since the previous release (not yet in the Chrome Store version)
- Process:
- In order to provide faster bug fixes, we may in future push new releases without the noisy notification.
+- Post-release minor fixes:
+ - 1.58.1 (2017-03-09) fix bug in `LinkHints.activateModeWithQueue` (#2445).
+ - 1.58.2 (2017-03-19) fix key handling bug (#2453).
+
1.57 (2016-10-01)
- New commands:
@@ -435,7 +500,7 @@ Changes since the previous release (not yet in the Chrome Store version)
- Arrow keys and function keys can now be mapped using &lt;left&gt;, &lt;right&gt;, &lt;up&gt;, &lt;down&gt;,
&lt;f1&gt;, &lt;f2&gt;, etc. in the mappings interface.
- There is a new command `goUp` (mapped to `gu` by default) that will go up one level in the URL hierarchy.
- For example: from http://vimium.github.com/foo/bar to http://vimium.github.com/foo. At the moment, `goUp`
+ For example: from https://vimium.github.io/foo/bar to https://vimium.github.io/foo. At the moment, `goUp`
does not support command repetition.
- Bug fixes and optimizations.
diff --git a/background_scripts/bg_utils.coffee b/background_scripts/bg_utils.coffee
index b8e618ff..698f5352 100644
--- a/background_scripts/bg_utils.coffee
+++ b/background_scripts/bg_utils.coffee
@@ -18,7 +18,7 @@ class TabRecency
@deregister removedTabId
@register addedTabId
- chrome.windows.onFocusChanged.addListener (wnd) =>
+ chrome.windows?.onFocusChanged.addListener (wnd) =>
if wnd != chrome.windows.WINDOW_ID_NONE
chrome.tabs.query {windowId: wnd, active: true}, (tabs) =>
@register tabs[0].id if tabs[0]
diff --git a/background_scripts/commands.coffee b/background_scripts/commands.coffee
index 8d3808a2..4d2e1606 100644
--- a/background_scripts/commands.coffee
+++ b/background_scripts/commands.coffee
@@ -9,7 +9,6 @@ Commands =
Settings.postUpdateHooks["keyMappings"] = @loadKeyMappings.bind this
@loadKeyMappings Settings.get "keyMappings"
- @prepareHelpPageData()
loadKeyMappings: (customKeyMappings) ->
@keyToCommandRegistry = {}
@@ -43,6 +42,7 @@ Commands =
chrome.storage.local.set mapKeyRegistry: @mapKeyRegistry
@installKeyStateMapping()
+ @prepareHelpPageData()
# Push the key mapping for passNextKey into Settings so that it's available in the front end for insert
# mode. We exclude single-key mappings (that is, printable keys) because when users press printable keys
@@ -113,6 +113,9 @@ Commands =
# We don't need these properties in the content scripts.
delete currentMapping[key][prop] for prop in ["keySequence", "description"]
chrome.storage.local.set normalModeKeyStateMapping: keyStateMapping
+ # Inform `KeyboardUtils.isEscape()` whether `<c-[>` should be interpreted as `Escape` (which it is by
+ # default).
+ chrome.storage.local.set useVimLikeEscape: "<c-[>" not of keyStateMapping
# Build the "helpPageData" data structure which the help page needs and place it in Chrome storage.
prepareHelpPageData: ->
@@ -337,8 +340,8 @@ commandDescriptions =
toggleViewSource: ["View page source", { noRepeat: true }]
copyCurrentUrl: ["Copy the current URL to the clipboard", { noRepeat: true }]
- openCopiedUrlInCurrentTab: ["Open the clipboard's URL in the current tab", { background: true, noRepeat: true }]
- openCopiedUrlInNewTab: ["Open the clipboard's URL in a new tab", { background: true, repeatLimit: 20 }]
+ openCopiedUrlInCurrentTab: ["Open the clipboard's URL in the current tab", { noRepeat: true }]
+ openCopiedUrlInNewTab: ["Open the clipboard's URL in a new tab", { repeatLimit: 20 }]
enterInsertMode: ["Enter insert mode", { noRepeat: true }]
passNextKey: ["Pass the next key to the page"]
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 66ad2e38..987ba8a4 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -763,6 +763,8 @@ HistoryCache =
return @callbacks.push(callback) if @callbacks
@callbacks = [callback]
chrome.history.search { text: "", maxResults: @size, startTime: 0 }, (history) =>
+ # On Firefox, some history entries do not have titles.
+ history.map (entry) -> entry.title ?= ""
history.sort @compareHistoryByUrl
@history = history
chrome.history.onVisited.addListener(@onPageVisited.bind(this))
@@ -778,6 +780,8 @@ HistoryCache =
# When a page we've seen before has been visited again, be sure to replace our History item so it has the
# correct "lastVisitTime". That's crucial for ranking Vomnibar suggestions.
onPageVisited: (newPage) ->
+ # On Firefox, some history entries do not have titles.
+ newPage.title ?= ""
i = HistoryCache.binarySearch(newPage, @history, @compareHistoryByUrl)
pageWasFound = (@history[i]?.url == newPage.url)
if pageWasFound
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index 5ec24ed6..a6ff6dc3 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -47,18 +47,18 @@ class GoogleXMLBaseEngine extends BaseEngine
class Google extends GoogleXMLBaseEngine
constructor: () ->
super
- engineUrl: "http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=%s"
- regexps: "^https?://[a-z]+\\.google\\.(com|ie|co\\.uk|ca|com\\.au)/"
+ engineUrl: "https://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=%s"
+ regexps: "^https?://[a-z]+\\.google\\.(com|ie|co\\.(uk|jp)|ca|com\\.au)/"
example:
- searchUrl: "http://www.google.com/search?q=%s"
+ searchUrl: "https://www.google.com/search?q=%s"
keyword: "g"
class GoogleMaps extends GoogleXMLBaseEngine
prefix: "map of "
constructor: () ->
super
- engineUrl: "http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=#{@prefix.split(' ').join '+'}%s"
- regexps: "^https?://[a-z]+\\.google\\.(com|ie|co\\.uk|ca|com\\.au)/maps"
+ engineUrl: "https://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=#{@prefix.split(' ').join '+'}%s"
+ regexps: "^https?://[a-z]+\\.google\\.(com|ie|co\\.(uk|jp)|ca|com\\.au)/maps"
example:
searchUrl: "https://www.google.com/maps?q=%s"
keyword: "m"
@@ -77,10 +77,10 @@ class GoogleMaps extends GoogleXMLBaseEngine
class Youtube extends GoogleXMLBaseEngine
constructor: ->
super
- engineUrl: "http://suggestqueries.google.com/complete/search?client=youtube&ds=yt&xml=t&q=%s"
+ engineUrl: "https://suggestqueries.google.com/complete/search?client=youtube&ds=yt&xml=t&q=%s"
regexps: "^https?://[a-z]+\\.youtube\\.com/results"
example:
- searchUrl: "http://www.youtube.com/results?search_query=%s"
+ searchUrl: "https://www.youtube.com/results?search_query=%s"
keyword: "y"
class Wikipedia extends BaseEngine
@@ -89,7 +89,7 @@ class Wikipedia extends BaseEngine
engineUrl: "https://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=%s"
regexps: "^https?://[a-z]+\\.wikipedia\\.org/"
example:
- searchUrl: "http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s"
+ searchUrl: "https://www.wikipedia.org/w/index.php?title=Special:Search&search=%s"
keyword: "w"
parse: (xhr) -> JSON.parse(xhr.responseText)[1]
@@ -97,7 +97,7 @@ class Wikipedia extends BaseEngine
class Bing extends BaseEngine
constructor: ->
super
- engineUrl: "http://api.bing.com/osjson.aspx?query=%s"
+ engineUrl: "https://api.bing.com/osjson.aspx?query=%s"
regexps: "^https?://www\\.bing\\.com/search"
example:
searchUrl: "https://www.bing.com/search?q=%s"
@@ -111,11 +111,22 @@ class Amazon extends BaseEngine
engineUrl: "https://completion.amazon.com/search/complete?method=completion&search-alias=aps&client=amazon-search-ui&mkt=1&q=%s"
regexps: "^https?://www\\.amazon\\.(com|co\\.uk|ca|de|com\\.au)/s/"
example:
- searchUrl: "http://www.amazon.com/s/?field-keywords=%s"
+ searchUrl: "https://www.amazon.com/s/?field-keywords=%s"
keyword: "a"
parse: (xhr) -> JSON.parse(xhr.responseText)[1]
+class AmazonJapan extends BaseEngine
+ constructor: ->
+ super
+ engineUrl: "https://completion.amazon.co.jp/search/complete?method=completion&search-alias=aps&client=amazon-search-ui&mkt=6&q=%s"
+ regexps: "^https?://www\\.amazon\\.co\\.jp/(s/|gp/search)"
+ example:
+ searchUrl: "https://www.amazon.co.jp/s/?field-keywords=%s"
+ keyword: "aj"
+
+ parse: (xhr) -> JSON.parse(xhr.responseText)[1]
+
class DuckDuckGo extends BaseEngine
constructor: ->
super
@@ -131,10 +142,10 @@ class DuckDuckGo extends BaseEngine
class Webster extends BaseEngine
constructor: ->
super
- engineUrl: "http://www.merriam-webster.com/autocomplete?query=%s"
+ engineUrl: "https://www.merriam-webster.com/autocomplete?query=%s"
regexps: "^https?://www.merriam-webster.com/dictionary/"
example:
- searchUrl: "http://www.merriam-webster.com/dictionary/%s"
+ searchUrl: "https://www.merriam-webster.com/dictionary/%s"
keyword: "dw"
description: "Dictionary"
@@ -152,6 +163,17 @@ class Qwant extends BaseEngine
parse: (xhr) ->
suggestion.value for suggestion in JSON.parse(xhr.responseText).data.items
+class UpToDate extends BaseEngine
+ constructor: ->
+ super
+ engineUrl: "https://www.uptodate.com/services/app/contents/search/autocomplete/json?term=%s&limit=10"
+ regexps: "^https?://www\\.uptodate\\.com/"
+ example:
+ searchUrl: "https://www.uptodate.com/contents/search?search=%s&searchType=PLAIN_TEXT&source=USER_INPUT&searchControl=TOP_PULLDOWN&autoComplete=false"
+ keyword: "upto"
+
+ parse: (xhr) -> JSON.parse(xhr.responseText).data.searchTerms
+
# A dummy search engine which is guaranteed to match any search URL, but never produces completions. This
# allows the rest of the logic to be written knowing that there will always be a completion engine match.
class DummyCompletionEngine extends BaseEngine
@@ -169,8 +191,10 @@ CompletionEngines = [
Wikipedia
Bing
Amazon
+ AmazonJapan
Webster
Qwant
+ UpToDate
DummyCompletionEngine
]
diff --git a/background_scripts/completion_search.coffee b/background_scripts/completion_search.coffee
index 7926b45b..52206c78 100644
--- a/background_scripts/completion_search.coffee
+++ b/background_scripts/completion_search.coffee
@@ -2,7 +2,7 @@
# This is a wrapper class for completion engines. It handles the case where a custom search engine includes a
# prefix query term (or terms). For example:
#
-# http://www.google.com/search?q=javascript+%s
+# https://www.google.com/search?q=javascript+%s
#
# In this case, we get better suggestions if we include the term "javascript" in queries sent to the
# completion engine. This wrapper handles adding such prefixes to completion-engine queries and removing them
diff --git a/background_scripts/exclusions.coffee b/background_scripts/exclusions.coffee
index 42d3b872..fec66f4c 100644
--- a/background_scripts/exclusions.coffee
+++ b/background_scripts/exclusions.coffee
@@ -20,12 +20,12 @@ Exclusions =
# Make RegexpCache, which is required on the page popup, accessible via the Exclusions object.
RegexpCache: RegexpCache
- rules: Settings.get("exclusionRules")
+ rules: Settings.get "exclusionRules"
# 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) ->
+ getRule: (url, rules = @rules) ->
matchingRules = (rule for rule in rules when rule.pattern and 0 <= url.search RegexpCache.get rule.pattern)
# An absolute exclusion rule (one with no passKeys) takes priority.
for rule in matchingRules
@@ -47,7 +47,10 @@ Exclusions =
@rules = rules.filter (rule) -> rule and rule.pattern
Settings.set "exclusionRules", @rules
- postUpdateHook: (@rules) ->
+ postUpdateHook: (rules) ->
+ # NOTE(mrmr1993): In FF, the |rules| argument will be garbage collected when the exclusions popup is
+ # closed. Do NOT store it/use it asynchronously.
+ @rules = Settings.get "exclusionRules"
RegexpCache.clear()
# Register postUpdateHook for exclusionRules setting.
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 72f87a7e..e3188a26 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -5,6 +5,7 @@ root = exports ? window
chrome.runtime.onInstalled.addListener ({ reason }) ->
# See https://developer.chrome.com/extensions/runtime#event-onInstalled
return if reason in [ "chrome_update", "shared_module_update" ]
+ return if Utils.isFirefox()
manifest = chrome.runtime.getManifest()
# Content scripts loaded on every page should be in the same group. We assume it is the first.
contentScripts = manifest.content_scripts[0]
@@ -19,7 +20,7 @@ chrome.runtime.onInstalled.addListener ({ reason }) ->
func tab.id, { file: file, allFrames: contentScripts.all_frames }, checkLastRuntimeError
frameIdsForTab = {}
-portsForTab = {}
+root.portsForTab = {}
root.urlForTab = {}
# This is exported for use by "marks.coffee".
@@ -50,6 +51,10 @@ completers =
completionHandlers =
filter: (completer, request, port) ->
completer.filter request, (response) ->
+ # NOTE(smblott): response contains `relevancyFunction` (function) properties which cause postMessage,
+ # below, to fail in Firefox. See #2576. We cannot simply delete these methods, as they're needed
+ # elsewhere. Converting the response to JSON and back is a quick and easy way to sanitize the object.
+ response = JSON.parse JSON.stringify response
# We use try here because this may fail if the sender has already navigated away from the original page.
# This can happen, for example, when posting completion suggestions from the SearchEngineCompleter
# (which is done asynchronously).
@@ -99,10 +104,27 @@ TabOperations =
tabConfig =
url: Utils.convertToUrl request.url
index: request.tab.index + 1
- selected: true
+ active: true
windowId: request.tab.windowId
- openerTabId: request.tab.id
- chrome.tabs.create tabConfig, callback
+ tabConfig.active = request.active if request.active?
+ # Firefox does not support "about:newtab" in chrome.tabs.create.
+ delete tabConfig["url"] if tabConfig["url"] == Settings.defaults.newTabUrl
+
+ # Firefox <57 throws an error when openerTabId is used (issue 1238314).
+ canUseOpenerTabId = not (Utils.isFirefox() and Utils.compareVersions(Utils.firefoxVersion(), "57") < 0)
+ tabConfig.openerTabId = request.tab.id if canUseOpenerTabId
+
+ chrome.tabs.create tabConfig, -> callback request
+
+ # Opens request.url in new window and switches to it.
+ openUrlInNewWindow: (request, callback = (->)) ->
+ winConfig =
+ url: Utils.convertToUrl request.url
+ active: true
+ winConfig.active = request.active if request.active?
+ # Firefox does not support "about:newtab" in chrome.tabs.create.
+ delete tabConfig["url"] if tabConfig["url"] == Settings.defaults.newTabUrl
+ chrome.windows.create winConfig, callback
toggleMuteTab = do ->
muteTab = (tab) -> chrome.tabs.update tab.id, {muted: !tab.mutedInfo.muted}
@@ -126,12 +148,12 @@ toggleMuteTab = do ->
#
selectSpecificTab = (request) ->
chrome.tabs.get(request.id, (tab) ->
- chrome.windows.update(tab.windowId, { focused: true })
- chrome.tabs.update(request.id, { selected: true }))
+ chrome.windows?.update(tab.windowId, { focused: true })
+ chrome.tabs.update(request.id, { active: true }))
moveTab = ({count, tab, registryEntry}) ->
count = -count if registryEntry.command == "moveTabLeft"
- chrome.tabs.getAllInWindow null, (tabs) ->
+ chrome.tabs.query { currentWindow: true }, (tabs) ->
pinnedCount = (tabs.filter (tab) -> tab.pinned).length
minIndex = if tab.pinned then 0 else pinnedCount
maxIndex = (if tab.pinned then pinnedCount else tabs.length) - 1
@@ -166,13 +188,20 @@ BackgroundCommands =
[if request.tab.incognito then "chrome://newtab" else chrome.runtime.getURL newTabUrl]
else
[newTabUrl]
- urls = request.urls[..].reverse()
- do openNextUrl = (request) ->
- if 0 < urls.length
- TabOperations.openUrlInNewTab (extend request, {url: urls.pop()}), (tab) ->
- openNextUrl extend request, {tab, tabId: tab.id}
- else
- callback request
+ if request.registryEntry.options.incognito or request.registryEntry.options.window
+ windowConfig =
+ url: request.urls
+ focused: true
+ incognito: request.registryEntry.options.incognito ? false
+ chrome.windows.create windowConfig, -> callback request
+ else
+ urls = request.urls[..].reverse()
+ do openNextUrl = (request) ->
+ if 0 < urls.length
+ TabOperations.openUrlInNewTab (extend request, {url: urls.pop()}), (tab) ->
+ openNextUrl extend request, {tab, tabId: tab.id}
+ else
+ callback request
duplicateTab: mkRepeatCommand (request, callback) ->
chrome.tabs.duplicate request.tabId, (tab) -> callback extend request, {tab, tabId: tab.id}
moveTabToNewWindow: ({count, tab}) ->
@@ -192,8 +221,6 @@ BackgroundCommands =
startTabIndex = Math.max 0, Math.min activeTabIndex, tabs.length - count
chrome.tabs.remove (tab.id for tab in tabs[startTabIndex...startTabIndex + count])
restoreTab: mkRepeatCommand (request, callback) -> chrome.sessions.restore null, callback request
- openCopiedUrlInCurrentTab: (request) -> TabOperations.openUrlInCurrentTab extend request, url: Clipboard.paste()
- openCopiedUrlInNewTab: (request) -> @createTab extend request, url: Clipboard.paste()
togglePinTab: ({tab}) -> chrome.tabs.update tab.id, {pinned: !tab.pinned}
toggleMuteTab: toggleMuteTab
moveTabLeft: moveTab
@@ -226,7 +253,7 @@ removeTabsRelative = (direction, {tab: activeTab}) ->
# Selects a tab before or after the currently selected tab.
# - direction: "next", "previous", "first" or "last".
selectTab = (direction, {count, tab}) ->
- chrome.tabs.getAllInWindow null, (tabs) ->
+ chrome.tabs.query { currentWindow: true }, (tabs) ->
if 1 < tabs.length
toSelect =
switch direction
@@ -238,12 +265,11 @@ selectTab = (direction, {count, tab}) ->
Math.min tabs.length - 1, count - 1
when "last"
Math.max 0, tabs.length - count
- chrome.tabs.update tabs[toSelect].id, selected: true
+ chrome.tabs.update tabs[toSelect].id, active: true
-chrome.tabs.onUpdated.addListener (tabId, changeInfo, tab) ->
- return unless changeInfo.status == "loading" # Only do this once per URL change.
+chrome.webNavigation.onCommitted.addListener ({tabId, frameId}) ->
cssConf =
- allFrames: true
+ frameId: frameId
code: Settings.get("userDefinedLinkHintCss")
runAt: "document_start"
chrome.tabs.insertCSS tabId, cssConf, -> chrome.runtime.lastError
@@ -275,8 +301,9 @@ for icon in [ENABLED_ICON, DISABLED_ICON, PARTIAL_ICON]
Frames =
onConnect: (sender, port) ->
[tabId, frameId] = [sender.tab.id, sender.frameId]
- port.onDisconnect.addListener -> Frames.unregisterFrame {tabId, frameId}
+ port.onDisconnect.addListener -> Frames.unregisterFrame {tabId, frameId, port}
port.postMessage handler: "registerFrameId", chromeFrameId: frameId
+ (portsForTab[tabId] ?= {})[frameId] = port
# Return our onMessage handler for this port.
(request, port) =>
@@ -284,13 +311,12 @@ Frames =
registerFrame: ({tabId, frameId, port}) ->
frameIdsForTab[tabId].push frameId unless frameId in frameIdsForTab[tabId] ?= []
- (portsForTab[tabId] ?= {})[frameId] = port
- unregisterFrame: ({tabId, frameId}) ->
- # FrameId 0 is the top/main frame. We never unregister that frame. If the tab is closing, then we tidy
- # up elsewhere. If the tab is navigating to a new page, then a new top frame will be along soon.
- # This mitigates against the unregister and register messages arriving out of order. See #2125.
- if 0 < frameId
+ unregisterFrame: ({tabId, frameId, port}) ->
+ # Check that the port trying to unregister the frame hasn't already been replaced by a new frame
+ # registering. See #2125.
+ registeredPort = portsForTab[tabId]?[frameId]
+ if registeredPort == port or not registeredPort
if tabId of frameIdsForTab
frameIdsForTab[tabId] = (fId for fId in frameIdsForTab[tabId] when fId != frameId)
if tabId of portsForTab
@@ -299,10 +325,11 @@ Frames =
isEnabledForUrl: ({request, tabId, port}) ->
urlForTab[tabId] = request.url if request.frameIsFocused
+ request.isFirefox = Utils.isFirefox() # Update the value for Utils.isFirefox in the frontend.
enabledState = Exclusions.isEnabledForUrl request.url
if request.frameIsFocused
- chrome.browserAction.setIcon tabId: tabId, imageData: do ->
+ chrome.browserAction.setIcon? tabId: tabId, imageData: do ->
enabledStateIcon =
if not enabledState.isEnabledForUrl
DISABLED_ICON
@@ -333,7 +360,7 @@ handleFrameFocused = ({tabId, frameId}) ->
# Rotate through frames to the frame count places after frameId.
cycleToFrame = (frames, frameId, count = 0) ->
- # We can't always track which frame chrome has focussed, but here we learn that it's frameId; so add an
+ # We can't always track which frame chrome has focused, but here we learn that it's frameId; so add an
# additional offset such that we do indeed start from frameId.
count = (count + Math.max 0, frames.indexOf frameId) % frames.length
[frames[count..]..., frames[0...count]...]
@@ -362,7 +389,8 @@ HintCoordinator =
prepareToActivateMode: (tabId, originatingFrameId, {modeIndex, isVimiumHelpDialog}) ->
@tabState[tabId] = {frameIds: frameIdsForTab[tabId][..], hintDescriptors: {}, originatingFrameId, modeIndex}
- @tabState[tabId].ports = extend {}, portsForTab[tabId]
+ @tabState[tabId].ports = {}
+ frameIdsForTab[tabId].map (frameId) => @tabState[tabId].ports[frameId] = portsForTab[tabId][frameId]
@sendMessage "getHintDescriptors", tabId, {modeIndex, isVimiumHelpDialog}
# Receive hint descriptors from all frames and activate link-hints mode when we have them all.
@@ -401,15 +429,14 @@ sendRequestHandlers =
# getCurrentTabUrl is used by the content scripts to get their full URL, because window.location cannot help
# with Chrome-specific URLs like "view-source:http:..".
getCurrentTabUrl: ({tab}) -> tab.url
- openUrlInNewTab: (request) -> TabOperations.openUrlInNewTab request
+ openUrlInNewTab: mkRepeatCommand (request, callback) -> TabOperations.openUrlInNewTab request, callback
+ openUrlInNewWindow: (request) -> TabOperations.openUrlInNewWindow request
openUrlInIncognito: (request) -> chrome.windows.create incognito: true, url: Utils.convertToUrl request.url
openUrlInCurrentTab: TabOperations.openUrlInCurrentTab
openOptionsPageInNewTab: (request) ->
chrome.tabs.create url: chrome.runtime.getURL("pages/options.html"), index: request.tab.index + 1
frameFocused: handleFrameFocused
nextFrame: BackgroundCommands.nextFrame
- copyToClipboard: Clipboard.copy.bind Clipboard
- pasteFromClipboard: Clipboard.paste.bind Clipboard
selectSpecificTab: selectSpecificTab
createMark: Marks.create.bind(Marks)
gotoMark: Marks.goto.bind(Marks)
@@ -426,7 +453,7 @@ chrome.tabs.onRemoved.addListener (tabId) ->
delete cache[tabId] for cache in [frameIdsForTab, urlForTab, portsForTab, HintCoordinator.tabState]
chrome.storage.local.get "findModeRawQueryListIncognito", (items) ->
if items.findModeRawQueryListIncognito
- chrome.windows.getAll null, (windows) ->
+ chrome.windows?.getAll null, (windows) ->
for window in windows
return if window.incognito
# There are no remaining incognito-mode tabs, and findModeRawQueryListIncognito is set.
@@ -467,7 +494,7 @@ do showUpgradeMessage = ->
Settings.set "previousVersion", currentVersion
chrome.notifications.onClicked.addListener (id) ->
if id == notificationId
- chrome.tabs.getSelected null, (tab) ->
+ chrome.tabs.query { active: true, currentWindow: true }, ([tab]) ->
TabOperations.openUrlInNewTab {tab, tabId: tab.id, url: "https://github.com/philc/vimium#release-notes"}
else
# We need to wait for the user to accept the "notifications" permission.
diff --git a/background_scripts/marks.coffee b/background_scripts/marks.coffee
index dbc14671..77b07b41 100644
--- a/background_scripts/marks.coffee
+++ b/background_scripts/marks.coffee
@@ -30,7 +30,7 @@ Marks =
saveMark: (markInfo) ->
item = {}
item[@getLocationKey markInfo.markName] = markInfo
- chrome.storage.sync.set item
+ Settings.storage.set item
# Goto a global mark. We try to find the original tab. If we can't find that, then we try to find another
# tab with the original URL, and use that. And if we can't find such an existing tab, then we create a new
@@ -39,7 +39,7 @@ Marks =
chrome.storage.local.get "vimiumSecret", (items) =>
vimiumSecret = items.vimiumSecret
key = @getLocationKey req.markName
- chrome.storage.sync.get key, (items) =>
+ Settings.storage.get key, (items) =>
markInfo = items[key]
if markInfo.vimiumSecret != vimiumSecret
# This is a different Vimium instantiation, so markInfo.tabId is definitely out of date.
@@ -57,7 +57,7 @@ Marks =
# Focus an existing tab and scroll to the given position within it.
gotoPositionInTab: ({ tabId, scrollX, scrollY, markName }) ->
- chrome.tabs.update tabId, {selected: true}, ->
+ chrome.tabs.update tabId, { active: true }, ->
chrome.tabs.sendMessage tabId, {name: "setScrollPosition", scrollX, scrollY}
# The tab we're trying to find no longer exists. We either find another tab with a matching URL and use it,
@@ -82,7 +82,7 @@ Marks =
# Given a list of tabs candidate tabs, pick one. Prefer tabs in the current window and tabs with shorter
# (matching) URLs.
pickTab: (tabs, callback) ->
- chrome.windows.getCurrent ({ id }) ->
+ tabPicker = ({ id }) ->
# Prefer tabs in the current window, if there are any.
tabsInWindow = tabs.filter (tab) -> tab.windowId == id
tabs = tabsInWindow if 0 < tabsInWindow.length
@@ -92,6 +92,10 @@ Marks =
# Prefer shorter URLs.
tabs.sort (a,b) -> a.url.length - b.url.length
callback tabs[0]
+ if chrome.windows?
+ chrome.windows.getCurrent tabPicker
+ else
+ tabPicker({id: undefined})
root = exports ? window
root.Marks = Marks
diff --git a/content_scripts/hud.coffee b/content_scripts/hud.coffee
index b2780491..42a960da 100644
--- a/content_scripts/hud.coffee
+++ b/content_scripts/hud.coffee
@@ -9,6 +9,8 @@ HUD =
findMode: null
abandon: -> @hudUI?.hide false
+ pasteListener: null # Set by @pasteFromClipboard to handle the value returned by pasteResponse
+
# This HUD is styled to precisely mimick the chrome HUD on Mac. Use the "has_popup_and_link_hud.html"
# test harness to tweak these styles to match Chrome's. One limitation of our HUD display is that
# it doesn't sit on top of horizontal scrollbars like Chrome's HUD does.
@@ -35,7 +37,9 @@ HUD =
@tween.fade 1.0, 150
search: (data) ->
- @findMode.findInPlace data.query
+ # NOTE(mrmr1993): On Firefox, window.find moves the window focus away from the HUD. We use postFindFocus
+ # to put it back, so the user can continue typing.
+ @findMode.findInPlace data.query, {"postFindFocus": @hudUI.iframeElement.contentWindow}
# Show the number of matches in the HUD UI.
matchCount = if FindMode.query.parsedQuery.length > 0 then FindMode.query.matchCount else 0
@@ -66,20 +70,47 @@ HUD =
focusNode = DomUtils.getSelectionFocusElement()
document.activeElement?.blur()
- focusNode?.focus()
+ focusNode?.focus?()
if exitEventIsEnter
- handleEnterForFindMode()
+ FindMode.handleEnter()
if FindMode.query.hasResults
postExit = -> new PostFindMode
else if exitEventIsEscape
- # We don't want FindMode to handle the click events that handleEscapeForFindMode can generate, so we
+ # We don't want FindMode to handle the click events that FindMode.handleEscape can generate, so we
# wait until the mode is closed before running it.
- postExit = handleEscapeForFindMode
+ postExit = FindMode.handleEscape
@findMode.exit()
postExit?()
+ # These commands manage copying and pasting from the clipboard in the HUD frame.
+ # NOTE(mrmr1993): We need this to copy and paste on Firefox:
+ # * an element can't be focused in the background page, so copying/pasting doesn't work
+ # * we don't want to disrupt the focus in the page, in case the page is listening for focus/blur events.
+ # * the HUD shouldn't be active for this frame while any of the copy/paste commands are running.
+ copyToClipboard: (text) ->
+ DomUtils.documentComplete =>
+ @init()
+ @hudUI?.postMessage {name: "copyToClipboard", data: text}
+
+ pasteFromClipboard: (@pasteListener) ->
+ DomUtils.documentComplete =>
+ @init()
+ # Show the HUD frame, so Firefox will actually perform the paste.
+ @hudUI.toggleIframeElementClasses "vimiumUIComponentHidden", "vimiumUIComponentVisible"
+ @tween.fade 0, 0
+ @hudUI.postMessage {name: "pasteFromClipboard"}
+
+ pasteResponse: ({data}) ->
+ # Hide the HUD frame again.
+ @hudUI.toggleIframeElementClasses "vimiumUIComponentVisible", "vimiumUIComponentHidden"
+ @unfocusIfFocused()
+ @pasteListener data
+
+ unfocusIfFocused: ->
+ document.activeElement.blur() if document.activeElement == @hudUI?.iframeElement
+
class Tween
opacity: 0
intervalId: -1
@@ -125,5 +156,6 @@ class Tween
}
"""
-root = exports ? window
+root = exports ? (window.root ?= {})
root.HUD = HUD
+extend window, root unless exports?
diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee
index 0014e20a..0592c96d 100644
--- a/content_scripts/link_hints.coffee
+++ b/content_scripts/link_hints.coffee
@@ -31,7 +31,7 @@ COPY_LINK_URL =
indicator: "Copy link URL to Clipboard"
linkActivator: (link) ->
if link.href?
- chrome.runtime.sendMessage handler: "copyToClipboard", data: link.href
+ HUD.copyToClipboard link.href
url = link.href
url = url[0..25] + "...." if 28 < url.length
HUD.showForDuration "Yanked #{url}", 2000
@@ -128,7 +128,7 @@ LinkHints =
if isSuccess
# Wait for the next tick to allow the previous mode to exit. It might yet generate a click event,
# which would cause our new mode to exit immediately.
- Utils.nextTick -> LinkHints.activateMode count-1, mode
+ Utils.nextTick -> LinkHints.activateMode count-1, {mode}
activateModeToOpenInNewTab: (count) -> @activateMode count, mode: OPEN_IN_NEW_BG_TAB
activateModeToOpenInNewForegroundTab: (count) -> @activateMode count, mode: OPEN_IN_NEW_FG_TAB
@@ -166,17 +166,15 @@ class LinkHintsMode
name: "hint/#{@mode.name}"
indicator: false
singleton: "link-hints-mode"
- passInitialKeyupEvents: true
suppressAllKeyboardEvents: true
suppressTrailingKeyEvents: true
exitOnEscape: true
exitOnClick: true
keydown: @onKeyDownInMode.bind this
- keypress: @onKeyPressInMode.bind this
@hintMode.onExit (event) =>
if event?.type == "click" or (event?.type == "keydown" and
- (KeyboardUtils.isEscape(event) or event.keyCode in [keyCodes.backspace, keyCodes.deleteKey]))
+ (KeyboardUtils.isEscape(event) or KeyboardUtils.isBackspace event))
HintCoordinator.sendMessage "exit", isSuccess: false
# Note(philc): Append these markers as top level children instead of as child nodes to the link itself,
@@ -230,86 +228,72 @@ class LinkHintsMode
linkText: desc.linkText
stableSortCount: ++@stableSortCount
- # Handles <Shift> and <Ctrl>.
+ # Handles all keyboard events.
onKeyDownInMode: (event) ->
return if event.repeat
- @keydownKeyChar = KeyboardUtils.getKeyChar(event).toLowerCase()
- previousTabCount = @tabCount
- @tabCount = 0
-
- # NOTE(smblott) As of 1.54, the Ctrl modifier doesn't work for filtered link hints; therefore we only
- # offer the control modifier for alphabet hints. It is not clear whether we should fix this. As of
- # 16-03-28, nobody has complained.
- modifiers = [keyCodes.shiftKey]
- modifiers.push keyCodes.ctrlKey unless Settings.get "filterLinkHints"
-
- if event.keyCode in modifiers and
+ # NOTE(smblott) The modifier behaviour here applies only to alphabet hints.
+ if event.key in ["Control", "Shift"] and not Settings.get("filterLinkHints") and
@mode in [ OPEN_IN_CURRENT_TAB, OPEN_WITH_QUEUE, OPEN_IN_NEW_BG_TAB, OPEN_IN_NEW_FG_TAB ]
- @tabCount = previousTabCount
# Toggle whether to open the link in a new or current tab.
previousMode = @mode
- keyCode = event.keyCode
+ key = event.key
- switch keyCode
- when keyCodes.shiftKey
+ switch key
+ when "Shift"
@setOpenLinkMode(if @mode is OPEN_IN_CURRENT_TAB then OPEN_IN_NEW_BG_TAB else OPEN_IN_CURRENT_TAB)
- when keyCodes.ctrlKey
+ when "Control"
@setOpenLinkMode(if @mode is OPEN_IN_NEW_FG_TAB then OPEN_IN_NEW_BG_TAB else OPEN_IN_NEW_FG_TAB)
- handlerId = handlerStack.push
+ handlerId = @hintMode.push
keyup: (event) =>
- if event.keyCode == keyCode
+ if event.key == key
handlerStack.remove()
@setOpenLinkMode previousMode
true # Continue bubbling the event.
- # For some (unknown) reason, we don't always receive the keyup event needed to remove this handler.
- # Therefore, we ensure that it's always removed when hint mode exits. See #1911 and #1926.
- @hintMode.onExit -> handlerStack.remove handlerId
-
- else if event.keyCode in [ keyCodes.backspace, keyCodes.deleteKey ]
+ else if KeyboardUtils.isBackspace event
if @markerMatcher.popKeyChar()
+ @tabCount = 0
@updateVisibleMarkers()
else
# Exit via @hintMode.exit(), so that the LinkHints.activate() "onExit" callback sees the key event and
# knows not to restart hints mode.
@hintMode.exit event
- else if event.keyCode == keyCodes.enter
+ else if event.key == "Enter"
# Activate the active hint, if there is one. Only FilterHints uses an active hint.
HintCoordinator.sendMessage "activateActiveHintMarker" if @markerMatcher.activeHintMarker
- else if event.keyCode == keyCodes.tab
- @tabCount = previousTabCount + (if event.shiftKey then -1 else 1)
- @updateVisibleMarkers @tabCount
+ else if event.key == "Tab"
+ if event.shiftKey then @tabCount-- else @tabCount++
+ @updateVisibleMarkers()
- else if event.keyCode == keyCodes.space and @markerMatcher.shouldRotateHints event
- @tabCount = previousTabCount
+ else if event.key == " " and @markerMatcher.shouldRotateHints event
HintCoordinator.sendMessage "rotateHints"
else
- @tabCount = previousTabCount if event.ctrlKey or event.metaKey or event.altKey
- return
-
- # We've handled the event, so suppress it and update the mode indicator.
- DomUtils.suppressEvent event
-
- # Handles normal input.
- onKeyPressInMode: (event) ->
- return if event.repeat
-
- keyChar = String.fromCharCode(event.charCode).toLowerCase()
- if keyChar
- @markerMatcher.pushKeyChar keyChar, @keydownKeyChar
- @updateVisibleMarkers()
+ unless event.repeat
+ keyChar =
+ if Settings.get "filterLinkHints"
+ KeyboardUtils.getKeyChar(event)
+ else
+ KeyboardUtils.getKeyChar(event).toLowerCase()
+ if keyChar
+ keyChar = " " if keyChar == "space"
+ if keyChar.length == 1
+ @tabCount = 0
+ @markerMatcher.pushKeyChar keyChar
+ @updateVisibleMarkers()
+ else
+ return handlerStack.suppressPropagation
- # We've handled the event, so suppress it.
- DomUtils.suppressEvent event
+ handlerStack.suppressEvent
- updateVisibleMarkers: (tabCount = 0) ->
+ updateVisibleMarkers: ->
{hintKeystrokeQueue, linkTextKeystrokeQueue} = @markerMatcher
- HintCoordinator.sendMessage "updateKeyState", {hintKeystrokeQueue, linkTextKeystrokeQueue, tabCount}
+ HintCoordinator.sendMessage "updateKeyState",
+ {hintKeystrokeQueue, linkTextKeystrokeQueue, tabCount: @tabCount}
updateKeyState: ({hintKeystrokeQueue, linkTextKeystrokeQueue, tabCount}) ->
extend @markerMatcher, {hintKeystrokeQueue, linkTextKeystrokeQueue}
@@ -318,7 +302,7 @@ class LinkHintsMode
if linksMatched.length == 0
@deactivateMode()
else if linksMatched.length == 1
- @activateLink linksMatched[0], userMightOverType ? false
+ @activateLink linksMatched[0], userMightOverType
else
@hideMarker marker for marker in @hintMarkers
@showMarker matched, @markerMatcher.hintKeystrokeQueue.length for matched in linksMatched
@@ -329,7 +313,7 @@ class LinkHintsMode
rotateHints: do ->
markerOverlapsStack = (marker, stack) ->
for otherMarker in stack
- return true if Rect.rectsOverlap marker.markerRect, otherMarker.markerRect
+ return true if Rect.intersects marker.markerRect, otherMarker.markerRect
false
->
@@ -372,7 +356,7 @@ class LinkHintsMode
# When only one hint remains, activate it in the appropriate way. The current frame may or may not contain
# the matched link, and may or may not have the focus. The resulting four cases are accounted for here by
# selectively pushing the appropriate HintCoordinator.onExit handlers.
- activateLink: (linkMatched, userMightOverType=false) ->
+ activateLink: (linkMatched, userMightOverType = false) ->
@removeHintMarkers()
if linkMatched.isLocalMarker
@@ -398,25 +382,26 @@ class LinkHintsMode
clickEl.focus()
linkActivator clickEl
- installKeyboardBlocker = (startKeyboardBlocker) ->
- if linkMatched.isLocalMarker
- {top: viewportTop, left: viewportLeft} = DomUtils.getViewportTopLeft()
- for rect in (Rect.copy rect for rect in clickEl.getClientRects())
- extend rect, top: rect.top + viewportTop, left: rect.left + viewportLeft
- flashEl = DomUtils.addFlashRect rect
- do (flashEl) -> HintCoordinator.onExit.push -> DomUtils.removeElement flashEl
-
- if windowIsFocused()
- startKeyboardBlocker (isSuccess) -> HintCoordinator.sendMessage "exit", {isSuccess}
+ # If flash elements are created, then this function can be used later to remove them.
+ removeFlashElements = ->
+ if linkMatched.isLocalMarker
+ {top: viewportTop, left: viewportLeft} = DomUtils.getViewportTopLeft()
+ flashElements = for rect in clickEl.getClientRects()
+ DomUtils.addFlashRect Rect.translate rect, viewportLeft, viewportTop
+ removeFlashElements = -> DomUtils.removeElement flashEl for flashEl in flashElements
# If we're using a keyboard blocker, then the frame with the focus sends the "exit" message, otherwise the
# frame containing the matched link does.
- if userMightOverType and Settings.get "waitForEnterForFilteredHints"
- installKeyboardBlocker (callback) -> new WaitForEnter callback
- else if userMightOverType
- installKeyboardBlocker (callback) -> new TypingProtector 200, callback
+ if userMightOverType
+ HintCoordinator.onExit.push removeFlashElements
+ if windowIsFocused()
+ callback = (isSuccess) -> HintCoordinator.sendMessage "exit", {isSuccess}
+ if Settings.get "waitForEnterForFilteredHints"
+ new WaitForEnter callback
+ else
+ new TypingProtector 200, callback
else if linkMatched.isLocalMarker
- DomUtils.flashRect linkMatched.rect
+ Utils.setTimeout 400, removeFlashElements
HintCoordinator.sendMessage "exit", isSuccess: true
#
@@ -444,12 +429,7 @@ class LinkHintsMode
# Use characters for hints, and do not filter links by their text.
class AlphabetHints
constructor: ->
- @linkHintCharacters = Settings.get "linkHintCharacters"
- # We use the keyChar from keydown if the link-hint characters are all "a-z0-9". This is the default
- # settings value, and preserves the legacy behavior (which always used keydown) for users which are
- # familiar with that behavior. Otherwise, we use keyChar from keypress, which admits non-Latin
- # characters. See #1722.
- @useKeydown = /^[a-z0-9]*$/.test @linkHintCharacters
+ @linkHintCharacters = Settings.get("linkHintCharacters").toLowerCase()
@hintKeystrokeQueue = []
fillInMarkers: (hintMarkers) ->
@@ -478,17 +458,17 @@ class AlphabetHints
matchString = @hintKeystrokeQueue.join ""
linksMatched: hintMarkers.filter (linkMarker) -> linkMarker.hintString.startsWith matchString
- pushKeyChar: (keyChar, keydownKeyChar) ->
- @hintKeystrokeQueue.push (if @useKeydown then keydownKeyChar else keyChar)
+ pushKeyChar: (keyChar) ->
+ @hintKeystrokeQueue.push keyChar
popKeyChar: -> @hintKeystrokeQueue.pop()
# For alphabet hints, <Space> always rotates the hints, regardless of modifiers.
shouldRotateHints: -> true
-# Use numbers (usually) for hints, and also filter links by their text.
+# Use characters for hints, and also filter links by their text.
class FilterHints
constructor: ->
- @linkHintNumbers = Settings.get "linkHintNumbers"
+ @linkHintNumbers = Settings.get("linkHintNumbers").toUpperCase()
@hintKeystrokeQueue = []
@linkTextKeystrokeQueue = []
@activeHintMarker = null
@@ -535,17 +515,18 @@ class FilterHints
linksMatched: linksMatched
userMightOverType: @hintKeystrokeQueue.length == 0 and 0 < @linkTextKeystrokeQueue.length
- pushKeyChar: (keyChar, keydownKeyChar) ->
- # For filtered hints, we *always* use the keyChar value from keypress, because there is no obvious and
- # easy-to-understand meaning for choosing one of keyChar or keydownKeyChar (as there is for alphabet
- # hints).
+ pushKeyChar: (keyChar) ->
if 0 <= @linkHintNumbers.indexOf keyChar
@hintKeystrokeQueue.push keyChar
+ else if keyChar.toLowerCase() != keyChar and @linkHintNumbers.toLowerCase() != @linkHintNumbers.toUpperCase()
+ # The the keyChar is upper case and the link hint "numbers" contain characters (e.g. [a-zA-Z]). We don't want
+ # some upper-case letters matching hints (above) and some matching text (below), so we ignore such keys.
+ return
# We only accept <Space> and characters which are not used for splitting (e.g. "a", "b", etc., but not "-").
else if keyChar == " " or not @splitRegexp.test keyChar
# Since we might renumber the hints, we should reset the current hintKeyStrokeQueue.
@hintKeystrokeQueue = []
- @linkTextKeystrokeQueue.push keyChar
+ @linkTextKeystrokeQueue.push keyChar.toLowerCase()
popKeyChar: ->
@hintKeystrokeQueue.pop() or @linkTextKeystrokeQueue.pop()
@@ -626,7 +607,9 @@ LocalHints =
# image), therefore we always return a array of element/rect pairs (which may also be a singleton or empty).
#
getVisibleClickable: (element) ->
- tagName = element.tagName.toLowerCase()
+ # Get the tag name. However, `element.tagName` can be an element (not a string, see #2305), so we guard
+ # against that.
+ tagName = element.tagName.toLowerCase?() ? ""
isClickable = false
onlyHasTabIndex = false
possibleFalsePositive = false
@@ -668,9 +651,12 @@ LocalHints =
isClickable ||= @checkForAngularJs element
# Check for attributes that make an element clickable regardless of its tagName.
- if (element.hasAttribute("onclick") or
- element.getAttribute("role")?.toLowerCase() in ["button", "link"] or
- element.getAttribute("contentEditable")?.toLowerCase() in ["", "contentEditable", "true"])
+ if element.hasAttribute("onclick") or
+ (role = element.getAttribute "role") and role.toLowerCase() in [
+ "button" , "tab" , "link", "checkbox", "menuitem", "menuitemcheckbox", "menuitemradio"
+ ] or
+ (contentEditable = element.getAttribute "contentEditable") and
+ contentEditable.toLowerCase() in ["", "contenteditable", "true"]
isClickable = true
# Check for jsaction event listeners on the element.
@@ -821,25 +807,10 @@ LocalHints =
hint.rect.left += left
if Settings.get "filterLinkHints"
- @withLabelMap (labelMap) =>
- extend hint, @generateLinkText labelMap, hint for hint in localHints
+ extend hint, @generateLinkText hint for hint in localHints
localHints
- # Generate a map of input element => label text, call a callback with it.
- withLabelMap: (callback) ->
- labelMap = {}
- labels = document.querySelectorAll "label"
- for label in labels
- forElement = label.getAttribute "for"
- if forElement
- labelText = label.textContent.trim()
- # Remove trailing ":" commonly found in labels.
- if labelText[labelText.length-1] == ":"
- labelText = labelText.substr 0, labelText.length-1
- labelMap[forElement] = labelText
- callback labelMap
-
- generateLinkText: (labelMap, hint) ->
+ generateLinkText: (hint) ->
element = hint.element
linkText = ""
showLinkText = false
@@ -847,9 +818,14 @@ LocalHints =
nodeName = element.nodeName.toLowerCase()
if nodeName == "input"
- if labelMap[element.id]
- linkText = labelMap[element.id]
+ if element.labels? and element.labels.length > 0
+ linkText = element.labels[0].textContent.trim()
+ # Remove trailing ":" commonly found in labels.
+ if linkText[linkText.length-1] == ":"
+ linkText = linkText[...linkText.length-1]
showLinkText = true
+ else if element.getAttribute("type")?.toLowerCase() == "file"
+ linkText = "Choose File"
else if element.type != "password"
linkText = element.value
if not linkText and 'placeholder' of element
@@ -899,15 +875,16 @@ class WaitForEnter extends Mode
@push
keydown: (event) =>
- if event.keyCode == keyCodes.enter
+ if event.key == "Enter"
@exit()
callback true # true -> isSuccess.
else if KeyboardUtils.isEscape event
@exit()
callback false # false -> isSuccess.
-root = exports ? window
+root = exports ? (window.root ?= {})
root.LinkHints = LinkHints
root.HintCoordinator = HintCoordinator
# For tests:
extend root, {LinkHintsMode, LocalHints, AlphabetHints, WaitForEnter}
+extend window, root unless exports?
diff --git a/content_scripts/marks.coffee b/content_scripts/marks.coffee
index 73191b1b..fb1d1b1d 100644
--- a/content_scripts/marks.coffee
+++ b/content_scripts/marks.coffee
@@ -40,22 +40,24 @@ Marks =
indicator: "Create mark..."
exitOnEscape: true
suppressAllKeyboardEvents: true
- keypress: (event) =>
- keyChar = String.fromCharCode event.charCode
- @exit =>
- if @isGlobalMark event, keyChar
- # We record the current scroll position, but only if this is the top frame within the tab.
- # Otherwise, we'll fetch the scroll position of the top frame from the background page later.
- [ scrollX, scrollY ] = [ window.scrollX, window.scrollY ] if DomUtils.isTopFrame()
- chrome.runtime.sendMessage
- handler: 'createMark'
- markName: keyChar
- scrollX: scrollX
- scrollY: scrollY
- , => @showMessage "Created global mark", keyChar
- else
- localStorage[@getLocationKey keyChar] = @getMarkString()
- @showMessage "Created local mark", keyChar
+ keydown: (event) =>
+ if KeyboardUtils.isPrintable event
+ keyChar = KeyboardUtils.getKeyChar event
+ @exit =>
+ if @isGlobalMark event, keyChar
+ # We record the current scroll position, but only if this is the top frame within the tab.
+ # Otherwise, we'll fetch the scroll position of the top frame from the background page later.
+ [ scrollX, scrollY ] = [ window.scrollX, window.scrollY ] if DomUtils.isTopFrame()
+ chrome.runtime.sendMessage
+ handler: 'createMark'
+ markName: keyChar
+ scrollX: scrollX
+ scrollY: scrollY
+ , => @showMessage "Created global mark", keyChar
+ else
+ localStorage[@getLocationKey keyChar] = @getMarkString()
+ @showMessage "Created local mark", keyChar
+ handlerStack.suppressEvent
activateGotoMode: (count, {registryEntry}) ->
@currentRegistryEntry = registryEntry
@@ -64,27 +66,30 @@ Marks =
indicator: "Go to mark..."
exitOnEscape: true
suppressAllKeyboardEvents: true
- keypress: (event) =>
- @exit =>
- markName = String.fromCharCode event.charCode
- if @isGlobalMark event, markName
- # This key must match @getLocationKey() in the back end.
- key = "vimiumGlobalMark|#{markName}"
- chrome.storage.sync.get key, (items) ->
- if key of items
- chrome.runtime.sendMessage handler: 'gotoMark', markName: markName
- HUD.showForDuration "Jumped to global mark '#{markName}'", 1000
- else
- HUD.showForDuration "Global mark not set '#{markName}'", 1000
- else
- markString = @localRegisters[markName] ? localStorage[@getLocationKey markName]
- if markString?
- @setPreviousPosition()
- position = JSON.parse markString
- window.scrollTo position.scrollX, position.scrollY
- @showMessage "Jumped to local mark", markName
+ keydown: (event) =>
+ if KeyboardUtils.isPrintable event
+ @exit =>
+ keyChar = KeyboardUtils.getKeyChar event
+ if @isGlobalMark event, keyChar
+ # This key must match @getLocationKey() in the back end.
+ key = "vimiumGlobalMark|#{keyChar}"
+ Settings.storage.get key, (items) ->
+ if key of items
+ chrome.runtime.sendMessage handler: 'gotoMark', markName: keyChar
+ HUD.showForDuration "Jumped to global mark '#{keyChar}'", 1000
+ else
+ HUD.showForDuration "Global mark not set '#{keyChar}'", 1000
else
- @showMessage "Local mark not set", markName
+ markString = @localRegisters[keyChar] ? localStorage[@getLocationKey keyChar]
+ if markString?
+ @setPreviousPosition()
+ position = JSON.parse markString
+ window.scrollTo position.scrollX, position.scrollY
+ @showMessage "Jumped to local mark", keyChar
+ else
+ @showMessage "Local mark not set", keyChar
+ handlerStack.suppressEvent
-root = exports ? window
+root = exports ? (window.root ?= {})
root.Marks = Marks
+extend window, root unless exports?
diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee
index 6508627e..a4a91c1f 100644
--- a/content_scripts/mode.coffee
+++ b/content_scripts/mode.coffee
@@ -55,7 +55,7 @@ class Mode
# the need for modes which suppress all keyboard events 1) to provide handlers for all of those events,
# or 2) to worry about event suppression and event-handler return values.
if @options.suppressAllKeyboardEvents
- for type in [ "keydown", "keypress", "keyup" ]
+ for type in [ "keydown", "keypress" ]
do (handler = @options[type]) =>
@options[type] = (event) => @alwaysSuppressPropagation => handler? event
@@ -81,8 +81,7 @@ class Mode
_name: "mode-#{@id}/exitOnEscape"
"keydown": (event) =>
return @continueBubbling unless KeyboardUtils.isEscape event
- DomUtils.suppressKeyupAfterEscape handlerStack
- @exit event, event.srcElement
+ @exit event, event.target
@suppressEvent
# If @options.exitOnBlur is truthy, then it should be an element. The mode will exit when that element
@@ -121,16 +120,6 @@ class Mode
singletons[key]?.exit()
singletons[key] = this
- # If @options.passInitialKeyupEvents is set, then we pass initial non-printable keyup events to the page
- # or to other extensions (because the corresponding keydown events were passed). This is used when
- # activating link hints, see #1522.
- if @options.passInitialKeyupEvents
- @push
- _name: "mode-#{@id}/passInitialKeyupEvents"
- keydown: => @alwaysContinueBubbling -> handlerStack.remove()
- keyup: (event) =>
- if KeyboardUtils.isPrintable event then @suppressPropagation else @passEventToPage
-
# if @options.suppressTrailingKeyEvents is set, then -- on exit -- we suppress all key events until a
# subsquent (non-repeat) keydown or keypress. In particular, the intention is to catch keyup events for
# keys which we have handled, but which otherwise might trigger page actions (if the page is listening for
@@ -148,7 +137,6 @@ class Mode
name: "suppress-trailing-key-events"
keydown: handler
keypress: handler
- keyup: -> handlerStack.suppressPropagation
Mode.modes.push this
@setIndicator()
@@ -174,15 +162,16 @@ class Mode
@exitHandlers.push handler
exit: (args...) ->
- if @modeIsActive
- @log "deactivate:", @id
- unless @modeIsExiting
- @modeIsExiting = true
- handler args... for handler in @exitHandlers
- handlerStack.remove handlerId for handlerId in @handlers
- Mode.modes = Mode.modes.filter (mode) => mode != this
- @modeIsActive = false
- @setIndicator()
+ return if @modeIsExiting or not @modeIsActive
+ @log "deactivate:", @id
+ @modeIsExiting = true
+
+ handler args... for handler in @exitHandlers
+ handlerStack.remove handlerId for handlerId in @handlers
+ Mode.modes = Mode.modes.filter (mode) => mode != this
+
+ @modeIsActive = false
+ @setIndicator()
# Debugging routines.
logModes: ->
@@ -209,5 +198,6 @@ class SuppressAllKeyboardEvents extends Mode
suppressAllKeyboardEvents: true
super extend defaults, options
-root = exports ? window
+root = exports ? (window.root ?= {})
extend root, {Mode, SuppressAllKeyboardEvents}
+extend window, root unless exports?
diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee
index 8621edf8..f19b5db4 100644
--- a/content_scripts/mode_find.coffee
+++ b/content_scripts/mode_find.coffee
@@ -6,7 +6,7 @@ class SuppressPrintable extends Mode
constructor: (options) ->
super options
handler = (event) => if KeyboardUtils.isPrintable event then @suppressEvent else @continueBubbling
- type = document.getSelection().type
+ type = DomUtils.getSelectionType()
# We use unshift here, so we see events after normal mode, so we only see unmapped keys.
@unshift
@@ -16,7 +16,7 @@ class SuppressPrintable extends Mode
keyup: (event) =>
# If the selection type has changed (usually, no longer "Range"), then the user is interacting with
# the input element, so we get out of the way. See discussion of option 5c from #1415.
- if document.getSelection().type != type then @exit() else handler event
+ @exit() if DomUtils.getSelectionType() != type
# When we use find, the selection/focus can land in a focusable/editable element. In this situation, special
# considerations apply. We implement three special cases:
@@ -47,7 +47,6 @@ class PostFindMode extends SuppressPrintable
_name: "mode-#{@id}/handle-escape"
keydown: (event) =>
if KeyboardUtils.isEscape event
- DomUtils.suppressKeyupAfterEscape handlerStack
@exit()
@suppressEvent
else
@@ -80,15 +79,16 @@ class FindMode extends Mode
exit: (event) ->
super()
- handleEscapeForFindMode() if event
+ FindMode.handleEscape() if event
restoreSelection: ->
+ return unless @initialRange
range = @initialRange
selection = getSelection()
selection.removeAllRanges()
selection.addRange range
- findInPlace: (query) ->
+ findInPlace: (query, options) ->
# If requested, restore the scroll position (so that failed searches leave the scroll position unchanged).
@checkReturnToViewPort()
FindMode.updateQuery query
@@ -96,7 +96,7 @@ class FindMode extends Mode
# match as the user adds matching characters, or removes previously-matched characters. See #1434.
@restoreSelection()
query = if FindMode.query.isRegex then FindMode.getNextQueryFromRegexMatches(0) else FindMode.query.parsedQuery
- FindMode.query.hasResults = FindMode.execute query
+ FindMode.query.hasResults = FindMode.execute query, options
@updateQuery: (query) ->
@query.rawQuery = query
@@ -179,7 +179,13 @@ class FindMode extends Mode
# ignore the selectionchange event generated by find()
document.removeEventListener("selectionchange", @restoreDefaultSelectionHighlight, true)
- result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false)
+ try
+ result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false)
+ catch # Failed searches throw on Firefox.
+
+ # window.find focuses the |window| that it is called on. This gives us an opportunity to (re-)focus
+ # another element/window, if that isn't the behaviour we want.
+ options.postFindFocus?.focus()
if options.colorSelection
setTimeout(
@@ -194,22 +200,73 @@ class FindMode extends Mode
result
- @restoreDefaultSelectionHighlight: -> document.body.classList.remove("vimiumFindMode")
+ @restoreDefaultSelectionHighlight: forTrusted -> document.body.classList.remove("vimiumFindMode")
+
+ # The user has found what they're looking for and is finished searching. We enter insert mode, if possible.
+ @handleEscape: ->
+ document.body.classList.remove("vimiumFindMode")
+ # Removing the class does not re-color existing selections. we recreate the current selection so it reverts
+ # back to the default color.
+ selection = window.getSelection()
+ unless selection.isCollapsed
+ range = window.getSelection().getRangeAt(0)
+ window.getSelection().removeAllRanges()
+ window.getSelection().addRange(range)
+ focusFoundLink() || selectFoundInputElement()
+
+ # Save the query so the user can do further searches with it.
+ @handleEnter: ->
+ focusFoundLink()
+ document.body.classList.add("vimiumFindMode")
+ FindMode.saveQuery()
+
+ @findNext: (backwards) ->
+ Marks.setPreviousPosition()
+ FindMode.query.hasResults = FindMode.execute null, {backwards}
+
+ if FindMode.query.hasResults
+ focusFoundLink()
+ new PostFindMode()
+ else
+ HUD.showForDuration("No matches for '#{FindMode.query.rawQuery}'", 1000)
checkReturnToViewPort: ->
window.scrollTo @scrollX, @scrollY if @options.returnToViewport
getCurrentRange = ->
selection = getSelection()
- if selection.type == "None"
+ if DomUtils.getSelectionType(selection) == "None"
range = document.createRange()
range.setStart document.body, 0
range.setEnd document.body, 0
range
else
- selection.collapseToStart() if selection.type == "Range"
+ selection.collapseToStart() if DomUtils.getSelectionType(selection) == "Range"
selection.getRangeAt 0
-root = exports ? window
+getLinkFromSelection = ->
+ node = window.getSelection().anchorNode
+ while (node && node != document.body)
+ return node if (node.nodeName.toLowerCase() == "a")
+ node = node.parentNode
+ null
+
+focusFoundLink = ->
+ if (FindMode.query.hasResults)
+ link = getLinkFromSelection()
+ link.focus() if link
+
+selectFoundInputElement = ->
+ # Since the last focused element might not be the one currently pointed to by find (e.g. the current one
+ # might be disabled and therefore unable to receive focus), we use the approximate heuristic of checking
+ # that the last anchor node is an ancestor of our element.
+ findModeAnchorNode = document.getSelection().anchorNode
+ if (FindMode.query.hasResults && document.activeElement &&
+ DomUtils.isSelectable(document.activeElement) &&
+ DomUtils.isDOMDescendant(findModeAnchorNode, document.activeElement))
+ DomUtils.simulateSelect(document.activeElement)
+
+root = exports ? (window.root ?= {})
root.PostFindMode = PostFindMode
root.FindMode = FindMode
+extend window, root unless exports?
diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee
index 4cb0a39e..511602e7 100644
--- a/content_scripts/mode_insert.coffee
+++ b/content_scripts/mode_insert.coffee
@@ -11,93 +11,44 @@ class InsertMode extends Mode
handleKeyEvent = (event) =>
return @continueBubbling unless @isActive event
+ # See comment here: https://github.com/philc/vimium/commit/48c169bd5a61685bb4e67b1e76c939dbf360a658.
+ activeElement = @getActiveElement()
+ return @passEventToPage if activeElement == document.body and activeElement.isContentEditable
+
# Check for a pass-next-key key.
if KeyboardUtils.getKeyCharString(event) in Settings.get "passNextKeyKeys"
new PassNextKeyMode
- return @suppressEvent
-
- return @passEventToPage unless event.type == 'keydown' and KeyboardUtils.isEscape event
- DomUtils.suppressKeyupAfterEscape handlerStack
- target = event.srcElement
- if target and DomUtils.isFocusable target
- # Remove the focus, so the user can't just get back into insert mode by typing in the same input box.
- target.blur()
- else if target?.shadowRoot and @insertModeLock
- # An editable element in a shadow DOM is focused; blur it.
- @insertModeLock.blur()
- @exit event, event.srcElement
- @suppressEvent
+
+ else if event.type == 'keydown' and KeyboardUtils.isEscape(event)
+ activeElement.blur() if DomUtils.isFocusable activeElement
+ @exit() unless @permanent
+
+ else
+ return @passEventToPage
+
+ return @suppressEvent
defaults =
name: "insert"
indicator: if not @permanent and not Settings.get "hideHud" then "Insert mode"
keypress: handleKeyEvent
- keyup: handleKeyEvent
keydown: handleKeyEvent
super extend defaults, options
- @insertModeLock =
- if options.targetElement and DomUtils.isEditable options.targetElement
- # The caller has told us which element to activate on.
- options.targetElement
- else if document.activeElement and DomUtils.isEditable document.activeElement
- # An input element is already active, so use it.
- document.activeElement
- else
- null
-
- @push
- _name: "mode-#{@id}-focus"
- "blur": (event) => @alwaysContinueBubbling =>
- target = event.target
- # We can't rely on focus and blur events arriving in the expected order. When the active element
- # changes, we might get "focus" before "blur". We track the active element in @insertModeLock, and
- # exit only when that element blurs.
- @exit event, target if @insertModeLock and target == @insertModeLock
- "focus": (event) => @alwaysContinueBubbling =>
- if @insertModeLock != event.target and DomUtils.isFocusable event.target
- @activateOnElement event.target
- else if event.target.shadowRoot
- # A focusable element inside the shadow DOM might have been selected. If so, we can catch the focus
- # event inside the shadow DOM. This fixes #853.
- shadowRoot = event.target.shadowRoot
- eventListeners = {}
- for type in [ "focus", "blur" ]
- eventListeners[type] = do (type) ->
- (event) -> handlerStack.bubbleEvent type, event
- shadowRoot.addEventListener type, eventListeners[type], true
-
- handlerStack.push
- _name: "shadow-DOM-input-mode"
- blur: (event) ->
- if event.target.shadowRoot == shadowRoot
- handlerStack.remove()
- for own type, listener of eventListeners
- shadowRoot.removeEventListener type, listener, true
-
# Only for tests. This gives us a hook to test the status of the permanently-installed instance.
InsertMode.permanentInstance = this if @permanent
isActive: (event) ->
return false if event == InsertMode.suppressedEvent
- return true if @insertModeLock or @global
- # Some sites (e.g. inbox.google.com) change the contentEditable property on the fly (see #1245); and
- # unfortunately, the focus event fires *before* the change. Therefore, we need to re-check whether the
- # active element is contentEditable.
- @activateOnElement document.activeElement if document.activeElement?.isContentEditable
- @insertModeLock != null
-
- activateOnElement: (element) ->
- @log "#{@id}: activating (permanent)" if @debug and @permanent
- @insertModeLock = element
-
- exit: (_, target) ->
- if (target and target == @insertModeLock) or @global or target == undefined
- @log "#{@id}: deactivating (permanent)" if @debug and @permanent and @insertModeLock
- @insertModeLock = null
- # Exit, but only if this isn't the permanently-installed instance.
- super() unless @permanent
+ return true if @global
+ DomUtils.isFocusable @getActiveElement()
+
+ getActiveElement: ->
+ activeElement = document.activeElement
+ while activeElement?.shadowRoot?.activeElement
+ activeElement = activeElement.shadowRoot.activeElement
+ activeElement
# Static stuff. This allows PostFindMode to suppress the permanently-installed InsertMode instance.
@suppressedEvent: null
@@ -129,6 +80,7 @@ class PassNextKeyMode extends Mode
@exit()
@passEventToPage
-root = exports ? window
+root = exports ? (window.root ?= {})
root.InsertMode = InsertMode
root.PassNextKeyMode = PassNextKeyMode
+extend window, root unless exports?
diff --git a/content_scripts/mode_key_handler.coffee b/content_scripts/mode_key_handler.coffee
index 480a79af..cca6b77a 100644
--- a/content_scripts/mode_key_handler.coffee
+++ b/content_scripts/mode_key_handler.coffee
@@ -12,7 +12,6 @@
# consists of a (non-empty) list of such mappings.
class KeyHandlerMode extends Mode
- keydownEvents: {}
setKeyMapping: (@keyMapping) -> @reset()
setPassKeys: (@passKeys) -> @reset()
# Only for tests.
@@ -28,59 +27,41 @@ class KeyHandlerMode extends Mode
super extend options,
keydown: @onKeydown.bind this
- keypress: @onKeypress.bind this
- keyup: @onKeyup.bind this
- # We cannot track keyup events if we lose the focus.
- blur: (event) => @alwaysContinueBubbling => @keydownEvents = {} if event.target == window
- @mapKeyRegistry = {}
- Utils.monitorChromeStorage "mapKeyRegistry", (value) => @mapKeyRegistry = value
+ if options.exitOnEscape
+ # If we're part way through a command's key sequence, then a first Escape should reset the key state,
+ # and only a second Escape should actually exit this mode.
+ @push
+ _name: "key-handler-escape-listener"
+ keydown: (event) =>
+ if KeyboardUtils.isEscape(event) and not @isInResetState()
+ @reset()
+ @suppressEvent
+ else
+ @continueBubbling
onKeydown: (event) ->
keyChar = KeyboardUtils.getKeyCharString event
- keyChar = @mapKeyRegistry[keyChar] ? keyChar
isEscape = KeyboardUtils.isEscape event
if isEscape and (@countPrefix != 0 or @keyState.length != 1)
- @keydownEvents[event.keyCode] = true
- @reset()
- @suppressEvent
+ DomUtils.consumeKeyup event, => @reset()
# If the help dialog loses the focus, then Escape should hide it; see point 2 in #2045.
else if isEscape and HelpDialog?.isShowing()
- @keydownEvents[event.keyCode] = true
HelpDialog.toggle()
@suppressEvent
else if isEscape
@continueBubbling
else if @isMappedKey keyChar
- @keydownEvents[event.keyCode] = true
- @handleKeyChar keyChar
- else if not keyChar and (keyChar = KeyboardUtils.getKeyChar event) and
- (@isMappedKey(keyChar) or @isCountKey keyChar)
- # We will possibly be handling a subsequent keypress event, so suppress propagation of this event to
- # prevent triggering page event listeners (e.g. Google instant Search).
- @keydownEvents[event.keyCode] = true
- @suppressPropagation
- else
- @continueBubbling
-
- onKeypress: (event) ->
- keyChar = KeyboardUtils.getKeyCharString event
- keyChar = @mapKeyRegistry[keyChar] ? keyChar
- if @isMappedKey keyChar
@handleKeyChar keyChar
+ @suppressEvent
else if @isCountKey keyChar
digit = parseInt keyChar
@reset if @keyState.length == 1 then @countPrefix * 10 + digit else digit
@suppressEvent
else
- @reset()
+ @reset() if keyChar
@continueBubbling
- onKeyup: (event) ->
- return @continueBubbling unless event.keyCode of @keydownEvents
- delete @keydownEvents[event.keyCode]
- @suppressPropagation
-
# This tests whether there is a mapping of keyChar in the current key state (and accounts for pass keys).
isMappedKey: (keyChar) ->
(mapping for mapping in @keyState when keyChar of mapping)[0]? and not @isPassKey keyChar
@@ -92,7 +73,10 @@ class KeyHandlerMode extends Mode
# Keystrokes are *never* considered pass keys if the user has begun entering a command. So, for example, if
# 't' is a passKey, then the "t"-s of 'gt' and '99t' are neverthless handled as regular keys.
isPassKey: (keyChar) ->
- @countPrefix == 0 and @keyState.length == 1 and keyChar in (@passKeys ? "")
+ @isInResetState() and keyChar in (@passKeys ? "")
+
+ isInResetState: ->
+ @countPrefix == 0 and @keyState.length == 1
handleKeyChar: (keyChar) ->
bgLog "handle key #{keyChar} (#{@name})"
@@ -106,7 +90,9 @@ class KeyHandlerMode extends Mode
bgLog " invoke #{command.command} count=#{count} "
@reset()
@commandHandler {command, count}
+ @exit() if @options.count? and --@options.count <= 0
@suppressEvent
-root = exports ? window
+root = exports ? (window.root ?= {})
root.KeyHandlerMode = KeyHandlerMode
+extend window, root unless exports?
diff --git a/content_scripts/mode_normal.coffee b/content_scripts/mode_normal.coffee
new file mode 100644
index 00000000..1fe0618e
--- /dev/null
+++ b/content_scripts/mode_normal.coffee
@@ -0,0 +1,369 @@
+class NormalMode extends KeyHandlerMode
+ constructor: (options = {}) ->
+ defaults =
+ name: "normal"
+ indicator: false # There is normally no mode indicator in normal mode.
+ commandHandler: @commandHandler.bind this
+
+ super extend defaults, options
+
+ chrome.storage.local.get "normalModeKeyStateMapping", (items) =>
+ @setKeyMapping items.normalModeKeyStateMapping
+
+ chrome.storage.onChanged.addListener (changes, area) =>
+ if area == "local" and changes.normalModeKeyStateMapping?.newValue
+ @setKeyMapping changes.normalModeKeyStateMapping.newValue
+
+ commandHandler: ({command: registryEntry, count}) ->
+ count *= registryEntry.options.count ? 1
+ count = 1 if registryEntry.noRepeat
+
+ if registryEntry.repeatLimit? and registryEntry.repeatLimit < count
+ return unless confirm """
+ You have asked Vimium to perform #{count} repetitions of the command: #{registryEntry.description}.\n
+ Are you sure you want to continue?"""
+
+ if registryEntry.topFrame
+ # We never return to a UI-component frame (e.g. the help dialog), it might have lost the focus.
+ sourceFrameId = if window.isVimiumUIComponent then 0 else frameId
+ chrome.runtime.sendMessage
+ handler: "sendMessageToFrames", message: {name: "runInTopFrame", sourceFrameId, registryEntry}
+ else if registryEntry.background
+ chrome.runtime.sendMessage {handler: "runBackgroundCommand", registryEntry, count}
+ else
+ NormalModeCommands[registryEntry.command] count, {registryEntry}
+
+enterNormalMode = (count) ->
+ new NormalMode
+ indicator: "Normal mode (pass keys disabled)"
+ exitOnEscape: true
+ singleton: "enterNormalMode"
+ count: count
+
+NormalModeCommands =
+ # Scrolling.
+ scrollToBottom: ->
+ Marks.setPreviousPosition()
+ Scroller.scrollTo "y", "max"
+ scrollToTop: (count) ->
+ Marks.setPreviousPosition()
+ Scroller.scrollTo "y", (count - 1) * Settings.get("scrollStepSize")
+ scrollToLeft: -> Scroller.scrollTo "x", 0
+ scrollToRight: -> Scroller.scrollTo "x", "max"
+ scrollUp: (count) -> Scroller.scrollBy "y", -1 * Settings.get("scrollStepSize") * count
+ scrollDown: (count) -> Scroller.scrollBy "y", Settings.get("scrollStepSize") * count
+ scrollPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1/2 * count
+ scrollPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1/2 * count
+ scrollFullPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1 * count
+ scrollFullPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1 * count
+ scrollLeft: (count) -> Scroller.scrollBy "x", -1 * Settings.get("scrollStepSize") * count
+ scrollRight: (count) -> Scroller.scrollBy "x", Settings.get("scrollStepSize") * count
+
+ # Page state.
+ reload: (count, options) ->
+ hard = options.registryEntry.options.hard ? false
+ window.location.reload(hard)
+ goBack: (count) -> history.go(-count)
+ goForward: (count) -> history.go(count)
+
+ # Url manipulation.
+ goUp: (count) ->
+ url = window.location.href
+ if (url[url.length - 1] == "/")
+ url = url.substring(0, url.length - 1)
+
+ urlsplit = url.split("/")
+ # make sure we haven't hit the base domain yet
+ if (urlsplit.length > 3)
+ urlsplit = urlsplit.slice(0, Math.max(3, urlsplit.length - count))
+ window.location.href = urlsplit.join('/')
+
+ goToRoot: ->
+ window.location.href = window.location.origin
+
+ toggleViewSource: ->
+ chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) ->
+ if (url.substr(0, 12) == "view-source:")
+ url = url.substr(12, url.length - 12)
+ else
+ url = "view-source:" + url
+ chrome.runtime.sendMessage {handler: "openUrlInNewTab", url}
+
+ copyCurrentUrl: ->
+ chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) ->
+ HUD.copyToClipboard url
+ url = url[0..25] + "...." if 28 < url.length
+ HUD.showForDuration("Yanked #{url}", 2000)
+
+ openCopiedUrlInNewTab: (count) ->
+ HUD.pasteFromClipboard (url) ->
+ chrome.runtime.sendMessage { handler: "openUrlInNewTab", url, count }
+
+ openCopiedUrlInCurrentTab: ->
+ HUD.pasteFromClipboard (url) ->
+ chrome.runtime.sendMessage { handler: "openUrlInCurrentTab", url }
+
+ # Mode changes.
+ enterInsertMode: ->
+ # If a focusable element receives the focus, then we exit and leave the permanently-installed insert-mode
+ # instance to take over.
+ new InsertMode global: true, exitOnFocus: true
+
+ enterVisualMode: ->
+ new VisualMode userLaunchedMode: true
+
+ enterVisualLineMode: ->
+ new VisualLineMode userLaunchedMode: true
+
+ enterFindMode: ->
+ Marks.setPreviousPosition()
+ new FindMode()
+
+ # Find.
+ performFind: (count) -> FindMode.findNext false for [0...count] by 1
+ performBackwardsFind: (count) -> FindMode.findNext true for [0...count] by 1
+
+ # Misc.
+ mainFrame: -> focusThisFrame highlight: true, forceFocusThisFrame: true
+ showHelp: (sourceFrameId) -> HelpDialog.toggle {sourceFrameId, showAllCommandDetails: false}
+
+ passNextKey: (count, options) ->
+ if options.registryEntry.options.normal
+ enterNormalMode count
+ else
+ new PassNextKeyMode count
+
+ goPrevious: ->
+ previousPatterns = Settings.get("previousPatterns") || ""
+ previousStrings = previousPatterns.split(",").filter( (s) -> s.trim().length )
+ findAndFollowRel("prev") || findAndFollowLink(previousStrings)
+
+ goNext: ->
+ nextPatterns = Settings.get("nextPatterns") || ""
+ nextStrings = nextPatterns.split(",").filter( (s) -> s.trim().length )
+ findAndFollowRel("next") || findAndFollowLink(nextStrings)
+
+ focusInput: (count) ->
+ # Focus the first input element on the page, and create overlays to highlight all the input elements, with
+ # the currently-focused element highlighted specially. Tabbing will shift focus to the next input element.
+ # Pressing any other key will remove the overlays and the special tab behavior.
+ resultSet = DomUtils.evaluateXPath textInputXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
+ visibleInputs =
+ for i in [0...resultSet.snapshotLength] by 1
+ element = resultSet.snapshotItem i
+ continue unless DomUtils.getVisibleClientRect element, true
+ { element, index: i, rect: Rect.copy element.getBoundingClientRect() }
+
+ visibleInputs.sort ({element: element1, index: i1}, {element: element2, index: i2}) ->
+ # Put elements with a lower positive tabIndex first, keeping elements in DOM order.
+ if element1.tabIndex > 0
+ if element2.tabIndex > 0
+ tabDifference = element1.tabIndex - element2.tabIndex
+ if tabDifference != 0
+ tabDifference
+ else
+ i1 - i2
+ else
+ -1
+ else if element2.tabIndex > 0
+ 1
+ else
+ i1 - i2
+
+ if visibleInputs.length == 0
+ HUD.showForDuration("There are no inputs to focus.", 1000)
+ return
+
+ # This is a hack to improve usability on the Vimium options page. We prime the recently-focused input
+ # to be the key-mappings input. Arguably, this is the input that the user is most likely to use.
+ recentlyFocusedElement = lastFocusedInput()
+
+ selectedInputIndex =
+ if count == 1
+ # As the starting index, we pick that of the most recently focused input element (or 0).
+ elements = visibleInputs.map (visibleInput) -> visibleInput.element
+ Math.max 0, elements.indexOf recentlyFocusedElement
+ else
+ Math.min(count, visibleInputs.length) - 1
+
+ hints = for tuple in visibleInputs
+ hint = DomUtils.createElement "div"
+ hint.className = "vimiumReset internalVimiumInputHint vimiumInputHint"
+
+ # minus 1 for the border
+ hint.style.left = (tuple.rect.left - 1) + window.scrollX + "px"
+ hint.style.top = (tuple.rect.top - 1) + window.scrollY + "px"
+ hint.style.width = tuple.rect.width + "px"
+ hint.style.height = tuple.rect.height + "px"
+
+ hint
+
+ new FocusSelector hints, visibleInputs, selectedInputIndex
+
+if LinkHints?
+ extend NormalModeCommands,
+ "LinkHints.activateMode": LinkHints.activateMode.bind LinkHints
+ "LinkHints.activateModeToOpenInNewTab": LinkHints.activateModeToOpenInNewTab.bind LinkHints
+ "LinkHints.activateModeToOpenInNewForegroundTab": LinkHints.activateModeToOpenInNewForegroundTab.bind LinkHints
+ "LinkHints.activateModeWithQueue": LinkHints.activateModeWithQueue.bind LinkHints
+ "LinkHints.activateModeToOpenIncognito": LinkHints.activateModeToOpenIncognito.bind LinkHints
+ "LinkHints.activateModeToDownloadLink": LinkHints.activateModeToDownloadLink.bind LinkHints
+ "LinkHints.activateModeToCopyLinkUrl": LinkHints.activateModeToCopyLinkUrl.bind LinkHints
+
+if Vomnibar?
+ extend NormalModeCommands,
+ "Vomnibar.activate": Vomnibar.activate.bind Vomnibar
+ "Vomnibar.activateInNewTab": Vomnibar.activateInNewTab.bind Vomnibar
+ "Vomnibar.activateTabSelection": Vomnibar.activateTabSelection.bind Vomnibar
+ "Vomnibar.activateBookmarks": Vomnibar.activateBookmarks.bind Vomnibar
+ "Vomnibar.activateBookmarksInNewTab": Vomnibar.activateBookmarksInNewTab.bind Vomnibar
+ "Vomnibar.activateEditUrl": Vomnibar.activateEditUrl.bind Vomnibar
+ "Vomnibar.activateEditUrlInNewTab": Vomnibar.activateEditUrlInNewTab.bind Vomnibar
+
+if Marks?
+ extend NormalModeCommands,
+ "Marks.activateCreateMode": Marks.activateCreateMode.bind Marks
+ "Marks.activateGotoMode": Marks.activateGotoMode.bind Marks
+
+# The types in <input type="..."> that we consider for focusInput command. Right now this is recalculated in
+# each content script. Alternatively we could calculate it once in the background page and use a request to
+# fetch it each time.
+# Should we include the HTML5 date pickers here?
+
+# The corresponding XPath for such elements.
+textInputXPath = (->
+ textInputTypes = [ "text", "search", "email", "url", "number", "password", "date", "tel" ]
+ inputElements = ["input[" +
+ "(" + textInputTypes.map((type) -> '@type="' + type + '"').join(" or ") + "or not(@type))" +
+ " and not(@disabled or @readonly)]",
+ "textarea", "*[@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"]
+ DomUtils?.makeXPath(inputElements)
+)()
+
+# used by the findAndFollow* functions.
+followLink = (linkElement) ->
+ if (linkElement.nodeName.toLowerCase() == "link")
+ window.location.href = linkElement.href
+ else
+ # if we can click on it, don't simply set location.href: some next/prev links are meant to trigger AJAX
+ # calls, like the 'more' button on GitHub's newsfeed.
+ linkElement.scrollIntoView()
+ DomUtils.simulateClick(linkElement)
+
+#
+# Find and follow a link which matches any one of a list of strings. If there are multiple such links, they
+# are prioritized for shortness, by their position in :linkStrings, how far down the page they are located,
+# and finally by whether the match is exact. Practically speaking, this means we favor 'next page' over 'the
+# next big thing', and 'more' over 'nextcompany', even if 'next' occurs before 'more' in :linkStrings.
+#
+findAndFollowLink = (linkStrings) ->
+ linksXPath = DomUtils.makeXPath(["a", "*[@onclick or @role='link' or contains(@class, 'button')]"])
+ links = DomUtils.evaluateXPath(linksXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE)
+ candidateLinks = []
+
+ # at the end of this loop, candidateLinks will contain all visible links that match our patterns
+ # links lower in the page are more likely to be the ones we want, so we loop through the snapshot backwards
+ for i in [(links.snapshotLength - 1)..0] by -1
+ link = links.snapshotItem(i)
+
+ # ensure link is visible (we don't mind if it is scrolled offscreen)
+ boundingClientRect = link.getBoundingClientRect()
+ if (boundingClientRect.width == 0 || boundingClientRect.height == 0)
+ continue
+ computedStyle = window.getComputedStyle(link, null)
+ if (computedStyle.getPropertyValue("visibility") != "visible" ||
+ computedStyle.getPropertyValue("display") == "none")
+ continue
+
+ linkMatches = false
+ for linkString in linkStrings
+ if link.innerText.toLowerCase().indexOf(linkString) != -1 ||
+ 0 <= link.value?.indexOf? linkString
+ linkMatches = true
+ break
+ continue unless linkMatches
+
+ candidateLinks.push(link)
+
+ return if (candidateLinks.length == 0)
+
+ for link in candidateLinks
+ link.wordCount = link.innerText.trim().split(/\s+/).length
+
+ # We can use this trick to ensure that Array.sort is stable. We need this property to retain the reverse
+ # in-page order of the links.
+
+ candidateLinks.forEach((a,i) -> a.originalIndex = i)
+
+ # favor shorter links, and ignore those that are more than one word longer than the shortest link
+ candidateLinks =
+ candidateLinks
+ .sort((a, b) ->
+ if (a.wordCount == b.wordCount) then a.originalIndex - b.originalIndex else a.wordCount - b.wordCount
+ )
+ .filter((a) -> a.wordCount <= candidateLinks[0].wordCount + 1)
+
+ for linkString in linkStrings
+ exactWordRegex =
+ if /\b/.test(linkString[0]) or /\b/.test(linkString[linkString.length - 1])
+ new RegExp "\\b" + linkString + "\\b", "i"
+ else
+ new RegExp linkString, "i"
+ for candidateLink in candidateLinks
+ if exactWordRegex.test(candidateLink.innerText) ||
+ (candidateLink.value && exactWordRegex.test(candidateLink.value))
+ followLink(candidateLink)
+ return true
+ false
+
+findAndFollowRel = (value) ->
+ relTags = ["link", "a", "area"]
+ for tag in relTags
+ elements = document.getElementsByTagName(tag)
+ for element in elements
+ if (element.hasAttribute("rel") && element.rel.toLowerCase() == value)
+ followLink(element)
+ return true
+
+class FocusSelector extends Mode
+ constructor: (hints, visibleInputs, selectedInputIndex) ->
+ super
+ name: "focus-selector"
+ exitOnClick: true
+ keydown: (event) =>
+ if event.key == "Tab"
+ hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint'
+ selectedInputIndex += hints.length + (if event.shiftKey then -1 else 1)
+ selectedInputIndex %= hints.length
+ hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
+ DomUtils.simulateSelect visibleInputs[selectedInputIndex].element
+ @suppressEvent
+ else unless event.key == "Shift"
+ @exit()
+ # Give the new mode the opportunity to handle the event.
+ @restartBubbling
+
+ @hintContainingDiv = DomUtils.addElementList hints,
+ id: "vimiumInputMarkerContainer"
+ className: "vimiumReset"
+
+ DomUtils.simulateSelect visibleInputs[selectedInputIndex].element
+ if visibleInputs.length == 1
+ @exit()
+ return
+ else
+ hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
+
+ exit: ->
+ super()
+ DomUtils.removeElement @hintContainingDiv
+ if document.activeElement and DomUtils.isEditable document.activeElement
+ new InsertMode
+ singleton: "post-find-mode/focus-input"
+ targetElement: document.activeElement
+ indicator: false
+
+root = exports ? (window.root ?= {})
+root.NormalMode = NormalMode
+root.NormalModeCommands = NormalModeCommands
+extend window, root unless exports?
diff --git a/content_scripts/mode_visual.coffee b/content_scripts/mode_visual.coffee
index 1c05cd85..4c6578cd 100644
--- a/content_scripts/mode_visual.coffee
+++ b/content_scripts/mode_visual.coffee
@@ -70,7 +70,7 @@ class Movement
else
@selection.modify @alterMethod, direction, granularity
- # Return a simple camparable value which depends on various aspects of the selection. This is used to
+ # Return a simple comparable value which depends on various aspects of the selection. This is used to
# detect, after a movement, whether the selection has changed.
hashSelection: ->
range = @selection.getRangeAt(0)
@@ -159,7 +159,7 @@ class Movement
# Scroll the focus into view.
scrollIntoView: ->
- unless @selection.type == "None"
+ unless DomUtils.getSelectionType(@selection) == "None"
elementWithFocus = DomUtils.getElementWithFocus @selection, @getDirection() == backward
Scroller.scrollIntoView elementWithFocus if elementWithFocus
@@ -240,7 +240,7 @@ class VisualMode extends KeyHandlerMode
commandHandler: @commandHandler.bind this
# If there was a range selection when the user lanuched visual mode, then we retain the selection on exit.
- @shouldRetainSelectionOnExit = @options.userLaunchedMode and @selection.type == "Range"
+ @shouldRetainSelectionOnExit = @options.userLaunchedMode and DomUtils.getSelectionType(@selection) == "Range"
@onExit (event = null) =>
if @shouldRetainSelectionOnExit
@@ -258,7 +258,7 @@ class VisualMode extends KeyHandlerMode
_name: "#{@id}/enter/click"
# Yank on <Enter>.
keypress: (event) =>
- if event.keyCode == keyCodes.enter
+ if event.key == "Enter"
unless event.metaKey or event.ctrlKey or event.altKey or event.shiftKey
@yank()
return @suppressEvent
@@ -269,7 +269,7 @@ class VisualMode extends KeyHandlerMode
# Establish or use the initial selection. If that's not possible, then enter caret mode.
unless @name == "caret"
- if @selection.type in [ "Caret", "Range" ]
+ if DomUtils.getSelectionType(@selection) in [ "Caret", "Range" ]
selectionRect = @selection.getRangeAt(0).getBoundingClientRect()
if window.vimiumDomTestsAreRunning
# We're running the DOM tests, where getBoundingClientRect() isn't available.
@@ -277,7 +277,7 @@ class VisualMode extends KeyHandlerMode
selectionRect = Rect.intersect selectionRect, Rect.create 0, 0, window.innerWidth, window.innerHeight
if selectionRect.height >= 0 and selectionRect.width >= 0
# The selection is visible in the current viewport.
- if @selection.type == "Caret"
+ if DomUtils.getSelectionType(@selection) == "Caret"
# The caret is in the viewport. Make make it visible.
@movement.extendByOneCharacter(forward) or @movement.extendByOneCharacter backward
else
@@ -285,7 +285,7 @@ class VisualMode extends KeyHandlerMode
# more likely to be interested in visible content.
@selection.removeAllRanges()
- if @selection.type != "Range" and @name != "caret"
+ if DomUtils.getSelectionType(@selection) != "Range" and @name != "caret"
new CaretMode
HUD.showForDuration "No usable selection, entering caret mode...", 2500
@@ -312,7 +312,7 @@ class VisualMode extends KeyHandlerMode
yank: (args = {}) ->
@yankedText = @selection.toString()
@exit()
- chrome.runtime.sendMessage handler: "copyToClipboard", data: @yankedText
+ HUD.copyToClipboard @yankedText
message = @yankedText.replace /\s+/g, " "
message = message[...12] + "..." if 15 < @yankedText.length
@@ -341,10 +341,10 @@ class CaretMode extends VisualMode
super extend options, name: "caret", indicator: "Caret mode", alterMethod: "move"
# Establish the initial caret.
- switch @selection.type
+ switch DomUtils.getSelectionType(@selection)
when "None"
@establishInitialSelectionAnchor()
- if @selection.type == "None"
+ if DomUtils.getSelectionType(@selection) == "None"
@exit()
HUD.showForDuration "Create a selection before entering visual mode.", 2500
return
@@ -380,6 +380,7 @@ class CaretMode extends VisualMode
return true
false
-root = exports ? window
+root = exports ? (window.root ?= {})
root.VisualMode = VisualMode
root.VisualLineMode = VisualLineMode
+extend window, root unless exports?
diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee
index 3a1b3772..f65062e4 100644
--- a/content_scripts/scroller.coffee
+++ b/content_scripts/scroller.coffee
@@ -95,7 +95,14 @@ findScrollableElement = (element, direction, amount, factor) ->
# On some pages, the scrolling element is not actually scrollable. Here, we search the document for the
# largest visible element which does scroll vertically. This is used to initialize activatedElement. See
# #1358.
-firstScrollableElement = (element=getScrollingElement()) ->
+firstScrollableElement = (element = null) ->
+ unless element
+ scrollingElement = getScrollingElement()
+ if doesScroll(scrollingElement, "y", 1, 1) or doesScroll(scrollingElement, "y", -1, 1)
+ return scrollingElement
+ else
+ element = document.body ? getScrollingElement()
+
if doesScroll(element, "y", 1, 1) or doesScroll(element, "y", -1, 1)
element
else
@@ -128,9 +135,11 @@ checkVisibility = (element) ->
CoreScroller =
init: ->
@time = 0
- @lastEvent = null
- @keyIsDown = false
+ @lastEvent = @keyIsDown = null
+ @installCanceEventListener()
+ # This installs listeners for events which should cancel smooth scrolling.
+ installCanceEventListener: ->
# NOTE(smblott) With extreme keyboard configurations, Chrome sometimes does not get a keyup event for
# every keydown, in which case tapping "j" scrolls indefinitely. This appears to be a Chrome/OS/XOrg bug
# of some kind. See #1549.
@@ -141,11 +150,11 @@ CoreScroller =
@keyIsDown = true
@time += 1 unless event.repeat
@lastEvent = event
- keyup: =>
+ keyup: (event) =>
handlerStack.alwaysContinueBubbling =>
@keyIsDown = false
@time += 1
- blur: =>
+ blur: (event) =>
handlerStack.alwaysContinueBubbling =>
@time += 1 if event.target == window
@@ -175,7 +184,7 @@ CoreScroller =
return if @lastEvent?.repeat
activationTime = ++@time
- myKeyIsStillDown = => @time == activationTime and @keyIsDown
+ myKeyIsStillDown = => @time == activationTime and @keyIsDown ? true
# Store amount's sign and make amount positive; the arithmetic is clearer when amount is positive.
sign = getSign amount
@@ -188,6 +197,7 @@ CoreScroller =
totalElapsed = 0.0
calibration = 1.0
previousTimestamp = null
+ cancelEventListener = @installCanceEventListener()
animate = (timestamp) =>
previousTimestamp ?= timestamp
@@ -215,13 +225,14 @@ CoreScroller =
requestAnimationFrame animate
else
# We're done.
+ handlerStack.remove cancelEventListener
checkVisibility element
# If we've been asked not to be continuous, then we advance time, so the myKeyIsStillDown test always
# fails.
++@time unless continuous
- # Launch animator.
+ # Start scrolling.
requestAnimationFrame animate
# Scroller contains the two main scroll functions which are used by clients.
@@ -297,5 +308,6 @@ Scroller =
element = findScrollableElement element, "x", amount, 1
CoreScroller.scroll element, "x", amount, false
-root = exports ? window
+root = exports ? (window.root ?= {})
root.Scroller = Scroller
+extend window, root unless exports?
diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee
index 203f0c8c..c71bfb35 100644
--- a/content_scripts/ui_component.coffee
+++ b/content_scripts/ui_component.coffee
@@ -96,5 +96,6 @@ class UIComponent
@options = null
@postMessage "hidden" # Inform the UI component that it is hidden.
-root = exports ? window
+root = exports ? (window.root ?= {})
root.UIComponent = UIComponent
+extend window, root unless exports?
diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css
index 3e8f65d6..54256199 100644
--- a/content_scripts/vimium.css
+++ b/content_scripts/vimium.css
@@ -80,7 +80,7 @@ div.internalVimiumHintMarker {
overflow: hidden;
font-size: 11px;
padding: 1px 3px 0px 3px;
- background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785), color-stop(100%,#FFC542));
+ background: linear-gradient(to bottom, #FFF785 0%,#FFC542 100%);
border: solid 1px #C38A22;
border-radius: 3px;
box-shadow: 0px 3px 7px 0px rgba(0, 0, 0, 0.3);
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index 21826944..432fa7a2 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -2,6 +2,12 @@
# This content script must be run prior to domReady so that we perform some operations very early.
#
+root = exports ? (window.root ?= {})
+# On Firefox, sometimes the variables assigned to window are lost (bug 1408996), so we reinstall them.
+# NOTE(mrmr1993): This bug leads to catastrophic failure (ie. nothing works and errors abound).
+DomUtils.documentReady ->
+ root.extend window, root unless extend?
+
isEnabledForUrl = true
isIncognitoMode = chrome.extension.inIncognitoContext
normalMode = null
@@ -10,25 +16,12 @@ normalMode = null
windowIsFocused = do ->
windowHasFocus = null
DomUtils.documentReady -> windowHasFocus = document.hasFocus()
- window.addEventListener "focus", (event) -> windowHasFocus = true if event.target == window; true
- window.addEventListener "blur", (event) -> windowHasFocus = false if event.target == window; true
+ window.addEventListener "focus", forTrusted (event) ->
+ windowHasFocus = true if event.target == window; true
+ window.addEventListener "blur", forTrusted (event) ->
+ windowHasFocus = false if event.target == window; true
-> windowHasFocus
-# The types in <input type="..."> that we consider for focusInput command. Right now this is recalculated in
-# each content script. Alternatively we could calculate it once in the background page and use a request to
-# fetch it each time.
-# Should we include the HTML5 date pickers here?
-
-# The corresponding XPath for such elements.
-textInputXPath = (->
- textInputTypes = [ "text", "search", "email", "url", "number", "password", "date", "tel" ]
- inputElements = ["input[" +
- "(" + textInputTypes.map((type) -> '@type="' + type + '"').join(" or ") + "or not(@type))" +
- " and not(@disabled or @readonly)]",
- "textarea", "*[@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"]
- DomUtils.makeXPath(inputElements)
-)()
-
# This is set by Frame.registerFrameId(). A frameId of 0 indicates that this is the top frame in the tab.
frameId = null
@@ -109,47 +102,13 @@ handlerStack.push
target = target.parentElement
true
-class NormalMode extends KeyHandlerMode
- constructor: (options = {}) ->
- super extend options,
- name: "normal"
- indicator: false # There is no mode indicator in normal mode.
- commandHandler: @commandHandler.bind this
-
- chrome.storage.local.get "normalModeKeyStateMapping", (items) =>
- @setKeyMapping items.normalModeKeyStateMapping
-
- chrome.storage.onChanged.addListener (changes, area) =>
- if area == "local" and changes.normalModeKeyStateMapping?.newValue
- @setKeyMapping changes.normalModeKeyStateMapping.newValue
-
- # Initialize components which normal mode depends upon.
- Scroller.init()
- FindModeHistory.init()
-
- commandHandler: ({command: registryEntry, count}) ->
- count *= registryEntry.options.count ? 1
- count = 1 if registryEntry.noRepeat
-
- if registryEntry.repeatLimit? and registryEntry.repeatLimit < count
- return unless confirm """
- You have asked Vimium to perform #{count} repetitions of the command: #{registryEntry.description}.\n
- Are you sure you want to continue?"""
-
- if registryEntry.topFrame
- # We never return to a UI-component frame (e.g. the help dialog), it might have lost the focus.
- sourceFrameId = if window.isVimiumUIComponent then 0 else frameId
- chrome.runtime.sendMessage
- handler: "sendMessageToFrames", message: {name: "runInTopFrame", sourceFrameId, registryEntry}
- else if registryEntry.background
- chrome.runtime.sendMessage {handler: "runBackgroundCommand", registryEntry, count}
- else
- Utils.invokeCommandString registryEntry.command, count, {registryEntry}
-
installModes = ->
# Install the permanent modes. The permanently-installed insert mode tracks focus/blur events, and
# activates/deactivates itself accordingly.
normalMode = new NormalMode
+ # Initialize components upon which normal mode depends.
+ Scroller.init()
+ FindModeHistory.init()
new InsertMode permanent: true
new GrabBackFocus if isEnabledForUrl
normalMode # Return the normalMode object (for the tests).
@@ -157,9 +116,14 @@ installModes = ->
initializeOnEnabledStateKnown = (isEnabledForUrl) ->
installModes() unless normalMode
if isEnabledForUrl
- # We only initialize (and activate) the Vomnibar in the top frame. Also, we do not initialize the
- # Vomnibar until we know that Vimium is enabled. Thereafter, there's no more initialization to do.
- DomUtils.documentComplete Vomnibar.init.bind Vomnibar if DomUtils.isTopFrame()
+ unless Utils.isFirefox() and document.documentElement.namespaceURI != "http://www.w3.org/1999/xhtml"
+ # We only initialize (and activate) the Vomnibar in the top frame. Also, we do not initialize the
+ # Vomnibar until we know that Vimium is enabled. Thereafter, there's no more initialization to do.
+ #
+ # NOTE(mrmr1993): In XML documents on Firefox, injecting HTML into the DOM breaks the rendering, so we
+ # lazy load the Vomnibar. This comes with the expected issues, but is better than breaking all XML
+ # documents.
+ DomUtils.documentComplete Vomnibar.init.bind Vomnibar if DomUtils.isTopFrame()
initializeOnEnabledStateKnown = ->
#
@@ -178,7 +142,7 @@ initializePreDomReady = ->
frameFocused: -> # A frame has received the focus; we don't care here (UI components handle this).
checkEnabledAfterURLChange: checkEnabledAfterURLChange
runInTopFrame: ({sourceFrameId, registryEntry}) ->
- Utils.invokeCommandString registryEntry.command, sourceFrameId, registryEntry if DomUtils.isTopFrame()
+ NormalModeCommands[registryEntry.command] sourceFrameId, registryEntry if DomUtils.isTopFrame()
linkHintsMessage: (request) -> HintCoordinator[request.messageType] request
chrome.runtime.onMessage.addListener (request, sender, sendResponse) ->
@@ -192,9 +156,10 @@ initializePreDomReady = ->
# Wrapper to install event listeners. Syntactic sugar.
installListener = (element, event, callback) ->
- element.addEventListener(event, ->
+ element.addEventListener(event, forTrusted(->
+ root.extend window, root unless extend? # See #2800.
if isEnabledForUrl then callback.apply(this, arguments) else true
- , true)
+ ), true)
#
# Installing or uninstalling listeners is error prone. Instead we elect to check isEnabledForUrl each time so
@@ -215,7 +180,7 @@ installListeners = Utils.makeIdempotent ->
# - Tell the background page this frame's URL.
# - Check if we should be enabled.
#
-onFocus = (event) ->
+onFocus = forTrusted (event) ->
if event.target == window
chrome.runtime.sendMessage handler: "frameFocused"
checkIfEnabledForUrl true
@@ -223,7 +188,7 @@ onFocus = (event) ->
# We install these listeners directly (that is, we don't use installListener) because we still need to receive
# events when Vimium is not enabled.
window.addEventListener "focus", onFocus
-window.addEventListener "hashchange", onFocus
+window.addEventListener "hashchange", checkEnabledAfterURLChange
initializeOnDomReady = ->
# Tell the background page we're in the domReady state.
@@ -237,7 +202,7 @@ Frame =
postMessage: (handler, request = {}) -> @port.postMessage extend request, {handler}
linkHintsMessage: (request) -> HintCoordinator[request.messageType] request
registerFrameId: ({chromeFrameId}) ->
- frameId = window.frameId = chromeFrameId
+ frameId = root.frameId = window.frameId = chromeFrameId
# We register a frame immediately only if it is focused or its window isn't tiny. We register tiny
# frames later, when necessary. This affects focusFrame() and link hints.
if windowIsFocused() or not DomUtils.windowIsTooSmall()
@@ -247,20 +212,21 @@ Frame =
window.removeEventListener "focus", focusHandler
window.removeEventListener "resize", resizeHandler
Frame.postMessage "registerFrame"
- window.addEventListener "focus", focusHandler = ->
+ window.addEventListener "focus", focusHandler = forTrusted (event) ->
postRegisterFrame() if event.target == window
- window.addEventListener "resize", resizeHandler = ->
+ window.addEventListener "resize", resizeHandler = forTrusted (event) ->
postRegisterFrame() unless DomUtils.windowIsTooSmall()
init: ->
@port = chrome.runtime.connect name: "frames"
@port.onMessage.addListener (request) =>
+ root.extend window, root unless extend? # See #2800 and #2831.
(@listeners[request.handler] ? this[request.handler]) request
# We disable the content scripts when we lose contact with the background page, or on unload.
@port.onDisconnect.addListener disconnect = Utils.makeIdempotent => @disconnect()
- window.addEventListener "unload", disconnect
+ window.addEventListener "unload", forTrusted disconnect
disconnect: ->
try @postMessage "unregisterFrame"
@@ -272,7 +238,7 @@ Frame =
handlerStack.reset()
isEnabledForUrl = false
window.removeEventListener "focus", onFocus
- window.removeEventListener "hashchange", onFocus
+ window.removeEventListener "hashchange", checkEnabledAfterURLChange
setScrollPosition = ({ scrollX, scrollY }) ->
DomUtils.documentReady ->
@@ -314,170 +280,30 @@ focusThisFrame = (request) ->
chrome.runtime.sendMessage handler: "nextFrame"
return
window.focus()
+ # On Firefox, window.focus doesn't always draw focus back from a child frame (bug 554039).
+ # We blur the active element if it is an iframe, which gives the window back focus as intended.
+ document.activeElement.blur() if document.activeElement.tagName.toLowerCase() == "iframe"
flashFrame() if request.highlight
-extend window,
- scrollToBottom: ->
- Marks.setPreviousPosition()
- Scroller.scrollTo "y", "max"
- scrollToTop: (count) ->
- Marks.setPreviousPosition()
- Scroller.scrollTo "y", (count - 1) * Settings.get("scrollStepSize")
- scrollToLeft: -> Scroller.scrollTo "x", 0
- scrollToRight: -> Scroller.scrollTo "x", "max"
- scrollUp: (count) -> Scroller.scrollBy "y", -1 * Settings.get("scrollStepSize") * count
- scrollDown: (count) -> Scroller.scrollBy "y", Settings.get("scrollStepSize") * count
- scrollPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1/2 * count
- scrollPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1/2 * count
- scrollFullPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1 * count
- scrollFullPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1 * count
- scrollLeft: (count) -> Scroller.scrollBy "x", -1 * Settings.get("scrollStepSize") * count
- scrollRight: (count) -> Scroller.scrollBy "x", Settings.get("scrollStepSize") * count
-
-extend window,
- reload: -> window.location.reload()
- goBack: (count) -> history.go(-count)
- goForward: (count) -> history.go(count)
-
- goUp: (count) ->
- url = window.location.href
- if (url[url.length - 1] == "/")
- url = url.substring(0, url.length - 1)
-
- urlsplit = url.split("/")
- # make sure we haven't hit the base domain yet
- if (urlsplit.length > 3)
- urlsplit = urlsplit.slice(0, Math.max(3, urlsplit.length - count))
- window.location.href = urlsplit.join('/')
-
- goToRoot: ->
- window.location.href = window.location.origin
-
- mainFrame: -> focusThisFrame highlight: true, forceFocusThisFrame: true
-
- toggleViewSource: ->
- chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) ->
- if (url.substr(0, 12) == "view-source:")
- url = url.substr(12, url.length - 12)
- else
- url = "view-source:" + url
- chrome.runtime.sendMessage {handler: "openUrlInNewTab", url}
-
- copyCurrentUrl: ->
- # TODO(ilya): When the following bug is fixed, revisit this approach of sending back to the background
- # page to copy.
- # http://code.google.com/p/chromium/issues/detail?id=55188
- chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) ->
- chrome.runtime.sendMessage { handler: "copyToClipboard", data: url }
- url = url[0..25] + "...." if 28 < url.length
- HUD.showForDuration("Yanked #{url}", 2000)
-
- enterInsertMode: ->
- # If a focusable element receives the focus, then we exit and leave the permanently-installed insert-mode
- # instance to take over.
- new InsertMode global: true, exitOnFocus: true
-
- enterVisualMode: ->
- new VisualMode userLaunchedMode: true
-
- enterVisualLineMode: ->
- new VisualLineMode userLaunchedMode: true
-
- passNextKey: (count) ->
- new PassNextKeyMode count
-
- focusInput: do ->
- # Track the most recently focused input element.
- recentlyFocusedElement = null
- window.addEventListener "focus",
- (event) -> recentlyFocusedElement = event.target if DomUtils.isEditable event.target
- , true
-
- (count) ->
- mode = InsertMode
- # Focus the first input element on the page, and create overlays to highlight all the input elements, with
- # the currently-focused element highlighted specially. Tabbing will shift focus to the next input element.
- # Pressing any other key will remove the overlays and the special tab behavior.
- # The mode argument is the mode to enter once an input is selected.
- resultSet = DomUtils.evaluateXPath textInputXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
- visibleInputs =
- for i in [0...resultSet.snapshotLength] by 1
- element = resultSet.snapshotItem i
- continue unless DomUtils.getVisibleClientRect element, true
- { element, rect: Rect.copy element.getBoundingClientRect() }
-
- if visibleInputs.length == 0
- HUD.showForDuration("There are no inputs to focus.", 1000)
- return
-
- # This is a hack to improve usability on the Vimium options page. We prime the recently-focused input
- # to be the key-mappings input. Arguably, this is the input that the user is most likely to use.
- recentlyFocusedElement ?= document.getElementById "keyMappings" if window.isVimiumOptionsPage
-
- selectedInputIndex =
- if count == 1
- # As the starting index, we pick that of the most recently focused input element (or 0).
- elements = visibleInputs.map (visibleInput) -> visibleInput.element
- Math.max 0, elements.indexOf recentlyFocusedElement
- else
- Math.min(count, visibleInputs.length) - 1
-
- hints = for tuple in visibleInputs
- hint = DomUtils.createElement "div"
- hint.className = "vimiumReset internalVimiumInputHint vimiumInputHint"
-
- # minus 1 for the border
- hint.style.left = (tuple.rect.left - 1) + window.scrollX + "px"
- hint.style.top = (tuple.rect.top - 1) + window.scrollY + "px"
- hint.style.width = tuple.rect.width + "px"
- hint.style.height = tuple.rect.height + "px"
-
- hint
-
- new class FocusSelector extends Mode
- constructor: ->
- super
- name: "focus-selector"
- exitOnClick: true
- keydown: (event) =>
- if event.keyCode == KeyboardUtils.keyCodes.tab
- hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint'
- selectedInputIndex += hints.length + (if event.shiftKey then -1 else 1)
- selectedInputIndex %= hints.length
- hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
- DomUtils.simulateSelect visibleInputs[selectedInputIndex].element
- @suppressEvent
- else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey
- @exit()
- # Give the new mode the opportunity to handle the event.
- @restartBubbling
-
- @hintContainingDiv = DomUtils.addElementList hints,
- id: "vimiumInputMarkerContainer"
- className: "vimiumReset"
-
- DomUtils.simulateSelect visibleInputs[selectedInputIndex].element
- if visibleInputs.length == 1
- @exit()
- return
- else
- hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
-
- exit: ->
- super()
- DomUtils.removeElement @hintContainingDiv
- if mode and document.activeElement and DomUtils.isEditable document.activeElement
- new mode
- singleton: "post-find-mode/focus-input"
- targetElement: document.activeElement
- indicator: false
+# Used by focusInput command.
+root.lastFocusedInput = do ->
+ # Track the most recently focused input element.
+ recentlyFocusedElement = null
+ window.addEventListener "focus",
+ forTrusted (event) ->
+ DomUtils = window.DomUtils ? root.DomUtils # Workaround FF bug 1408996.
+ if DomUtils.isEditable event.target
+ recentlyFocusedElement = event.target
+ , true
+ -> recentlyFocusedElement
# Checks if Vimium should be enabled or not in this frame. As a side effect, it also informs the background
# page whether this frame has the focus, allowing the background page to track the active frame's URL and set
# the page icon.
checkIfEnabledForUrl = do ->
Frame.addEventListener "isEnabledForUrl", (response) ->
- {isEnabledForUrl, passKeys, frameIsFocused} = response
+ {isEnabledForUrl, passKeys, frameIsFocused, isFirefox} = response
+ Utils.isFirefox = -> isFirefox
initializeOnEnabledStateKnown isEnabledForUrl
normalMode.setPassKeys passKeys
# Hide the HUD if we're not enabled.
@@ -488,168 +314,11 @@ checkIfEnabledForUrl = do ->
# When we're informed by the background page that a URL in this tab has changed, we check if we have the
# correct enabled state (but only if this frame has the focus).
-checkEnabledAfterURLChange = ->
+checkEnabledAfterURLChange = forTrusted ->
checkIfEnabledForUrl() if windowIsFocused()
-handleEscapeForFindMode = ->
- document.body.classList.remove("vimiumFindMode")
- # removing the class does not re-color existing selections. we recreate the current selection so it reverts
- # back to the default color.
- selection = window.getSelection()
- unless selection.isCollapsed
- range = window.getSelection().getRangeAt(0)
- window.getSelection().removeAllRanges()
- window.getSelection().addRange(range)
- focusFoundLink() || selectFoundInputElement()
-
-# <esc> sends us into insert mode if possible, but <cr> does not.
-# <esc> corresponds approximately to 'nevermind, I have found it already' while <cr> means 'I want to save
-# this query and do more searches with it'
-handleEnterForFindMode = ->
- focusFoundLink()
- document.body.classList.add("vimiumFindMode")
- FindMode.saveQuery()
-
-focusFoundLink = ->
- if (FindMode.query.hasResults)
- link = getLinkFromSelection()
- link.focus() if link
-
-selectFoundInputElement = ->
- # Since the last focused element might not be the one currently pointed to by find (e.g. the current one
- # might be disabled and therefore unable to receive focus), we use the approximate heuristic of checking
- # that the last anchor node is an ancestor of our element.
- findModeAnchorNode = document.getSelection().anchorNode
- if (FindMode.query.hasResults && document.activeElement &&
- DomUtils.isSelectable(document.activeElement) &&
- DomUtils.isDOMDescendant(findModeAnchorNode, document.activeElement))
- DomUtils.simulateSelect(document.activeElement)
-
-findAndFocus = (backwards) ->
- Marks.setPreviousPosition()
- FindMode.query.hasResults = FindMode.execute null, {backwards}
-
- if FindMode.query.hasResults
- focusFoundLink()
- new PostFindMode()
- else
- HUD.showForDuration("No matches for '#{FindMode.query.rawQuery}'", 1000)
-
-performFind = (count) -> findAndFocus false for [0...count] by 1
-performBackwardsFind = (count) -> findAndFocus true for [0...count] by 1
-
-getLinkFromSelection = ->
- node = window.getSelection().anchorNode
- while (node && node != document.body)
- return node if (node.nodeName.toLowerCase() == "a")
- node = node.parentNode
- null
-
-# used by the findAndFollow* functions.
-followLink = (linkElement) ->
- if (linkElement.nodeName.toLowerCase() == "link")
- window.location.href = linkElement.href
- else
- # if we can click on it, don't simply set location.href: some next/prev links are meant to trigger AJAX
- # calls, like the 'more' button on GitHub's newsfeed.
- linkElement.scrollIntoView()
- DomUtils.simulateClick(linkElement)
-
-#
-# Find and follow a link which matches any one of a list of strings. If there are multiple such links, they
-# are prioritized for shortness, by their position in :linkStrings, how far down the page they are located,
-# and finally by whether the match is exact. Practically speaking, this means we favor 'next page' over 'the
-# next big thing', and 'more' over 'nextcompany', even if 'next' occurs before 'more' in :linkStrings.
-#
-findAndFollowLink = (linkStrings) ->
- linksXPath = DomUtils.makeXPath(["a", "*[@onclick or @role='link' or contains(@class, 'button')]"])
- links = DomUtils.evaluateXPath(linksXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE)
- candidateLinks = []
-
- # at the end of this loop, candidateLinks will contain all visible links that match our patterns
- # links lower in the page are more likely to be the ones we want, so we loop through the snapshot backwards
- for i in [(links.snapshotLength - 1)..0] by -1
- link = links.snapshotItem(i)
-
- # ensure link is visible (we don't mind if it is scrolled offscreen)
- boundingClientRect = link.getBoundingClientRect()
- if (boundingClientRect.width == 0 || boundingClientRect.height == 0)
- continue
- computedStyle = window.getComputedStyle(link, null)
- if (computedStyle.getPropertyValue("visibility") != "visible" ||
- computedStyle.getPropertyValue("display") == "none")
- continue
-
- linkMatches = false
- for linkString in linkStrings
- if link.innerText.toLowerCase().indexOf(linkString) != -1 ||
- 0 <= link.value?.indexOf? linkString
- linkMatches = true
- break
- continue unless linkMatches
-
- candidateLinks.push(link)
-
- return if (candidateLinks.length == 0)
-
- for link in candidateLinks
- link.wordCount = link.innerText.trim().split(/\s+/).length
-
- # We can use this trick to ensure that Array.sort is stable. We need this property to retain the reverse
- # in-page order of the links.
-
- candidateLinks.forEach((a,i) -> a.originalIndex = i)
-
- # favor shorter links, and ignore those that are more than one word longer than the shortest link
- candidateLinks =
- candidateLinks
- .sort((a, b) ->
- if (a.wordCount == b.wordCount) then a.originalIndex - b.originalIndex else a.wordCount - b.wordCount
- )
- .filter((a) -> a.wordCount <= candidateLinks[0].wordCount + 1)
-
- for linkString in linkStrings
- exactWordRegex =
- if /\b/.test(linkString[0]) or /\b/.test(linkString[linkString.length - 1])
- new RegExp "\\b" + linkString + "\\b", "i"
- else
- new RegExp linkString, "i"
- for candidateLink in candidateLinks
- if exactWordRegex.test(candidateLink.innerText) ||
- (candidateLink.value && exactWordRegex.test(candidateLink.value))
- followLink(candidateLink)
- return true
- false
-
-findAndFollowRel = (value) ->
- relTags = ["link", "a", "area"]
- for tag in relTags
- elements = document.getElementsByTagName(tag)
- for element in elements
- if (element.hasAttribute("rel") && element.rel.toLowerCase() == value)
- followLink(element)
- return true
-
-window.goPrevious = ->
- previousPatterns = Settings.get("previousPatterns") || ""
- previousStrings = previousPatterns.split(",").filter( (s) -> s.trim().length )
- findAndFollowRel("prev") || findAndFollowLink(previousStrings)
-
-window.goNext = ->
- nextPatterns = Settings.get("nextPatterns") || ""
- nextStrings = nextPatterns.split(",").filter( (s) -> s.trim().length )
- findAndFollowRel("next") || findAndFollowLink(nextStrings)
-
-# Enters find mode. Returns the new find-mode instance.
-enterFindMode = ->
- Marks.setPreviousPosition()
- new FindMode()
-
-window.showHelp = (sourceFrameId) ->
- HelpDialog.toggle {sourceFrameId, showAllCommandDetails: false}
-
# If we are in the help dialog iframe, then HelpDialog is already defined with the necessary functions.
-window.HelpDialog ?=
+root.HelpDialog ?=
helpUI: null
isShowing: -> @helpUI?.showing
abort: -> @helpUI.hide false if @isShowing()
@@ -666,14 +335,13 @@ window.HelpDialog ?=
initializePreDomReady()
DomUtils.documentReady initializeOnDomReady
-root = exports ? window
root.handlerStack = handlerStack
root.frameId = frameId
root.Frame = Frame
root.windowIsFocused = windowIsFocused
root.bgLog = bgLog
-# These are exported for find mode and link-hints mode.
-extend root, {handleEscapeForFindMode, handleEnterForFindMode, performFind, performBackwardsFind,
- enterFindMode, focusThisFrame}
+# These are exported for normal mode and link-hints mode.
+extend root, {focusThisFrame}
# These are exported only for the tests.
-extend root, {installModes, installListeners}
+extend root, {installModes}
+extend window, root unless exports?
diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee
index 04499523..ad98aa48 100644
--- a/content_scripts/vomnibar.coffee
+++ b/content_scripts/vomnibar.coffee
@@ -53,10 +53,11 @@ Vomnibar =
# selectFirst - Optional, boolean. Whether to select the first entry.
# newTab - Optional, boolean. Whether to open the result in a new tab.
open: (sourceFrameId, options) ->
- if @vomnibarUI?
- # The Vomnibar cannot coexist with the help dialog (it causes focus issues).
- HelpDialog.abort()
- @vomnibarUI.activate extend options, { name: "activate", sourceFrameId, focus: true }
+ @init()
+ # The Vomnibar cannot coexist with the help dialog (it causes focus issues).
+ HelpDialog.abort()
+ @vomnibarUI.activate extend options, { name: "activate", sourceFrameId, focus: true }
-root = exports ? window
+root = exports ? (window.root ?= {})
root.Vomnibar = Vomnibar
+extend window, root unless exports?
diff --git a/lib/clipboard.coffee b/lib/clipboard.coffee
index af143dd9..a9e2e82e 100644
--- a/lib/clipboard.coffee
+++ b/lib/clipboard.coffee
@@ -1,8 +1,9 @@
Clipboard =
- _createTextArea: ->
- textArea = document.createElement "textarea"
+ _createTextArea: (tagName = "textarea") ->
+ textArea = document.createElement tagName
textArea.style.position = "absolute"
textArea.style.left = "-100%"
+ textArea.contentEditable = "true"
textArea
# http://groups.google.com/group/chromium-extensions/browse_thread/thread/49027e7f3b04f68/f6ab2457dee5bf55
@@ -16,14 +17,15 @@ Clipboard =
document.body.removeChild(textArea)
paste: ->
- textArea = @_createTextArea()
+ textArea = @_createTextArea "div" # Use a <div> so Firefox pastes rich text.
document.body.appendChild(textArea)
textArea.focus()
document.execCommand("Paste")
- value = textArea.value
+ value = textArea.innerText
document.body.removeChild(textArea)
value
-root = exports ? window
+root = exports ? (window.root ?= {})
root.Clipboard = Clipboard
+extend window, root unless exports?
diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee
index 82c13287..67d5a44c 100644
--- a/lib/dom_utils.coffee
+++ b/lib/dom_utils.coffee
@@ -5,7 +5,7 @@ DomUtils =
documentReady: do ->
[isReady, callbacks] = [document.readyState != "loading", []]
unless isReady
- window.addEventListener "DOMContentLoaded", onDOMContentLoaded = ->
+ window.addEventListener "DOMContentLoaded", onDOMContentLoaded = forTrusted ->
window.removeEventListener "DOMContentLoaded", onDOMContentLoaded
isReady = true
callback() for callback in callbacks
@@ -16,7 +16,7 @@ DomUtils =
documentComplete: do ->
[isComplete, callbacks] = [document.readyState == "complete", []]
unless isComplete
- window.addEventListener "load", onLoad = ->
+ window.addEventListener "load", onLoad = forTrusted ->
window.removeEventListener "load", onLoad
isComplete = true
callback() for callback in callbacks
@@ -219,7 +219,7 @@ DomUtils =
node = selection.anchorNode
node and @isDOMDescendant element, node
else
- if selection.type == "Range" and selection.isCollapsed
+ if DomUtils.getSelectionType(selection) == "Range" and selection.isCollapsed
# The selection is inside the Shadow DOM of a node. We can check the node it registers as being
# before, since this represents the node whose Shadow DOM it's inside.
containerNode = selection.anchorNode.childNodes[selection.anchorOffset]
@@ -249,7 +249,11 @@ DomUtils =
simulateClick: (element, modifiers) ->
eventSequence = ["mouseover", "mousedown", "mouseup", "click"]
for event in eventSequence
- @simulateMouseEvent event, element, modifiers
+ defaultActionShouldTrigger = @simulateMouseEvent event, element, modifiers
+ if event == "click" and defaultActionShouldTrigger and Utils.isFirefox()
+ # Firefox doesn't (currently) trigger the default action for modified keys.
+ DomUtils.simulateClickDefaultAction element, modifiers
+ defaultActionShouldTrigger # return the values returned by each @simulateMouseEvent call.
simulateMouseEvent: do ->
lastHoveredElement = undefined
@@ -272,6 +276,29 @@ DomUtils =
# but Webkit will. Dispatching a click on an input box does not seem to focus it; we do that separately
element.dispatchEvent(mouseEvent)
+ simulateClickDefaultAction: (element, modifiers = {}) ->
+ return unless element.tagName?.toLowerCase() == "a" and element.href?
+
+ {ctrlKey, shiftKey, metaKey, altKey} = modifiers
+
+ # Mac uses a different new tab modifier (meta vs. ctrl).
+ if KeyboardUtils.platform == "Mac"
+ newTabModifier = metaKey == true and ctrlKey == false
+ else
+ newTabModifier = metaKey == false and ctrlKey == true
+
+ if newTabModifier
+ # Open in new tab. Shift determines whether the tab is focused when created. Alt is ignored.
+ chrome.runtime.sendMessage {handler: "openUrlInNewTab", url: element.href, active:
+ shiftKey == true}
+ else if shiftKey == true and metaKey == false and ctrlKey == false and altKey == false
+ # Open in new window.
+ chrome.runtime.sendMessage {handler: "openUrlInNewWindow", url: element.href}
+ else if element.target == "_blank"
+ chrome.runtime.sendMessage {handler: "openUrlInNewTab", url: element.href, active: true}
+
+ return
+
addFlashRect: (rect) ->
flashEl = @createElement "div"
flashEl.classList.add "vimiumReset"
@@ -289,11 +316,23 @@ DomUtils =
setTimeout((-> DomUtils.removeElement flashEl), 400)
getViewportTopLeft: ->
- if getComputedStyle(document.documentElement).position == "static"
- top: window.scrollY, left: window.scrollX
+ box = document.documentElement
+ style = getComputedStyle box
+ rect = box.getBoundingClientRect()
+ if style.position == "static" and not /content|paint|strict/.test(style.contain or "")
+ # The margin is included in the client rect, so we need to subtract it back out.
+ marginTop = parseInt style.marginTop
+ marginLeft = parseInt style.marginLeft
+ top: -rect.top + marginTop, left: -rect.left + marginLeft
else
- rect = document.documentElement.getBoundingClientRect()
- top: -rect.top, left: -rect.left
+ if Utils.isFirefox()
+ # These are always 0 for documentElement on Firefox, so we derive them from CSS border.
+ clientTop = parseInt style.borderTopWidth
+ clientLeft = parseInt style.borderLeftWidth
+ else
+ {clientTop, clientLeft} = box
+ top: -rect.top - clientTop, left: -rect.left - clientLeft
+
suppressPropagation: (event) ->
event.stopImmediatePropagation()
@@ -302,20 +341,50 @@ DomUtils =
event.preventDefault()
@suppressPropagation(event)
- # Suppress the next keyup event for Escape.
- suppressKeyupAfterEscape: (handlerStack) ->
- handlerStack.push
- _name: "dom_utils/suppressKeyupAfterEscape"
- keyup: (event) ->
- return true unless KeyboardUtils.isEscape event
- @remove()
- false
+ consumeKeyup: do ->
+ handlerId = null
+
+ (event, callback = null, suppressPropagation) ->
+ unless event.repeat
+ handlerStack.remove handlerId if handlerId?
+ code = event.code
+ handlerId = handlerStack.push
+ _name: "dom_utils/consumeKeyup"
+ keyup: (event) ->
+ return handlerStack.continueBubbling unless event.code == code
+ @remove()
+ if suppressPropagation
+ DomUtils.suppressPropagation event
+ else
+ DomUtils.suppressEvent event
+ handlerStack.continueBubbling
+ # We cannot track keyup events if we lose the focus.
+ blur: (event) ->
+ @remove() if event.target == window
+ handlerStack.continueBubbling
+ callback?()
+ if suppressPropagation
+ DomUtils.suppressPropagation event
+ handlerStack.suppressPropagation
+ else
+ DomUtils.suppressEvent event
+ handlerStack.suppressEvent
+
+ # Polyfill for selection.type (which is not available in Firefox).
+ getSelectionType: (selection = document.getSelection()) ->
+ selection.type or do ->
+ if selection.rangeCount == 0
+ "None"
+ else if selection.isCollapsed
+ "Caret"
+ else
+ "Range"
# Adapted from: http://roysharon.com/blog/37.
# This finds the element containing the selection focus.
getElementWithFocus: (selection, backwards) ->
r = t = selection.getRangeAt 0
- if selection.type == "Range"
+ if DomUtils.getSelectionType(selection) == "Range"
r = t.cloneRange()
r.collapse backwards
t = r.startContainer
@@ -341,11 +410,20 @@ DomUtils =
# If the element is rendered in a shadow DOM via a <content> element, the <content> element will be
# returned, so the shadow DOM is traversed rather than passed over.
getContainingElement: (element) ->
- element.getDestinationInsertionPoints()[0] or element.parentElement
+ element.getDestinationInsertionPoints?()[0] or element.parentElement
# This tests whether a window is too small to be useful.
windowIsTooSmall: ->
return window.innerWidth < 3 or window.innerHeight < 3
-root = exports ? window
+ # Inject user styles manually. This is only necessary for our chrome-extension:// pages and frames.
+ injectUserCss: ->
+ Settings.onLoaded ->
+ style = document.createElement "style"
+ style.type = "text/css"
+ style.textContent = Settings.get "userDefinedLinkHintCss"
+ document.head.appendChild style
+
+root = exports ? (window.root ?= {})
root.DomUtils = DomUtils
+extend window, root unless exports?
diff --git a/lib/find_mode_history.coffee b/lib/find_mode_history.coffee
index ff660bd2..93698266 100644
--- a/lib/find_mode_history.coffee
+++ b/lib/find_mode_history.coffee
@@ -46,5 +46,6 @@ FindModeHistory =
refreshRawQueryList: (query, rawQueryList) ->
([ query ].concat rawQueryList.filter (q) => q != query)[0..@max]
-root = exports ? window
+root = exports ? (window.root ?= {})
root.FindModeHistory = FindModeHistory
+extend window, root unless exports?
diff --git a/lib/handler_stack.coffee b/lib/handler_stack.coffee
index 806b707f..a43fc356 100644
--- a/lib/handler_stack.coffee
+++ b/lib/handler_stack.coffee
@@ -1,4 +1,4 @@
-root = exports ? window
+root = exports ? (window.root ?= {})
class HandlerStack
constructor: ->
@@ -57,7 +57,10 @@ class HandlerStack
if result == @passEventToPage
return true
else if result == @suppressPropagation
- DomUtils.suppressPropagation event
+ if type == "keydown"
+ DomUtils.consumeKeyup event, null, true
+ else
+ DomUtils.suppressPropagation event
return false
else if result == @restartBubbling
return @bubbleEvent type, event
@@ -65,7 +68,11 @@ class HandlerStack
true # Do nothing, but continue bubbling.
else
# result is @suppressEvent or falsy.
- DomUtils.suppressEvent event if @isChromeEvent event
+ if @isChromeEvent event
+ if type == "keydown"
+ DomUtils.consumeKeyup event
+ else
+ DomUtils.suppressEvent event
return false
# None of our handlers care about this event, so pass it to the page.
@@ -120,3 +127,4 @@ class HandlerStack
root.HandlerStack = HandlerStack
root.handlerStack = new HandlerStack()
+extend window, root unless exports?
diff --git a/lib/keyboard_utils.coffee b/lib/keyboard_utils.coffee
index c49fb3f4..673289b9 100644
--- a/lib/keyboard_utils.coffee
+++ b/lib/keyboard_utils.coffee
@@ -1,26 +1,11 @@
-KeyboardUtils =
- keyCodes:
- { ESC: 27, backspace: 8, deleteKey: 46, enter: 13, ctrlEnter: 10, space: 32, shiftKey: 16, ctrlKey: 17, f1: 112,
- f12: 123, tab: 9, downArrow: 40, upArrow: 38 }
+mapKeyRegistry = {}
+# NOTE: "?" here for the tests.
+Utils?.monitorChromeStorage "mapKeyRegistry", (value) => mapKeyRegistry = value
+KeyboardUtils =
+ # This maps event.key key names to Vimium key names.
keyNames:
- { 37: "left", 38: "up", 39: "right", 40: "down", 32: "space", 8: "backspace" }
-
- # This is a mapping of the incorrect keyIdentifiers generated by Webkit on Windows during keydown events to
- # the correct identifiers, which are correctly generated on Mac. We require this mapping to properly handle
- # these keys on Windows. See https://bugs.webkit.org/show_bug.cgi?id=19906 for more details.
- keyIdentifierCorrectionMap:
- "U+00C0": ["U+0060", "U+007E"] # `~
- "U+00BD": ["U+002D", "U+005F"] # -_
- "U+00BB": ["U+003D", "U+002B"] # =+
- "U+00DB": ["U+005B", "U+007B"] # [{
- "U+00DD": ["U+005D", "U+007D"] # ]}
- "U+00DC": ["U+005C", "U+007C"] # \|
- "U+00BA": ["U+003B", "U+003A"] # ;:
- "U+00DE": ["U+0027", "U+0022"] # '"
- "U+00BC": ["U+002C", "U+003C"] # ,<
- "U+00BE": ["U+002E", "U+003E"] # .>
- "U+00BF": ["U+002F", "U+003F"] # /?
+ "ArrowLeft": "left", "ArrowUp": "up", "ArrowRight": "right", "ArrowDown": "down", " ": "space"
init: ->
if (navigator.userAgent.indexOf("Mac") != -1)
@@ -30,106 +15,95 @@ KeyboardUtils =
else
@platform = "Windows"
- # We are migrating from using event.keyIdentifier to using event.key. For some period of time, we must
- # support both. This wrapper can be removed once Chrome 52 is considered too old to support.
getKeyChar: (event) ->
- # We favor using event.keyIdentifier due to Chromium's currently (Chrome 51) incorrect implementataion of
- # event.key; see #2147.
- if event.keyIdentifier?
- @getKeyCharUsingKeyIdentifier event
- else
- @getKeyCharUsingKey event
-
- getKeyCharUsingKey: (event) ->
- if event.keyCode of @keyNames
- @keyNames[event.keyCode]
- else if event.key.length == 1
- event.key
- else if event.key.length == 2 and "F1" <= event.key <= "F9"
- event.key.toLowerCase() # F1 to F9.
- else if event.key.length == 3 and "F10" <= event.key <= "F12"
- event.key.toLowerCase() # F10 to F12.
+ unless Settings.get "ignoreKeyboardLayout"
+ key = event.key
+ else unless event.code
+ key = ""
+ else if event.code[...6] == "Numpad"
+ # We cannot correctly emulate the numpad, so fall back to event.key; see #2626.
+ key = event.key
else
+ # The logic here is from the vim-like-key-notation project (https://github.com/lydell/vim-like-key-notation).
+ key = event.code
+ key = key[3..] if key[...3] == "Key"
+ # Translate some special keys to event.key-like strings and handle <Shift>.
+ if @enUsTranslations[key]
+ key = if event.shiftKey then @enUsTranslations[key][1] else @enUsTranslations[key][0]
+ else if key.length == 1 and not event.shiftKey
+ key = key.toLowerCase()
+
+ # It appears that key is not always defined (see #2453).
+ unless key
""
+ else if key of @keyNames
+ @keyNames[key]
+ else if @isModifier event
+ "" # Don't resolve modifier keys.
+ else if key.length == 1
+ key
+ else
+ key.toLowerCase()
- getKeyCharUsingKeyIdentifier: (event) ->
- # Handle named keys.
- keyCode = event.keyCode
- if keyCode
- if keyCode of @keyNames
- return @keyNames[keyCode]
- # Function keys.
- if @keyCodes.f1 <= keyCode <= @keyCodes.f12
- return "f" + (1 + keyCode - keyCodes.f1)
-
- keyIdentifier = event.keyIdentifier
-
- # Not a letter.
- if not keyIdentifier.startsWith "U+"
- return ""
+ getKeyCharString: (event) ->
+ if keyChar = @getKeyChar event
+ modifiers = []
- # On Windows, the keyIdentifiers for non-letter keys are incorrect. See
- # https://bugs.webkit.org/show_bug.cgi?id=19906 for more details.
- if ((@platform == "Windows" || @platform == "Linux") && @keyIdentifierCorrectionMap[keyIdentifier])
- correctedIdentifiers = @keyIdentifierCorrectionMap[keyIdentifier]
- keyIdentifier = if event.shiftKey then correctedIdentifiers[1] else correctedIdentifiers[0]
- unicodeKeyInHex = "0x" + keyIdentifier.substring(2)
- character = String.fromCharCode(parseInt(unicodeKeyInHex)).toLowerCase()
- if event.shiftKey then character.toUpperCase() else character
+ keyChar = keyChar.toUpperCase() if event.shiftKey and keyChar.length == 1
+ # These must be in alphabetical order (to match the sorted modifier order in Commands.normalizeKey).
+ modifiers.push "a" if event.altKey
+ modifiers.push "c" if event.ctrlKey
+ modifiers.push "m" if event.metaKey
- isPrimaryModifierKey: (event) -> if (@platform == "Mac") then event.metaKey else event.ctrlKey
+ keyChar = [modifiers..., keyChar].join "-"
+ keyChar = "<#{keyChar}>" if 1 < keyChar.length
+ keyChar = mapKeyRegistry[keyChar] ? keyChar
+ keyChar
isEscape: do ->
- mapKeyRegistry = {}
- # NOTE: "?" here for the tests.
- Utils?.monitorChromeStorage "mapKeyRegistry", (value) => mapKeyRegistry = value
+ useVimLikeEscape = true
+ Utils.monitorChromeStorage "useVimLikeEscape", (value) -> useVimLikeEscape = value
- # TODO(smblott) Change this to use event.key.
(event) ->
- event.keyCode == @keyCodes.ESC || do =>
- keyChar = @getKeyCharString event, true
- keyChar = mapKeyRegistry[keyChar] ? keyChar
- # <c-[> is mapped to Escape in Vim by default.
- keyChar == "<c-[>"
-
- # TODO. This is probably a poor way of detecting printable characters. However, it shouldn't incorrectly
- # identify any of chrome's own keyboard shortcuts as printable.
- isPrintable: (event) ->
- return false if event.metaKey or event.ctrlKey or event.altKey
- keyChar =
- if event.type == "keypress"
- String.fromCharCode event.charCode
- else
- @getKeyChar event
- keyChar.length == 1
+ # <c-[> is mapped to Escape in Vim by default.
+ event.key == "Escape" or (useVimLikeEscape and @getKeyCharString(event) == "<c-[>")
- # Return the Vimium key representation for this keyboard event. Return a falsy value (the empty string or
- # undefined) when no Vimium representation is appropriate.
- getKeyCharString: (event, allKeydownEvents = false) ->
- switch event.type
- when "keypress"
- # Ignore modifier keys by themselves.
- if 31 < event.keyCode
- String.fromCharCode event.charCode
+ isBackspace: (event) ->
+ event.key in ["Backspace", "Delete"]
- when "keydown"
- # Handle special keys and normal input keys with modifiers being pressed.
- keyChar = @getKeyChar event
- if 1 < keyChar.length or (keyChar.length == 1 and (event.metaKey or event.ctrlKey or event.altKey)) or allKeydownEvents
- modifiers = []
-
- keyChar = keyChar.toUpperCase() if event.shiftKey
- # These must be in alphabetical order (to match the sorted modifier order in Commands.normalizeKey).
- modifiers.push "a" if event.altKey
- modifiers.push "c" if event.ctrlKey
- modifiers.push "m" if event.metaKey
-
- keyChar = [modifiers..., keyChar].join "-"
- if 1 < keyChar.length then "<#{keyChar}>" else keyChar
+ isPrintable: (event) ->
+ @getKeyCharString(event)?.length == 1
+
+ isModifier: (event) ->
+ event.key in ["Control", "Shift", "Alt", "OS", "AltGraph", "Meta"]
+
+ enUsTranslations:
+ "Backquote": ["`", "~"]
+ "Minus": ["-", "_"]
+ "Equal": ["=", "+"]
+ "Backslash": ["\\","|"]
+ "IntlBackslash": ["\\","|"]
+ "BracketLeft": ["[", "{"]
+ "BracketRight": ["]", "}"]
+ "Semicolon": [";", ":"]
+ "Quote": ["'", '"']
+ "Comma": [",", "<"]
+ "Period": [".", ">"]
+ "Slash": ["/", "?"]
+ "Space": [" ", " "]
+ "Digit1": ["1", "!"]
+ "Digit2": ["2", "@"]
+ "Digit3": ["3", "#"]
+ "Digit4": ["4", "$"]
+ "Digit5": ["5", "%"]
+ "Digit6": ["6", "^"]
+ "Digit7": ["7", "&"]
+ "Digit8": ["8", "*"]
+ "Digit9": ["9", "("]
+ "Digit0": ["0", ")"]
KeyboardUtils.init()
-root = exports ? window
+root = exports ? (window.root ?= {})
root.KeyboardUtils = KeyboardUtils
-# TODO(philc): A lot of code uses this keyCodes hash... maybe we shouldn't export it as a global.
-root.keyCodes = KeyboardUtils.keyCodes
+extend window, root unless exports?
diff --git a/lib/rect.coffee b/lib/rect.coffee
index d4807cc2..0e9c3417 100644
--- a/lib/rect.coffee
+++ b/lib/rect.coffee
@@ -67,12 +67,18 @@ Rect =
rects.filter (rect) -> rect.height > 0 and rect.width > 0
- contains: (rect1, rect2) ->
+ # Determine whether two rects overlap.
+ intersects: (rect1, rect2) ->
rect1.right > rect2.left and
rect1.left < rect2.right and
rect1.bottom > rect2.top and
rect1.top < rect2.bottom
+ # Determine whether two rects overlap, including 0-width intersections at borders.
+ intersectsStrict: (rect1, rect2) ->
+ rect1.right >= rect2.left and rect1.left <= rect2.right and
+ rect1.bottom >= rect2.top and rect1.top <= rect2.bottom
+
equals: (rect1, rect2) ->
for property in ["top", "bottom", "left", "right", "width", "height"]
return false if rect1[property] != rect2[property]
@@ -82,14 +88,6 @@ Rect =
@create (Math.max rect1.left, rect2.left), (Math.max rect1.top, rect2.top),
(Math.min rect1.right, rect2.right), (Math.min rect1.bottom, rect2.bottom)
- # Determine whether two rects overlap.
- rectsOverlap: do ->
- halfOverlapChecker = (rect1, rect2) ->
- (rect1.left <= rect2.left <= rect1.right or rect1.left <= rect2.right <= rect1.right) and
- (rect1.top <= rect2.top <= rect1.bottom or rect1.top <= rect2.bottom <= rect1.bottom)
-
- (rect1, rect2) ->
- halfOverlapChecker(rect1, rect2) or halfOverlapChecker rect2, rect1
-
-root = exports ? window
+root = exports ? (window.root ?= {})
root.Rect = Rect
+extend window, root unless exports?
diff --git a/lib/settings.coffee b/lib/settings.coffee
index e16261d0..fd1ef268 100644
--- a/lib/settings.coffee
+++ b/lib/settings.coffee
@@ -10,9 +10,14 @@
#
# In all cases except Settings.defaults, values are stored as jsonified strings.
+# If the current frame is the Vomnibar or the HUD, then we'll need our Chrome stubs for the tests.
+window.chrome ?= window.top?.chrome
+
+storageArea = if chrome.storage.sync? then "sync" else "local"
+
Settings =
debug: false
- storage: chrome.storage.sync
+ storage: chrome.storage[storageArea]
cache: {}
isLoaded: false
onLoadedCallbacks: []
@@ -25,16 +30,26 @@ Settings =
@cache = if Utils.isBackgroundPage() then localStorage else extend {}, localStorage
@runOnLoadedCallbacks()
- chrome.storage.local.get null, (localItems) =>
- localItems = {} if chrome.runtime.lastError
- @storage.get null, (syncedItems) =>
- unless chrome.runtime.lastError
- @handleUpdateFromChromeStorage key, value for own key, value of extend localItems, syncedItems
+ # Test chrome.storage.sync to see if it is enabled.
+ # NOTE(mrmr1993, 2017-04-18): currently the API is defined in FF, but it is disabled behind a flag in
+ # about:config. Every use sets chrome.runtime.lastError, so we use that to check whether we can use it.
+ chrome.storage.sync.get null, =>
+ if chrome.runtime.lastError
+ storageArea = "local"
+ @storage = chrome.storage[storageArea]
+
+ # Delay this initialisation until after the correct storage area is known. The significance of this is
+ # that it delays the on-loaded callbacks.
+ chrome.storage.local.get null, (localItems) =>
+ localItems = {} if chrome.runtime.lastError
+ @storage.get null, (syncedItems) =>
+ unless chrome.runtime.lastError
+ @handleUpdateFromChromeStorage key, value for own key, value of extend localItems, syncedItems
- chrome.storage.onChanged.addListener (changes, area) =>
- @propagateChangesFromChromeStorage changes if area == "sync"
+ chrome.storage.onChanged.addListener (changes, area) =>
+ @propagateChangesFromChromeStorage changes if area == storageArea
- @runOnLoadedCallbacks()
+ @runOnLoadedCallbacks()
# Called after @cache has been initialized. On extension pages, this will be called twice, but that does
# not matter because it's idempotent.
@@ -71,12 +86,15 @@ Settings =
if @shouldSyncKey key
if shouldSetInSyncedStorage
setting = {}; setting[key] = @cache[key]
- @log " chrome.storage.sync.set(#{key})"
+ @log " chrome.storage.#{storageArea}.set(#{key})"
@storage.set setting
- if Utils.isBackgroundPage()
+ if Utils.isBackgroundPage() and storageArea == "sync"
# Remove options installed by the "copyNonDefaultsToChromeStorage-20150717" migration; see below.
@log " chrome.storage.local.remove(#{key})"
chrome.storage.local.remove key
+ # NOTE(mrmr1993): In FF, |value| will be garbage collected when the page owning it is unloaded.
+ # Any postUpdateHooks that can be called from the options page/exclusions popup should be careful not to
+ # use |value| asynchronously, or else it may refer to a |DeadObject| and accesses will throw an error.
@performPostUpdateHook key, value
clear: (key) ->
@@ -98,7 +116,7 @@ Settings =
nuke: (key) ->
delete localStorage[key]
chrome.storage.local.remove key
- chrome.storage.sync.remove key
+ chrome.storage.sync?.remove key
# For development only.
log: (args...) ->
@@ -153,23 +171,23 @@ Settings =
# put in an example search engine
searchEngines:
"""
- w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia
+ w: https://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia
# More examples.
#
# (Vimium supports search completion Wikipedia, as
# above, and for these.)
#
- # g: http://www.google.com/search?q=%s Google
- # l: http://www.google.com/search?q=%s&btnI I'm feeling lucky...
- # y: http://www.youtube.com/results?search_query=%s Youtube
+ # g: https://www.google.com/search?q=%s Google
+ # l: https://www.google.com/search?q=%s&btnI I'm feeling lucky...
+ # y: https://www.youtube.com/results?search_query=%s Youtube
# gm: https://www.google.com/maps?q=%s Google maps
# b: https://www.bing.com/search?q=%s Bing
# d: https://duckduckgo.com/?q=%s DuckDuckGo
- # az: http://www.amazon.com/s/?field-keywords=%s Amazon
+ # az: https://www.amazon.com/s/?field-keywords=%s Amazon
# qw: https://www.qwant.com/?q=%s Qwant
"""
- newTabUrl: "chrome://newtab"
+ newTabUrl: "about:newtab"
grabBackFocus: false
regexFindMode: false
waitForEnterForFilteredHints: false # Note: this defaults to true for new users; see below.
@@ -178,26 +196,30 @@ Settings =
helpDialog_showAdvancedCommands: false
optionsPage_showAdvancedOptions: false
passNextKeyKeys: []
+ ignoreKeyboardLayout: false
Settings.init()
# Perform migration from old settings versions, if this is the background page.
if Utils.isBackgroundPage()
+ Settings.applyMigrations = ->
+ unless Settings.get "settingsVersion"
+ # This is a new install. For some settings, we retain a legacy default behaviour for existing users but
+ # use a non-default behaviour for new users.
- unless Settings.get "settingsVersion"
- # This is a new install. For some settings, we retain a legacy default behaviour for existing users but
- # use a non-default behaviour for new users.
+ # For waitForEnterForFilteredHints, "true" gives a better UX; see #1950. However, forcing the change on
+ # existing users would be unnecessarily disruptive. So, only new users default to "true".
+ Settings.set "waitForEnterForFilteredHints", true
- # For waitForEnterForFilteredHints, "true" gives a better UX; see #1950. However, forcing the change on
- # existing users would be unnecessarily disruptive. So, only new users default to "true".
- Settings.set "waitForEnterForFilteredHints", true
+ # We use settingsVersion to coordinate any necessary schema changes.
+ Settings.set("settingsVersion", Utils.getCurrentVersion())
- # We use settingsVersion to coordinate any necessary schema changes.
- Settings.set("settingsVersion", Utils.getCurrentVersion())
+ # Remove legacy key which was used to control storage migration. This was after 1.57 (2016-10-01), and can
+ # be removed after 1.58 has been out for sufficiently long.
+ Settings.nuke "copyNonDefaultsToChromeStorage-20150717"
- # Remove legacy key which was used to control storage migration. This was after 1.57 (2016-10-01), and can
- # be removed after 1.58 has been out for sufficiently long.
- Settings.nuke "copyNonDefaultsToChromeStorage-20150717"
+ Settings.onLoaded Settings.applyMigrations.bind Settings
-root = exports ? window
+root = exports ? (window.root ?= {})
root.Settings = Settings
+extend window, root unless exports?
diff --git a/lib/utils.coffee b/lib/utils.coffee
index babb5f96..6f38be8f 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -1,4 +1,27 @@
+# Only pass events to the handler if they are marked as trusted by the browser.
+# This is kept in the global namespace for brevity and ease of use.
+window.forTrusted ?= (handler) -> (event) ->
+ if event?.isTrusted
+ handler.apply this, arguments
+ else
+ true
+
+browserInfo = browser?.runtime?.getBrowserInfo?()
+
Utils =
+ isFirefox: do ->
+ # NOTE(mrmr1993): This test only works in the background page, this is overwritten by isEnabledForUrl for
+ # content scripts.
+ isFirefox = false
+ browserInfo?.then? (browserInfo) ->
+ isFirefox = browserInfo?.name == "Firefox"
+ -> isFirefox
+ firefoxVersion: do ->
+ # NOTE(mrmr1993): This only works in the background page.
+ ffVersion = undefined
+ browserInfo?.then? (browserInfo) ->
+ ffVersion = browserInfo?.version
+ -> ffVersion
getCurrentVersion: ->
chrome.runtime.getManifest().version
@@ -9,14 +32,6 @@ Utils =
# Returns true whenever the current page is the extension's background page.
isBackgroundPage: -> @isExtensionPage() and chrome.extension.getBackgroundPage?() == window
- # Takes a dot-notation object string and calls the function that it points to with the correct value for
- # 'this'.
- invokeCommandString: (str, args...) ->
- [names..., name] = str.split '.'
- obj = window
- obj = obj[component] for component in names
- obj[name].apply obj, args
-
# Escape all special characters, so RegExp will parse the string 'as is'.
# Taken from http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
escapeRegexSpecialCharacters: do ->
@@ -41,7 +56,7 @@ Utils =
url.startsWith "javascript:"
hasFullUrlPrefix: do ->
- urlPrefix = new RegExp "^[a-z]{3,}://."
+ urlPrefix = new RegExp "^[a-z][-+.a-z0-9]{2,}://."
(url) -> urlPrefix.test url
# Decode valid escape sequences in a URI. This is intended to mimic the best-effort decoding
@@ -312,8 +327,11 @@ class JobRunner
onReady: (callback) ->
@fetcher.use callback
-root = exports ? window
+root = exports ? (window.root ?= {})
root.Utils = Utils
root.SimpleCache = SimpleCache
root.AsyncDataFetcher = AsyncDataFetcher
root.JobRunner = JobRunner
+unless exports?
+ root.extend = extend
+ extend window, root
diff --git a/manifest.json b/manifest.json
index a40cd134..62a19b35 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,18 +1,18 @@
{
"manifest_version": 2,
"name": "Vimium",
- "version": "1.57",
+ "version": "1.62",
"description": "The Hacker's Browser. Vimium provides keyboard shortcuts for navigation and control in the spirit of Vim.",
"icons": { "16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png" },
+ "minimum_chrome_version": "51.0",
"background": {
"scripts": [
"lib/utils.js",
"lib/settings.js",
"background_scripts/bg_utils.js",
"background_scripts/commands.js",
- "lib/clipboard.js",
"background_scripts/exclusions.js",
"background_scripts/completion_engines.js",
"background_scripts/completion_search.js",
@@ -21,12 +21,17 @@
"background_scripts/main.js"
]
},
- "options_page": "pages/options.html",
+ "options_ui": {
+ "page": "pages/options.html",
+ "chrome_style": false,
+ "open_in_tab": true
+ },
"permissions": [
"tabs",
"bookmarks",
"history",
"clipboardRead",
+ "clipboardWrite",
"storage",
"sessions",
"notifications",
@@ -36,7 +41,7 @@
"content_scripts": [
{
"_comment":
- "IMPORTANT: All resources listed here must also be listed in ./pages/vimium_resources.html.",
+ "IMPORTANT: All resources listed here must also be listed with the others -- in order -- in ./pages/*.html",
"matches": ["<all_urls>"],
"js": ["lib/utils.js",
"lib/keyboard_utils.js",
@@ -56,11 +61,13 @@
"content_scripts/mode_key_handler.js",
"content_scripts/mode_visual.js",
"content_scripts/hud.js",
+ "content_scripts/mode_normal.js",
"content_scripts/vimium_frontend.js"
],
"css": ["content_scripts/vimium.css"],
"run_at": "document_start",
- "all_frames": true
+ "all_frames": true,
+ "match_about_blank": true
},
{
"matches": ["file:///", "file:///*/"],
diff --git a/pages/blank.html b/pages/blank.html
index c238282d..d026912e 100644
--- a/pages/blank.html
+++ b/pages/blank.html
@@ -1,7 +1,28 @@
<html>
<head>
<title>New Tab</title>
- <link rel="import" href="vimium_resources.html">
+ <script src="../lib/utils.js"></script>
+ <script src="../lib/keyboard_utils.js"></script>
+ <script src="../lib/dom_utils.js"></script>
+ <script src="../lib/rect.js"></script>
+ <script src="../lib/handler_stack.js"></script>
+ <script src="../lib/settings.js"></script>
+ <script src="../lib/find_mode_history.js"></script>
+ <script src="../content_scripts/mode.js"></script>
+ <script src="../content_scripts/ui_component.js"></script>
+ <script src="../content_scripts/link_hints.js"></script>
+ <script src="../content_scripts/vomnibar.js"></script>
+ <script src="../content_scripts/scroller.js"></script>
+ <script src="../content_scripts/marks.js"></script>
+ <script src="../content_scripts/mode_insert.js"></script>
+ <script src="../content_scripts/mode_find.js"></script>
+ <script src="../content_scripts/mode_key_handler.js"></script>
+ <script src="../content_scripts/mode_visual.js"></script>
+ <script src="../content_scripts/hud.js"></script>
+ <script src="../content_scripts/mode_normal.js"></script>
+ <script src="../content_scripts/vimium_frontend.js"></script>
+ <link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" />
+
</head>
<body>
</body>
diff --git a/pages/completion_engines.html b/pages/completion_engines.html
index 0c350664..3313b26c 100644
--- a/pages/completion_engines.html
+++ b/pages/completion_engines.html
@@ -4,7 +4,28 @@
<!-- We re-use some styling from the options page, so that the look and feel here is similar -->
<link rel="stylesheet" type="text/css" href="options.css">
<link rel="stylesheet" type="text/css" href="completion_engines.css">
- <link rel="import" href="vimium_resources.html">
+ <script src="../lib/utils.js"></script>
+ <script src="../lib/keyboard_utils.js"></script>
+ <script src="../lib/dom_utils.js"></script>
+ <script src="../lib/rect.js"></script>
+ <script src="../lib/handler_stack.js"></script>
+ <script src="../lib/settings.js"></script>
+ <script src="../lib/find_mode_history.js"></script>
+ <script src="../content_scripts/mode.js"></script>
+ <script src="../content_scripts/ui_component.js"></script>
+ <script src="../content_scripts/link_hints.js"></script>
+ <script src="../content_scripts/vomnibar.js"></script>
+ <script src="../content_scripts/scroller.js"></script>
+ <script src="../content_scripts/marks.js"></script>
+ <script src="../content_scripts/mode_insert.js"></script>
+ <script src="../content_scripts/mode_find.js"></script>
+ <script src="../content_scripts/mode_key_handler.js"></script>
+ <script src="../content_scripts/mode_visual.js"></script>
+ <script src="../content_scripts/hud.js"></script>
+ <script src="../content_scripts/mode_normal.js"></script>
+ <script src="../content_scripts/vimium_frontend.js"></script>
+ <link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" />
+
<script src="../background_scripts/completion_engines.js"></script>
<script src="completion_engines.js"></script>
</head>
diff --git a/pages/help_dialog.coffee b/pages/help_dialog.coffee
index 4ac9116b..08180a72 100644
--- a/pages/help_dialog.coffee
+++ b/pages/help_dialog.coffee
@@ -73,7 +73,7 @@ HelpDialog =
for key in command.keys.sort compareKeys
@instantiateHtmlTemplate keysElement, "#keysTemplate", (element) ->
lastElement = element
- $$(element, ".vimiumHelpDialogKey").innerHTML = Utils.escapeHtml key
+ $$(element, ".vimiumHelpDialogKey").textContent = key
# And strip off the trailing ", ", if necessary.
lastElement.removeChild $$ lastElement, ".commaSeparator" if lastElement
@@ -83,7 +83,7 @@ HelpDialog =
commandNameElement.textContent = command.command
commandNameElement.title = "Click to copy \"#{command.command}\" to clipboard."
commandNameElement.addEventListener "click", ->
- chrome.runtime.sendMessage handler: "copyToClipboard", data: commandNameElement.textContent
+ HUD.copyToClipboard commandNameElement.textContent
HUD.showForDuration("Yanked #{commandNameElement.textContent}.", 2000)
@showAdvancedCommands(@getShowAdvancedCommands())
@@ -109,7 +109,7 @@ HelpDialog =
vimiumHelpDialogContainer.scrollTop += scrollHeightDelta if 0 < scrollHeightDelta
showAdvancedCommands: (visible) ->
- document.getElementById("toggleAdvancedCommands").innerHTML =
+ document.getElementById("toggleAdvancedCommands").textContent =
if visible then "Hide advanced commands" else "Show advanced commands"
# Add/remove the showAdvanced class to show/hide advanced commands.
@@ -131,6 +131,9 @@ UIComponentServer.registerHandler (event) ->
# Abandon any HUD which might be showing within the help dialog.
HUD.abandon()
+document.addEventListener "DOMContentLoaded", ->
+ DomUtils.injectUserCss() # Manually inject custom user styles.
+
root = exports ? window
root.HelpDialog = HelpDialog
root.isVimiumHelpDialog = true
diff --git a/pages/help_dialog.html b/pages/help_dialog.html
index 7bc0d86c..7f053265 100644
--- a/pages/help_dialog.html
+++ b/pages/help_dialog.html
@@ -1,7 +1,28 @@
<html>
<head>
<title>Vimium Help</title>
- <link rel="import" href="vimium_resources.html">
+ <script src="../lib/utils.js"></script>
+ <script src="../lib/keyboard_utils.js"></script>
+ <script src="../lib/dom_utils.js"></script>
+ <script src="../lib/rect.js"></script>
+ <script src="../lib/handler_stack.js"></script>
+ <script src="../lib/settings.js"></script>
+ <script src="../lib/find_mode_history.js"></script>
+ <script src="../content_scripts/mode.js"></script>
+ <script src="../content_scripts/ui_component.js"></script>
+ <script src="../content_scripts/link_hints.js"></script>
+ <script src="../content_scripts/vomnibar.js"></script>
+ <script src="../content_scripts/scroller.js"></script>
+ <script src="../content_scripts/marks.js"></script>
+ <script src="../content_scripts/mode_insert.js"></script>
+ <script src="../content_scripts/mode_find.js"></script>
+ <script src="../content_scripts/mode_key_handler.js"></script>
+ <script src="../content_scripts/mode_visual.js"></script>
+ <script src="../content_scripts/hud.js"></script>
+ <script src="../content_scripts/mode_normal.js"></script>
+ <script src="../content_scripts/vimium_frontend.js"></script>
+ <link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" />
+
<script type="text/javascript" src="ui_component_server.js"></script>
<script type="text/javascript" src="help_dialog.js"></script>
</head>
@@ -87,7 +108,7 @@
<a class="vimiumHelDialogLink" target="_blank"
href="https://chrome.google.com/webstore/detail/vimium/dbepggeogbaibhgnhhndojpepiihcmeb/reviews">Leave us
feedback</a>.<br/>
- Found a bug? <a class="vimiumHelDialogLink" target="_blank" href="http://github.com/philc/vimium/issues">Report it here</a>.
+ Found a bug? <a class="vimiumHelDialogLink" target="_blank" href="https://github.com/philc/vimium/issues">Report it here</a>.
</div>
<div class="vimiumReset vimiumColumn" style="text-align:right">
<span class="vimiumReset">Version <span id="help-dialog-version"></span></span><br/>
diff --git a/pages/hud.coffee b/pages/hud.coffee
index 36e4cbd2..5ff2e07e 100644
--- a/pages/hud.coffee
+++ b/pages/hud.coffee
@@ -12,25 +12,28 @@ setTextInInputElement = (inputElement, text) ->
selection.removeAllRanges()
selection.addRange range
+document.addEventListener "DOMContentLoaded", ->
+ DomUtils.injectUserCss() # Manually inject custom user styles.
+
document.addEventListener "keydown", (event) ->
inputElement = document.getElementById "hud-find-input"
return unless inputElement? # Don't do anything if we're not in find mode.
- if (event.keyCode in [keyCodes.backspace, keyCodes.deleteKey] and inputElement.textContent.length == 0) or
- event.keyCode == keyCodes.enter or KeyboardUtils.isEscape event
+ if (KeyboardUtils.isBackspace(event) and inputElement.textContent.length == 0) or
+ event.key == "Enter" or KeyboardUtils.isEscape event
UIComponentServer.postMessage
name: "hideFindMode"
- exitEventIsEnter: event.keyCode == keyCodes.enter
+ exitEventIsEnter: event.key == "Enter"
exitEventIsEscape: KeyboardUtils.isEscape event
- else if event.keyCode == keyCodes.upArrow
+ else if event.key == "ArrowUp"
if rawQuery = FindModeHistory.getQuery findMode.historyIndex + 1
findMode.historyIndex += 1
findMode.partialQuery = findMode.rawQuery if findMode.historyIndex == 0
setTextInInputElement inputElement, rawQuery
findMode.executeQuery()
- else if event.keyCode == keyCodes.downArrow
+ else if event.key == "ArrowDown"
findMode.historyIndex = Math.max -1, findMode.historyIndex - 1
rawQuery = if 0 <= findMode.historyIndex then FindModeHistory.getQuery findMode.historyIndex else findMode.partialQuery
setTextInInputElement inputElement, rawQuery
@@ -58,7 +61,10 @@ handlers =
hud.innerText = "/\u200A" # \u200A is a "hair space", to leave enough space before the caret/first char.
inputElement = document.createElement "span"
- inputElement.contentEditable = "plaintext-only"
+ try # NOTE(mrmr1993): Chrome supports non-standard "plaintext-only", which is what we *really* want.
+ inputElement.contentEditable = "plaintext-only"
+ catch # Fallback to standard-compliant version.
+ inputElement.contentEditable = "true"
inputElement.id = "hud-find-input"
hud.appendChild inputElement
@@ -89,5 +95,19 @@ handlers =
" (No matches)"
countElement.textContent = if showMatchText then countText else ""
+ copyToClipboard: (data) ->
+ focusedElement = document.activeElement
+ Clipboard.copy data
+ focusedElement?.focus()
+ window.parent.focus()
+ UIComponentServer.postMessage {name: "unfocusIfFocused"}
+
+ pasteFromClipboard: ->
+ focusedElement = document.activeElement
+ data = Clipboard.paste()
+ focusedElement?.focus()
+ window.parent.focus()
+ UIComponentServer.postMessage {name: "pasteResponse", data}
+
UIComponentServer.registerHandler ({data}) -> handlers[data.name ? data]? data
FindModeHistory.init()
diff --git a/pages/hud.html b/pages/hud.html
index 60d737e1..7bd27171 100644
--- a/pages/hud.html
+++ b/pages/hud.html
@@ -2,9 +2,12 @@
<head>
<title>HUD</title>
<link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" />
+ <script type="text/javascript" src="../lib/utils.js"></script>
<script type="text/javascript" src="../lib/dom_utils.js"></script>
+ <script type="text/javascript" src="../lib/settings.js"></script>
<script type="text/javascript" src="../lib/keyboard_utils.js"></script>
<script type="text/javascript" src="../lib/find_mode_history.js"></script>
+ <script type="text/javascript" src="../lib/clipboard.js"></script>
<script type="text/javascript" src="ui_component_server.js"></script>
<script type="text/javascript" src="hud.js"></script>
</head>
diff --git a/pages/logging.coffee b/pages/logging.coffee
index 3ccef4ff..a437b442 100644
--- a/pages/logging.coffee
+++ b/pages/logging.coffee
@@ -1,6 +1,7 @@
$ = (id) -> document.getElementById id
document.addEventListener "DOMContentLoaded", ->
+ DomUtils.injectUserCss() # Manually inject custom user styles.
$("vimiumVersion").innerText = Utils.getCurrentVersion()
chrome.storage.local.get "installDate", (items) ->
diff --git a/pages/logging.html b/pages/logging.html
index bc4ffb80..17aafd70 100644
--- a/pages/logging.html
+++ b/pages/logging.html
@@ -1,7 +1,28 @@
<html>
<head>
<title>Vimium Logging</title>
- <link rel="import" href="vimium_resources.html">
+ <script src="../lib/utils.js"></script>
+ <script src="../lib/keyboard_utils.js"></script>
+ <script src="../lib/dom_utils.js"></script>
+ <script src="../lib/rect.js"></script>
+ <script src="../lib/handler_stack.js"></script>
+ <script src="../lib/settings.js"></script>
+ <script src="../lib/find_mode_history.js"></script>
+ <script src="../content_scripts/mode.js"></script>
+ <script src="../content_scripts/ui_component.js"></script>
+ <script src="../content_scripts/link_hints.js"></script>
+ <script src="../content_scripts/vomnibar.js"></script>
+ <script src="../content_scripts/scroller.js"></script>
+ <script src="../content_scripts/marks.js"></script>
+ <script src="../content_scripts/mode_insert.js"></script>
+ <script src="../content_scripts/mode_find.js"></script>
+ <script src="../content_scripts/mode_key_handler.js"></script>
+ <script src="../content_scripts/mode_visual.js"></script>
+ <script src="../content_scripts/hud.js"></script>
+ <script src="../content_scripts/mode_normal.js"></script>
+ <script src="../content_scripts/vimium_frontend.js"></script>
+ <link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" />
+
<script src="logging.js"></script>
<style type="text/css">
body {
diff --git a/pages/options.coffee b/pages/options.coffee
index 7021322a..9e95bcd3 100644
--- a/pages/options.coffee
+++ b/pages/options.coffee
@@ -39,9 +39,14 @@ class Option
bgSettings.clear @field
@fetch()
+ @onSaveCallbacks: []
+ @onSave: (callback) ->
+ @onSaveCallbacks.push callback
+
# Static method.
@saveOptions: ->
Option.all.map (option) -> option.save()
+ callback() for callback in @onSaveCallbacks
# Abstract method; only implemented in sub-classes.
# Populate the option's DOM element (@element) with the setting's current value.
@@ -93,8 +98,10 @@ class ExclusionRulesOption extends Option
element
populateElement: (rules) ->
- for rule in rules
- @appendRule rule
+ # For the case of restoring a backup, we first have to remove existing rules.
+ exclusionRules = $ "exclusionRules"
+ exclusionRules.deleteRow 1 while exclusionRules.rows[1]
+ @appendRule rule for rule in rules
# Append a row for a new rule. Return the newly-added element.
appendRule: (rule) ->
@@ -193,6 +200,7 @@ Options =
nextPatterns: NonEmptyTextOption
previousPatterns: NonEmptyTextOption
regexFindMode: CheckBoxOption
+ ignoreKeyboardLayout: CheckBoxOption
scrollStepSize: NumberOption
smoothScroll: CheckBoxOption
grabBackFocus: CheckBoxOption
@@ -203,7 +211,7 @@ Options =
initOptionsPage = ->
onUpdated = ->
$("saveOptions").removeAttribute "disabled"
- $("saveOptions").innerHTML = "Save Changes"
+ $("saveOptions").textContent = "Save Changes"
# Display either "linkHintNumbers" or "linkHintCharacters", depending upon "filterLinkHints".
maintainLinkHintsView = ->
@@ -221,10 +229,10 @@ initOptionsPage = ->
maintainAdvancedOptions = ->
if bgSettings.get "optionsPage_showAdvancedOptions"
$("advancedOptions").style.display = "table-row-group"
- $("advancedOptionsButton").innerHTML = "Hide Advanced Options"
+ $("advancedOptionsButton").textContent = "Hide Advanced Options"
else
$("advancedOptions").style.display = "none"
- $("advancedOptionsButton").innerHTML = "Show Advanced Options"
+ $("advancedOptionsButton").textContent = "Show Advanced Options"
maintainAdvancedOptions()
toggleAdvancedOptions = (event) ->
@@ -237,9 +245,10 @@ initOptionsPage = ->
HelpDialog.toggle showAllCommandDetails: true
saveOptions = ->
+ $("linkHintCharacters").value = $("linkHintCharacters").value.toLowerCase()
Option.saveOptions()
$("saveOptions").disabled = true
- $("saveOptions").innerHTML = "No Changes"
+ $("saveOptions").textContent = "Saved"
$("saveOptions").addEventListener "click", saveOptions
$("advancedOptionsButton").addEventListener "click", toggleAdvancedOptions
@@ -248,7 +257,7 @@ initOptionsPage = ->
for element in document.getElementsByClassName "nonEmptyTextOption"
element.className = element.className + " example info"
- element.innerHTML = "Leave empty to reset this option."
+ element.textContent = "Leave empty to reset this option."
window.onbeforeunload = -> "You have unsaved changes to options." unless $("saveOptions").disabled
@@ -264,10 +273,30 @@ initOptionsPage = ->
maintainLinkHintsView()
initPopupPage = ->
- chrome.tabs.getSelected null, (tab) ->
+ chrome.tabs.query { active: true, currentWindow: true }, ([tab]) ->
exclusions = null
document.getElementById("optionsLink").setAttribute "href", chrome.runtime.getURL("pages/options.html")
+ tabPorts = chrome.extension.getBackgroundPage().portsForTab[tab.id]
+ unless tabPorts and Object.keys(tabPorts).length > 0
+ # The browser has disabled Vimium on this page. Place a message explaining this into the popup.
+ document.body.innerHTML = """
+ <div style="width: 400px; margin: 5px;">
+ <p style="margin-bottom: 5px;">
+ Vimium is not running on this page.
+ </p>
+ <p style="margin-bottom: 5px;">
+ Your browser does not run web extensions like Vimium on certain pages,
+ usually for security reasons.
+ </p>
+ <p>
+ Unless your browser's developers change their policy, then unfortunately it is not possible to make Vimium (or any other
+ web extension, for that matter) work on this page.
+ </p>
+ </div>
+ """
+ return
+
# As the active URL, we choose the most recently registered URL from a frame in the tab, or the tab's own
# URL.
url = chrome.extension.getBackgroundPage().urlForTab[tab.id] || tab.url
@@ -285,12 +314,12 @@ initPopupPage = ->
onUpdated = ->
$("helpText").innerHTML = "Type <strong>Ctrl-Enter</strong> to save and close."
$("saveOptions").removeAttribute "disabled"
- $("saveOptions").innerHTML = "Save Changes"
+ $("saveOptions").textContent = "Save Changes"
updateState() if exclusions
saveOptions = ->
Option.saveOptions()
- $("saveOptions").innerHTML = "Saved"
+ $("saveOptions").textContent = "Saved"
$("saveOptions").disabled = true
$("saveOptions").addEventListener "click", saveOptions
@@ -309,6 +338,7 @@ initPopupPage = ->
#
# Initialization.
document.addEventListener "DOMContentLoaded", ->
+ DomUtils.injectUserCss() # Manually inject custom user styles.
xhr = new XMLHttpRequest()
xhr.open 'GET', chrome.extension.getURL('pages/exclusions.html'), true
xhr.onreadystatechange = ->
@@ -320,6 +350,55 @@ document.addEventListener "DOMContentLoaded", ->
xhr.send()
+#
+# Backup and restore. "?" is for the tests."
+DomUtils?.documentReady ->
+ # Only initialize backup/restore on the options page (not the popup).
+ return unless location.pathname == "/pages/options.html"
+
+ restoreSettingsVersion = null
+
+ populateBackupLinkUrl = ->
+ backup = settingsVersion: bgSettings.get "settingsVersion"
+ for option in Option.all
+ backup[option.field] = option.readValueFromElement()
+ # Create the blob in the background page so it isn't garbage collected when the page closes in FF.
+ bgWin = chrome.extension.getBackgroundPage()
+ blob = new bgWin.Blob [ JSON.stringify backup, null, 2 ]
+ $("backupLink").href = bgWin.URL.createObjectURL blob
+
+ $("backupLink").addEventListener "mousedown", populateBackupLinkUrl, true
+
+ $("chooseFile").addEventListener "change", (event) ->
+ document.activeElement?.blur()
+ files = event.target.files
+ if files.length == 1
+ file = files[0]
+ reader = new FileReader
+ reader.readAsText file
+ reader.onload = (event) ->
+ try
+ backup = JSON.parse reader.result
+ catch
+ alert "Failed to parse Vimium backup."
+ return
+
+ restoreSettingsVersion = backup["settingsVersion"] if "settingsVersion" of backup
+ for option in Option.all
+ if option.field of backup
+ option.populateElement backup[option.field]
+ option.onUpdated()
+
+ Option.onSave ->
+ # If we're restoring a backup, then restore the backed up settingsVersion.
+ if restoreSettingsVersion?
+ bgSettings.set "settingsVersion", restoreSettingsVersion
+ restoreSettingsVersion = null
+ # Reset the restore-backup input.
+ $("chooseFile").value = ""
+ # We need to apply migrations in case we are restoring an old backup.
+ bgSettings.applyMigrations()
+
# Exported for tests.
root = exports ? window
extend root, {Options, isVimiumOptionsPage: true}
diff --git a/pages/options.css b/pages/options.css
index 490ae164..dab342a3 100644
--- a/pages/options.css
+++ b/pages/options.css
@@ -115,7 +115,7 @@ input#scrollStepSize {
textarea#userDefinedLinkHintCss, textarea#keyMappings, textarea#searchEngines {
width: 100%;;
min-height: 140px;
- white-space: nowrap;
+ white-space: pre;
}
input#previousPatterns, input#nextPatterns {
width: 100%;
@@ -227,7 +227,10 @@ input.pattern, input.passKeys, .exclusionHeaderText {
#saveOptionsTableData {
float: right;
}
-#saveOptions {
+#saveOptions, #exclusionAddButton {
white-space: nowrap;
width: 110px;
}
+#backupLink {
+ cursor: pointer;
+}
diff --git a/pages/options.html b/pages/options.html
index 92bed6f0..b118bbd9 100644
--- a/pages/options.html
+++ b/pages/options.html
@@ -2,7 +2,28 @@
<head>
<title>Vimium Options</title>
<link rel="stylesheet" type="text/css" href="options.css">
- <link rel="import" href="vimium_resources.html">
+ <script src="../lib/utils.js"></script>
+ <script src="../lib/keyboard_utils.js"></script>
+ <script src="../lib/dom_utils.js"></script>
+ <script src="../lib/rect.js"></script>
+ <script src="../lib/handler_stack.js"></script>
+ <script src="../lib/settings.js"></script>
+ <script src="../lib/find_mode_history.js"></script>
+ <script src="../content_scripts/mode.js"></script>
+ <script src="../content_scripts/ui_component.js"></script>
+ <script src="../content_scripts/link_hints.js"></script>
+ <script src="../content_scripts/vomnibar.js"></script>
+ <script src="../content_scripts/scroller.js"></script>
+ <script src="../content_scripts/marks.js"></script>
+ <script src="../content_scripts/mode_insert.js"></script>
+ <script src="../content_scripts/mode_find.js"></script>
+ <script src="../content_scripts/mode_key_handler.js"></script>
+ <script src="../content_scripts/mode_visual.js"></script>
+ <script src="../content_scripts/hud.js"></script>
+ <script src="../content_scripts/mode_normal.js"></script>
+ <script src="../content_scripts/vimium_frontend.js"></script>
+ <link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" />
+
<script type="text/javascript" src="options.js"></script>
</head>
@@ -46,7 +67,7 @@ unmapAll
<a href="#" id="showCommands">Show available commands</a>.
</div>
</div>
- <textarea id="keyMappings" type="text"></textarea>
+ <textarea id="keyMappings" type="text" tabIndex="1"></textarea>
</td>
</tr>
<tr>
@@ -95,11 +116,11 @@ b: http://b.com/?q=%s description
</td>
</tr>
<tr id="linkHintNumbersContainer">
- <td class="caption">Numbers used<br/> for link hints</td>
+ <td class="caption">Characters used<br/> for link hints</td>
<td verticalAlign="top">
<div class="help">
<div class="example">
- The numbers placed next to each link after typing "f" to enter link-hint mode.
+ The characters placed next to each link after typing "f" to enter link-hint mode.
</div>
</div>
<input id="linkHintNumbers" type="text" />
@@ -125,7 +146,7 @@ b: http://b.com/?q=%s description
</div>
<label>
<input id="filterLinkHints" type="checkbox"/>
- Use the link's name and numbers for link-hint filtering
+ Use the link's name and characters for link-hint filtering
</label>
</td>
</tr>
@@ -182,7 +203,22 @@ b: http://b.com/?q=%s description
</div>
<label>
<input id="regexFindMode" type="checkbox"/>
- Treat find queries as regular expressions
+ Treat find queries as JavaScript regular expressions
+ </label>
+ </td>
+ </tr>
+ <tr>
+ <td class="caption"></td>
+ <td verticalAlign="top" class="booleanOption">
+ <div class="help">
+ <div class="example">
+ This forces the use of <code>en-US</code> QWERTY layout and
+ can be helpful for non-Latin keyboards.
+ </div>
+ </div>
+ <label>
+ <input id="ignoreKeyboardLayout" type="checkbox"/>
+ Ignore keyboard layout
</label>
</td>
</tr>
@@ -228,7 +264,7 @@ b: http://b.com/?q=%s description
<td verticalAlign="top">
<div class="help">
<div class="example">
- The search engine to use in the Vomnibar <br> (e.g.: "http://duckduckgo.com/?q=").
+ The search engine to use in the Vomnibar <br> (e.g.: "https://duckduckgo.com/?q=").
</div>
</div>
<input id="searchUrl" type="text" />
@@ -236,11 +272,12 @@ b: http://b.com/?q=%s description
</td>
</tr>
<tr>
- <td class="caption">CSS for link hints</td>
+ <td class="caption">CSS for Vimium UI</td>
<td verticalAlign="top">
<div class="help">
<div class="example">
- The CSS used to style the characters next to each link hint.<br/><br/>
+ These styles are applied to link hints, the Vomnibar, the help dialog, the exclusions pop-up and the HUD.<br />
+ By default, this CSS is used to style the characters next to each link hint.<br/><br/>
These styles are used in addition to and take precedence over Vimium's
default styles.
</div>
@@ -280,6 +317,33 @@ b: http://b.com/?q=%s description
</tr>
-->
</tbody>
+ <tbody id='backupAndRestore'>
+ <tr>
+ <td colspan="2"><header>Backup and Restore</header></td>
+ </tr>
+ <tr>
+ <td class="caption">Backup</td>
+ <td>
+ <div class="help">
+ <div class="example">
+ Click to backup your settings, or right-click and select <i>Save As</i>.
+ </div>
+ </div>
+ <a id="backupLink" download="vimium-options.json">Click to download backup</a>
+ </td>
+ </tr>
+ <tr>
+ <td class="caption">Restore</td>
+ <td>
+ <div class="help">
+ <div class="example">
+ Choose a backup file to restore, then click <i>Save Changes</i>, below, to confirm.
+ </div>
+ </div>
+ <input id="chooseFile" type="file" accept=".json" style="width: 200px;"/>
+ </td>
+ </tr>
+ </tbody>
</table>
</div>
diff --git a/pages/popup.html b/pages/popup.html
index fdf116e5..03c466d6 100644
--- a/pages/popup.html
+++ b/pages/popup.html
@@ -13,7 +13,6 @@
}
#helpText, #stateLine, #state { color: #979ca0; }
- #exclusionAddButton { width: 80px; }
#saveOptions {
margin-top: 5px; /* Match #exclusionAddButton */
@@ -49,6 +48,7 @@
</style>
<script src="../lib/utils.js"></script>
+ <script src="../lib/dom_utils.js"></script>
<script src="../lib/settings.js"></script>
<script src="options.js"></script>
</head>
diff --git a/pages/vimium_resources.html b/pages/vimium_resources.html
deleted file mode 100644
index 2fad22a2..00000000
--- a/pages/vimium_resources.html
+++ /dev/null
@@ -1,24 +0,0 @@
-<!-- All content scripts (and CSS) listed in the manifest must be listed here too.
- These load Vimium on Vimium's internal pages (such as the options page). -->
-
-<script src="/lib/utils.js"></script>
-<script src="/lib/keyboard_utils.js"></script>
-<script src="/lib/dom_utils.js"></script>
-<script src="/lib/rect.js"></script>
-<script src="/lib/handler_stack.js"></script>
-<script src="/lib/settings.js"></script>
-<script src="/lib/find_mode_history.js"></script>
-<script src="/content_scripts/mode.js"></script>
-<script src="/content_scripts/ui_component.js"></script>
-<script src="/content_scripts/link_hints.js"></script>
-<script src="/content_scripts/vomnibar.js"></script>
-<script src="/content_scripts/scroller.js"></script>
-<script src="/content_scripts/marks.js"></script>
-<script src="/content_scripts/mode_insert.js"></script>
-<script src="/content_scripts/mode_find.js"></script>
-<script src="/content_scripts/mode_key_handler.js"></script>
-<script src="/content_scripts/mode_visual.js"></script>
-<script src="/content_scripts/hud.js"></script>
-<script src="/content_scripts/vimium_frontend.js"></script>
-
-<link rel="stylesheet" type="text/css" href="/content_scripts/vimium.css" />
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index 95ef8151..8c790ca8 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -106,17 +106,17 @@ class VomnibarUI
if (KeyboardUtils.isEscape(event))
return "dismiss"
else if (key == "up" ||
- (event.shiftKey && event.keyCode == keyCodes.tab) ||
+ (event.shiftKey && event.key == "Tab") ||
(event.ctrlKey && (key == "k" || key == "p")))
return "up"
- else if (event.keyCode == keyCodes.tab && !event.shiftKey)
+ else if (event.key == "Tab" && !event.shiftKey)
return "tab"
else if (key == "down" ||
(event.ctrlKey && (key == "j" || key == "n")))
return "down"
- else if (event.keyCode == keyCodes.enter)
+ else if (event.key == "Enter")
return "enter"
- else if event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey
+ else if KeyboardUtils.isBackspace event
return "delete"
null
@@ -125,8 +125,7 @@ class VomnibarUI
@lastAction = action = @actionFromKeyEvent event
return true unless action # pass through
- openInNewTab = @forceNewTab ||
- (event.shiftKey || event.ctrlKey || event.altKey || KeyboardUtils.isPrimaryModifierKey(event))
+ openInNewTab = @forceNewTab || event.shiftKey || event.ctrlKey || event.altKey || event.metaKey
if (action == "dismiss")
@hide()
else if action in [ "tab", "down" ]
@@ -336,5 +335,8 @@ UIComponentServer.registerHandler (event) ->
when "hidden" then Vomnibar.onHidden()
when "activate" then Vomnibar.activate event.data
+document.addEventListener "DOMContentLoaded", ->
+ DomUtils.injectUserCss() # Manually inject custom user styles.
+
root = exports ? window
root.Vomnibar = Vomnibar
diff --git a/pages/vomnibar.html b/pages/vomnibar.html
index 87acc081..19736d78 100644
--- a/pages/vomnibar.html
+++ b/pages/vomnibar.html
@@ -2,6 +2,7 @@
<head>
<title>Vomnibar</title>
<script type="text/javascript" src="../lib/utils.js"></script>
+ <script type="text/javascript" src="../lib/settings.js"></script>
<script type="text/javascript" src="../lib/keyboard_utils.js"></script>
<script type="text/javascript" src="../lib/dom_utils.js"></script>
<script type="text/javascript" src="../lib/handler_stack.js"></script>
diff --git a/tests/dom_tests/chrome.coffee b/tests/dom_tests/chrome.coffee
index d4e6930d..1d04b654 100644
--- a/tests/dom_tests/chrome.coffee
+++ b/tests/dom_tests/chrome.coffee
@@ -7,6 +7,8 @@ root.chromeMessages = []
document.hasFocus = -> true
+window.forTrusted = (handler) -> handler
+
fakeManifest =
version: "1.51"
diff --git a/tests/dom_tests/dom_tests.coffee b/tests/dom_tests/dom_tests.coffee
index 9088fe30..6e422d46 100644
--- a/tests/dom_tests/dom_tests.coffee
+++ b/tests/dom_tests/dom_tests.coffee
@@ -1,33 +1,11 @@
window.vimiumDomTestsAreRunning = true
# Install frontend event handlers.
-installListeners()
HUD.init()
Frame.registerFrameId chromeFrameId: 0
-installListener = (element, event, callback) ->
- element.addEventListener event, (-> callback.apply(this, arguments)), true
-
getSelection = ->
- window.getSelection().toString()
-
-# A count of the number of keyboard events received by the page (for the most recently-sent keystroke). E.g.,
-# we expect 3 if the keystroke is passed through (keydown, keypress, keyup), and 0 if it is suppressed.
-pageKeyboardEventCount = 0
-
-sendKeyboardEvent = (key) ->
- pageKeyboardEventCount = 0
- response = window.callPhantom
- request: "keyboard"
- key: key
-
-sendKeyboardEvents = (keys) ->
- sendKeyboardEvent ch for ch in keys.split()
-
-# These listeners receive events after the main frontend listeners, and do not receive suppressed events.
-for type in [ "keydown", "keypress", "keyup" ]
- installListener window, type, (event) ->
- pageKeyboardEventCount += 1
+ window.getSelection().toString()
commandName = commandCount = null
@@ -98,7 +76,7 @@ createGeneralHintTests = (isFilteredMode) ->
assertStartPosition = (element1, element2) ->
assert.equal element1.getClientRects()[0].left, element2.getClientRects()[0].left
assert.equal element1.getClientRects()[0].top, element2.getClientRects()[0].top
- stub document.body, "style", "static"
+ stub document.body.style, "position", "static"
linkHints = activateLinkHintsMode()
hintMarkers = getHintMarkers()
assertStartPosition document.getElementsByTagName("a")[0], hintMarkers[0]
@@ -163,6 +141,16 @@ context "jsaction matching",
linkHints.deactivateMode()
assert.equal 0, hintMarkers.length
+sendKeyboardEvent = (key, type="keydown", extra={}) ->
+ handlerStack.bubbleEvent type, extend extra,
+ type: type
+ key: key
+ preventDefault: ->
+ stopImmediatePropagation: ->
+
+sendKeyboardEvents = (keys) ->
+ sendKeyboardEvent key for key in keys.split ""
+
inputs = []
context "Test link hints for focusing input elements correctly",
@@ -227,16 +215,16 @@ context "Test link hints for changing mode",
should "change mode on shift", ->
assert.equal "curr-tab", @linkHints.mode.name
- sendKeyboardEvent "shift-down"
+ sendKeyboardEvent "Shift", "keydown"
assert.equal "bg-tab", @linkHints.mode.name
- sendKeyboardEvent "shift-up"
+ sendKeyboardEvent "Shift", "keyup"
assert.equal "curr-tab", @linkHints.mode.name
should "change mode on ctrl", ->
assert.equal "curr-tab", @linkHints.mode.name
- sendKeyboardEvent "ctrl-down"
+ sendKeyboardEvent "Control", "keydown"
assert.equal "fg-tab", @linkHints.mode.name
- sendKeyboardEvent "ctrl-up"
+ sendKeyboardEvent "Control", "keyup"
assert.equal "curr-tab", @linkHints.mode.name
context "Alphabetical link hints",
@@ -247,6 +235,7 @@ context "Alphabetical link hints",
stubSettings "linkHintCharacters", "ab"
stub window, "windowIsFocused", -> true
+ document.getElementById("test-div").innerHTML = ""
# Three hints will trigger double hint chars.
createLinks 3
@linkHints = activateLinkHintsMode()
@@ -258,12 +247,13 @@ context "Alphabetical link hints",
should "label the hints correctly", ->
hintMarkers = getHintMarkers()
expectedHints = ["aa", "b", "ab"]
+ assert.equal 3, hintMarkers.length
for hint, i in expectedHints
assert.equal hint, hintMarkers[i].hintString
should "narrow the hints", ->
hintMarkers = getHintMarkers()
- sendKeyboardEvent "A"
+ sendKeyboardEvent "a"
assert.equal "none", hintMarkers[1].style.display
assert.equal "", hintMarkers[0].style.display
@@ -314,24 +304,25 @@ context "Filtered link hints",
should "narrow the hints", ->
hintMarkers = getHintMarkers()
- sendKeyboardEvent "T"
- sendKeyboardEvent "R"
+ sendKeyboardEvent "t"
+ sendKeyboardEvent "r"
assert.equal "none", hintMarkers[0].style.display
assert.equal "3", hintMarkers[1].hintString
assert.equal "", hintMarkers[1].style.display
- sendKeyboardEvent "A"
+ sendKeyboardEvent "a"
assert.equal "1", hintMarkers[3].hintString
- # This test is the same as above, but with an extra non-matching character.
+ # This test is the same as above, but with an extra non-matching character. The effect should be the
+ # same.
should "narrow the hints and ignore typing mistakes", ->
hintMarkers = getHintMarkers()
- sendKeyboardEvent "T"
- sendKeyboardEvent "R"
- sendKeyboardEvent "X"
+ sendKeyboardEvent "t"
+ sendKeyboardEvent "r"
+ sendKeyboardEvent "x"
assert.equal "none", hintMarkers[0].style.display
assert.equal "3", hintMarkers[1].hintString
assert.equal "", hintMarkers[1].style.display
- sendKeyboardEvent "A"
+ sendKeyboardEvent "a"
assert.equal "1", hintMarkers[3].hintString
context "Image hints",
@@ -428,9 +419,9 @@ context "Filtered link hints",
should "use tab to select the active hint", ->
sendKeyboardEvents "abc"
assert.equal "8", @getActiveHintMarker()
- sendKeyboardEvent "tab"
+ sendKeyboardEvent "Tab", "keydown"
assert.equal "7", @getActiveHintMarker()
- sendKeyboardEvent "tab"
+ sendKeyboardEvent "Tab", "keydown"
assert.equal "9", @getActiveHintMarker()
context "Input focus",
@@ -445,29 +436,29 @@ context "Input focus",
document.getElementById("test-div").innerHTML = ""
should "focus the first element", ->
- focusInput 1
+ NormalModeCommands.focusInput 1
assert.equal "first", document.activeElement.id
should "focus the nth element", ->
- focusInput 100
+ NormalModeCommands.focusInput 100
assert.equal "third", document.activeElement.id
should "activate insert mode on the first element", ->
- focusInput 1
+ NormalModeCommands.focusInput 1
assert.isTrue InsertMode.permanentInstance.isActive()
should "activate insert mode on the first element", ->
- focusInput 100
+ NormalModeCommands.focusInput 100
assert.isTrue InsertMode.permanentInstance.isActive()
should "activate the most recently-selected input if the count is 1", ->
- focusInput 3
- focusInput 1
+ NormalModeCommands.focusInput 3
+ NormalModeCommands.focusInput 1
assert.equal "third", document.activeElement.id
should "not trigger insert if there are no inputs", ->
document.getElementById("test-div").innerHTML = ""
- focusInput 1
+ NormalModeCommands.focusInput 1
assert.isFalse InsertMode.permanentInstance.isActive()
# TODO: these find prev/next link tests could be refactored into unit tests which invoke a function which has
@@ -488,7 +479,7 @@ context "Find prev / next links",
<a href='#second'>next page</a>
"""
stubSettings "nextPatterns", "next"
- goNext()
+ NormalModeCommands.goNext()
assert.equal '#second', window.location.hash
should "match against non-word patterns", ->
@@ -496,7 +487,7 @@ context "Find prev / next links",
<a href='#first'>&gt;&gt;</a>
"""
stubSettings "nextPatterns", ">>"
- goNext()
+ NormalModeCommands.goNext()
assert.equal '#first', window.location.hash
should "favor matches with fewer words", ->
@@ -505,14 +496,14 @@ context "Find prev / next links",
<a href='#second'>next!</a>
"""
stubSettings "nextPatterns", "next"
- goNext()
+ NormalModeCommands.goNext()
assert.equal '#second', window.location.hash
should "find link relation in header", ->
document.getElementById("test-div").innerHTML = """
<link rel='next' href='#first'>
"""
- goNext()
+ NormalModeCommands.goNext()
assert.equal '#first', window.location.hash
should "favor link relation to text matching", ->
@@ -520,14 +511,14 @@ context "Find prev / next links",
<link rel='next' href='#first'>
<a href='#second'>next</a>
"""
- goNext()
+ NormalModeCommands.goNext()
assert.equal '#first', window.location.hash
should "match mixed case link relation", ->
document.getElementById("test-div").innerHTML = """
<link rel='Next' href='#first'>
"""
- goNext()
+ NormalModeCommands.goNext()
assert.equal '#first', window.location.hash
createLinks = (n) ->
@@ -576,93 +567,55 @@ context "Key mapping",
should "set and call command handler", ->
sendKeyboardEvent "m"
assert.isTrue @handlerCalled
- assert.equal 0, pageKeyboardEventCount
should "not call command handler for pass keys", ->
sendKeyboardEvent "p"
assert.isFalse @handlerCalled
- assert.equal 3, pageKeyboardEventCount
should "accept a count prefix with a single digit", ->
sendKeyboardEvent "2"
sendKeyboardEvent "m"
assert.equal 2, @handlerCalledCount
- assert.equal 0, pageKeyboardEventCount
should "accept a count prefix with multiple digits", ->
sendKeyboardEvent "2"
sendKeyboardEvent "0"
sendKeyboardEvent "m"
assert.equal 20, @handlerCalledCount
- assert.equal 0, pageKeyboardEventCount
should "cancel a count prefix", ->
sendKeyboardEvent "2"
sendKeyboardEvent "z"
sendKeyboardEvent "m"
assert.equal 1, @handlerCalledCount
- assert.equal 0, pageKeyboardEventCount
should "accept a count prefix for multi-key command mappings", ->
- sendKeyboardEvent "2"
+ sendKeyboardEvent "5"
sendKeyboardEvent "z"
sendKeyboardEvent "p"
- assert.equal 2, @handlerCalledCount
- assert.equal 0, pageKeyboardEventCount
+ assert.equal 5, @handlerCalledCount
should "cancel a key prefix", ->
sendKeyboardEvent "z"
sendKeyboardEvent "m"
assert.equal 1, @handlerCalledCount
- assert.equal 0, pageKeyboardEventCount
should "cancel a count prefix after a prefix key", ->
sendKeyboardEvent "2"
sendKeyboardEvent "z"
sendKeyboardEvent "m"
assert.equal 1, @handlerCalledCount
- assert.equal 0, pageKeyboardEventCount
should "cancel a prefix key on escape", ->
sendKeyboardEvent "z"
- sendKeyboardEvent "escape"
+ sendKeyboardEvent "Escape", "keydown"
sendKeyboardEvent "p"
assert.equal 0, @handlerCalledCount
- should "not handle escape on its own", ->
- sendKeyboardEvent "escape"
- assert.equal 2, pageKeyboardEventCount
-
context "Normal mode",
setup ->
initializeModeState()
- should "suppress mapped keys", ->
- sendKeyboardEvent "m"
- assert.equal 0, pageKeyboardEventCount
-
- should "not suppress unmapped keys", ->
- sendKeyboardEvent "u"
- assert.equal 3, pageKeyboardEventCount
-
- should "not suppress escape", ->
- sendKeyboardEvent "escape"
- assert.equal 2, pageKeyboardEventCount
-
- should "not suppress passKeys", ->
- sendKeyboardEvent "p"
- assert.equal 3, pageKeyboardEventCount
-
- should "suppress passKeys with a non-empty key state (a count)", ->
- sendKeyboardEvent "5"
- sendKeyboardEvent "p"
- assert.equal 0, pageKeyboardEventCount
-
- should "suppress passKeys with a non-empty key state (a key)", ->
- sendKeyboardEvent "z"
- sendKeyboardEvent "p"
- assert.equal 0, pageKeyboardEventCount
-
should "invoke commands for mapped keys", ->
sendKeyboardEvent "m"
assert.equal "m", commandName
@@ -706,7 +659,7 @@ context "Normal mode",
assert.equal 2, commandCount
should "accept count prefixes of length 2", ->
- sendKeyboardEvent "12"
+ sendKeyboardEvents "12"
sendKeyboardEvent "m"
assert.equal 12, commandCount
@@ -763,19 +716,16 @@ context "Insert mode",
initializeModeState()
@insertMode = new InsertMode global: true
- should "not suppress mapped keys in insert mode", ->
- sendKeyboardEvent "m"
- assert.equal 3, pageKeyboardEventCount
-
should "exit on escape", ->
assert.isTrue @insertMode.modeIsActive
- sendKeyboardEvent "escape"
+ sendKeyboardEvent "Escape", "keydown"
assert.isFalse @insertMode.modeIsActive
should "resume normal mode after leaving insert mode", ->
+ assert.equal null, commandCount
@insertMode.exit()
sendKeyboardEvent "m"
- assert.equal 0, pageKeyboardEventCount
+ assert.equal 1, commandCount
context "Triggering insert mode",
setup ->
@@ -833,7 +783,7 @@ context "Caret mode",
assert.equal "I", getSelection()
should "exit caret mode on escape", ->
- sendKeyboardEvent "escape"
+ sendKeyboardEvent "Escape", "keydown"
assert.equal "", getSelection()
should "move caret with l and h", ->
@@ -868,7 +818,7 @@ context "Caret mode",
assert.equal "I", getSelection()
sendKeyboardEvents "ww"
assert.equal "a", getSelection()
- sendKeyboardEvent "escape"
+ sendKeyboardEvent "Escape", "keydown"
new VisualMode
assert.equal "a", getSelection()
@@ -983,16 +933,14 @@ context "Mode utilities",
test = new Mode exitOnEscape: true
assert.isTrue test.modeIsActive
- sendKeyboardEvent "escape"
- assert.equal 0, pageKeyboardEventCount
+ sendKeyboardEvent "Escape", "keydown"
assert.isFalse test.modeIsActive
should "not exit on escape if not enabled", ->
test = new Mode exitOnEscape: false
assert.isTrue test.modeIsActive
- sendKeyboardEvent "escape"
- assert.equal 2, pageKeyboardEventCount
+ sendKeyboardEvent "Escape", "keydown"
assert.isTrue test.modeIsActive
should "exit on blur", ->
@@ -1031,21 +979,21 @@ context "PostFindMode",
assert.isFalse @postFindMode.modeIsActive
should "suppress unmapped printable keys", ->
- sendKeyboardEvent "m"
- assert.equal 0, pageKeyboardEventCount
+ sendKeyboardEvent "a"
+ assert.equal null, commandCount
should "be deactivated on click events", ->
handlerStack.bubbleEvent "click", target: document.activeElement
assert.isFalse @postFindMode.modeIsActive
should "enter insert mode on immediate escape", ->
- sendKeyboardEvent "escape"
- assert.equal 0, pageKeyboardEventCount
+ sendKeyboardEvent "Escape", "keydown"
+ assert.equal null, commandCount
assert.isFalse @postFindMode.modeIsActive
should "not enter insert mode on subsequent escapes", ->
sendKeyboardEvent "a"
- sendKeyboardEvent "escape"
+ sendKeyboardEvent "Escape", "keydown"
assert.isTrue @postFindMode.modeIsActive
context "WaitForEnter",
@@ -1057,14 +1005,14 @@ context "WaitForEnter",
should "exit with success on Enter", ->
assert.isTrue @waitForEnter.modeIsActive
assert.isFalse @isSuccess?
- sendKeyboardEvent "enter"
+ sendKeyboardEvent "Enter", "keydown"
assert.isFalse @waitForEnter.modeIsActive
assert.isTrue @isSuccess? and @isSuccess == true
should "exit without success on Escape", ->
assert.isTrue @waitForEnter.modeIsActive
assert.isFalse @isSuccess?
- sendKeyboardEvent "escape"
+ sendKeyboardEvent "Escape", "keydown"
assert.isFalse @waitForEnter.modeIsActive
assert.isTrue @isSuccess? and @isSuccess == false
@@ -1075,17 +1023,6 @@ context "WaitForEnter",
assert.isTrue @waitForEnter.modeIsActive
assert.isFalse @isSuccess?
-context "SuppressAllKeyboardEvents",
- setup ->
- initializeModeState()
-
- should "supress keyboard events", ->
- sendKeyboardEvent "a"
- assert.equal 3, pageKeyboardEventCount
- new SuppressAllKeyboardEvents
- sendKeyboardEvent "a"
- assert.equal 0, pageKeyboardEventCount
-
context "GrabBackFocus",
setup ->
testContent = "<input type='text' value='some value' id='input'/>"
diff --git a/tests/dom_tests/dom_tests.html b/tests/dom_tests/dom_tests.html
index d2e795d1..37cd43e3 100644
--- a/tests/dom_tests/dom_tests.html
+++ b/tests/dom_tests/dom_tests.html
@@ -49,6 +49,7 @@
<script type="text/javascript" src="../../content_scripts/mode_key_handler.js"></script>
<script type="text/javascript" src="../../content_scripts/mode_visual.js"></script>
<script type="text/javascript" src="../../content_scripts/hud.js"></script>
+ <script type="text/javascript" src="../../content_scripts/mode_normal.js"></script>
<script type="text/javascript" src="../../content_scripts/vimium_frontend.js"></script>
<script type="text/javascript" src="../shoulda.js/shoulda.js"></script>
diff --git a/tests/dom_tests/phantom_runner.coffee b/tests/dom_tests/phantom_runner.coffee
index 09d7d584..b91919bb 100644
--- a/tests/dom_tests/phantom_runner.coffee
+++ b/tests/dom_tests/phantom_runner.coffee
@@ -21,30 +21,6 @@ page.onError = (msg, trace) ->
page.onResourceError = (resourceError) ->
console.log(resourceError.errorString)
-page.onCallback = (request) ->
- switch request.request
- when "keyboard"
- switch request.key
- when "escape"
- page.sendEvent "keydown", page.event.key.Escape
- page.sendEvent "keyup", page.event.key.Escape
- when "enter"
- page.sendEvent "keydown", page.event.key.Enter
- page.sendEvent "keyup", page.event.key.Enter
- when "tab"
- page.sendEvent "keydown", page.event.key.Tab
- page.sendEvent "keyup", page.event.key.Tab
- when "shift-down"
- page.sendEvent "keydown", page.event.key.Shift
- when "shift-up"
- page.sendEvent "keyup", page.event.key.Shift
- when "ctrl-down"
- page.sendEvent "keydown", page.event.key.Control
- when "ctrl-up"
- page.sendEvent "keyup", page.event.key.Control
- else
- page.sendEvent "keypress", request.key
-
testfile = path.join(path.dirname(system.args[0]), 'dom_tests.html')
page.open testfile, (status) ->
if status != 'success'
diff --git a/tests/unit_tests/commands_test.coffee b/tests/unit_tests/commands_test.coffee
index 0e0be1d6..49dd2570 100644
--- a/tests/unit_tests/commands_test.coffee
+++ b/tests/unit_tests/commands_test.coffee
@@ -4,6 +4,14 @@ extend global, require "../../background_scripts/bg_utils.js"
global.Settings = {postUpdateHooks: {}, get: (-> ""), set: ->}
{Commands} = require "../../background_scripts/commands.js"
+# Include mode_normal to check that all commands have been implemented.
+global.KeyHandlerMode = global.Mode = {}
+global.KeyboardUtils = {platform: ""}
+extend global, require "../../content_scripts/link_hints.js"
+extend global, require "../../content_scripts/marks.js"
+extend global, require "../../content_scripts/vomnibar.js"
+{NormalModeCommands} = require "../../content_scripts/mode_normal.js"
+
context "Key mappings",
setup ->
@testKeySequence = (key, expectedKeyText, expectedKeyLength) ->
@@ -114,6 +122,14 @@ context "Parse commands",
assert.equal "a", BgUtils.parseLines(" a \n b")[0]
assert.equal "b", BgUtils.parseLines(" a \n b")[1]
-# TODO (smblott) More tests:
-# - Ensure each background command has an implmentation in BackgroundCommands
-# - Ensure each foreground command has an implmentation in vimium_frontent.coffee
+context "Commands implemented",
+ (for own command, options of Commands.availableCommands
+ do (command, options) ->
+ if options.background
+ should "#{command} (background command)", ->
+ # TODO: Import background_scripts/main.js and expose BackgroundCommands from there.
+ # assert.isTrue BackgroundCommands[command]
+ else
+ should "#{command} (foreground command)", ->
+ assert.isTrue NormalModeCommands[command]
+ )...
diff --git a/tests/unit_tests/exclusion_test.coffee b/tests/unit_tests/exclusion_test.coffee
index f53d23f6..06e0a51f 100644
--- a/tests/unit_tests/exclusion_test.coffee
+++ b/tests/unit_tests/exclusion_test.coffee
@@ -29,7 +29,7 @@ isEnabledForUrl = (request) ->
context "Excluded URLs and pass keys",
setup ->
- Exclusions.postUpdateHook(
+ Settings.set "exclusionRules",
[
{ pattern: "http*://mail.google.com/*", passKeys: "" }
{ pattern: "http*://www.facebook.com/*", passKeys: "abab" }
@@ -39,7 +39,8 @@ context "Excluded URLs and pass keys",
{ pattern: "http*://www.example.com/*", passKeys: "a bb c bba a" }
{ pattern: "http*://www.duplicate.com/*", passKeys: "ace" }
{ pattern: "http*://www.duplicate.com/*", passKeys: "bdf" }
- ])
+ ]
+ Exclusions.postUpdateHook()
should "be disabled for excluded sites", ->
rule = isEnabledForUrl({ url: 'http://mail.google.com/calendar/page' })
diff --git a/tests/unit_tests/handler_stack_test.coffee b/tests/unit_tests/handler_stack_test.coffee
index 7b62af07..374c235b 100644
--- a/tests/unit_tests/handler_stack_test.coffee
+++ b/tests/unit_tests/handler_stack_test.coffee
@@ -4,6 +4,7 @@ extend(global, require "../../lib/handler_stack.js")
context "handlerStack",
setup ->
stub global, "DomUtils", {}
+ stub DomUtils, "consumeKeyup", ->
stub DomUtils, "suppressEvent", ->
stub DomUtils, "suppressPropagation", ->
@handlerStack = new HandlerStack
diff --git a/tests/unit_tests/rect_test.coffee b/tests/unit_tests/rect_test.coffee
index 0773dbcf..5054e029 100644
--- a/tests/unit_tests/rect_test.coffee
+++ b/tests/unit_tests/rect_test.coffee
@@ -201,7 +201,7 @@ context "Rect subtraction",
subtractRect = Rect.create x, y, (x + width), (y + height)
resultRects = Rect.subtract rect, subtractRect
for resultRect in resultRects
- assert.isFalse Rect.contains subtractRect, resultRect
+ assert.isFalse Rect.intersects subtractRect, resultRect
should "be contained in original rect", ->
rect = Rect.create 0, 0, 3, 3
@@ -212,7 +212,7 @@ context "Rect subtraction",
subtractRect = Rect.create x, y, (x + width), (y + height)
resultRects = Rect.subtract rect, subtractRect
for resultRect in resultRects
- assert.isTrue Rect.contains rect, resultRect
+ assert.isTrue Rect.intersects rect, resultRect
should "contain the subtracted rect in the original minus the results", ->
rect = Rect.create 0, 0, 3, 3
@@ -229,60 +229,60 @@ context "Rect subtraction",
assert.isTrue (resultComplement.length == 0 or resultComplement.length == 1)
if resultComplement.length == 1
complementRect = resultComplement[0]
- assert.isTrue Rect.contains subtractRect, complementRect
+ assert.isTrue Rect.intersects subtractRect, complementRect
context "Rect overlaps",
should "detect that a rect overlaps itself", ->
rect = Rect.create 2, 2, 4, 4
- assert.isTrue Rect.rectsOverlap rect, rect
+ assert.isTrue Rect.intersectsStrict rect, rect
should "detect that non-overlapping rectangles do not overlap on the left", ->
rect1 = Rect.create 2, 2, 4, 4
rect2 = Rect.create 0, 2, 1, 4
- assert.isFalse Rect.rectsOverlap rect1, rect2
+ assert.isFalse Rect.intersectsStrict rect1, rect2
should "detect that non-overlapping rectangles do not overlap on the right", ->
rect1 = Rect.create 2, 2, 4, 4
rect2 = Rect.create 5, 2, 6, 4
- assert.isFalse Rect.rectsOverlap rect1, rect2
+ assert.isFalse Rect.intersectsStrict rect1, rect2
should "detect that non-overlapping rectangles do not overlap on the top", ->
rect1 = Rect.create 2, 2, 4, 4
rect2 = Rect.create 2, 0, 2, 1
- assert.isFalse Rect.rectsOverlap rect1, rect2
+ assert.isFalse Rect.intersectsStrict rect1, rect2
should "detect that non-overlapping rectangles do not overlap on the bottom", ->
rect1 = Rect.create 2, 2, 4, 4
rect2 = Rect.create 2, 5, 2, 6
- assert.isFalse Rect.rectsOverlap rect1, rect2
+ assert.isFalse Rect.intersectsStrict rect1, rect2
should "detect overlapping rectangles on the left", ->
rect1 = Rect.create 2, 2, 4, 4
rect2 = Rect.create 0, 2, 2, 4
- assert.isTrue Rect.rectsOverlap rect1, rect2
+ assert.isTrue Rect.intersectsStrict rect1, rect2
should "detect overlapping rectangles on the right", ->
rect1 = Rect.create 2, 2, 4, 4
rect2 = Rect.create 4, 2, 5, 4
- assert.isTrue Rect.rectsOverlap rect1, rect2
+ assert.isTrue Rect.intersectsStrict rect1, rect2
should "detect overlapping rectangles on the top", ->
rect1 = Rect.create 2, 2, 4, 4
rect2 = Rect.create 2, 4, 4, 5
- assert.isTrue Rect.rectsOverlap rect1, rect2
+ assert.isTrue Rect.intersectsStrict rect1, rect2
should "detect overlapping rectangles on the bottom", ->
rect1 = Rect.create 2, 2, 4, 4
rect2 = Rect.create 2, 0, 4, 2
- assert.isTrue Rect.rectsOverlap rect1, rect2
+ assert.isTrue Rect.intersectsStrict rect1, rect2
should "detect overlapping rectangles when second rectangle is contained in first", ->
rect1 = Rect.create 1, 1, 4, 4
rect2 = Rect.create 2, 2, 3, 3
- assert.isTrue Rect.rectsOverlap rect1, rect2
+ assert.isTrue Rect.intersectsStrict rect1, rect2
should "detect overlapping rectangles when first rectangle is contained in second", ->
rect1 = Rect.create 1, 1, 4, 4
rect2 = Rect.create 2, 2, 3, 3
- assert.isTrue Rect.rectsOverlap rect2, rect1
+ assert.isTrue Rect.intersectsStrict rect2, rect1
diff --git a/tests/unit_tests/test_chrome_stubs.coffee b/tests/unit_tests/test_chrome_stubs.coffee
index bea13df3..44ec4f66 100644
--- a/tests/unit_tests/test_chrome_stubs.coffee
+++ b/tests/unit_tests/test_chrome_stubs.coffee
@@ -41,8 +41,6 @@ exports.chrome =
getViews: -> []
tabs:
- onSelectionChanged:
- addListener: () -> true
onUpdated:
addListener: () -> true
onAttached:
@@ -51,8 +49,6 @@ exports.chrome =
addListener: () -> true
onRemoved:
addListener: () -> true
- onActiveChanged:
- addListener: () -> true
onActivated:
addListener: () -> true
onReplaced:
@@ -64,6 +60,8 @@ exports.chrome =
addListener: () ->
onReferenceFragmentUpdated:
addListener: () ->
+ onCommitted:
+ addListener: () ->
windows:
onRemoved:
diff --git a/tests/unit_tests/utils_test.coffee b/tests/unit_tests/utils_test.coffee
index 04cf34b6..2794a6d7 100644
--- a/tests/unit_tests/utils_test.coffee
+++ b/tests/unit_tests/utils_test.coffee
@@ -22,6 +22,9 @@ context "isUrl",
assert.isTrue Utils.isUrl "illinois.state.museum"
assert.isTrue Utils.isUrl "eqt5g4fuenphqinx.onion"
+ # Internal URLs.
+ assert.isTrue Utils.isUrl "moz-extension://c66906b4-3785-4a60-97bc-094a6366017e/pages/options.html"
+
should "reject invalid URLs", ->
assert.isFalse Utils.isUrl "a.x"
assert.isFalse Utils.isUrl "www-domain-tld"
@@ -135,26 +138,6 @@ context "distinctCharacters",
should "eliminate duplicate characters", ->
assert.equal "abc", Utils.distinctCharacters "bbabaabbacabbbab"
-context "invokeCommandString",
- setup ->
- @beenCalled = false
- window.singleComponentCommand = => @beenCalled = true
- window.twoComponentCommand = command: window.singleComponentCommand
-
- tearDown ->
- delete window.singleComponentCommand
- delete window.twoComponentCommand
-
- should "invoke single-component commands", ->
- assert.isFalse @beenCalled
- Utils.invokeCommandString "singleComponentCommand"
- assert.isTrue @beenCalled
-
- should "invoke multi-component commands", ->
- assert.isFalse @beenCalled
- Utils.invokeCommandString "twoComponentCommand.command"
- assert.isTrue @beenCalled
-
context "escapeRegexSpecialCharacters",
should "escape regexp special characters", ->
str = "-[]/{}()*+?.^$|"