#
# This content script takes input from its webpage and executes commands locally on behalf of the background
# page. It must be run prior to domReady so that we perform some operations very early. We tell the
# background page that we're in domReady and ready to accept normal commands by connectiong to a port named
# "domReady".
#
isShowingHelpDialog = false
keyPort = null
isEnabledForUrl = true
isIncognitoMode = chrome.extension.inIncognitoContext
passKeys = null
keyQueue = null
# The user's operating system.
currentCompletionKeys = ""
validFirstKeys = ""
# We track whther the current window has the focus or not.
windowIsFocused = do ->
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
-> windowHasFocus
# The types in 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)
)()
#
# Give this frame a unique (non-zero) id.
#
frameId = 1 + Math.floor(Math.random()*999999999)
# For debugging only. This logs to the console on the background page.
bgLog = (args...) ->
args = (arg.toString() for arg in args)
chrome.runtime.sendMessage handler: "log", frameId: frameId, message: args.join " "
# If an input grabs the focus before the user has interacted with the page, then grab it back (if the
# grabBackFocus option is set).
class GrabBackFocus extends Mode
constructor: ->
super
name: "grab-back-focus"
keydown: => @alwaysContinueBubbling => @exit()
@push
_name: "grab-back-focus-mousedown"
mousedown: => @alwaysContinueBubbling => @exit()
Settings.use "grabBackFocus", (grabBackFocus) =>
if grabBackFocus
@push
_name: "grab-back-focus-focus"
focus: (event) => @grabBackFocus event.target
# An input may already be focused. If so, grab back the focus.
@grabBackFocus document.activeElement if document.activeElement
else
@exit()
grabBackFocus: (element) ->
return @continueBubbling unless DomUtils.isEditable element
element.blur()
@suppressEvent
# Pages can load new content dynamically and change the displayed URL using history.pushState. Since this can
# often be indistinguishable from an actual new page load for the user, we should also re-start GrabBackFocus
# for these as well. This fixes issue #1622.
handlerStack.push
_name: "GrabBackFocus-pushState-monitor"
click: (event) ->
# If a focusable element is focused, the user must have clicked on it. Retain focus and bail.
return true if DomUtils.isFocusable document.activeElement
target = event.target
while target
# Often, a link which triggers a content load and url change with javascript will also have the new
# url as it's href attribute.
if target.tagName == "A" and
target.origin == document.location.origin and
# Clicking the link will change the url of this frame.
(target.pathName != document.location.pathName or
target.search != document.location.search) and
(target.target in ["", "_self"] or
(target.target == "_parent" and window.parent == window) or
(target.target == "_top" and window.top == window))
return new GrabBackFocus()
else
target = target.parentElement
true
# Only exported for tests.
window.initializeModes = ->
class NormalMode extends Mode
constructor: ->
super
name: "normal"
indicator: false # There is no mode indicator in normal mode.
keydown: (event) => onKeydown.call @, event
keypress: (event) => onKeypress.call @, event
keyup: (event) => onKeyup.call @, event
# Install the permanent modes. The permanently-installed insert mode tracks focus/blur events, and
# activates/deactivates itself accordingly.
new NormalMode
new PassKeysMode
new InsertMode permanent: true
Scroller.init()
#
# Complete initialization work that sould be done prior to DOMReady.
#
initializePreDomReady = ->
checkIfEnabledForUrl()
refreshCompletionKeys()
# Send the key to the key handler in the background page.
keyPort = chrome.runtime.connect({ name: "keyDown" })
# If the port is closed, the background page has gone away (since we never close it ourselves). Disable all
# our event listeners, and stub out chrome.runtime.sendMessage/connect (to prevent errors).
# TODO(mrmr1993): Do some actual cleanup to free resources, hide UI, etc.
keyPort.onDisconnect.addListener ->
isEnabledForUrl = false
chrome.runtime.sendMessage = ->
chrome.runtime.connect = ->
window.removeEventListener "focus", onFocus
requestHandlers =
showHUDforDuration: handleShowHUDforDuration
toggleHelpDialog: (request) -> toggleHelpDialog(request.dialogHtml, request.frameId)
focusFrame: (request) -> if (frameId == request.frameId) then focusThisFrame request
refreshCompletionKeys: refreshCompletionKeys
getScrollPosition: -> scrollX: window.scrollX, scrollY: window.scrollY
setScrollPosition: setScrollPosition
executePageCommand: executePageCommand
currentKeyQueue: (request) ->
keyQueue = request.keyQueue
handlerStack.bubbleEvent "registerKeyQueue", { keyQueue: keyQueue }
# A frame has received the focus. We don't care here (the Vomnibar/UI-component handles this).
frameFocused: ->
checkEnabledAfterURLChange: checkEnabledAfterURLChange
chrome.runtime.onMessage.addListener (request, sender, sendResponse) ->
# In the options page, we will receive requests from both content and background scripts. ignore those
# from the former.
return if sender.tab and not sender.tab.url.startsWith 'chrome-extension://'
# These requests are delivered to the options page, but there are no handlers there.
return if request.handler in [ "registerFrame", "frameFocused", "unregisterFrame", "setIcon" ]
shouldHandleRequest = isEnabledForUrl
# We always handle the message if it's one of these listed message types.
shouldHandleRequest ||= request.name in [ "executePageCommand", "checkEnabledAfterURLChange" ]
# Requests with a frameId of zero should always and only be handled in the main/top frame (regardless of
# whether Vimium is enabled there).
if request.frameId == 0 and DomUtils.isTopFrame()
request.frameId = frameId
shouldHandleRequest = true
sendResponse requestHandlers[request.name](request, sender) if shouldHandleRequest
# Ensure the sendResponse callback is freed.
false
# Wrapper to install event listeners. Syntactic sugar.
installListener = (element, event, callback) ->
element.addEventListener(event, ->
if isEnabledForUrl then callback.apply(this, arguments) else true
, true)
#
# Installing or uninstalling listeners is error prone. Instead we elect to check isEnabledForUrl each time so
# we know whether the listener should run or not.
# Run this as early as possible, so the page can't register any event handlers before us.
# Note: We install the listeners even if Vimium is disabled. See comment in commit
# 6446cf04c7b44c3d419dc450a73b60bcaf5cdf02.
#
installedListeners = false
window.installListeners = ->
unless installedListeners
initializeModes()
# Key event handlers fire on window before they do on document. Prefer window for key events so the page
# can't set handlers to grab the keys before us.
for type in [ "keydown", "keypress", "keyup", "click", "focus", "blur", "mousedown", "scroll" ]
do (type) -> installListener window, type, (event) -> handlerStack.bubbleEvent type, event
installListener document, "DOMActivate", (event) -> handlerStack.bubbleEvent 'DOMActivate', event
installedListeners = true
# Other once-only initialisation.
FindModeHistory.init()
new GrabBackFocus if isEnabledForUrl
#
# Whenever we get the focus:
# - Tell the background page this frame's URL.
# - Check if we should be enabled.
#
onFocus = (event) ->
if event.target == window
chrome.runtime.sendMessage handler: "frameFocused", frameId: frameId
checkIfEnabledForUrl true
# 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
#
# Initialization tasks that must wait for the document to be ready.
#
initializeOnDomReady = ->
# Tell the background page we're in the dom ready state.
chrome.runtime.connect({ name: "domReady" })
CursorHider.init()
# We only initialize the vomnibar in the tab's main frame, because it's only ever opened there.
Vomnibar.init() if DomUtils.isTopFrame()
HUD.init()
registerFrame = ->
# Don't register frameset containers; focusing them is no use.
unless document.body?.tagName.toLowerCase() == "frameset"
chrome.runtime.sendMessage
handler: "registerFrame"
frameId: frameId
# Unregister the frame if we're going to exit.
unregisterFrame = ->
chrome.runtime.sendMessage
handler: "unregisterFrame"
frameId: frameId
tab_is_closing: DomUtils.isTopFrame()
executePageCommand = (request) ->
commandType = request.command.split(".")[0]
# Vomnibar commands are handled in the tab's main/top frame. They are handled even if Vimium is otherwise
# disabled in the frame.
if commandType == "Vomnibar"
if DomUtils.isTopFrame()
# We pass the frameId from request. That's the frame which originated the request, so that's the frame
# which should receive the focus when the vomnibar closes.
Utils.invokeCommandString request.command, [ request.frameId, request.registryEntry ]
refreshCompletionKeys request
return
# All other commands are handled in their frame (but only if Vimium is enabled).
return unless frameId == request.frameId and isEnabledForUrl
if request.registryEntry.passCountToFunction
Utils.invokeCommandString(request.command, [request.count])
else
Utils.invokeCommandString(request.command) for i in [0...request.count]
refreshCompletionKeys(request)
handleShowHUDforDuration = ({ text, duration }) ->
if DomUtils.isTopFrame()
DomUtils.documentReady -> HUD.showForDuration text, duration
setScrollPosition = ({ scrollX, scrollY }) ->
if DomUtils.isTopFrame()
DomUtils.documentReady ->
window.focus()
document.body.focus()
if 0 < scrollX or 0 < scrollY
Marks.setPreviousPosition()
window.scrollTo scrollX, scrollY
#
# Called from the backend in order to change frame focus.
#
setTimeout (->
# Create a shadow DOM wrapping the frame so the page's styles don't interfere with ours.
highlightedFrameElement = DomUtils.createElement "div"
# PhantomJS doesn't support createShadowRoot, so guard against its non-existance.
_shadowDOM = highlightedFrameElement.createShadowRoot?() ? highlightedFrameElement
# Inject stylesheet.
_styleSheet = DomUtils.createElement "style"
_styleSheet.innerHTML = "@import url(\"#{chrome.runtime.getURL("content_scripts/vimium.css")}\");"
_shadowDOM.appendChild _styleSheet
_frameEl = DomUtils.createElement "div"
_frameEl.className = "vimiumReset vimiumHighlightedFrame"
_shadowDOM.appendChild _frameEl
window.focusThisFrame = (request) ->
if window.innerWidth < 3 or window.innerHeight < 3
# This frame is too small to focus. Cancel and tell the background frame to focus the next one instead.
# This affects sites like Google Inbox, which have many tiny iframes. See #1317.
# Here we're assuming that there is at least one frame large enough to focus.
chrome.runtime.sendMessage({ handler: "nextFrame", frameId: frameId })
return
window.focus()
shouldHighlight = request.highlight
shouldHighlight ||= request.highlightOnlyIfNotTop and not DomUtils.isTopFrame()
if shouldHighlight
document.documentElement.appendChild highlightedFrameElement
setTimeout (-> highlightedFrameElement.remove()), 200
), 0
extend window,
scrollToBottom: ->
Marks.setPreviousPosition()
Scroller.scrollTo "y", "max"
scrollToTop: ->
Marks.setPreviousPosition()
Scroller.scrollTo "y", 0
scrollToLeft: -> Scroller.scrollTo "x", 0
scrollToRight: -> Scroller.scrollTo "x", "max"
scrollUp: -> Scroller.scrollBy "y", -1 * Settings.get("scrollStepSize")
scrollDown: -> Scroller.scrollBy "y", Settings.get("scrollStepSize")
scrollPageUp: -> Scroller.scrollBy "y", "viewSize", -1/2
scrollPageDown: -> Scroller.scrollBy "y", "viewSize", 1/2
scrollFullPageUp: -> Scroller.scrollBy "y", "viewSize", -1
scrollFullPageDown: -> Scroller.scrollBy "y", "viewSize"
scrollLeft: -> Scroller.scrollBy "x", -1 * Settings.get("scrollStepSize")
scrollRight: -> Scroller.scrollBy "x", Settings.get("scrollStepSize")
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
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: url, selected: true })
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()
enterVisualLineMode: ->
new VisualLineMode
enterEditMode: ->
@focusInput 1, EditMode
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
rect = DomUtils.getVisibleClientRect element, true
continue if rect == null
{ element: element, rect: rect }
if visibleInputs.length == 0
HUD.showForDuration("There are no inputs to focus.", 1000)
return
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'
# Deactivate any active modes on this element (PostFindMode, or a suspended edit mode).
@deactivateSingleton visibleInputs[selectedInputIndex].element
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"
# Deactivate any active modes on this element (PostFindMode, or a suspended edit mode).
@deactivateSingleton visibleInputs[selectedInputIndex].element
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: document.activeElement
targetElement: document.activeElement
indicator: false
# Track which keydown events we have handled, so that we can subsequently suppress the corresponding keyup
# event.
KeydownEvents =
handledEvents: {}
stringify: (event) ->
JSON.stringify
metaKey: event.metaKey
altKey: event.altKey
ctrlKey: event.ctrlKey
keyIdentifier: event.keyIdentifier
keyCode: event.keyCode
push: (event) ->
@handledEvents[@stringify event] = true
# Yields truthy or falsy depending upon whether a corresponding keydown event is present (and removes that
# event).
pop: (event) ->
detailString = @stringify event
value = @handledEvents[detailString]
delete @handledEvents[detailString]
value
#
# Sends everything except i & ESC to the handler in background_page. i & ESC are special because they control
# insert mode which is local state to the page. The key will be are either a single ascii letter or a
# key-modifier pair, e.g. for control a.
#
# Note that some keys will only register keydown events and not keystroke events, e.g. ESC.
#
# @/this, here, is the the normal-mode Mode object.
onKeypress = (event) ->
keyChar = ""
# Ignore modifier keys by themselves.
if (event.keyCode > 31)
keyChar = String.fromCharCode(event.charCode)
if (keyChar)
if currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)
DomUtils.suppressEvent(event)
keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
return @stopBubblingAndTrue
keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
return @continueBubbling
# @/this, here, is the the normal-mode Mode object.
onKeydown = (event) ->
keyChar = ""
# handle special keys, and normal input keys with modifiers being pressed. don't handle shiftKey alone (to
# avoid / being interpreted as ?
if (((event.metaKey || event.ctrlKey || event.altKey) && event.keyCode > 31) || (
# TODO(philc): some events don't have a keyidentifier. How is that possible?
event.keyIdentifier && event.keyIdentifier.slice(0, 2) != "U+"))
keyChar = KeyboardUtils.getKeyChar(event)
# Again, ignore just modifiers. Maybe this should replace the keyCode>31 condition.
if (keyChar != "")
modifiers = []
if (event.shiftKey)
keyChar = keyChar.toUpperCase()
if (event.metaKey)
modifiers.push("m")
if (event.ctrlKey)
modifiers.push("c")
if (event.altKey)
modifiers.push("a")
for i of modifiers
keyChar = modifiers[i] + "-" + keyChar
if (modifiers.length > 0 || keyChar.length > 1)
keyChar = "<" + keyChar + ">"
if (isShowingHelpDialog && KeyboardUtils.isEscape(event))
hideHelpDialog()
DomUtils.suppressEvent event
KeydownEvents.push event
return @stopBubblingAndTrue
else
if (keyChar)
if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar))
DomUtils.suppressEvent event
KeydownEvents.push event
keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
return @stopBubblingAndTrue
keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
else if (KeyboardUtils.isEscape(event))
keyPort.postMessage({ keyChar:"", frameId:frameId })
# Added to prevent propagating this event to other listeners if it's one that'll trigger a Vimium command.
# The goal is to avoid the scenario where Google Instant Search uses every keydown event to dump us
# back into the search box. As a side effect, this should also prevent overriding by other sites.
#
# Subject to internationalization issues since we're using keyIdentifier instead of charCode (in keypress).
#
# TOOD(ilya): Revisit this. Not sure it's the absolute best approach.
if keyChar == "" &&
(currentCompletionKeys.indexOf(KeyboardUtils.getKeyChar(event)) != -1 ||
isValidFirstKey(KeyboardUtils.getKeyChar(event)))
DomUtils.suppressPropagation(event)
KeydownEvents.push event
return @stopBubblingAndTrue
return @continueBubbling
# @/this, here, is the the normal-mode Mode object.
onKeyup = (event) ->
return @continueBubbling unless KeydownEvents.pop event
DomUtils.suppressPropagation(event)
@stopBubblingAndTrue
# 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.
checkIfEnabledForUrl = (frameIsFocused = windowIsFocused()) ->
url = window.location.toString()
chrome.runtime.sendMessage { handler: "isEnabledForUrl", url: url, frameIsFocused: frameIsFocused }, (response) ->
{ isEnabledForUrl, passKeys } = response
installListeners() # But only if they have not been installed already.
if HUD.isReady() and not isEnabledForUrl
# Quickly hide any HUD we might already be showing, e.g. if we entered insert mode on page load.
HUD.hide()
handlerStack.bubbleEvent "registerStateChange",
enabled: isEnabledForUrl
passKeys: passKeys
# Update the page icon, if necessary.
if windowIsFocused()
chrome.runtime.sendMessage
handler: "setIcon"
icon:
if isEnabledForUrl and not passKeys then "enabled"
else if isEnabledForUrl then "partial"
else "disabled"
null
# 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 = ->
checkIfEnabledForUrl() if windowIsFocused()
# Exported to window, but only for DOM tests.
window.refreshCompletionKeys = (response) ->
if (response)
currentCompletionKeys = response.completionKeys
if (response.validFirstKeys)
validFirstKeys = response.validFirstKeys
else
chrome.runtime.sendMessage({ handler: "getCompletionKeys" }, refreshCompletionKeys)
isValidFirstKey = (keyChar) ->
validFirstKeys[keyChar] || /^[1-9]/.test(keyChar)
window.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()
# sends us into insert mode if possible, but does not.
# corresponds approximately to 'nevermind, I have found it already' while means 'I want to save
# this query and do more searches with it'
window.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)
window.performFind = -> findAndFocus()
window.performBackwardsFind = -> findAndFocus(true)
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()
linkElement.focus()
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)
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))
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.
window.enterFindMode = ->
Marks.setPreviousPosition()
new FindMode()
window.showHelpDialog = (html, fid) ->
return if (isShowingHelpDialog || !document.body || fid != frameId)
isShowingHelpDialog = true
container = DomUtils.createElement("div")
container.id = "vimiumHelpDialogContainer"
container.className = "vimiumReset"
document.body.appendChild(container)
container.innerHTML = html
container.getElementsByClassName("closeButton")[0].addEventListener("click", hideHelpDialog, false)
VimiumHelpDialog =
# This setting is pulled out of local storage. It's false by default.
getShowAdvancedCommands: -> Settings.get("helpDialog_showAdvancedCommands")
init: () ->
this.dialogElement = document.getElementById("vimiumHelpDialog")
this.dialogElement.getElementsByClassName("toggleAdvancedCommands")[0].addEventListener("click",
VimiumHelpDialog.toggleAdvancedCommands, false)
this.dialogElement.style.maxHeight = window.innerHeight - 80
this.showAdvancedCommands(this.getShowAdvancedCommands())
#
# Advanced commands are hidden by default so they don't overwhelm new and casual users.
#
toggleAdvancedCommands: (event) ->
event.preventDefault()
showAdvanced = VimiumHelpDialog.getShowAdvancedCommands()
VimiumHelpDialog.showAdvancedCommands(!showAdvanced)
Settings.set("helpDialog_showAdvancedCommands", !showAdvanced)
showAdvancedCommands: (visible) ->
VimiumHelpDialog.dialogElement.getElementsByClassName("toggleAdvancedCommands")[0].innerHTML =
if visible then "Hide advanced commands" else "Show advanced commands"
advancedEls = VimiumHelpDialog.dialogElement.getElementsByClassName("advanced")
for el in advancedEls
el.style.display = if visible then "table-row" else "none"
VimiumHelpDialog.init()
container.getElementsByClassName("optionsPage")[0].addEventListener("click", (clickEvent) ->
clickEvent.preventDefault()
chrome.runtime.sendMessage({handler: "openOptionsPageInNewTab"})
false)
# Simulating a click on the help dialog makes it the active element for scrolling.
DomUtils.simulateClick document.getElementById "vimiumHelpDialog"
hideHelpDialog = (clickEvent) ->
isShowingHelpDialog = false
helpDialog = document.getElementById("vimiumHelpDialogContainer")
if (helpDialog)
helpDialog.parentNode.removeChild(helpDialog)
if (clickEvent)
clickEvent.preventDefault()
toggleHelpDialog = (html, fid) ->
if (isShowingHelpDialog)
hideHelpDialog()
else
showHelpDialog(html, fid)
CursorHider =
#
# Hide the cursor when the browser scrolls, and prevent mouse from hovering while invisible.
#
cursorHideStyle: null
isScrolling: false
onScroll: (event) ->
CursorHider.isScrolling = true
unless CursorHider.cursorHideStyle.parentElement
document.head.appendChild CursorHider.cursorHideStyle
onMouseMove: (event) ->
if CursorHider.cursorHideStyle.parentElement and not CursorHider.isScrolling
CursorHider.cursorHideStyle.remove()
CursorHider.isScrolling = false
init: ->
# Temporarily disabled pending consideration of #1359 (in particular, whether cursor hiding is too fragile
# as to provide a consistent UX).
return
# Disable cursor hiding for Chrome versions less than 39.0.2171.71 due to a suspected browser error.
# See #1345 and #1348.
return unless Utils.haveChromeVersion "39.0.2171.71"
@cursorHideStyle = DomUtils.createElement("style")
@cursorHideStyle.innerHTML = """
body * {pointer-events: none !important; cursor: none !important;}
body, html {cursor: none !important;}
"""
window.addEventListener "mousemove", @onMouseMove
window.addEventListener "scroll", @onScroll
initializePreDomReady()
DomUtils.documentReady initializeOnDomReady
DomUtils.documentReady registerFrame
window.addEventListener "unload", unregisterFrame
window.onbeforeunload = ->
chrome.runtime.sendMessage(
handler: "updateScrollPosition"
scrollX: window.scrollX
scrollY: window.scrollY)
root = exports ? window
root.handlerStack = handlerStack
root.frameId = frameId
root.windowIsFocused = windowIsFocused
root.bgLog = bgLog