aboutsummaryrefslogtreecommitdiffstats
path: root/content_scripts
diff options
context:
space:
mode:
Diffstat (limited to 'content_scripts')
-rw-r--r--content_scripts/hud.coffee44
-rw-r--r--content_scripts/link_hints.coffee199
-rw-r--r--content_scripts/marks.coffee79
-rw-r--r--content_scripts/mode.coffee38
-rw-r--r--content_scripts/mode_find.coffee79
-rw-r--r--content_scripts/mode_insert.coffee94
-rw-r--r--content_scripts/mode_key_handler.coffee56
-rw-r--r--content_scripts/mode_normal.coffee369
-rw-r--r--content_scripts/mode_visual.coffee23
-rw-r--r--content_scripts/scroller.coffee28
-rw-r--r--content_scripts/ui_component.coffee3
-rw-r--r--content_scripts/vimium.css2
-rw-r--r--content_scripts/vimium_frontend.coffee442
-rw-r--r--content_scripts/vomnibar.coffee11
14 files changed, 759 insertions, 708 deletions
diff --git a/content_scripts/hud.coffee b/content_scripts/hud.coffee
index b2780491..42a960da 100644
--- a/content_scripts/hud.coffee
+++ b/content_scripts/hud.coffee
@@ -9,6 +9,8 @@ HUD =
findMode: null
abandon: -> @hudUI?.hide false
+ pasteListener: null # Set by @pasteFromClipboard to handle the value returned by pasteResponse
+
# This HUD is styled to precisely mimick the chrome HUD on Mac. Use the "has_popup_and_link_hud.html"
# test harness to tweak these styles to match Chrome's. One limitation of our HUD display is that
# it doesn't sit on top of horizontal scrollbars like Chrome's HUD does.
@@ -35,7 +37,9 @@ HUD =
@tween.fade 1.0, 150
search: (data) ->
- @findMode.findInPlace data.query
+ # NOTE(mrmr1993): On Firefox, window.find moves the window focus away from the HUD. We use postFindFocus
+ # to put it back, so the user can continue typing.
+ @findMode.findInPlace data.query, {"postFindFocus": @hudUI.iframeElement.contentWindow}
# Show the number of matches in the HUD UI.
matchCount = if FindMode.query.parsedQuery.length > 0 then FindMode.query.matchCount else 0
@@ -66,20 +70,47 @@ HUD =
focusNode = DomUtils.getSelectionFocusElement()
document.activeElement?.blur()
- focusNode?.focus()
+ focusNode?.focus?()
if exitEventIsEnter
- handleEnterForFindMode()
+ FindMode.handleEnter()
if FindMode.query.hasResults
postExit = -> new PostFindMode
else if exitEventIsEscape
- # We don't want FindMode to handle the click events that handleEscapeForFindMode can generate, so we
+ # We don't want FindMode to handle the click events that FindMode.handleEscape can generate, so we
# wait until the mode is closed before running it.
- postExit = handleEscapeForFindMode
+ postExit = FindMode.handleEscape
@findMode.exit()
postExit?()
+ # These commands manage copying and pasting from the clipboard in the HUD frame.
+ # NOTE(mrmr1993): We need this to copy and paste on Firefox:
+ # * an element can't be focused in the background page, so copying/pasting doesn't work
+ # * we don't want to disrupt the focus in the page, in case the page is listening for focus/blur events.
+ # * the HUD shouldn't be active for this frame while any of the copy/paste commands are running.
+ copyToClipboard: (text) ->
+ DomUtils.documentComplete =>
+ @init()
+ @hudUI?.postMessage {name: "copyToClipboard", data: text}
+
+ pasteFromClipboard: (@pasteListener) ->
+ DomUtils.documentComplete =>
+ @init()
+ # Show the HUD frame, so Firefox will actually perform the paste.
+ @hudUI.toggleIframeElementClasses "vimiumUIComponentHidden", "vimiumUIComponentVisible"
+ @tween.fade 0, 0
+ @hudUI.postMessage {name: "pasteFromClipboard"}
+
+ pasteResponse: ({data}) ->
+ # Hide the HUD frame again.
+ @hudUI.toggleIframeElementClasses "vimiumUIComponentVisible", "vimiumUIComponentHidden"
+ @unfocusIfFocused()
+ @pasteListener data
+
+ unfocusIfFocused: ->
+ document.activeElement.blur() if document.activeElement == @hudUI?.iframeElement
+
class Tween
opacity: 0
intervalId: -1
@@ -125,5 +156,6 @@ class Tween
}
"""
-root = exports ? window
+root = exports ? (window.root ?= {})
root.HUD = HUD
+extend window, root unless exports?
diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee
index 0014e20a..0592c96d 100644
--- a/content_scripts/link_hints.coffee
+++ b/content_scripts/link_hints.coffee
@@ -31,7 +31,7 @@ COPY_LINK_URL =
indicator: "Copy link URL to Clipboard"
linkActivator: (link) ->
if link.href?
- chrome.runtime.sendMessage handler: "copyToClipboard", data: link.href
+ HUD.copyToClipboard link.href
url = link.href
url = url[0..25] + "...." if 28 < url.length
HUD.showForDuration "Yanked #{url}", 2000
@@ -128,7 +128,7 @@ LinkHints =
if isSuccess
# Wait for the next tick to allow the previous mode to exit. It might yet generate a click event,
# which would cause our new mode to exit immediately.
- Utils.nextTick -> LinkHints.activateMode count-1, mode
+ Utils.nextTick -> LinkHints.activateMode count-1, {mode}
activateModeToOpenInNewTab: (count) -> @activateMode count, mode: OPEN_IN_NEW_BG_TAB
activateModeToOpenInNewForegroundTab: (count) -> @activateMode count, mode: OPEN_IN_NEW_FG_TAB
@@ -166,17 +166,15 @@ class LinkHintsMode
name: "hint/#{@mode.name}"
indicator: false
singleton: "link-hints-mode"
- passInitialKeyupEvents: true
suppressAllKeyboardEvents: true
suppressTrailingKeyEvents: true
exitOnEscape: true
exitOnClick: true
keydown: @onKeyDownInMode.bind this
- keypress: @onKeyPressInMode.bind this
@hintMode.onExit (event) =>
if event?.type == "click" or (event?.type == "keydown" and
- (KeyboardUtils.isEscape(event) or event.keyCode in [keyCodes.backspace, keyCodes.deleteKey]))
+ (KeyboardUtils.isEscape(event) or KeyboardUtils.isBackspace event))
HintCoordinator.sendMessage "exit", isSuccess: false
# Note(philc): Append these markers as top level children instead of as child nodes to the link itself,
@@ -230,86 +228,72 @@ class LinkHintsMode
linkText: desc.linkText
stableSortCount: ++@stableSortCount
- # Handles <Shift> and <Ctrl>.
+ # Handles all keyboard events.
onKeyDownInMode: (event) ->
return if event.repeat
- @keydownKeyChar = KeyboardUtils.getKeyChar(event).toLowerCase()
- previousTabCount = @tabCount
- @tabCount = 0
-
- # NOTE(smblott) As of 1.54, the Ctrl modifier doesn't work for filtered link hints; therefore we only
- # offer the control modifier for alphabet hints. It is not clear whether we should fix this. As of
- # 16-03-28, nobody has complained.
- modifiers = [keyCodes.shiftKey]
- modifiers.push keyCodes.ctrlKey unless Settings.get "filterLinkHints"
-
- if event.keyCode in modifiers and
+ # NOTE(smblott) The modifier behaviour here applies only to alphabet hints.
+ if event.key in ["Control", "Shift"] and not Settings.get("filterLinkHints") and
@mode in [ OPEN_IN_CURRENT_TAB, OPEN_WITH_QUEUE, OPEN_IN_NEW_BG_TAB, OPEN_IN_NEW_FG_TAB ]
- @tabCount = previousTabCount
# Toggle whether to open the link in a new or current tab.
previousMode = @mode
- keyCode = event.keyCode
+ key = event.key
- switch keyCode
- when keyCodes.shiftKey
+ switch key
+ when "Shift"
@setOpenLinkMode(if @mode is OPEN_IN_CURRENT_TAB then OPEN_IN_NEW_BG_TAB else OPEN_IN_CURRENT_TAB)
- when keyCodes.ctrlKey
+ when "Control"
@setOpenLinkMode(if @mode is OPEN_IN_NEW_FG_TAB then OPEN_IN_NEW_BG_TAB else OPEN_IN_NEW_FG_TAB)
- handlerId = handlerStack.push
+ handlerId = @hintMode.push
keyup: (event) =>
- if event.keyCode == keyCode
+ if event.key == key
handlerStack.remove()
@setOpenLinkMode previousMode
true # Continue bubbling the event.
- # For some (unknown) reason, we don't always receive the keyup event needed to remove this handler.
- # Therefore, we ensure that it's always removed when hint mode exits. See #1911 and #1926.
- @hintMode.onExit -> handlerStack.remove handlerId
-
- else if event.keyCode in [ keyCodes.backspace, keyCodes.deleteKey ]
+ else if KeyboardUtils.isBackspace event
if @markerMatcher.popKeyChar()
+ @tabCount = 0
@updateVisibleMarkers()
else
# Exit via @hintMode.exit(), so that the LinkHints.activate() "onExit" callback sees the key event and
# knows not to restart hints mode.
@hintMode.exit event
- else if event.keyCode == keyCodes.enter
+ else if event.key == "Enter"
# Activate the active hint, if there is one. Only FilterHints uses an active hint.
HintCoordinator.sendMessage "activateActiveHintMarker" if @markerMatcher.activeHintMarker
- else if event.keyCode == keyCodes.tab
- @tabCount = previousTabCount + (if event.shiftKey then -1 else 1)
- @updateVisibleMarkers @tabCount
+ else if event.key == "Tab"
+ if event.shiftKey then @tabCount-- else @tabCount++
+ @updateVisibleMarkers()
- else if event.keyCode == keyCodes.space and @markerMatcher.shouldRotateHints event
- @tabCount = previousTabCount
+ else if event.key == " " and @markerMatcher.shouldRotateHints event
HintCoordinator.sendMessage "rotateHints"
else
- @tabCount = previousTabCount if event.ctrlKey or event.metaKey or event.altKey
- return
-
- # We've handled the event, so suppress it and update the mode indicator.
- DomUtils.suppressEvent event
-
- # Handles normal input.
- onKeyPressInMode: (event) ->
- return if event.repeat
-
- keyChar = String.fromCharCode(event.charCode).toLowerCase()
- if keyChar
- @markerMatcher.pushKeyChar keyChar, @keydownKeyChar
- @updateVisibleMarkers()
+ unless event.repeat
+ keyChar =
+ if Settings.get "filterLinkHints"
+ KeyboardUtils.getKeyChar(event)
+ else
+ KeyboardUtils.getKeyChar(event).toLowerCase()
+ if keyChar
+ keyChar = " " if keyChar == "space"
+ if keyChar.length == 1
+ @tabCount = 0
+ @markerMatcher.pushKeyChar keyChar
+ @updateVisibleMarkers()
+ else
+ return handlerStack.suppressPropagation
- # We've handled the event, so suppress it.
- DomUtils.suppressEvent event
+ handlerStack.suppressEvent
- updateVisibleMarkers: (tabCount = 0) ->
+ updateVisibleMarkers: ->
{hintKeystrokeQueue, linkTextKeystrokeQueue} = @markerMatcher
- HintCoordinator.sendMessage "updateKeyState", {hintKeystrokeQueue, linkTextKeystrokeQueue, tabCount}
+ HintCoordinator.sendMessage "updateKeyState",
+ {hintKeystrokeQueue, linkTextKeystrokeQueue, tabCount: @tabCount}
updateKeyState: ({hintKeystrokeQueue, linkTextKeystrokeQueue, tabCount}) ->
extend @markerMatcher, {hintKeystrokeQueue, linkTextKeystrokeQueue}
@@ -318,7 +302,7 @@ class LinkHintsMode
if linksMatched.length == 0
@deactivateMode()
else if linksMatched.length == 1
- @activateLink linksMatched[0], userMightOverType ? false
+ @activateLink linksMatched[0], userMightOverType
else
@hideMarker marker for marker in @hintMarkers
@showMarker matched, @markerMatcher.hintKeystrokeQueue.length for matched in linksMatched
@@ -329,7 +313,7 @@ class LinkHintsMode
rotateHints: do ->
markerOverlapsStack = (marker, stack) ->
for otherMarker in stack
- return true if Rect.rectsOverlap marker.markerRect, otherMarker.markerRect
+ return true if Rect.intersects marker.markerRect, otherMarker.markerRect
false
->
@@ -372,7 +356,7 @@ class LinkHintsMode
# When only one hint remains, activate it in the appropriate way. The current frame may or may not contain
# the matched link, and may or may not have the focus. The resulting four cases are accounted for here by
# selectively pushing the appropriate HintCoordinator.onExit handlers.
- activateLink: (linkMatched, userMightOverType=false) ->
+ activateLink: (linkMatched, userMightOverType = false) ->
@removeHintMarkers()
if linkMatched.isLocalMarker
@@ -398,25 +382,26 @@ class LinkHintsMode
clickEl.focus()
linkActivator clickEl
- installKeyboardBlocker = (startKeyboardBlocker) ->
- if linkMatched.isLocalMarker
- {top: viewportTop, left: viewportLeft} = DomUtils.getViewportTopLeft()
- for rect in (Rect.copy rect for rect in clickEl.getClientRects())
- extend rect, top: rect.top + viewportTop, left: rect.left + viewportLeft
- flashEl = DomUtils.addFlashRect rect
- do (flashEl) -> HintCoordinator.onExit.push -> DomUtils.removeElement flashEl
-
- if windowIsFocused()
- startKeyboardBlocker (isSuccess) -> HintCoordinator.sendMessage "exit", {isSuccess}
+ # If flash elements are created, then this function can be used later to remove them.
+ removeFlashElements = ->
+ if linkMatched.isLocalMarker
+ {top: viewportTop, left: viewportLeft} = DomUtils.getViewportTopLeft()
+ flashElements = for rect in clickEl.getClientRects()
+ DomUtils.addFlashRect Rect.translate rect, viewportLeft, viewportTop
+ removeFlashElements = -> DomUtils.removeElement flashEl for flashEl in flashElements
# If we're using a keyboard blocker, then the frame with the focus sends the "exit" message, otherwise the
# frame containing the matched link does.
- if userMightOverType and Settings.get "waitForEnterForFilteredHints"
- installKeyboardBlocker (callback) -> new WaitForEnter callback
- else if userMightOverType
- installKeyboardBlocker (callback) -> new TypingProtector 200, callback
+ if userMightOverType
+ HintCoordinator.onExit.push removeFlashElements
+ if windowIsFocused()
+ callback = (isSuccess) -> HintCoordinator.sendMessage "exit", {isSuccess}
+ if Settings.get "waitForEnterForFilteredHints"
+ new WaitForEnter callback
+ else
+ new TypingProtector 200, callback
else if linkMatched.isLocalMarker
- DomUtils.flashRect linkMatched.rect
+ Utils.setTimeout 400, removeFlashElements
HintCoordinator.sendMessage "exit", isSuccess: true
#
@@ -444,12 +429,7 @@ class LinkHintsMode
# Use characters for hints, and do not filter links by their text.
class AlphabetHints
constructor: ->
- @linkHintCharacters = Settings.get "linkHintCharacters"
- # We use the keyChar from keydown if the link-hint characters are all "a-z0-9". This is the default
- # settings value, and preserves the legacy behavior (which always used keydown) for users which are
- # familiar with that behavior. Otherwise, we use keyChar from keypress, which admits non-Latin
- # characters. See #1722.
- @useKeydown = /^[a-z0-9]*$/.test @linkHintCharacters
+ @linkHintCharacters = Settings.get("linkHintCharacters").toLowerCase()
@hintKeystrokeQueue = []
fillInMarkers: (hintMarkers) ->
@@ -478,17 +458,17 @@ class AlphabetHints
matchString = @hintKeystrokeQueue.join ""
linksMatched: hintMarkers.filter (linkMarker) -> linkMarker.hintString.startsWith matchString
- pushKeyChar: (keyChar, keydownKeyChar) ->
- @hintKeystrokeQueue.push (if @useKeydown then keydownKeyChar else keyChar)
+ pushKeyChar: (keyChar) ->
+ @hintKeystrokeQueue.push keyChar
popKeyChar: -> @hintKeystrokeQueue.pop()
# For alphabet hints, <Space> always rotates the hints, regardless of modifiers.
shouldRotateHints: -> true
-# Use numbers (usually) for hints, and also filter links by their text.
+# Use characters for hints, and also filter links by their text.
class FilterHints
constructor: ->
- @linkHintNumbers = Settings.get "linkHintNumbers"
+ @linkHintNumbers = Settings.get("linkHintNumbers").toUpperCase()
@hintKeystrokeQueue = []
@linkTextKeystrokeQueue = []
@activeHintMarker = null
@@ -535,17 +515,18 @@ class FilterHints
linksMatched: linksMatched
userMightOverType: @hintKeystrokeQueue.length == 0 and 0 < @linkTextKeystrokeQueue.length
- pushKeyChar: (keyChar, keydownKeyChar) ->
- # For filtered hints, we *always* use the keyChar value from keypress, because there is no obvious and
- # easy-to-understand meaning for choosing one of keyChar or keydownKeyChar (as there is for alphabet
- # hints).
+ pushKeyChar: (keyChar) ->
if 0 <= @linkHintNumbers.indexOf keyChar
@hintKeystrokeQueue.push keyChar
+ else if keyChar.toLowerCase() != keyChar and @linkHintNumbers.toLowerCase() != @linkHintNumbers.toUpperCase()
+ # The the keyChar is upper case and the link hint "numbers" contain characters (e.g. [a-zA-Z]). We don't want
+ # some upper-case letters matching hints (above) and some matching text (below), so we ignore such keys.
+ return
# We only accept <Space> and characters which are not used for splitting (e.g. "a", "b", etc., but not "-").
else if keyChar == " " or not @splitRegexp.test keyChar
# Since we might renumber the hints, we should reset the current hintKeyStrokeQueue.
@hintKeystrokeQueue = []
- @linkTextKeystrokeQueue.push keyChar
+ @linkTextKeystrokeQueue.push keyChar.toLowerCase()
popKeyChar: ->
@hintKeystrokeQueue.pop() or @linkTextKeystrokeQueue.pop()
@@ -626,7 +607,9 @@ LocalHints =
# image), therefore we always return a array of element/rect pairs (which may also be a singleton or empty).
#
getVisibleClickable: (element) ->
- tagName = element.tagName.toLowerCase()
+ # Get the tag name. However, `element.tagName` can be an element (not a string, see #2305), so we guard
+ # against that.
+ tagName = element.tagName.toLowerCase?() ? ""
isClickable = false
onlyHasTabIndex = false
possibleFalsePositive = false
@@ -668,9 +651,12 @@ LocalHints =
isClickable ||= @checkForAngularJs element
# Check for attributes that make an element clickable regardless of its tagName.
- if (element.hasAttribute("onclick") or
- element.getAttribute("role")?.toLowerCase() in ["button", "link"] or
- element.getAttribute("contentEditable")?.toLowerCase() in ["", "contentEditable", "true"])
+ if element.hasAttribute("onclick") or
+ (role = element.getAttribute "role") and role.toLowerCase() in [
+ "button" , "tab" , "link", "checkbox", "menuitem", "menuitemcheckbox", "menuitemradio"
+ ] or
+ (contentEditable = element.getAttribute "contentEditable") and
+ contentEditable.toLowerCase() in ["", "contenteditable", "true"]
isClickable = true
# Check for jsaction event listeners on the element.
@@ -821,25 +807,10 @@ LocalHints =
hint.rect.left += left
if Settings.get "filterLinkHints"
- @withLabelMap (labelMap) =>
- extend hint, @generateLinkText labelMap, hint for hint in localHints
+ extend hint, @generateLinkText hint for hint in localHints
localHints
- # Generate a map of input element => label text, call a callback with it.
- withLabelMap: (callback) ->
- labelMap = {}
- labels = document.querySelectorAll "label"
- for label in labels
- forElement = label.getAttribute "for"
- if forElement
- labelText = label.textContent.trim()
- # Remove trailing ":" commonly found in labels.
- if labelText[labelText.length-1] == ":"
- labelText = labelText.substr 0, labelText.length-1
- labelMap[forElement] = labelText
- callback labelMap
-
- generateLinkText: (labelMap, hint) ->
+ generateLinkText: (hint) ->
element = hint.element
linkText = ""
showLinkText = false
@@ -847,9 +818,14 @@ LocalHints =
nodeName = element.nodeName.toLowerCase()
if nodeName == "input"
- if labelMap[element.id]
- linkText = labelMap[element.id]
+ if element.labels? and element.labels.length > 0
+ linkText = element.labels[0].textContent.trim()
+ # Remove trailing ":" commonly found in labels.
+ if linkText[linkText.length-1] == ":"
+ linkText = linkText[...linkText.length-1]
showLinkText = true
+ else if element.getAttribute("type")?.toLowerCase() == "file"
+ linkText = "Choose File"
else if element.type != "password"
linkText = element.value
if not linkText and 'placeholder' of element
@@ -899,15 +875,16 @@ class WaitForEnter extends Mode
@push
keydown: (event) =>
- if event.keyCode == keyCodes.enter
+ if event.key == "Enter"
@exit()
callback true # true -> isSuccess.
else if KeyboardUtils.isEscape event
@exit()
callback false # false -> isSuccess.
-root = exports ? window
+root = exports ? (window.root ?= {})
root.LinkHints = LinkHints
root.HintCoordinator = HintCoordinator
# For tests:
extend root, {LinkHintsMode, LocalHints, AlphabetHints, WaitForEnter}
+extend window, root unless exports?
diff --git a/content_scripts/marks.coffee b/content_scripts/marks.coffee
index 73191b1b..fb1d1b1d 100644
--- a/content_scripts/marks.coffee
+++ b/content_scripts/marks.coffee
@@ -40,22 +40,24 @@ Marks =
indicator: "Create mark..."
exitOnEscape: true
suppressAllKeyboardEvents: true
- keypress: (event) =>
- keyChar = String.fromCharCode event.charCode
- @exit =>
- if @isGlobalMark event, keyChar
- # We record the current scroll position, but only if this is the top frame within the tab.
- # Otherwise, we'll fetch the scroll position of the top frame from the background page later.
- [ scrollX, scrollY ] = [ window.scrollX, window.scrollY ] if DomUtils.isTopFrame()
- chrome.runtime.sendMessage
- handler: 'createMark'
- markName: keyChar
- scrollX: scrollX
- scrollY: scrollY
- , => @showMessage "Created global mark", keyChar
- else
- localStorage[@getLocationKey keyChar] = @getMarkString()
- @showMessage "Created local mark", keyChar
+ keydown: (event) =>
+ if KeyboardUtils.isPrintable event
+ keyChar = KeyboardUtils.getKeyChar event
+ @exit =>
+ if @isGlobalMark event, keyChar
+ # We record the current scroll position, but only if this is the top frame within the tab.
+ # Otherwise, we'll fetch the scroll position of the top frame from the background page later.
+ [ scrollX, scrollY ] = [ window.scrollX, window.scrollY ] if DomUtils.isTopFrame()
+ chrome.runtime.sendMessage
+ handler: 'createMark'
+ markName: keyChar
+ scrollX: scrollX
+ scrollY: scrollY
+ , => @showMessage "Created global mark", keyChar
+ else
+ localStorage[@getLocationKey keyChar] = @getMarkString()
+ @showMessage "Created local mark", keyChar
+ handlerStack.suppressEvent
activateGotoMode: (count, {registryEntry}) ->
@currentRegistryEntry = registryEntry
@@ -64,27 +66,30 @@ Marks =
indicator: "Go to mark..."
exitOnEscape: true
suppressAllKeyboardEvents: true
- keypress: (event) =>
- @exit =>
- markName = String.fromCharCode event.charCode
- if @isGlobalMark event, markName
- # This key must match @getLocationKey() in the back end.
- key = "vimiumGlobalMark|#{markName}"
- chrome.storage.sync.get key, (items) ->
- if key of items
- chrome.runtime.sendMessage handler: 'gotoMark', markName: markName
- HUD.showForDuration "Jumped to global mark '#{markName}'", 1000
- else
- HUD.showForDuration "Global mark not set '#{markName}'", 1000
- else
- markString = @localRegisters[markName] ? localStorage[@getLocationKey markName]
- if markString?
- @setPreviousPosition()
- position = JSON.parse markString
- window.scrollTo position.scrollX, position.scrollY
- @showMessage "Jumped to local mark", markName
+ keydown: (event) =>
+ if KeyboardUtils.isPrintable event
+ @exit =>
+ keyChar = KeyboardUtils.getKeyChar event
+ if @isGlobalMark event, keyChar
+ # This key must match @getLocationKey() in the back end.
+ key = "vimiumGlobalMark|#{keyChar}"
+ Settings.storage.get key, (items) ->
+ if key of items
+ chrome.runtime.sendMessage handler: 'gotoMark', markName: keyChar
+ HUD.showForDuration "Jumped to global mark '#{keyChar}'", 1000
+ else
+ HUD.showForDuration "Global mark not set '#{keyChar}'", 1000
else
- @showMessage "Local mark not set", markName
+ markString = @localRegisters[keyChar] ? localStorage[@getLocationKey keyChar]
+ if markString?
+ @setPreviousPosition()
+ position = JSON.parse markString
+ window.scrollTo position.scrollX, position.scrollY
+ @showMessage "Jumped to local mark", keyChar
+ else
+ @showMessage "Local mark not set", keyChar
+ handlerStack.suppressEvent
-root = exports ? window
+root = exports ? (window.root ?= {})
root.Marks = Marks
+extend window, root unless exports?
diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee
index 6508627e..a4a91c1f 100644
--- a/content_scripts/mode.coffee
+++ b/content_scripts/mode.coffee
@@ -55,7 +55,7 @@ class Mode
# the need for modes which suppress all keyboard events 1) to provide handlers for all of those events,
# or 2) to worry about event suppression and event-handler return values.
if @options.suppressAllKeyboardEvents
- for type in [ "keydown", "keypress", "keyup" ]
+ for type in [ "keydown", "keypress" ]
do (handler = @options[type]) =>
@options[type] = (event) => @alwaysSuppressPropagation => handler? event
@@ -81,8 +81,7 @@ class Mode
_name: "mode-#{@id}/exitOnEscape"
"keydown": (event) =>
return @continueBubbling unless KeyboardUtils.isEscape event
- DomUtils.suppressKeyupAfterEscape handlerStack
- @exit event, event.srcElement
+ @exit event, event.target
@suppressEvent
# If @options.exitOnBlur is truthy, then it should be an element. The mode will exit when that element
@@ -121,16 +120,6 @@ class Mode
singletons[key]?.exit()
singletons[key] = this
- # If @options.passInitialKeyupEvents is set, then we pass initial non-printable keyup events to the page
- # or to other extensions (because the corresponding keydown events were passed). This is used when
- # activating link hints, see #1522.
- if @options.passInitialKeyupEvents
- @push
- _name: "mode-#{@id}/passInitialKeyupEvents"
- keydown: => @alwaysContinueBubbling -> handlerStack.remove()
- keyup: (event) =>
- if KeyboardUtils.isPrintable event then @suppressPropagation else @passEventToPage
-
# if @options.suppressTrailingKeyEvents is set, then -- on exit -- we suppress all key events until a
# subsquent (non-repeat) keydown or keypress. In particular, the intention is to catch keyup events for
# keys which we have handled, but which otherwise might trigger page actions (if the page is listening for
@@ -148,7 +137,6 @@ class Mode
name: "suppress-trailing-key-events"
keydown: handler
keypress: handler
- keyup: -> handlerStack.suppressPropagation
Mode.modes.push this
@setIndicator()
@@ -174,15 +162,16 @@ class Mode
@exitHandlers.push handler
exit: (args...) ->
- if @modeIsActive
- @log "deactivate:", @id
- unless @modeIsExiting
- @modeIsExiting = true
- handler args... for handler in @exitHandlers
- handlerStack.remove handlerId for handlerId in @handlers
- Mode.modes = Mode.modes.filter (mode) => mode != this
- @modeIsActive = false
- @setIndicator()
+ return if @modeIsExiting or not @modeIsActive
+ @log "deactivate:", @id
+ @modeIsExiting = true
+
+ handler args... for handler in @exitHandlers
+ handlerStack.remove handlerId for handlerId in @handlers
+ Mode.modes = Mode.modes.filter (mode) => mode != this
+
+ @modeIsActive = false
+ @setIndicator()
# Debugging routines.
logModes: ->
@@ -209,5 +198,6 @@ class SuppressAllKeyboardEvents extends Mode
suppressAllKeyboardEvents: true
super extend defaults, options
-root = exports ? window
+root = exports ? (window.root ?= {})
extend root, {Mode, SuppressAllKeyboardEvents}
+extend window, root unless exports?
diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee
index 8621edf8..f19b5db4 100644
--- a/content_scripts/mode_find.coffee
+++ b/content_scripts/mode_find.coffee
@@ -6,7 +6,7 @@ class SuppressPrintable extends Mode
constructor: (options) ->
super options
handler = (event) => if KeyboardUtils.isPrintable event then @suppressEvent else @continueBubbling
- type = document.getSelection().type
+ type = DomUtils.getSelectionType()
# We use unshift here, so we see events after normal mode, so we only see unmapped keys.
@unshift
@@ -16,7 +16,7 @@ class SuppressPrintable extends Mode
keyup: (event) =>
# If the selection type has changed (usually, no longer "Range"), then the user is interacting with
# the input element, so we get out of the way. See discussion of option 5c from #1415.
- if document.getSelection().type != type then @exit() else handler event
+ @exit() if DomUtils.getSelectionType() != type
# When we use find, the selection/focus can land in a focusable/editable element. In this situation, special
# considerations apply. We implement three special cases:
@@ -47,7 +47,6 @@ class PostFindMode extends SuppressPrintable
_name: "mode-#{@id}/handle-escape"
keydown: (event) =>
if KeyboardUtils.isEscape event
- DomUtils.suppressKeyupAfterEscape handlerStack
@exit()
@suppressEvent
else
@@ -80,15 +79,16 @@ class FindMode extends Mode
exit: (event) ->
super()
- handleEscapeForFindMode() if event
+ FindMode.handleEscape() if event
restoreSelection: ->
+ return unless @initialRange
range = @initialRange
selection = getSelection()
selection.removeAllRanges()
selection.addRange range
- findInPlace: (query) ->
+ findInPlace: (query, options) ->
# If requested, restore the scroll position (so that failed searches leave the scroll position unchanged).
@checkReturnToViewPort()
FindMode.updateQuery query
@@ -96,7 +96,7 @@ class FindMode extends Mode
# match as the user adds matching characters, or removes previously-matched characters. See #1434.
@restoreSelection()
query = if FindMode.query.isRegex then FindMode.getNextQueryFromRegexMatches(0) else FindMode.query.parsedQuery
- FindMode.query.hasResults = FindMode.execute query
+ FindMode.query.hasResults = FindMode.execute query, options
@updateQuery: (query) ->
@query.rawQuery = query
@@ -179,7 +179,13 @@ class FindMode extends Mode
# ignore the selectionchange event generated by find()
document.removeEventListener("selectionchange", @restoreDefaultSelectionHighlight, true)
- result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false)
+ try
+ result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false)
+ catch # Failed searches throw on Firefox.
+
+ # window.find focuses the |window| that it is called on. This gives us an opportunity to (re-)focus
+ # another element/window, if that isn't the behaviour we want.
+ options.postFindFocus?.focus()
if options.colorSelection
setTimeout(
@@ -194,22 +200,73 @@ class FindMode extends Mode
result
- @restoreDefaultSelectionHighlight: -> document.body.classList.remove("vimiumFindMode")
+ @restoreDefaultSelectionHighlight: forTrusted -> document.body.classList.remove("vimiumFindMode")
+
+ # The user has found what they're looking for and is finished searching. We enter insert mode, if possible.
+ @handleEscape: ->
+ document.body.classList.remove("vimiumFindMode")
+ # Removing the class does not re-color existing selections. we recreate the current selection so it reverts
+ # back to the default color.
+ selection = window.getSelection()
+ unless selection.isCollapsed
+ range = window.getSelection().getRangeAt(0)
+ window.getSelection().removeAllRanges()
+ window.getSelection().addRange(range)
+ focusFoundLink() || selectFoundInputElement()
+
+ # Save the query so the user can do further searches with it.
+ @handleEnter: ->
+ focusFoundLink()
+ document.body.classList.add("vimiumFindMode")
+ FindMode.saveQuery()
+
+ @findNext: (backwards) ->
+ Marks.setPreviousPosition()
+ FindMode.query.hasResults = FindMode.execute null, {backwards}
+
+ if FindMode.query.hasResults
+ focusFoundLink()
+ new PostFindMode()
+ else
+ HUD.showForDuration("No matches for '#{FindMode.query.rawQuery}'", 1000)
checkReturnToViewPort: ->
window.scrollTo @scrollX, @scrollY if @options.returnToViewport
getCurrentRange = ->
selection = getSelection()
- if selection.type == "None"
+ if DomUtils.getSelectionType(selection) == "None"
range = document.createRange()
range.setStart document.body, 0
range.setEnd document.body, 0
range
else
- selection.collapseToStart() if selection.type == "Range"
+ selection.collapseToStart() if DomUtils.getSelectionType(selection) == "Range"
selection.getRangeAt 0
-root = exports ? window
+getLinkFromSelection = ->
+ node = window.getSelection().anchorNode
+ while (node && node != document.body)
+ return node if (node.nodeName.toLowerCase() == "a")
+ node = node.parentNode
+ null
+
+focusFoundLink = ->
+ if (FindMode.query.hasResults)
+ link = getLinkFromSelection()
+ link.focus() if link
+
+selectFoundInputElement = ->
+ # Since the last focused element might not be the one currently pointed to by find (e.g. the current one
+ # might be disabled and therefore unable to receive focus), we use the approximate heuristic of checking
+ # that the last anchor node is an ancestor of our element.
+ findModeAnchorNode = document.getSelection().anchorNode
+ if (FindMode.query.hasResults && document.activeElement &&
+ DomUtils.isSelectable(document.activeElement) &&
+ DomUtils.isDOMDescendant(findModeAnchorNode, document.activeElement))
+ DomUtils.simulateSelect(document.activeElement)
+
+root = exports ? (window.root ?= {})
root.PostFindMode = PostFindMode
root.FindMode = FindMode
+extend window, root unless exports?
diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee
index 4cb0a39e..511602e7 100644
--- a/content_scripts/mode_insert.coffee
+++ b/content_scripts/mode_insert.coffee
@@ -11,93 +11,44 @@ class InsertMode extends Mode
handleKeyEvent = (event) =>
return @continueBubbling unless @isActive event
+ # See comment here: https://github.com/philc/vimium/commit/48c169bd5a61685bb4e67b1e76c939dbf360a658.
+ activeElement = @getActiveElement()
+ return @passEventToPage if activeElement == document.body and activeElement.isContentEditable
+
# Check for a pass-next-key key.
if KeyboardUtils.getKeyCharString(event) in Settings.get "passNextKeyKeys"
new PassNextKeyMode
- return @suppressEvent
-
- return @passEventToPage unless event.type == 'keydown' and KeyboardUtils.isEscape event
- DomUtils.suppressKeyupAfterEscape handlerStack
- target = event.srcElement
- if target and DomUtils.isFocusable target
- # Remove the focus, so the user can't just get back into insert mode by typing in the same input box.
- target.blur()
- else if target?.shadowRoot and @insertModeLock
- # An editable element in a shadow DOM is focused; blur it.
- @insertModeLock.blur()
- @exit event, event.srcElement
- @suppressEvent
+
+ else if event.type == 'keydown' and KeyboardUtils.isEscape(event)
+ activeElement.blur() if DomUtils.isFocusable activeElement
+ @exit() unless @permanent
+
+ else
+ return @passEventToPage
+
+ return @suppressEvent
defaults =
name: "insert"
indicator: if not @permanent and not Settings.get "hideHud" then "Insert mode"
keypress: handleKeyEvent
- keyup: handleKeyEvent
keydown: handleKeyEvent
super extend defaults, options
- @insertModeLock =
- if options.targetElement and DomUtils.isEditable options.targetElement
- # The caller has told us which element to activate on.
- options.targetElement
- else if document.activeElement and DomUtils.isEditable document.activeElement
- # An input element is already active, so use it.
- document.activeElement
- else
- null
-
- @push
- _name: "mode-#{@id}-focus"
- "blur": (event) => @alwaysContinueBubbling =>
- target = event.target
- # We can't rely on focus and blur events arriving in the expected order. When the active element
- # changes, we might get "focus" before "blur". We track the active element in @insertModeLock, and
- # exit only when that element blurs.
- @exit event, target if @insertModeLock and target == @insertModeLock
- "focus": (event) => @alwaysContinueBubbling =>
- if @insertModeLock != event.target and DomUtils.isFocusable event.target
- @activateOnElement event.target
- else if event.target.shadowRoot
- # A focusable element inside the shadow DOM might have been selected. If so, we can catch the focus
- # event inside the shadow DOM. This fixes #853.
- shadowRoot = event.target.shadowRoot
- eventListeners = {}
- for type in [ "focus", "blur" ]
- eventListeners[type] = do (type) ->
- (event) -> handlerStack.bubbleEvent type, event
- shadowRoot.addEventListener type, eventListeners[type], true
-
- handlerStack.push
- _name: "shadow-DOM-input-mode"
- blur: (event) ->
- if event.target.shadowRoot == shadowRoot
- handlerStack.remove()
- for own type, listener of eventListeners
- shadowRoot.removeEventListener type, listener, true
-
# Only for tests. This gives us a hook to test the status of the permanently-installed instance.
InsertMode.permanentInstance = this if @permanent
isActive: (event) ->
return false if event == InsertMode.suppressedEvent
- return true if @insertModeLock or @global
- # Some sites (e.g. inbox.google.com) change the contentEditable property on the fly (see #1245); and
- # unfortunately, the focus event fires *before* the change. Therefore, we need to re-check whether the
- # active element is contentEditable.
- @activateOnElement document.activeElement if document.activeElement?.isContentEditable
- @insertModeLock != null
-
- activateOnElement: (element) ->
- @log "#{@id}: activating (permanent)" if @debug and @permanent
- @insertModeLock = element
-
- exit: (_, target) ->
- if (target and target == @insertModeLock) or @global or target == undefined
- @log "#{@id}: deactivating (permanent)" if @debug and @permanent and @insertModeLock
- @insertModeLock = null
- # Exit, but only if this isn't the permanently-installed instance.
- super() unless @permanent
+ return true if @global
+ DomUtils.isFocusable @getActiveElement()
+
+ getActiveElement: ->
+ activeElement = document.activeElement
+ while activeElement?.shadowRoot?.activeElement
+ activeElement = activeElement.shadowRoot.activeElement
+ activeElement
# Static stuff. This allows PostFindMode to suppress the permanently-installed InsertMode instance.
@suppressedEvent: null
@@ -129,6 +80,7 @@ class PassNextKeyMode extends Mode
@exit()
@passEventToPage
-root = exports ? window
+root = exports ? (window.root ?= {})
root.InsertMode = InsertMode
root.PassNextKeyMode = PassNextKeyMode
+extend window, root unless exports?
diff --git a/content_scripts/mode_key_handler.coffee b/content_scripts/mode_key_handler.coffee
index 480a79af..cca6b77a 100644
--- a/content_scripts/mode_key_handler.coffee
+++ b/content_scripts/mode_key_handler.coffee
@@ -12,7 +12,6 @@
# consists of a (non-empty) list of such mappings.
class KeyHandlerMode extends Mode
- keydownEvents: {}
setKeyMapping: (@keyMapping) -> @reset()
setPassKeys: (@passKeys) -> @reset()
# Only for tests.
@@ -28,59 +27,41 @@ class KeyHandlerMode extends Mode
super extend options,
keydown: @onKeydown.bind this
- keypress: @onKeypress.bind this
- keyup: @onKeyup.bind this
- # We cannot track keyup events if we lose the focus.
- blur: (event) => @alwaysContinueBubbling => @keydownEvents = {} if event.target == window
- @mapKeyRegistry = {}
- Utils.monitorChromeStorage "mapKeyRegistry", (value) => @mapKeyRegistry = value
+ if options.exitOnEscape
+ # If we're part way through a command's key sequence, then a first Escape should reset the key state,
+ # and only a second Escape should actually exit this mode.
+ @push
+ _name: "key-handler-escape-listener"
+ keydown: (event) =>
+ if KeyboardUtils.isEscape(event) and not @isInResetState()
+ @reset()
+ @suppressEvent
+ else
+ @continueBubbling
onKeydown: (event) ->
keyChar = KeyboardUtils.getKeyCharString event
- keyChar = @mapKeyRegistry[keyChar] ? keyChar
isEscape = KeyboardUtils.isEscape event
if isEscape and (@countPrefix != 0 or @keyState.length != 1)
- @keydownEvents[event.keyCode] = true
- @reset()
- @suppressEvent
+ DomUtils.consumeKeyup event, => @reset()
# If the help dialog loses the focus, then Escape should hide it; see point 2 in #2045.
else if isEscape and HelpDialog?.isShowing()
- @keydownEvents[event.keyCode] = true
HelpDialog.toggle()
@suppressEvent
else if isEscape
@continueBubbling
else if @isMappedKey keyChar
- @keydownEvents[event.keyCode] = true
- @handleKeyChar keyChar
- else if not keyChar and (keyChar = KeyboardUtils.getKeyChar event) and
- (@isMappedKey(keyChar) or @isCountKey keyChar)
- # We will possibly be handling a subsequent keypress event, so suppress propagation of this event to
- # prevent triggering page event listeners (e.g. Google instant Search).
- @keydownEvents[event.keyCode] = true
- @suppressPropagation
- else
- @continueBubbling
-
- onKeypress: (event) ->
- keyChar = KeyboardUtils.getKeyCharString event
- keyChar = @mapKeyRegistry[keyChar] ? keyChar
- if @isMappedKey keyChar
@handleKeyChar keyChar
+ @suppressEvent
else if @isCountKey keyChar
digit = parseInt keyChar
@reset if @keyState.length == 1 then @countPrefix * 10 + digit else digit
@suppressEvent
else
- @reset()
+ @reset() if keyChar
@continueBubbling
- onKeyup: (event) ->
- return @continueBubbling unless event.keyCode of @keydownEvents
- delete @keydownEvents[event.keyCode]
- @suppressPropagation
-
# This tests whether there is a mapping of keyChar in the current key state (and accounts for pass keys).
isMappedKey: (keyChar) ->
(mapping for mapping in @keyState when keyChar of mapping)[0]? and not @isPassKey keyChar
@@ -92,7 +73,10 @@ class KeyHandlerMode extends Mode
# Keystrokes are *never* considered pass keys if the user has begun entering a command. So, for example, if
# 't' is a passKey, then the "t"-s of 'gt' and '99t' are neverthless handled as regular keys.
isPassKey: (keyChar) ->
- @countPrefix == 0 and @keyState.length == 1 and keyChar in (@passKeys ? "")
+ @isInResetState() and keyChar in (@passKeys ? "")
+
+ isInResetState: ->
+ @countPrefix == 0 and @keyState.length == 1
handleKeyChar: (keyChar) ->
bgLog "handle key #{keyChar} (#{@name})"
@@ -106,7 +90,9 @@ class KeyHandlerMode extends Mode
bgLog " invoke #{command.command} count=#{count} "
@reset()
@commandHandler {command, count}
+ @exit() if @options.count? and --@options.count <= 0
@suppressEvent
-root = exports ? window
+root = exports ? (window.root ?= {})
root.KeyHandlerMode = KeyHandlerMode
+extend window, root unless exports?
diff --git a/content_scripts/mode_normal.coffee b/content_scripts/mode_normal.coffee
new file mode 100644
index 00000000..1fe0618e
--- /dev/null
+++ b/content_scripts/mode_normal.coffee
@@ -0,0 +1,369 @@
+class NormalMode extends KeyHandlerMode
+ constructor: (options = {}) ->
+ defaults =
+ name: "normal"
+ indicator: false # There is normally no mode indicator in normal mode.
+ commandHandler: @commandHandler.bind this
+
+ super extend defaults, options
+
+ chrome.storage.local.get "normalModeKeyStateMapping", (items) =>
+ @setKeyMapping items.normalModeKeyStateMapping
+
+ chrome.storage.onChanged.addListener (changes, area) =>
+ if area == "local" and changes.normalModeKeyStateMapping?.newValue
+ @setKeyMapping changes.normalModeKeyStateMapping.newValue
+
+ commandHandler: ({command: registryEntry, count}) ->
+ count *= registryEntry.options.count ? 1
+ count = 1 if registryEntry.noRepeat
+
+ if registryEntry.repeatLimit? and registryEntry.repeatLimit < count
+ return unless confirm """
+ You have asked Vimium to perform #{count} repetitions of the command: #{registryEntry.description}.\n
+ Are you sure you want to continue?"""
+
+ if registryEntry.topFrame
+ # We never return to a UI-component frame (e.g. the help dialog), it might have lost the focus.
+ sourceFrameId = if window.isVimiumUIComponent then 0 else frameId
+ chrome.runtime.sendMessage
+ handler: "sendMessageToFrames", message: {name: "runInTopFrame", sourceFrameId, registryEntry}
+ else if registryEntry.background
+ chrome.runtime.sendMessage {handler: "runBackgroundCommand", registryEntry, count}
+ else
+ NormalModeCommands[registryEntry.command] count, {registryEntry}
+
+enterNormalMode = (count) ->
+ new NormalMode
+ indicator: "Normal mode (pass keys disabled)"
+ exitOnEscape: true
+ singleton: "enterNormalMode"
+ count: count
+
+NormalModeCommands =
+ # Scrolling.
+ scrollToBottom: ->
+ Marks.setPreviousPosition()
+ Scroller.scrollTo "y", "max"
+ scrollToTop: (count) ->
+ Marks.setPreviousPosition()
+ Scroller.scrollTo "y", (count - 1) * Settings.get("scrollStepSize")
+ scrollToLeft: -> Scroller.scrollTo "x", 0
+ scrollToRight: -> Scroller.scrollTo "x", "max"
+ scrollUp: (count) -> Scroller.scrollBy "y", -1 * Settings.get("scrollStepSize") * count
+ scrollDown: (count) -> Scroller.scrollBy "y", Settings.get("scrollStepSize") * count
+ scrollPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1/2 * count
+ scrollPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1/2 * count
+ scrollFullPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1 * count
+ scrollFullPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1 * count
+ scrollLeft: (count) -> Scroller.scrollBy "x", -1 * Settings.get("scrollStepSize") * count
+ scrollRight: (count) -> Scroller.scrollBy "x", Settings.get("scrollStepSize") * count
+
+ # Page state.
+ reload: (count, options) ->
+ hard = options.registryEntry.options.hard ? false
+ window.location.reload(hard)
+ goBack: (count) -> history.go(-count)
+ goForward: (count) -> history.go(count)
+
+ # Url manipulation.
+ goUp: (count) ->
+ url = window.location.href
+ if (url[url.length - 1] == "/")
+ url = url.substring(0, url.length - 1)
+
+ urlsplit = url.split("/")
+ # make sure we haven't hit the base domain yet
+ if (urlsplit.length > 3)
+ urlsplit = urlsplit.slice(0, Math.max(3, urlsplit.length - count))
+ window.location.href = urlsplit.join('/')
+
+ goToRoot: ->
+ window.location.href = window.location.origin
+
+ toggleViewSource: ->
+ chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) ->
+ if (url.substr(0, 12) == "view-source:")
+ url = url.substr(12, url.length - 12)
+ else
+ url = "view-source:" + url
+ chrome.runtime.sendMessage {handler: "openUrlInNewTab", url}
+
+ copyCurrentUrl: ->
+ chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) ->
+ HUD.copyToClipboard url
+ url = url[0..25] + "...." if 28 < url.length
+ HUD.showForDuration("Yanked #{url}", 2000)
+
+ openCopiedUrlInNewTab: (count) ->
+ HUD.pasteFromClipboard (url) ->
+ chrome.runtime.sendMessage { handler: "openUrlInNewTab", url, count }
+
+ openCopiedUrlInCurrentTab: ->
+ HUD.pasteFromClipboard (url) ->
+ chrome.runtime.sendMessage { handler: "openUrlInCurrentTab", url }
+
+ # Mode changes.
+ enterInsertMode: ->
+ # If a focusable element receives the focus, then we exit and leave the permanently-installed insert-mode
+ # instance to take over.
+ new InsertMode global: true, exitOnFocus: true
+
+ enterVisualMode: ->
+ new VisualMode userLaunchedMode: true
+
+ enterVisualLineMode: ->
+ new VisualLineMode userLaunchedMode: true
+
+ enterFindMode: ->
+ Marks.setPreviousPosition()
+ new FindMode()
+
+ # Find.
+ performFind: (count) -> FindMode.findNext false for [0...count] by 1
+ performBackwardsFind: (count) -> FindMode.findNext true for [0...count] by 1
+
+ # Misc.
+ mainFrame: -> focusThisFrame highlight: true, forceFocusThisFrame: true
+ showHelp: (sourceFrameId) -> HelpDialog.toggle {sourceFrameId, showAllCommandDetails: false}
+
+ passNextKey: (count, options) ->
+ if options.registryEntry.options.normal
+ enterNormalMode count
+ else
+ new PassNextKeyMode count
+
+ goPrevious: ->
+ previousPatterns = Settings.get("previousPatterns") || ""
+ previousStrings = previousPatterns.split(",").filter( (s) -> s.trim().length )
+ findAndFollowRel("prev") || findAndFollowLink(previousStrings)
+
+ goNext: ->
+ nextPatterns = Settings.get("nextPatterns") || ""
+ nextStrings = nextPatterns.split(",").filter( (s) -> s.trim().length )
+ findAndFollowRel("next") || findAndFollowLink(nextStrings)
+
+ focusInput: (count) ->
+ # Focus the first input element on the page, and create overlays to highlight all the input elements, with
+ # the currently-focused element highlighted specially. Tabbing will shift focus to the next input element.
+ # Pressing any other key will remove the overlays and the special tab behavior.
+ resultSet = DomUtils.evaluateXPath textInputXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
+ visibleInputs =
+ for i in [0...resultSet.snapshotLength] by 1
+ element = resultSet.snapshotItem i
+ continue unless DomUtils.getVisibleClientRect element, true
+ { element, index: i, rect: Rect.copy element.getBoundingClientRect() }
+
+ visibleInputs.sort ({element: element1, index: i1}, {element: element2, index: i2}) ->
+ # Put elements with a lower positive tabIndex first, keeping elements in DOM order.
+ if element1.tabIndex > 0
+ if element2.tabIndex > 0
+ tabDifference = element1.tabIndex - element2.tabIndex
+ if tabDifference != 0
+ tabDifference
+ else
+ i1 - i2
+ else
+ -1
+ else if element2.tabIndex > 0
+ 1
+ else
+ i1 - i2
+
+ if visibleInputs.length == 0
+ HUD.showForDuration("There are no inputs to focus.", 1000)
+ return
+
+ # This is a hack to improve usability on the Vimium options page. We prime the recently-focused input
+ # to be the key-mappings input. Arguably, this is the input that the user is most likely to use.
+ recentlyFocusedElement = lastFocusedInput()
+
+ selectedInputIndex =
+ if count == 1
+ # As the starting index, we pick that of the most recently focused input element (or 0).
+ elements = visibleInputs.map (visibleInput) -> visibleInput.element
+ Math.max 0, elements.indexOf recentlyFocusedElement
+ else
+ Math.min(count, visibleInputs.length) - 1
+
+ hints = for tuple in visibleInputs
+ hint = DomUtils.createElement "div"
+ hint.className = "vimiumReset internalVimiumInputHint vimiumInputHint"
+
+ # minus 1 for the border
+ hint.style.left = (tuple.rect.left - 1) + window.scrollX + "px"
+ hint.style.top = (tuple.rect.top - 1) + window.scrollY + "px"
+ hint.style.width = tuple.rect.width + "px"
+ hint.style.height = tuple.rect.height + "px"
+
+ hint
+
+ new FocusSelector hints, visibleInputs, selectedInputIndex
+
+if LinkHints?
+ extend NormalModeCommands,
+ "LinkHints.activateMode": LinkHints.activateMode.bind LinkHints
+ "LinkHints.activateModeToOpenInNewTab": LinkHints.activateModeToOpenInNewTab.bind LinkHints
+ "LinkHints.activateModeToOpenInNewForegroundTab": LinkHints.activateModeToOpenInNewForegroundTab.bind LinkHints
+ "LinkHints.activateModeWithQueue": LinkHints.activateModeWithQueue.bind LinkHints
+ "LinkHints.activateModeToOpenIncognito": LinkHints.activateModeToOpenIncognito.bind LinkHints
+ "LinkHints.activateModeToDownloadLink": LinkHints.activateModeToDownloadLink.bind LinkHints
+ "LinkHints.activateModeToCopyLinkUrl": LinkHints.activateModeToCopyLinkUrl.bind LinkHints
+
+if Vomnibar?
+ extend NormalModeCommands,
+ "Vomnibar.activate": Vomnibar.activate.bind Vomnibar
+ "Vomnibar.activateInNewTab": Vomnibar.activateInNewTab.bind Vomnibar
+ "Vomnibar.activateTabSelection": Vomnibar.activateTabSelection.bind Vomnibar
+ "Vomnibar.activateBookmarks": Vomnibar.activateBookmarks.bind Vomnibar
+ "Vomnibar.activateBookmarksInNewTab": Vomnibar.activateBookmarksInNewTab.bind Vomnibar
+ "Vomnibar.activateEditUrl": Vomnibar.activateEditUrl.bind Vomnibar
+ "Vomnibar.activateEditUrlInNewTab": Vomnibar.activateEditUrlInNewTab.bind Vomnibar
+
+if Marks?
+ extend NormalModeCommands,
+ "Marks.activateCreateMode": Marks.activateCreateMode.bind Marks
+ "Marks.activateGotoMode": Marks.activateGotoMode.bind Marks
+
+# The types in <input type="..."> that we consider for focusInput command. Right now this is recalculated in
+# each content script. Alternatively we could calculate it once in the background page and use a request to
+# fetch it each time.
+# Should we include the HTML5 date pickers here?
+
+# The corresponding XPath for such elements.
+textInputXPath = (->
+ textInputTypes = [ "text", "search", "email", "url", "number", "password", "date", "tel" ]
+ inputElements = ["input[" +
+ "(" + textInputTypes.map((type) -> '@type="' + type + '"').join(" or ") + "or not(@type))" +
+ " and not(@disabled or @readonly)]",
+ "textarea", "*[@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"]
+ DomUtils?.makeXPath(inputElements)
+)()
+
+# used by the findAndFollow* functions.
+followLink = (linkElement) ->
+ if (linkElement.nodeName.toLowerCase() == "link")
+ window.location.href = linkElement.href
+ else
+ # if we can click on it, don't simply set location.href: some next/prev links are meant to trigger AJAX
+ # calls, like the 'more' button on GitHub's newsfeed.
+ linkElement.scrollIntoView()
+ DomUtils.simulateClick(linkElement)
+
+#
+# Find and follow a link which matches any one of a list of strings. If there are multiple such links, they
+# are prioritized for shortness, by their position in :linkStrings, how far down the page they are located,
+# and finally by whether the match is exact. Practically speaking, this means we favor 'next page' over 'the
+# next big thing', and 'more' over 'nextcompany', even if 'next' occurs before 'more' in :linkStrings.
+#
+findAndFollowLink = (linkStrings) ->
+ linksXPath = DomUtils.makeXPath(["a", "*[@onclick or @role='link' or contains(@class, 'button')]"])
+ links = DomUtils.evaluateXPath(linksXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE)
+ candidateLinks = []
+
+ # at the end of this loop, candidateLinks will contain all visible links that match our patterns
+ # links lower in the page are more likely to be the ones we want, so we loop through the snapshot backwards
+ for i in [(links.snapshotLength - 1)..0] by -1
+ link = links.snapshotItem(i)
+
+ # ensure link is visible (we don't mind if it is scrolled offscreen)
+ boundingClientRect = link.getBoundingClientRect()
+ if (boundingClientRect.width == 0 || boundingClientRect.height == 0)
+ continue
+ computedStyle = window.getComputedStyle(link, null)
+ if (computedStyle.getPropertyValue("visibility") != "visible" ||
+ computedStyle.getPropertyValue("display") == "none")
+ continue
+
+ linkMatches = false
+ for linkString in linkStrings
+ if link.innerText.toLowerCase().indexOf(linkString) != -1 ||
+ 0 <= link.value?.indexOf? linkString
+ linkMatches = true
+ break
+ continue unless linkMatches
+
+ candidateLinks.push(link)
+
+ return if (candidateLinks.length == 0)
+
+ for link in candidateLinks
+ link.wordCount = link.innerText.trim().split(/\s+/).length
+
+ # We can use this trick to ensure that Array.sort is stable. We need this property to retain the reverse
+ # in-page order of the links.
+
+ candidateLinks.forEach((a,i) -> a.originalIndex = i)
+
+ # favor shorter links, and ignore those that are more than one word longer than the shortest link
+ candidateLinks =
+ candidateLinks
+ .sort((a, b) ->
+ if (a.wordCount == b.wordCount) then a.originalIndex - b.originalIndex else a.wordCount - b.wordCount
+ )
+ .filter((a) -> a.wordCount <= candidateLinks[0].wordCount + 1)
+
+ for linkString in linkStrings
+ exactWordRegex =
+ if /\b/.test(linkString[0]) or /\b/.test(linkString[linkString.length - 1])
+ new RegExp "\\b" + linkString + "\\b", "i"
+ else
+ new RegExp linkString, "i"
+ for candidateLink in candidateLinks
+ if exactWordRegex.test(candidateLink.innerText) ||
+ (candidateLink.value && exactWordRegex.test(candidateLink.value))
+ followLink(candidateLink)
+ return true
+ false
+
+findAndFollowRel = (value) ->
+ relTags = ["link", "a", "area"]
+ for tag in relTags
+ elements = document.getElementsByTagName(tag)
+ for element in elements
+ if (element.hasAttribute("rel") && element.rel.toLowerCase() == value)
+ followLink(element)
+ return true
+
+class FocusSelector extends Mode
+ constructor: (hints, visibleInputs, selectedInputIndex) ->
+ super
+ name: "focus-selector"
+ exitOnClick: true
+ keydown: (event) =>
+ if event.key == "Tab"
+ hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint'
+ selectedInputIndex += hints.length + (if event.shiftKey then -1 else 1)
+ selectedInputIndex %= hints.length
+ hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
+ DomUtils.simulateSelect visibleInputs[selectedInputIndex].element
+ @suppressEvent
+ else unless event.key == "Shift"
+ @exit()
+ # Give the new mode the opportunity to handle the event.
+ @restartBubbling
+
+ @hintContainingDiv = DomUtils.addElementList hints,
+ id: "vimiumInputMarkerContainer"
+ className: "vimiumReset"
+
+ DomUtils.simulateSelect visibleInputs[selectedInputIndex].element
+ if visibleInputs.length == 1
+ @exit()
+ return
+ else
+ hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
+
+ exit: ->
+ super()
+ DomUtils.removeElement @hintContainingDiv
+ if document.activeElement and DomUtils.isEditable document.activeElement
+ new InsertMode
+ singleton: "post-find-mode/focus-input"
+ targetElement: document.activeElement
+ indicator: false
+
+root = exports ? (window.root ?= {})
+root.NormalMode = NormalMode
+root.NormalModeCommands = NormalModeCommands
+extend window, root unless exports?
diff --git a/content_scripts/mode_visual.coffee b/content_scripts/mode_visual.coffee
index 1c05cd85..4c6578cd 100644
--- a/content_scripts/mode_visual.coffee
+++ b/content_scripts/mode_visual.coffee
@@ -70,7 +70,7 @@ class Movement
else
@selection.modify @alterMethod, direction, granularity
- # Return a simple camparable value which depends on various aspects of the selection. This is used to
+ # Return a simple comparable value which depends on various aspects of the selection. This is used to
# detect, after a movement, whether the selection has changed.
hashSelection: ->
range = @selection.getRangeAt(0)
@@ -159,7 +159,7 @@ class Movement
# Scroll the focus into view.
scrollIntoView: ->
- unless @selection.type == "None"
+ unless DomUtils.getSelectionType(@selection) == "None"
elementWithFocus = DomUtils.getElementWithFocus @selection, @getDirection() == backward
Scroller.scrollIntoView elementWithFocus if elementWithFocus
@@ -240,7 +240,7 @@ class VisualMode extends KeyHandlerMode
commandHandler: @commandHandler.bind this
# If there was a range selection when the user lanuched visual mode, then we retain the selection on exit.
- @shouldRetainSelectionOnExit = @options.userLaunchedMode and @selection.type == "Range"
+ @shouldRetainSelectionOnExit = @options.userLaunchedMode and DomUtils.getSelectionType(@selection) == "Range"
@onExit (event = null) =>
if @shouldRetainSelectionOnExit
@@ -258,7 +258,7 @@ class VisualMode extends KeyHandlerMode
_name: "#{@id}/enter/click"
# Yank on <Enter>.
keypress: (event) =>
- if event.keyCode == keyCodes.enter
+ if event.key == "Enter"
unless event.metaKey or event.ctrlKey or event.altKey or event.shiftKey
@yank()
return @suppressEvent
@@ -269,7 +269,7 @@ class VisualMode extends KeyHandlerMode
# Establish or use the initial selection. If that's not possible, then enter caret mode.
unless @name == "caret"
- if @selection.type in [ "Caret", "Range" ]
+ if DomUtils.getSelectionType(@selection) in [ "Caret", "Range" ]
selectionRect = @selection.getRangeAt(0).getBoundingClientRect()
if window.vimiumDomTestsAreRunning
# We're running the DOM tests, where getBoundingClientRect() isn't available.
@@ -277,7 +277,7 @@ class VisualMode extends KeyHandlerMode
selectionRect = Rect.intersect selectionRect, Rect.create 0, 0, window.innerWidth, window.innerHeight
if selectionRect.height >= 0 and selectionRect.width >= 0
# The selection is visible in the current viewport.
- if @selection.type == "Caret"
+ if DomUtils.getSelectionType(@selection) == "Caret"
# The caret is in the viewport. Make make it visible.
@movement.extendByOneCharacter(forward) or @movement.extendByOneCharacter backward
else
@@ -285,7 +285,7 @@ class VisualMode extends KeyHandlerMode
# more likely to be interested in visible content.
@selection.removeAllRanges()
- if @selection.type != "Range" and @name != "caret"
+ if DomUtils.getSelectionType(@selection) != "Range" and @name != "caret"
new CaretMode
HUD.showForDuration "No usable selection, entering caret mode...", 2500
@@ -312,7 +312,7 @@ class VisualMode extends KeyHandlerMode
yank: (args = {}) ->
@yankedText = @selection.toString()
@exit()
- chrome.runtime.sendMessage handler: "copyToClipboard", data: @yankedText
+ HUD.copyToClipboard @yankedText
message = @yankedText.replace /\s+/g, " "
message = message[...12] + "..." if 15 < @yankedText.length
@@ -341,10 +341,10 @@ class CaretMode extends VisualMode
super extend options, name: "caret", indicator: "Caret mode", alterMethod: "move"
# Establish the initial caret.
- switch @selection.type
+ switch DomUtils.getSelectionType(@selection)
when "None"
@establishInitialSelectionAnchor()
- if @selection.type == "None"
+ if DomUtils.getSelectionType(@selection) == "None"
@exit()
HUD.showForDuration "Create a selection before entering visual mode.", 2500
return
@@ -380,6 +380,7 @@ class CaretMode extends VisualMode
return true
false
-root = exports ? window
+root = exports ? (window.root ?= {})
root.VisualMode = VisualMode
root.VisualLineMode = VisualLineMode
+extend window, root unless exports?
diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee
index 3a1b3772..f65062e4 100644
--- a/content_scripts/scroller.coffee
+++ b/content_scripts/scroller.coffee
@@ -95,7 +95,14 @@ findScrollableElement = (element, direction, amount, factor) ->
# On some pages, the scrolling element is not actually scrollable. Here, we search the document for the
# largest visible element which does scroll vertically. This is used to initialize activatedElement. See
# #1358.
-firstScrollableElement = (element=getScrollingElement()) ->
+firstScrollableElement = (element = null) ->
+ unless element
+ scrollingElement = getScrollingElement()
+ if doesScroll(scrollingElement, "y", 1, 1) or doesScroll(scrollingElement, "y", -1, 1)
+ return scrollingElement
+ else
+ element = document.body ? getScrollingElement()
+
if doesScroll(element, "y", 1, 1) or doesScroll(element, "y", -1, 1)
element
else
@@ -128,9 +135,11 @@ checkVisibility = (element) ->
CoreScroller =
init: ->
@time = 0
- @lastEvent = null
- @keyIsDown = false
+ @lastEvent = @keyIsDown = null
+ @installCanceEventListener()
+ # This installs listeners for events which should cancel smooth scrolling.
+ installCanceEventListener: ->
# NOTE(smblott) With extreme keyboard configurations, Chrome sometimes does not get a keyup event for
# every keydown, in which case tapping "j" scrolls indefinitely. This appears to be a Chrome/OS/XOrg bug
# of some kind. See #1549.
@@ -141,11 +150,11 @@ CoreScroller =
@keyIsDown = true
@time += 1 unless event.repeat
@lastEvent = event
- keyup: =>
+ keyup: (event) =>
handlerStack.alwaysContinueBubbling =>
@keyIsDown = false
@time += 1
- blur: =>
+ blur: (event) =>
handlerStack.alwaysContinueBubbling =>
@time += 1 if event.target == window
@@ -175,7 +184,7 @@ CoreScroller =
return if @lastEvent?.repeat
activationTime = ++@time
- myKeyIsStillDown = => @time == activationTime and @keyIsDown
+ myKeyIsStillDown = => @time == activationTime and @keyIsDown ? true
# Store amount's sign and make amount positive; the arithmetic is clearer when amount is positive.
sign = getSign amount
@@ -188,6 +197,7 @@ CoreScroller =
totalElapsed = 0.0
calibration = 1.0
previousTimestamp = null
+ cancelEventListener = @installCanceEventListener()
animate = (timestamp) =>
previousTimestamp ?= timestamp
@@ -215,13 +225,14 @@ CoreScroller =
requestAnimationFrame animate
else
# We're done.
+ handlerStack.remove cancelEventListener
checkVisibility element
# If we've been asked not to be continuous, then we advance time, so the myKeyIsStillDown test always
# fails.
++@time unless continuous
- # Launch animator.
+ # Start scrolling.
requestAnimationFrame animate
# Scroller contains the two main scroll functions which are used by clients.
@@ -297,5 +308,6 @@ Scroller =
element = findScrollableElement element, "x", amount, 1
CoreScroller.scroll element, "x", amount, false
-root = exports ? window
+root = exports ? (window.root ?= {})
root.Scroller = Scroller
+extend window, root unless exports?
diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee
index 203f0c8c..c71bfb35 100644
--- a/content_scripts/ui_component.coffee
+++ b/content_scripts/ui_component.coffee
@@ -96,5 +96,6 @@ class UIComponent
@options = null
@postMessage "hidden" # Inform the UI component that it is hidden.
-root = exports ? window
+root = exports ? (window.root ?= {})
root.UIComponent = UIComponent
+extend window, root unless exports?
diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css
index 3e8f65d6..54256199 100644
--- a/content_scripts/vimium.css
+++ b/content_scripts/vimium.css
@@ -80,7 +80,7 @@ div.internalVimiumHintMarker {
overflow: hidden;
font-size: 11px;
padding: 1px 3px 0px 3px;
- background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785), color-stop(100%,#FFC542));
+ background: linear-gradient(to bottom, #FFF785 0%,#FFC542 100%);
border: solid 1px #C38A22;
border-radius: 3px;
box-shadow: 0px 3px 7px 0px rgba(0, 0, 0, 0.3);
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index 21826944..432fa7a2 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -2,6 +2,12 @@
# This content script must be run prior to domReady so that we perform some operations very early.
#
+root = exports ? (window.root ?= {})
+# On Firefox, sometimes the variables assigned to window are lost (bug 1408996), so we reinstall them.
+# NOTE(mrmr1993): This bug leads to catastrophic failure (ie. nothing works and errors abound).
+DomUtils.documentReady ->
+ root.extend window, root unless extend?
+
isEnabledForUrl = true
isIncognitoMode = chrome.extension.inIncognitoContext
normalMode = null
@@ -10,25 +16,12 @@ normalMode = null
windowIsFocused = do ->
windowHasFocus = null
DomUtils.documentReady -> windowHasFocus = document.hasFocus()
- window.addEventListener "focus", (event) -> windowHasFocus = true if event.target == window; true
- window.addEventListener "blur", (event) -> windowHasFocus = false if event.target == window; true
+ window.addEventListener "focus", forTrusted (event) ->
+ windowHasFocus = true if event.target == window; true
+ window.addEventListener "blur", forTrusted (event) ->
+ windowHasFocus = false if event.target == window; true
-> windowHasFocus
-# The types in <input type="..."> that we consider for focusInput command. Right now this is recalculated in
-# each content script. Alternatively we could calculate it once in the background page and use a request to
-# fetch it each time.
-# Should we include the HTML5 date pickers here?
-
-# The corresponding XPath for such elements.
-textInputXPath = (->
- textInputTypes = [ "text", "search", "email", "url", "number", "password", "date", "tel" ]
- inputElements = ["input[" +
- "(" + textInputTypes.map((type) -> '@type="' + type + '"').join(" or ") + "or not(@type))" +
- " and not(@disabled or @readonly)]",
- "textarea", "*[@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"]
- DomUtils.makeXPath(inputElements)
-)()
-
# This is set by Frame.registerFrameId(). A frameId of 0 indicates that this is the top frame in the tab.
frameId = null
@@ -109,47 +102,13 @@ handlerStack.push
target = target.parentElement
true
-class NormalMode extends KeyHandlerMode
- constructor: (options = {}) ->
- super extend options,
- name: "normal"
- indicator: false # There is no mode indicator in normal mode.
- commandHandler: @commandHandler.bind this
-
- chrome.storage.local.get "normalModeKeyStateMapping", (items) =>
- @setKeyMapping items.normalModeKeyStateMapping
-
- chrome.storage.onChanged.addListener (changes, area) =>
- if area == "local" and changes.normalModeKeyStateMapping?.newValue
- @setKeyMapping changes.normalModeKeyStateMapping.newValue
-
- # Initialize components which normal mode depends upon.
- Scroller.init()
- FindModeHistory.init()
-
- commandHandler: ({command: registryEntry, count}) ->
- count *= registryEntry.options.count ? 1
- count = 1 if registryEntry.noRepeat
-
- if registryEntry.repeatLimit? and registryEntry.repeatLimit < count
- return unless confirm """
- You have asked Vimium to perform #{count} repetitions of the command: #{registryEntry.description}.\n
- Are you sure you want to continue?"""
-
- if registryEntry.topFrame
- # We never return to a UI-component frame (e.g. the help dialog), it might have lost the focus.
- sourceFrameId = if window.isVimiumUIComponent then 0 else frameId
- chrome.runtime.sendMessage
- handler: "sendMessageToFrames", message: {name: "runInTopFrame", sourceFrameId, registryEntry}
- else if registryEntry.background
- chrome.runtime.sendMessage {handler: "runBackgroundCommand", registryEntry, count}
- else
- Utils.invokeCommandString registryEntry.command, count, {registryEntry}
-
installModes = ->
# Install the permanent modes. The permanently-installed insert mode tracks focus/blur events, and
# activates/deactivates itself accordingly.
normalMode = new NormalMode
+ # Initialize components upon which normal mode depends.
+ Scroller.init()
+ FindModeHistory.init()
new InsertMode permanent: true
new GrabBackFocus if isEnabledForUrl
normalMode # Return the normalMode object (for the tests).
@@ -157,9 +116,14 @@ installModes = ->
initializeOnEnabledStateKnown = (isEnabledForUrl) ->
installModes() unless normalMode
if isEnabledForUrl
- # We only initialize (and activate) the Vomnibar in the top frame. Also, we do not initialize the
- # Vomnibar until we know that Vimium is enabled. Thereafter, there's no more initialization to do.
- DomUtils.documentComplete Vomnibar.init.bind Vomnibar if DomUtils.isTopFrame()
+ unless Utils.isFirefox() and document.documentElement.namespaceURI != "http://www.w3.org/1999/xhtml"
+ # We only initialize (and activate) the Vomnibar in the top frame. Also, we do not initialize the
+ # Vomnibar until we know that Vimium is enabled. Thereafter, there's no more initialization to do.
+ #
+ # NOTE(mrmr1993): In XML documents on Firefox, injecting HTML into the DOM breaks the rendering, so we
+ # lazy load the Vomnibar. This comes with the expected issues, but is better than breaking all XML
+ # documents.
+ DomUtils.documentComplete Vomnibar.init.bind Vomnibar if DomUtils.isTopFrame()
initializeOnEnabledStateKnown = ->
#
@@ -178,7 +142,7 @@ initializePreDomReady = ->
frameFocused: -> # A frame has received the focus; we don't care here (UI components handle this).
checkEnabledAfterURLChange: checkEnabledAfterURLChange
runInTopFrame: ({sourceFrameId, registryEntry}) ->
- Utils.invokeCommandString registryEntry.command, sourceFrameId, registryEntry if DomUtils.isTopFrame()
+ NormalModeCommands[registryEntry.command] sourceFrameId, registryEntry if DomUtils.isTopFrame()
linkHintsMessage: (request) -> HintCoordinator[request.messageType] request
chrome.runtime.onMessage.addListener (request, sender, sendResponse) ->
@@ -192,9 +156,10 @@ initializePreDomReady = ->
# Wrapper to install event listeners. Syntactic sugar.
installListener = (element, event, callback) ->
- element.addEventListener(event, ->
+ element.addEventListener(event, forTrusted(->
+ root.extend window, root unless extend? # See #2800.
if isEnabledForUrl then callback.apply(this, arguments) else true
- , true)
+ ), true)
#
# Installing or uninstalling listeners is error prone. Instead we elect to check isEnabledForUrl each time so
@@ -215,7 +180,7 @@ installListeners = Utils.makeIdempotent ->
# - Tell the background page this frame's URL.
# - Check if we should be enabled.
#
-onFocus = (event) ->
+onFocus = forTrusted (event) ->
if event.target == window
chrome.runtime.sendMessage handler: "frameFocused"
checkIfEnabledForUrl true
@@ -223,7 +188,7 @@ onFocus = (event) ->
# We install these listeners directly (that is, we don't use installListener) because we still need to receive
# events when Vimium is not enabled.
window.addEventListener "focus", onFocus
-window.addEventListener "hashchange", onFocus
+window.addEventListener "hashchange", checkEnabledAfterURLChange
initializeOnDomReady = ->
# Tell the background page we're in the domReady state.
@@ -237,7 +202,7 @@ Frame =
postMessage: (handler, request = {}) -> @port.postMessage extend request, {handler}
linkHintsMessage: (request) -> HintCoordinator[request.messageType] request
registerFrameId: ({chromeFrameId}) ->
- frameId = window.frameId = chromeFrameId
+ frameId = root.frameId = window.frameId = chromeFrameId
# We register a frame immediately only if it is focused or its window isn't tiny. We register tiny
# frames later, when necessary. This affects focusFrame() and link hints.
if windowIsFocused() or not DomUtils.windowIsTooSmall()
@@ -247,20 +212,21 @@ Frame =
window.removeEventListener "focus", focusHandler
window.removeEventListener "resize", resizeHandler
Frame.postMessage "registerFrame"
- window.addEventListener "focus", focusHandler = ->
+ window.addEventListener "focus", focusHandler = forTrusted (event) ->
postRegisterFrame() if event.target == window
- window.addEventListener "resize", resizeHandler = ->
+ window.addEventListener "resize", resizeHandler = forTrusted (event) ->
postRegisterFrame() unless DomUtils.windowIsTooSmall()
init: ->
@port = chrome.runtime.connect name: "frames"
@port.onMessage.addListener (request) =>
+ root.extend window, root unless extend? # See #2800 and #2831.
(@listeners[request.handler] ? this[request.handler]) request
# We disable the content scripts when we lose contact with the background page, or on unload.
@port.onDisconnect.addListener disconnect = Utils.makeIdempotent => @disconnect()
- window.addEventListener "unload", disconnect
+ window.addEventListener "unload", forTrusted disconnect
disconnect: ->
try @postMessage "unregisterFrame"
@@ -272,7 +238,7 @@ Frame =
handlerStack.reset()
isEnabledForUrl = false
window.removeEventListener "focus", onFocus
- window.removeEventListener "hashchange", onFocus
+ window.removeEventListener "hashchange", checkEnabledAfterURLChange
setScrollPosition = ({ scrollX, scrollY }) ->
DomUtils.documentReady ->
@@ -314,170 +280,30 @@ focusThisFrame = (request) ->
chrome.runtime.sendMessage handler: "nextFrame"
return
window.focus()
+ # On Firefox, window.focus doesn't always draw focus back from a child frame (bug 554039).
+ # We blur the active element if it is an iframe, which gives the window back focus as intended.
+ document.activeElement.blur() if document.activeElement.tagName.toLowerCase() == "iframe"
flashFrame() if request.highlight
-extend window,
- scrollToBottom: ->
- Marks.setPreviousPosition()
- Scroller.scrollTo "y", "max"
- scrollToTop: (count) ->
- Marks.setPreviousPosition()
- Scroller.scrollTo "y", (count - 1) * Settings.get("scrollStepSize")
- scrollToLeft: -> Scroller.scrollTo "x", 0
- scrollToRight: -> Scroller.scrollTo "x", "max"
- scrollUp: (count) -> Scroller.scrollBy "y", -1 * Settings.get("scrollStepSize") * count
- scrollDown: (count) -> Scroller.scrollBy "y", Settings.get("scrollStepSize") * count
- scrollPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1/2 * count
- scrollPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1/2 * count
- scrollFullPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1 * count
- scrollFullPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1 * count
- scrollLeft: (count) -> Scroller.scrollBy "x", -1 * Settings.get("scrollStepSize") * count
- scrollRight: (count) -> Scroller.scrollBy "x", Settings.get("scrollStepSize") * count
-
-extend window,
- reload: -> window.location.reload()
- goBack: (count) -> history.go(-count)
- goForward: (count) -> history.go(count)
-
- goUp: (count) ->
- url = window.location.href
- if (url[url.length - 1] == "/")
- url = url.substring(0, url.length - 1)
-
- urlsplit = url.split("/")
- # make sure we haven't hit the base domain yet
- if (urlsplit.length > 3)
- urlsplit = urlsplit.slice(0, Math.max(3, urlsplit.length - count))
- window.location.href = urlsplit.join('/')
-
- goToRoot: ->
- window.location.href = window.location.origin
-
- mainFrame: -> focusThisFrame highlight: true, forceFocusThisFrame: true
-
- toggleViewSource: ->
- chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) ->
- if (url.substr(0, 12) == "view-source:")
- url = url.substr(12, url.length - 12)
- else
- url = "view-source:" + url
- chrome.runtime.sendMessage {handler: "openUrlInNewTab", url}
-
- copyCurrentUrl: ->
- # TODO(ilya): When the following bug is fixed, revisit this approach of sending back to the background
- # page to copy.
- # http://code.google.com/p/chromium/issues/detail?id=55188
- chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) ->
- chrome.runtime.sendMessage { handler: "copyToClipboard", data: url }
- url = url[0..25] + "...." if 28 < url.length
- HUD.showForDuration("Yanked #{url}", 2000)
-
- enterInsertMode: ->
- # If a focusable element receives the focus, then we exit and leave the permanently-installed insert-mode
- # instance to take over.
- new InsertMode global: true, exitOnFocus: true
-
- enterVisualMode: ->
- new VisualMode userLaunchedMode: true
-
- enterVisualLineMode: ->
- new VisualLineMode userLaunchedMode: true
-
- passNextKey: (count) ->
- new PassNextKeyMode count
-
- focusInput: do ->
- # Track the most recently focused input element.
- recentlyFocusedElement = null
- window.addEventListener "focus",
- (event) -> recentlyFocusedElement = event.target if DomUtils.isEditable event.target
- , true
-
- (count) ->
- mode = InsertMode
- # Focus the first input element on the page, and create overlays to highlight all the input elements, with
- # the currently-focused element highlighted specially. Tabbing will shift focus to the next input element.
- # Pressing any other key will remove the overlays and the special tab behavior.
- # The mode argument is the mode to enter once an input is selected.
- resultSet = DomUtils.evaluateXPath textInputXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
- visibleInputs =
- for i in [0...resultSet.snapshotLength] by 1
- element = resultSet.snapshotItem i
- continue unless DomUtils.getVisibleClientRect element, true
- { element, rect: Rect.copy element.getBoundingClientRect() }
-
- if visibleInputs.length == 0
- HUD.showForDuration("There are no inputs to focus.", 1000)
- return
-
- # This is a hack to improve usability on the Vimium options page. We prime the recently-focused input
- # to be the key-mappings input. Arguably, this is the input that the user is most likely to use.
- recentlyFocusedElement ?= document.getElementById "keyMappings" if window.isVimiumOptionsPage
-
- selectedInputIndex =
- if count == 1
- # As the starting index, we pick that of the most recently focused input element (or 0).
- elements = visibleInputs.map (visibleInput) -> visibleInput.element
- Math.max 0, elements.indexOf recentlyFocusedElement
- else
- Math.min(count, visibleInputs.length) - 1
-
- hints = for tuple in visibleInputs
- hint = DomUtils.createElement "div"
- hint.className = "vimiumReset internalVimiumInputHint vimiumInputHint"
-
- # minus 1 for the border
- hint.style.left = (tuple.rect.left - 1) + window.scrollX + "px"
- hint.style.top = (tuple.rect.top - 1) + window.scrollY + "px"
- hint.style.width = tuple.rect.width + "px"
- hint.style.height = tuple.rect.height + "px"
-
- hint
-
- new class FocusSelector extends Mode
- constructor: ->
- super
- name: "focus-selector"
- exitOnClick: true
- keydown: (event) =>
- if event.keyCode == KeyboardUtils.keyCodes.tab
- hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint'
- selectedInputIndex += hints.length + (if event.shiftKey then -1 else 1)
- selectedInputIndex %= hints.length
- hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
- DomUtils.simulateSelect visibleInputs[selectedInputIndex].element
- @suppressEvent
- else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey
- @exit()
- # Give the new mode the opportunity to handle the event.
- @restartBubbling
-
- @hintContainingDiv = DomUtils.addElementList hints,
- id: "vimiumInputMarkerContainer"
- className: "vimiumReset"
-
- DomUtils.simulateSelect visibleInputs[selectedInputIndex].element
- if visibleInputs.length == 1
- @exit()
- return
- else
- hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
-
- exit: ->
- super()
- DomUtils.removeElement @hintContainingDiv
- if mode and document.activeElement and DomUtils.isEditable document.activeElement
- new mode
- singleton: "post-find-mode/focus-input"
- targetElement: document.activeElement
- indicator: false
+# Used by focusInput command.
+root.lastFocusedInput = do ->
+ # Track the most recently focused input element.
+ recentlyFocusedElement = null
+ window.addEventListener "focus",
+ forTrusted (event) ->
+ DomUtils = window.DomUtils ? root.DomUtils # Workaround FF bug 1408996.
+ if DomUtils.isEditable event.target
+ recentlyFocusedElement = event.target
+ , true
+ -> recentlyFocusedElement
# Checks if Vimium should be enabled or not in this frame. As a side effect, it also informs the background
# page whether this frame has the focus, allowing the background page to track the active frame's URL and set
# the page icon.
checkIfEnabledForUrl = do ->
Frame.addEventListener "isEnabledForUrl", (response) ->
- {isEnabledForUrl, passKeys, frameIsFocused} = response
+ {isEnabledForUrl, passKeys, frameIsFocused, isFirefox} = response
+ Utils.isFirefox = -> isFirefox
initializeOnEnabledStateKnown isEnabledForUrl
normalMode.setPassKeys passKeys
# Hide the HUD if we're not enabled.
@@ -488,168 +314,11 @@ checkIfEnabledForUrl = do ->
# When we're informed by the background page that a URL in this tab has changed, we check if we have the
# correct enabled state (but only if this frame has the focus).
-checkEnabledAfterURLChange = ->
+checkEnabledAfterURLChange = forTrusted ->
checkIfEnabledForUrl() if windowIsFocused()
-handleEscapeForFindMode = ->
- document.body.classList.remove("vimiumFindMode")
- # removing the class does not re-color existing selections. we recreate the current selection so it reverts
- # back to the default color.
- selection = window.getSelection()
- unless selection.isCollapsed
- range = window.getSelection().getRangeAt(0)
- window.getSelection().removeAllRanges()
- window.getSelection().addRange(range)
- focusFoundLink() || selectFoundInputElement()
-
-# <esc> sends us into insert mode if possible, but <cr> does not.
-# <esc> corresponds approximately to 'nevermind, I have found it already' while <cr> means 'I want to save
-# this query and do more searches with it'
-handleEnterForFindMode = ->
- focusFoundLink()
- document.body.classList.add("vimiumFindMode")
- FindMode.saveQuery()
-
-focusFoundLink = ->
- if (FindMode.query.hasResults)
- link = getLinkFromSelection()
- link.focus() if link
-
-selectFoundInputElement = ->
- # Since the last focused element might not be the one currently pointed to by find (e.g. the current one
- # might be disabled and therefore unable to receive focus), we use the approximate heuristic of checking
- # that the last anchor node is an ancestor of our element.
- findModeAnchorNode = document.getSelection().anchorNode
- if (FindMode.query.hasResults && document.activeElement &&
- DomUtils.isSelectable(document.activeElement) &&
- DomUtils.isDOMDescendant(findModeAnchorNode, document.activeElement))
- DomUtils.simulateSelect(document.activeElement)
-
-findAndFocus = (backwards) ->
- Marks.setPreviousPosition()
- FindMode.query.hasResults = FindMode.execute null, {backwards}
-
- if FindMode.query.hasResults
- focusFoundLink()
- new PostFindMode()
- else
- HUD.showForDuration("No matches for '#{FindMode.query.rawQuery}'", 1000)
-
-performFind = (count) -> findAndFocus false for [0...count] by 1
-performBackwardsFind = (count) -> findAndFocus true for [0...count] by 1
-
-getLinkFromSelection = ->
- node = window.getSelection().anchorNode
- while (node && node != document.body)
- return node if (node.nodeName.toLowerCase() == "a")
- node = node.parentNode
- null
-
-# used by the findAndFollow* functions.
-followLink = (linkElement) ->
- if (linkElement.nodeName.toLowerCase() == "link")
- window.location.href = linkElement.href
- else
- # if we can click on it, don't simply set location.href: some next/prev links are meant to trigger AJAX
- # calls, like the 'more' button on GitHub's newsfeed.
- linkElement.scrollIntoView()
- DomUtils.simulateClick(linkElement)
-
-#
-# Find and follow a link which matches any one of a list of strings. If there are multiple such links, they
-# are prioritized for shortness, by their position in :linkStrings, how far down the page they are located,
-# and finally by whether the match is exact. Practically speaking, this means we favor 'next page' over 'the
-# next big thing', and 'more' over 'nextcompany', even if 'next' occurs before 'more' in :linkStrings.
-#
-findAndFollowLink = (linkStrings) ->
- linksXPath = DomUtils.makeXPath(["a", "*[@onclick or @role='link' or contains(@class, 'button')]"])
- links = DomUtils.evaluateXPath(linksXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE)
- candidateLinks = []
-
- # at the end of this loop, candidateLinks will contain all visible links that match our patterns
- # links lower in the page are more likely to be the ones we want, so we loop through the snapshot backwards
- for i in [(links.snapshotLength - 1)..0] by -1
- link = links.snapshotItem(i)
-
- # ensure link is visible (we don't mind if it is scrolled offscreen)
- boundingClientRect = link.getBoundingClientRect()
- if (boundingClientRect.width == 0 || boundingClientRect.height == 0)
- continue
- computedStyle = window.getComputedStyle(link, null)
- if (computedStyle.getPropertyValue("visibility") != "visible" ||
- computedStyle.getPropertyValue("display") == "none")
- continue
-
- linkMatches = false
- for linkString in linkStrings
- if link.innerText.toLowerCase().indexOf(linkString) != -1 ||
- 0 <= link.value?.indexOf? linkString
- linkMatches = true
- break
- continue unless linkMatches
-
- candidateLinks.push(link)
-
- return if (candidateLinks.length == 0)
-
- for link in candidateLinks
- link.wordCount = link.innerText.trim().split(/\s+/).length
-
- # We can use this trick to ensure that Array.sort is stable. We need this property to retain the reverse
- # in-page order of the links.
-
- candidateLinks.forEach((a,i) -> a.originalIndex = i)
-
- # favor shorter links, and ignore those that are more than one word longer than the shortest link
- candidateLinks =
- candidateLinks
- .sort((a, b) ->
- if (a.wordCount == b.wordCount) then a.originalIndex - b.originalIndex else a.wordCount - b.wordCount
- )
- .filter((a) -> a.wordCount <= candidateLinks[0].wordCount + 1)
-
- for linkString in linkStrings
- exactWordRegex =
- if /\b/.test(linkString[0]) or /\b/.test(linkString[linkString.length - 1])
- new RegExp "\\b" + linkString + "\\b", "i"
- else
- new RegExp linkString, "i"
- for candidateLink in candidateLinks
- if exactWordRegex.test(candidateLink.innerText) ||
- (candidateLink.value && exactWordRegex.test(candidateLink.value))
- followLink(candidateLink)
- return true
- false
-
-findAndFollowRel = (value) ->
- relTags = ["link", "a", "area"]
- for tag in relTags
- elements = document.getElementsByTagName(tag)
- for element in elements
- if (element.hasAttribute("rel") && element.rel.toLowerCase() == value)
- followLink(element)
- return true
-
-window.goPrevious = ->
- previousPatterns = Settings.get("previousPatterns") || ""
- previousStrings = previousPatterns.split(",").filter( (s) -> s.trim().length )
- findAndFollowRel("prev") || findAndFollowLink(previousStrings)
-
-window.goNext = ->
- nextPatterns = Settings.get("nextPatterns") || ""
- nextStrings = nextPatterns.split(",").filter( (s) -> s.trim().length )
- findAndFollowRel("next") || findAndFollowLink(nextStrings)
-
-# Enters find mode. Returns the new find-mode instance.
-enterFindMode = ->
- Marks.setPreviousPosition()
- new FindMode()
-
-window.showHelp = (sourceFrameId) ->
- HelpDialog.toggle {sourceFrameId, showAllCommandDetails: false}
-
# If we are in the help dialog iframe, then HelpDialog is already defined with the necessary functions.
-window.HelpDialog ?=
+root.HelpDialog ?=
helpUI: null
isShowing: -> @helpUI?.showing
abort: -> @helpUI.hide false if @isShowing()
@@ -666,14 +335,13 @@ window.HelpDialog ?=
initializePreDomReady()
DomUtils.documentReady initializeOnDomReady
-root = exports ? window
root.handlerStack = handlerStack
root.frameId = frameId
root.Frame = Frame
root.windowIsFocused = windowIsFocused
root.bgLog = bgLog
-# These are exported for find mode and link-hints mode.
-extend root, {handleEscapeForFindMode, handleEnterForFindMode, performFind, performBackwardsFind,
- enterFindMode, focusThisFrame}
+# These are exported for normal mode and link-hints mode.
+extend root, {focusThisFrame}
# These are exported only for the tests.
-extend root, {installModes, installListeners}
+extend root, {installModes}
+extend window, root unless exports?
diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee
index 04499523..ad98aa48 100644
--- a/content_scripts/vomnibar.coffee
+++ b/content_scripts/vomnibar.coffee
@@ -53,10 +53,11 @@ Vomnibar =
# selectFirst - Optional, boolean. Whether to select the first entry.
# newTab - Optional, boolean. Whether to open the result in a new tab.
open: (sourceFrameId, options) ->
- if @vomnibarUI?
- # The Vomnibar cannot coexist with the help dialog (it causes focus issues).
- HelpDialog.abort()
- @vomnibarUI.activate extend options, { name: "activate", sourceFrameId, focus: true }
+ @init()
+ # The Vomnibar cannot coexist with the help dialog (it causes focus issues).
+ HelpDialog.abort()
+ @vomnibarUI.activate extend options, { name: "activate", sourceFrameId, focus: true }
-root = exports ? window
+root = exports ? (window.root ?= {})
root.Vomnibar = Vomnibar
+extend window, root unless exports?