aboutsummaryrefslogtreecommitdiffstats
path: root/content_scripts
diff options
context:
space:
mode:
Diffstat (limited to 'content_scripts')
-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
13 files changed, 564 insertions, 466 deletions
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?