aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTING.md9
-rw-r--r--Cakefile8
-rw-r--r--README.md27
-rw-r--r--background_scripts/bg_utils.coffee2
-rw-r--r--background_scripts/commands.coffee7
-rw-r--r--background_scripts/main.coffee37
-rw-r--r--background_scripts/marks.coffee6
-rw-r--r--content_scripts/hud.coffee38
-rw-r--r--content_scripts/link_hints.coffee84
-rw-r--r--content_scripts/marks.coffee7
-rw-r--r--content_scripts/mode.coffee18
-rw-r--r--content_scripts/mode_find.coffee66
-rw-r--r--content_scripts/mode_insert.coffee6
-rw-r--r--content_scripts/mode_key_handler.coffee13
-rw-r--r--content_scripts/mode_normal.coffee369
-rw-r--r--content_scripts/mode_visual.coffee5
-rw-r--r--content_scripts/scroller.coffee12
-rw-r--r--content_scripts/ui_component.coffee3
-rw-r--r--content_scripts/vimium_frontend.coffee406
-rw-r--r--content_scripts/vomnibar.coffee3
-rw-r--r--lib/clipboard.coffee12
-rw-r--r--lib/dom_utils.coffee25
-rw-r--r--lib/find_mode_history.coffee3
-rw-r--r--lib/handler_stack.coffee14
-rw-r--r--lib/keyboard_utils.coffee32
-rw-r--r--lib/rect.coffee20
-rw-r--r--lib/settings.coffee7
-rw-r--r--lib/utils.coffee13
-rw-r--r--manifest.json6
-rw-r--r--pages/blank.html1
-rw-r--r--pages/completion_engines.html1
-rw-r--r--pages/help_dialog.coffee2
-rw-r--r--pages/help_dialog.html1
-rw-r--r--pages/hud.coffee14
-rw-r--r--pages/hud.html1
-rw-r--r--pages/logging.html1
-rw-r--r--pages/options.coffee57
-rw-r--r--pages/options.css3
-rw-r--r--pages/options.html35
-rw-r--r--tests/dom_tests/dom_tests.coffee26
-rw-r--r--tests/dom_tests/dom_tests.html1
-rw-r--r--tests/unit_tests/commands_test.coffee22
-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/utils_test.coffee20
45 files changed, 866 insertions, 606 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 61c32456..e4ed8b8b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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
diff --git a/Cakefile b/Cakefile
index a106365a..d188ea12 100644
--- a/Cakefile
+++ b/Cakefile
@@ -77,6 +77,9 @@ task "package", "Builds a zip file for submission to the Chrome store. The outpu
spawn "rsync", rsyncOptions, false, true
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", ->
@@ -157,8 +160,3 @@ task "coverage", "generate coverage report", ->
source: (Utils.escapeHtml fs.readFileSync fname, 'utf-8').split '\n'
fs.writeFileSync 'jscoverage.json', JSON.stringify(result)
-
-task "zip", "build Firefox zip file in ../vimium.zip", ->
- spawn "zip", "-r -FS ../vimium.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+/
-
diff --git a/README.md b/README.md
index 07d0d010..4eef7cb0 100644
--- a/README.md
+++ b/README.md
@@ -141,9 +141,10 @@ 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.
@@ -168,6 +169,26 @@ PRs are welcome.
Release Notes
-------------
+In `master` (not yet released)
+
+- 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 create new normal and incognito windows
+ ([examples](https://github.com/philc/vimium/wiki/Tips-and-Tricks#creating-tabs-with-urls-and-windows)).
+- 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.
+
+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:
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 cda036e6..4d2e1606 100644
--- a/background_scripts/commands.coffee
+++ b/background_scripts/commands.coffee
@@ -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/main.coffee b/background_scripts/main.coffee
index 97d8fa65..8220545d 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -114,7 +114,7 @@ TabOperations =
canUseOpenerTabId = not (Utils.isFirefox() and Utils.compareVersions(Utils.firefoxVersion(), "57") < 0)
tabConfig.openerTabId = request.tab.id if canUseOpenerTabId
- chrome.tabs.create tabConfig, callback
+ chrome.tabs.create tabConfig, -> callback request
# Opens request.url in new window and switches to it.
openUrlInNewWindow: (request, callback = (->)) ->
@@ -148,7 +148,7 @@ toggleMuteTab = do ->
#
selectSpecificTab = (request) ->
chrome.tabs.get(request.id, (tab) ->
- chrome.windows.update(tab.windowId, { focused: true })
+ chrome.windows?.update(tab.windowId, { focused: true })
chrome.tabs.update(request.id, { active: true }))
moveTab = ({count, tab, registryEntry}) ->
@@ -188,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}) ->
@@ -214,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
@@ -324,7 +329,7 @@ Frames =
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
@@ -355,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]...]
@@ -423,7 +428,7 @@ 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
@@ -431,8 +436,6 @@ sendRequestHandlers =
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)
@@ -449,7 +452,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.
diff --git a/background_scripts/marks.coffee b/background_scripts/marks.coffee
index a6491b9e..77b07b41 100644
--- a/background_scripts/marks.coffee
+++ b/background_scripts/marks.coffee
@@ -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 c2170914..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.
@@ -71,17 +73,44 @@ HUD =
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
@@ -127,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 d9ce5d06..d4a95d36 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
@@ -166,7 +166,6 @@ class LinkHintsMode
name: "hint/#{@mode.name}"
indicator: false
singleton: "link-hints-mode"
- passInitialKeyupEvents: true
suppressAllKeyboardEvents: true
suppressTrailingKeyEvents: true
exitOnEscape: true
@@ -233,13 +232,9 @@ class LinkHintsMode
onKeyDownInMode: (event) ->
return if event.repeat
- previousTabCount = @tabCount
- @tabCount = 0
-
# 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
key = event.key
@@ -250,19 +245,16 @@ class LinkHintsMode
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.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 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
@@ -274,15 +266,13 @@ class LinkHintsMode
HintCoordinator.sendMessage "activateActiveHintMarker" if @markerMatcher.activeHintMarker
else if event.key == "Tab"
- @tabCount = previousTabCount + (if event.shiftKey then -1 else 1)
- @updateVisibleMarkers @tabCount
+ if event.shiftKey then @tabCount-- else @tabCount++
+ @updateVisibleMarkers()
else if event.key == " " and @markerMatcher.shouldRotateHints event
- @tabCount = previousTabCount
HintCoordinator.sendMessage "rotateHints"
else
- @tabCount = previousTabCount if event.ctrlKey or event.metaKey or event.altKey
unless event.repeat
keyChar =
if Settings.get "filterLinkHints"
@@ -292,17 +282,18 @@ class LinkHintsMode
if keyChar
keyChar = " " if keyChar == "space"
if keyChar.length == 1
+ @tabCount = 0
@markerMatcher.pushKeyChar keyChar
@updateVisibleMarkers()
- DomUtils.consumeKeyup event
- return
+ else
+ return handlerStack.suppressPropagation
- # We've handled the event, so suppress it and update the mode indicator.
- 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}
@@ -311,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
@@ -322,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
->
@@ -365,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
@@ -391,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
#
@@ -659,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.
@@ -841,6 +836,8 @@ LocalHints =
if labelMap[element.id]
linkText = labelMap[element.id]
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
@@ -897,8 +894,9 @@ class WaitForEnter extends Mode
@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 6eab3be6..ac653a52 100644
--- a/content_scripts/marks.coffee
+++ b/content_scripts/marks.coffee
@@ -52,7 +52,7 @@ Marks =
else
localStorage[@getLocationKey keyChar] = @getMarkString()
@showMessage "Created local mark", keyChar
- DomUtils.consumeKeyup event
+ handlerStack.suppressEvent
activateGotoMode: ->
@mode = new Mode
@@ -82,7 +82,8 @@ Marks =
@showMessage "Jumped to local mark", keyChar
else
@showMessage "Local mark not set", keyChar
- DomUtils.consumeKeyup event
+ 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 9de423ff..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
@@ -82,7 +82,7 @@ class Mode
"keydown": (event) =>
return @continueBubbling unless KeyboardUtils.isEscape event
@exit event, event.target
- DomUtils.consumeKeyup event
+ @suppressEvent
# If @options.exitOnBlur is truthy, then it should be an element. The mode will exit when that element
# loses the focus.
@@ -120,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
@@ -147,7 +137,6 @@ class Mode
name: "suppress-trailing-key-events"
keydown: handler
keypress: handler
- keyup: -> handlerStack.suppressPropagation
Mode.modes.push this
@setIndicator()
@@ -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 5a2da741..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:
@@ -48,7 +48,7 @@ class PostFindMode extends SuppressPrintable
keydown: (event) =>
if KeyboardUtils.isEscape event
@exit()
- DomUtils.consumeKeyup event
+ @suppressEvent
else
handlerStack.remove()
@continueBubbling
@@ -79,9 +79,10 @@ class FindMode extends Mode
exit: (event) ->
super()
- handleEscapeForFindMode() if event
+ FindMode.handleEscape() if event
restoreSelection: ->
+ return unless @initialRange
range = @initialRange
selection = getSelection()
selection.removeAllRanges()
@@ -201,20 +202,71 @@ class FindMode extends Mode
@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 1dc66d52..d2a33091 100644
--- a/content_scripts/mode_insert.coffee
+++ b/content_scripts/mode_insert.coffee
@@ -26,13 +26,12 @@ class InsertMode extends Mode
# An editable element in a shadow DOM is focused; blur it.
@insertModeLock.blur()
@exit event, event.target
- DomUtils.consumeKeyup event
+ @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
@@ -129,6 +128,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 1b3b21e7..cca6b77a 100644
--- a/content_scripts/mode_key_handler.coffee
+++ b/content_scripts/mode_key_handler.coffee
@@ -27,8 +27,6 @@ class KeyHandlerMode extends Mode
super extend options,
keydown: @onKeydown.bind this
- # We cannot track keyup events if we lose the focus.
- blur: (event) => @alwaysContinueBubbling => @keydownEvents = {} if event.target == window
if options.exitOnEscape
# If we're part way through a command's key sequence, then a first Escape should reset the key state,
@@ -38,7 +36,7 @@ class KeyHandlerMode extends Mode
keydown: (event) =>
if KeyboardUtils.isEscape(event) and not @isInResetState()
@reset()
- DomUtils.consumeKeyup event
+ @suppressEvent
else
@continueBubbling
@@ -49,11 +47,13 @@ class KeyHandlerMode extends Mode
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()
- DomUtils.consumeKeyup event, -> HelpDialog.toggle()
+ HelpDialog.toggle()
+ @suppressEvent
else if isEscape
@continueBubbling
else if @isMappedKey keyChar
- DomUtils.consumeKeyup event, => @handleKeyChar keyChar
+ @handleKeyChar keyChar
+ @suppressEvent
else if @isCountKey keyChar
digit = parseInt keyChar
@reset if @keyState.length == 1 then @countPrefix * 10 + digit else digit
@@ -93,5 +93,6 @@ class KeyHandlerMode extends Mode
@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 28097005..4c6578cd 100644
--- a/content_scripts/mode_visual.coffee
+++ b/content_scripts/mode_visual.coffee
@@ -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
@@ -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 56862d49..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
@@ -301,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_frontend.coffee b/content_scripts/vimium_frontend.coffee
index 4f2e805d..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
@@ -16,21 +22,6 @@ windowIsFocused = do ->
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
@@ -111,41 +102,6 @@ handlerStack.push
target = target.parentElement
true
-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
- 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.
@@ -186,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) ->
@@ -201,6 +157,7 @@ initializePreDomReady = ->
# Wrapper to install event listeners. Syntactic sugar.
installListener = (element, event, callback) ->
element.addEventListener(event, forTrusted(->
+ root.extend window, root unless extend? # See #2800.
if isEnabledForUrl then callback.apply(this, arguments) else true
), true)
@@ -245,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()
@@ -264,6 +221,7 @@ Frame =
@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.
@@ -280,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 ->
@@ -327,171 +285,17 @@ focusThisFrame = (request) ->
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, options) ->
- if options.registryEntry.options.normal
- enterNormalMode count
- else
- new PassNextKeyMode count
-
- enterNormalMode: (count) ->
- new NormalMode
- indicator: "Normal mode (pass keys disabled)"
- exitOnEscape: true
- singleton: "enterNormalMode"
- count: count
-
- focusInput: do ->
- # Track the most recently focused input element.
- recentlyFocusedElement = null
- window.addEventListener "focus",
- forTrusted (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.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 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
@@ -513,165 +317,8 @@ checkIfEnabledForUrl = do ->
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()
@@ -688,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}
+extend window, root unless exports?
diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee
index 14d72e87..ad98aa48 100644
--- a/content_scripts/vomnibar.coffee
+++ b/content_scripts/vomnibar.coffee
@@ -58,5 +58,6 @@ Vomnibar =
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 ff5991dc..67d5a44c 100644
--- a/lib/dom_utils.coffee
+++ b/lib/dom_utils.coffee
@@ -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]
@@ -344,7 +344,7 @@ DomUtils =
consumeKeyup: do ->
handlerId = null
- (event, callback = null) ->
+ (event, callback = null, suppressPropagation) ->
unless event.repeat
handlerStack.remove handlerId if handlerId?
code = event.code
@@ -353,17 +353,25 @@ DomUtils =
keyup: (event) ->
return handlerStack.continueBubbling unless event.code == code
@remove()
- handlerStack.suppressEvent
+ 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?()
- @suppressEvent event
- handlerStack.suppressEvent
+ 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) ->
+ getSelectionType: (selection = document.getSelection()) ->
selection.type or do ->
if selection.rangeCount == 0
"None"
@@ -376,7 +384,7 @@ DomUtils =
# 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
@@ -416,5 +424,6 @@ DomUtils =
style.textContent = Settings.get "userDefinedLinkHintCss"
document.head.appendChild style
-root = exports ? window
+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 e14e8b3e..09623e50 100644
--- a/lib/keyboard_utils.coffee
+++ b/lib/keyboard_utils.coffee
@@ -5,7 +5,7 @@ Utils?.monitorChromeStorage "mapKeyRegistry", (value) => mapKeyRegistry = value
KeyboardUtils =
# This maps event.key key names to Vimium key names.
keyNames:
- "ArrowLeft": "left", "ArrowUp": "up", "ArrowRight": "right", "ArrowDown": "down", " ": "space", "Backspace": "backspace"
+ "ArrowLeft": "left", "ArrowUp": "up", "ArrowRight": "right", "ArrowDown": "down", " ": "space"
init: ->
if (navigator.userAgent.indexOf("Mac") != -1)
@@ -31,19 +31,17 @@ KeyboardUtils =
else if key.length == 1 and not event.shiftKey
key = key.toLowerCase()
- if key of @keyNames
- @keyNames[key]
# It appears that key is not always defined (see #2453).
- else if not key?
+ 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 if key.length == 2 and "F1" <= key <= "F9"
- key.toLowerCase() # F1 to F9.
- else if key.length == 3 and "F10" <= key <= "F12"
- key.toLowerCase() # F10 to F12.
else
- ""
+ key.toLowerCase()
getKeyCharString: (event) ->
if keyChar = @getKeyChar event
@@ -60,9 +58,13 @@ KeyboardUtils =
keyChar = mapKeyRegistry[keyChar] ? keyChar
keyChar
- isEscape: (event) ->
- # <c-[> is mapped to Escape in Vim by default.
- event.key == "Escape" || @getKeyCharString(event) == "<c-[>"
+ isEscape: do ->
+ useVimLikeEscape = true
+ Utils.monitorChromeStorage "useVimLikeEscape", (value) -> useVimLikeEscape = value
+
+ (event) ->
+ # <c-[> is mapped to Escape in Vim by default.
+ event.key == "Escape" or (useVimLikeEscape and @getKeyCharString(event) == "<c-[>")
isBackspace: (event) ->
event.key in ["Backspace", "Delete"]
@@ -70,6 +72,9 @@ KeyboardUtils =
isPrintable: (event) ->
@getKeyCharString(event)?.length == 1
+ isModifier: (event) ->
+ event.key in ["Control", "Shift", "Alt", "OS", "AltGraph", "Meta"]
+
enUsTranslations:
"Backquote": ["`", "~"]
"Minus": ["-", "_"]
@@ -97,5 +102,6 @@ KeyboardUtils =
KeyboardUtils.init()
-root = exports ? window
+root = exports ? (window.root ?= {})
root.KeyboardUtils = KeyboardUtils
+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 38718990..fd1ef268 100644
--- a/lib/settings.coffee
+++ b/lib/settings.coffee
@@ -202,7 +202,7 @@ Settings.init()
# Perform migration from old settings versions, if this is the background page.
if Utils.isBackgroundPage()
- Settings.onLoaded ->
+ 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.
@@ -218,5 +218,8 @@ if Utils.isBackgroundPage()
# be removed after 1.58 has been out for sufficiently long.
Settings.nuke "copyNonDefaultsToChromeStorage-20150717"
-root = exports ? window
+ Settings.onLoaded Settings.applyMigrations.bind Settings
+
+root = exports ? (window.root ?= {})
root.Settings = Settings
+extend window, root unless exports?
diff --git a/lib/utils.coffee b/lib/utils.coffee
index d0a82cf7..6f38be8f 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -32,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 ->
@@ -335,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 a9629530..ee5b0655 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,18 +1,18 @@
{
"manifest_version": 2,
"name": "Vimium",
- "version": "1.60.4",
+ "version": "1.61.1",
"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",
@@ -31,6 +31,7 @@
"bookmarks",
"history",
"clipboardRead",
+ "clipboardWrite",
"storage",
"sessions",
"notifications",
@@ -60,6 +61,7 @@
"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"],
diff --git a/pages/blank.html b/pages/blank.html
index 8f10c7f6..d026912e 100644
--- a/pages/blank.html
+++ b/pages/blank.html
@@ -19,6 +19,7 @@
<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" />
diff --git a/pages/completion_engines.html b/pages/completion_engines.html
index 0c86edf7..3313b26c 100644
--- a/pages/completion_engines.html
+++ b/pages/completion_engines.html
@@ -22,6 +22,7 @@
<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" />
diff --git a/pages/help_dialog.coffee b/pages/help_dialog.coffee
index f36155e4..08180a72 100644
--- a/pages/help_dialog.coffee
+++ b/pages/help_dialog.coffee
@@ -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())
diff --git a/pages/help_dialog.html b/pages/help_dialog.html
index 1da54efd..7f053265 100644
--- a/pages/help_dialog.html
+++ b/pages/help_dialog.html
@@ -19,6 +19,7 @@
<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" />
diff --git a/pages/hud.coffee b/pages/hud.coffee
index 0d2ec2f7..5ff2e07e 100644
--- a/pages/hud.coffee
+++ b/pages/hud.coffee
@@ -95,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 3e8cf976..7bd27171 100644
--- a/pages/hud.html
+++ b/pages/hud.html
@@ -7,6 +7,7 @@
<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.html b/pages/logging.html
index 6eff58c4..17aafd70 100644
--- a/pages/logging.html
+++ b/pages/logging.html
@@ -19,6 +19,7 @@
<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" />
diff --git a/pages/options.coffee b/pages/options.coffee
index 035dd403..86b6122d 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) ->
@@ -323,6 +330,52 @@ document.addEventListener "DOMContentLoaded", ->
xhr.send()
+#
+# Backup and restore. "?" is for the tests."
+DomUtils?.documentReady ->
+ 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 5e2a3dfc..dab342a3 100644
--- a/pages/options.css
+++ b/pages/options.css
@@ -231,3 +231,6 @@ input.pattern, input.passKeys, .exclusionHeaderText {
white-space: nowrap;
width: 110px;
}
+#backupLink {
+ cursor: pointer;
+}
diff --git a/pages/options.html b/pages/options.html
index 46307b6f..b118bbd9 100644
--- a/pages/options.html
+++ b/pages/options.html
@@ -20,6 +20,7 @@
<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" />
@@ -66,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>
@@ -271,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>
@@ -315,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/tests/dom_tests/dom_tests.coffee b/tests/dom_tests/dom_tests.coffee
index a5c85606..6e422d46 100644
--- a/tests/dom_tests/dom_tests.coffee
+++ b/tests/dom_tests/dom_tests.coffee
@@ -436,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
@@ -479,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", ->
@@ -487,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", ->
@@ -496,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", ->
@@ -511,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) ->
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/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/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/utils_test.coffee b/tests/unit_tests/utils_test.coffee
index cc1081dd..2794a6d7 100644
--- a/tests/unit_tests/utils_test.coffee
+++ b/tests/unit_tests/utils_test.coffee
@@ -138,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 = "-[]/{}()*+?.^$|"