#
# 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".
#
insertModeLock = null
findMode = false
findModeQuery = { rawQuery: "" }
findModeQueryHasResults = false
findModeAnchorNode = null
isShowingHelpDialog = false
handlerStack = new HandlerStack
keyPort = null
# Users can disable Vimium on URL patterns via the settings page.
isEnabledForUrl = true
# The user's operating system.
currentCompletionKeys = null
validFirstKeys = null
activatedElement = null
# 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"]
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)
)()
#
# settings provides a browser-global localStorage-backed dict. get() and set() are synchronous, but load()
# must be called beforehand to ensure get() will return up-to-date values.
#
settings =
port: null
values: {}
loadedValues: 0
valuesToLoad: ["scrollStepSize", "linkHintCharacters", "filterLinkHints", "hideHud", "previousPatterns",
"nextPatterns", "findModeRawQuery", "regexFindMode", "userDefinedLinkHintCss",
"helpDialog_showAdvancedCommands"]
isLoaded: false
eventListeners: {}
init: ->
@port = chrome.extension.connect({ name: "settings" })
@port.onMessage.addListener(@receiveMessage)
get: (key) -> @values[key]
set: (key, value) ->
@init() unless @port
@values[key] = value
@port.postMessage({ operation: "set", key: key, value: value })
load: ->
@init() unless @port
for i of @valuesToLoad
@port.postMessage({ operation: "get", key: @valuesToLoad[i] })
receiveMessage: (args) ->
# not using 'this' due to issues with binding on callback
settings.values[args.key] = args.value
# since load() can be called more than once, loadedValues can be greater than valuesToLoad, but we test
# for equality so initializeOnReady only runs once
if (++settings.loadedValues == settings.valuesToLoad.length)
settings.isLoaded = true
listener = null
while (listener = settings.eventListeners["load"].pop())
listener()
addEventListener: (eventName, callback) ->
if (!(eventName of @eventListeners))
@eventListeners[eventName] = []
@eventListeners[eventName].push(callback)
#
# Give this frame a unique id.
#
frameId = Math.floor(Math.random()*999999999)
hasModifiersRegex = /^<([amc]-)+.>/
#
# Complete initialization work that sould be done prior to DOMReady.
#
initializePreDomReady = ->
settings.addEventListener("load", LinkHints.init.bind(LinkHints))
settings.load()
checkIfEnabledForUrl()
refreshCompletionKeys()
# Send the key to the key handler in the background page.
keyPort = chrome.extension.connect({ name: "keyDown" })
requestHandlers =
hideUpgradeNotification: -> HUD.hideUpgradeNotification()
showUpgradeNotification: (request) -> HUD.showUpgradeNotification(request.version)
toggleHelpDialog: (request) -> toggleHelpDialog(request.dialogHtml, request.frameId)
focusFrame: (request) -> if (frameId == request.frameId) then focusThisFrame(request.highlight)
refreshCompletionKeys: refreshCompletionKeys
getScrollPosition: -> scrollX: window.scrollX, scrollY: window.scrollY
setScrollPosition: (request) -> setScrollPosition request.scrollX, request.scrollY
executePageCommand: executePageCommand
getActiveState: -> { enabled: isEnabledForUrl }
disableVimium: disableVimium
chrome.extension.onRequest.addListener (request, sender, sendResponse) ->
# in the options page, we will receive requests from both content and background scripts. ignore those
# from the former.
return unless sender.tab?.url.startsWith 'chrome-extension://'
return unless isEnabledForUrl or request.name == 'getActiveState'
sendResponse requestHandlers[request.name](request, sender)
# Ensure the sendResponse callback is freed.
false
#
# This is called once the background page has told us that Vimium should be enabled for the current URL.
#
initializeWhenEnabled = ->
document.addEventListener("keydown", onKeydown, true)
document.addEventListener("keypress", onKeypress, true)
document.addEventListener("keyup", onKeyup, true)
document.addEventListener("focus", onFocusCapturePhase, true)
document.addEventListener("blur", onBlurCapturePhase, true)
document.addEventListener("DOMActivate", onDOMActivate, true)
enterInsertModeIfElementIsFocused()
#
# Used to disable Vimium without needing to reload the page.
# This is called if the current page's url is blacklisted using the popup UI.
#
disableVimium = ->
document.removeEventListener("keydown", onKeydown, true)
document.removeEventListener("keypress", onKeypress, true)
document.removeEventListener("keyup", onKeyup, true)
document.removeEventListener("focus", onFocusCapturePhase, true)
document.removeEventListener("blur", onBlurCapturePhase, true)
document.removeEventListener("DOMActivate", onDOMActivate, true)
isEnabledForUrl = false
#
# The backend needs to know which frame has focus.
#
window.addEventListener "focus", ->
# settings may have changed since the frame last had focus
settings.load()
chrome.extension.sendRequest({ handler: "frameFocused", frameId: frameId })
#
# Initialization tasks that must wait for the document to be ready.
#
initializeOnDomReady = ->
registerFrameIfSizeAvailable(window.top == window.self)
enterInsertModeIfElementIsFocused() if isEnabledForUrl
# Tell the background page we're in the dom ready state.
chrome.extension.connect({ name: "domReady" })
# This is a little hacky but sometimes the size wasn't available on domReady?
registerFrameIfSizeAvailable = (is_top) ->
if (innerWidth != undefined && innerWidth != 0 && innerHeight != undefined && innerHeight != 0)
chrome.extension.sendRequest(
handler: "registerFrame"
frameId: frameId
area: innerWidth * innerHeight
is_top: is_top
total: frames.length + 1)
else
setTimeout((-> registerFrameIfSizeAvailable(is_top)), 100)
#
# Enters insert mode if the currently focused element in the DOM is focusable.
#
enterInsertModeIfElementIsFocused = ->
if (document.activeElement && isEditable(document.activeElement) && !findMode)
enterInsertModeWithoutShowingIndicator(document.activeElement)
onDOMActivate = (event) -> activatedElement = event.target
executePageCommand = (request) ->
return unless frameId == request.frameId
if (request.passCountToFunction)
Utils.invokeCommandString(request.command, [request.count])
else
Utils.invokeCommandString(request.command) for i in [0...request.count]
refreshCompletionKeys(request)
#
# activatedElement is different from document.activeElement -- the latter seems to be reserved mostly for
# input elements. This mechanism allows us to decide whether to scroll a div or to scroll the whole document.
#
scrollActivatedElementBy = (direction, amount) ->
# if this is called before domReady, just use the window scroll function
if (!document.body)
if (direction == "x")
window.scrollBy(amount, 0)
else
window.scrollBy(0, amount)
return
# TODO refactor and put this together with the code in getVisibleClientRect
isRendered = (element) ->
computedStyle = window.getComputedStyle(element, null)
return !(computedStyle.getPropertyValue("visibility") != "visible" ||
computedStyle.getPropertyValue("display") == "none")
if (!activatedElement || !isRendered(activatedElement))
activatedElement = document.body
scrollName = if (direction == "x") then "scrollLeft" else "scrollTop"
# Chrome does not report scrollHeight accurately for nodes with pseudo-elements of height 0 (bug 110149).
# Therefore we just try to increase scrollTop blindly -- if it fails we know we have reached the end of the
# content.
if (amount != 0)
element = activatedElement
loop
oldScrollValue = element[scrollName]
element[scrollName] += amount
lastElement = element
# we may have an orphaned element. if so, just scroll the body element.
element = element.parentElement || document.body
break unless (lastElement[scrollName] == oldScrollValue && lastElement != document.body)
# if the activated element has been scrolled completely offscreen, subsequent changes in its scroll
# position will not provide any more visual feedback to the user. therefore we deactivate it so that
# subsequent scrolls only move the parent element.
rect = activatedElement.getBoundingClientRect()
if (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth)
activatedElement = lastElement
setScrollPosition = (scrollX, scrollY) ->
if (scrollX > 0 || scrollY > 0)
DomUtils.documentReady(-> window.scrollBy(scrollX, scrollY))
#
# Called from the backend in order to change frame focus.
#
window.focusThisFrame = (shouldHighlight) ->
window.focus()
if (document.body && shouldHighlight)
borderWas = document.body.style.border
document.body.style.border = '5px solid yellow'
setTimeout((-> document.body.style.border = borderWas), 200)
extend window,
scrollToBottom: -> window.scrollTo(window.pageXOffset, document.body.scrollHeight)
scrollToTop: -> window.scrollTo(window.pageXOffset, 0)
scrollToLeft: -> window.scrollTo(0, window.pageYOffset)
scrollToRight: -> window.scrollTo(document.body.scrollWidth, window.pageYOffset)
scrollUp: -> scrollActivatedElementBy("y", -1 * settings.get("scrollStepSize"))
scrollDown: ->
scrollActivatedElementBy("y", parseFloat(settings.get("scrollStepSize")))
scrollPageUp: -> scrollActivatedElementBy("y", -1 * window.innerHeight / 2)
scrollPageDown: -> scrollActivatedElementBy("y", window.innerHeight / 2)
scrollFullPageUp: -> scrollActivatedElementBy("y", -window.innerHeight)
scrollFullPageDown: -> scrollActivatedElementBy("y", window.innerHeight)
scrollLeft: -> scrollActivatedElementBy("x", -1 * settings.get("scrollStepSize"))
scrollRight: -> scrollActivatedElementBy("x", parseFloat(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('/')
toggleViewSource: ->
chrome.extension.sendRequest { handler: "getCurrentTabUrl" }, (url) ->
if (url.substr(0, 12) == "view-source:")
url = url.substr(12, url.length - 12)
else
url = "view-source:" + url
chrome.extension.sendRequest({ 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.extension.sendRequest { handler: "getCurrentTabUrl" }, (url) ->
chrome.extension.sendRequest { handler: "copyToClipboard", data: url }
HUD.showForDuration("Yanked URL", 1000)
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)
rect = DomUtils.getVisibleClientRect(element)
continue if rect == null
{ element: element, rect: rect }
return if visibleInputs.length == 0
selectedInputIndex = Math.min(count - 1, visibleInputs.length - 1)
visibleInputs[selectedInputIndex].element.focus()
return if visibleInputs.length == 1
hints = for tuple in visibleInputs
hint = document.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
hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
hintContainingDiv = DomUtils.addElementList(hints,
{ id: "vimiumInputMarkerContainer", className: "vimiumReset" })
handlerStack.push keydown: (event) ->
if event.keyCode == KeyboardUtils.keyCodes.tab
hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint'
if event.shiftKey
if --selectedInputIndex == -1
selectedInputIndex = hints.length - 1
else
if ++selectedInputIndex == hints.length
selectedInputIndex = 0
hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
visibleInputs[selectedInputIndex].element.focus()
else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey
DomUtils.removeElement hintContainingDiv
@remove()
return true
false
#
# 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.
#
onKeypress = (event) ->
return unless handlerStack.bubbleEvent('keypress', event)
keyChar = ""
# Ignore modifier keys by themselves.
if (event.keyCode > 31)
keyChar = String.fromCharCode(event.charCode)
# Enter insert mode when the user enables the native find interface.
if (keyChar == "f" && KeyboardUtils.isPrimaryModifierKey(event))
enterInsertModeWithoutShowingIndicator()
return
if (keyChar)
if (findMode)
handleKeyCharForFindMode(keyChar)
DomUtils.suppressEvent(event)
else if (!isInsertMode() && !findMode)
if (currentCompletionKeys.indexOf(keyChar) != -1)
DomUtils.suppressEvent(event)
keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
onKeydown = (event) ->
return unless handlerStack.bubbleEvent('keydown', 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) ||
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 (isInsertMode() && KeyboardUtils.isEscape(event))
# Note that we can't programmatically blur out of Flash embeds from Javascript.
if (!isEmbed(event.srcElement))
# Remove focus so the user can't just get himself back into insert mode by typing in the same input
# box.
if (isEditable(event.srcElement))
event.srcElement.blur()
exitInsertMode()
DomUtils.suppressEvent(event)
else if (findMode)
if (KeyboardUtils.isEscape(event))
handleEscapeForFindMode()
DomUtils.suppressEvent(event)
else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey)
handleDeleteForFindMode()
DomUtils.suppressEvent(event)
else if (event.keyCode == keyCodes.enter)
handleEnterForFindMode()
DomUtils.suppressEvent(event)
else if (!modifiers)
event.stopPropagation()
else if (isShowingHelpDialog && KeyboardUtils.isEscape(event))
hideHelpDialog()
else if (!isInsertMode() && !findMode)
if (keyChar)
if (currentCompletionKeys.indexOf(keyChar) != -1)
DomUtils.suppressEvent(event)
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 @ Not sure it's the absolute best approach.
if (keyChar == "" && !isInsertMode() &&
(currentCompletionKeys.indexOf(KeyboardUtils.getKeyChar(event)) != -1 ||
isValidFirstKey(KeyboardUtils.getKeyChar(event))))
event.stopPropagation()
onKeyup = (event) -> return unless handlerStack.bubbleEvent('keyup', event)
checkIfEnabledForUrl = ->
url = window.location.toString()
chrome.extension.sendRequest { handler: "isEnabledForUrl", url: url }, (response) ->
isEnabledForUrl = response.isEnabledForUrl
if (isEnabledForUrl)
initializeWhenEnabled()
else if (HUD.isReady())
# Quickly hide any HUD we might already be showing, e.g. if we entered insert mode on page load.
HUD.hide()
refreshCompletionKeys = (response) ->
if (response)
currentCompletionKeys = response.completionKeys
if (response.validFirstKeys)
validFirstKeys = response.validFirstKeys
else
chrome.extension.sendRequest({ handler: "getCompletionKeys" }, refreshCompletionKeys)
isValidFirstKey = (keyChar) ->
validFirstKeys[keyChar] || /[1-9]/.test(keyChar)
onFocusCapturePhase = (event) ->
if (isFocusable(event.target) && !findMode)
enterInsertModeWithoutShowingIndicator(event.target)
onBlurCapturePhase = (event) ->
if (isFocusable(event.target))
exitInsertMode(event.target)
#
# Returns true if the element is focusable. This includes embeds like Flash, which steal the keybaord focus.
#
isFocusable = (element) -> isEditable(element) || isEmbed(element)
#
# Embedded elements like Flash and quicktime players can obtain focus but cannot be programmatically
# unfocused.
#
isEmbed = (element) -> ["embed", "object"].indexOf(element.nodeName.toLowerCase()) > 0
#
# Input or text elements are considered focusable and able to receieve their own keyboard events,
# and will enter enter mode if focused. Also note that the "contentEditable" attribute can be set on
# any element which makes it a rich text editor, like the notes on jjot.com.
#
isEditable = (target) ->
return true if target.isContentEditable
nodeName = target.nodeName.toLowerCase()
# use a blacklist instead of a whitelist because new form controls are still being implemented for html5
noFocus = ["radio", "checkbox"]
if (nodeName == "input" && noFocus.indexOf(target.type) == -1)
return true
focusableElements = ["textarea", "select"]
focusableElements.indexOf(nodeName) >= 0
#
# Enters insert mode and show an "Insert mode" message. Showing the UI is only useful when entering insert
# mode manually by pressing "i". In most cases we do not show any UI (enterInsertModeWithoutShowingIndicator)
#
window.enterInsertMode = (target) ->
enterInsertModeWithoutShowingIndicator(target)
HUD.show("Insert mode")
#
# We cannot count on 'focus' and 'blur' events to happen sequentially. For example, if blurring element A
# causes element B to come into focus, we may get "B focus" before "A blur". Thus we only leave insert mode
# when the last editable element that came into focus -- which insertModeLock points to -- has been blurred.
# If insert mode is entered manually (via pressing 'i'), then we set insertModeLock to 'undefined', and only
# leave insert mode when the user presses .
#
enterInsertModeWithoutShowingIndicator = (target) -> insertModeLock = target
exitInsertMode = (target) ->
if (target == undefined || insertModeLock == target)
insertModeLock = null
HUD.hide()
isInsertMode = -> insertModeLock != null
# should be called whenever rawQuery is modified.
updateFindModeQuery = ->
# the query can be treated differently (e.g. as a plain string versus regex depending on the presence of
# escape sequences. '\' is the escape character and needs to be escaped itself to be used as a normal
# character. here we grep for the relevant escape sequences.
findModeQuery.isRegex = settings.get 'regexFindMode'
hasNoIgnoreCaseFlag = false
findModeQuery.parsedQuery = findModeQuery.rawQuery.replace /\\./g, (match) ->
switch (match)
when "\\r"
findModeQuery.isRegex = true
return ""
when "\\R"
findModeQuery.isRegex = false
return ""
when "\\I"
hasNoIgnoreCaseFlag = true
return ""
when "\\\\"
return "\\"
else
return match
# default to 'smartcase' mode, unless noIgnoreCase is explicitly specified
findModeQuery.ignoreCase = !hasNoIgnoreCaseFlag && !/[A-Z]/.test(findModeQuery.parsedQuery)
# if we are dealing with a regex, grep for all matches in the text, and then call window.find() on them
# sequentially so the browser handles the scrolling / text selection.
if findModeQuery.isRegex
try
pattern = new RegExp(findModeQuery.parsedQuery, "g" + (if findModeQuery.ignoreCase then "i" else ""))
catch error
# if we catch a SyntaxError, assume the user is not done typing yet and return quietly
return
# innerText will not return the text of hidden elements, and strip out tags while preserving newlines
text = document.body.innerText
findModeQuery.regexMatches = text.match(pattern)
findModeQuery.activeRegexIndex = 0
handleKeyCharForFindMode = (keyChar) ->
findModeQuery.rawQuery += keyChar
updateFindModeQuery()
performFindInPlace()
showFindModeHUDForQuery()
handleEscapeForFindMode = ->
exitFindMode()
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()
handleDeleteForFindMode = ->
if (findModeQuery.rawQuery.length == 0)
exitFindMode()
performFindInPlace()
else
findModeQuery.rawQuery = findModeQuery.rawQuery.substring(0, findModeQuery.rawQuery.length - 1)
updateFindModeQuery()
performFindInPlace()
showFindModeHUDForQuery()
# 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'
handleEnterForFindMode = ->
exitFindMode()
focusFoundLink()
document.body.classList.add("vimiumFindMode")
settings.set("findModeRawQuery", findModeQuery.rawQuery)
performFindInPlace = ->
cachedScrollX = window.scrollX
cachedScrollY = window.scrollY
query = if findModeQuery.isRegex then getNextQueryFromRegexMatches(0) else findModeQuery.parsedQuery
# Search backwards first to "free up" the current word as eligible for the real forward search. This allows
# us to search in place without jumping around between matches as the query grows.
executeFind(query, { backwards: true, caseSensitive: !findModeQuery.ignoreCase })
# We need to restore the scroll position because we might've lost the right position by searching
# backwards.
window.scrollTo(cachedScrollX, cachedScrollY)
findModeQueryHasResults = executeFind(query, { caseSensitive: !findModeQuery.ignoreCase })
# :options is an optional dict. valid parameters are 'caseSensitive' and 'backwards'.
executeFind = (query, options) ->
options = options || {}
# rather hacky, but this is our way of signalling to the insertMode listener not to react to the focus
# changes that find() induces.
oldFindMode = findMode
findMode = true
document.body.classList.add("vimiumFindMode")
# prevent find from matching its own search query in the HUD
HUD.hide(true)
# ignore the selectionchange event generated by find()
document.removeEventListener("selectionchange",restoreDefaultSelectionHighlight, true)
result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false)
setTimeout(
-> document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true)
0)
findMode = oldFindMode
# we need to save the anchor node here because seems to nullify it, regardless of whether we do
# preventDefault()
findModeAnchorNode = document.getSelection().anchorNode
result
restoreDefaultSelectionHighlight = -> document.body.classList.remove("vimiumFindMode")
focusFoundLink = ->
if (findModeQueryHasResults)
link = getLinkFromSelection()
link.focus() if link
isDOMDescendant = (parent, child) ->
node = child
while (node != null)
return true if (node == parent)
node = node.parentNode
false
selectFoundInputElement = ->
# if the found text is in an input element, getSelection().anchorNode will be null, so we use activeElement
# instead. however, 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.
if (findModeQueryHasResults && document.activeElement &&
DomUtils.isSelectable(document.activeElement) &&
isDOMDescendant(findModeAnchorNode, document.activeElement))
DomUtils.simulateSelect(document.activeElement)
# the element has already received focus via find(), so invoke insert mode manually
enterInsertModeWithoutShowingIndicator(document.activeElement)
getNextQueryFromRegexMatches = (stepSize) ->
# find()ing an empty query always returns false
return "" unless findModeQuery.regexMatches
totalMatches = findModeQuery.regexMatches.length
findModeQuery.activeRegexIndex += stepSize + totalMatches
findModeQuery.activeRegexIndex %= totalMatches
findModeQuery.regexMatches[findModeQuery.activeRegexIndex]
findAndFocus = (backwards) ->
# check if the query has been changed by a script in another frame
mostRecentQuery = settings.get("findModeRawQuery") || ""
if (mostRecentQuery != findModeQuery.rawQuery)
findModeQuery.rawQuery = mostRecentQuery
updateFindModeQuery()
query =
if findModeQuery.isRegex
getNextQueryFromRegexMatches(if backwards then -1 else 1)
else
findModeQuery.parsedQuery
findModeQueryHasResults =
executeFind(query, { backwards: backwards, caseSensitive: !findModeQuery.ignoreCase })
if (!findModeQueryHasResults)
HUD.showForDuration("No matches for '" + findModeQuery.rawQuery + "'", 1000)
return
# if we have found an input element via 'n', pressing immediately afterwards sends us into insert
# mode
elementCanTakeInput = document.activeElement &&
DomUtils.isSelectable(document.activeElement) &&
isDOMDescendant(findModeAnchorNode, document.activeElement)
if (elementCanTakeInput)
handlerStack.push({
keydown: (event) ->
@remove()
if (KeyboardUtils.isEscape(event))
DomUtils.simulateSelect(document.activeElement)
enterInsertModeWithoutShowingIndicator(document.activeElement)
return false # we have "consumed" this event, so do not propagate
return true
})
focusFoundLink()
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 == value)
followLink(element)
return true
window.goPrevious = ->
previousPatterns = settings.get("previousPatterns") || ""
previousStrings = previousPatterns.split(",").filter((s) -> s.length)
findAndFollowRel("prev") || findAndFollowLink(previousStrings)
window.goNext = ->
nextPatterns = settings.get("nextPatterns") || ""
nextStrings = nextPatterns.split(",").filter((s) -> s.length)
findAndFollowRel("next") || findAndFollowLink(nextStrings)
showFindModeHUDForQuery = ->
if (findModeQueryHasResults || findModeQuery.parsedQuery.length == 0)
HUD.show("/" + findModeQuery.rawQuery)
else
HUD.show("/" + findModeQuery.rawQuery + " (No Matches)")
window.enterFindMode = ->
findModeQuery = { rawQuery: "" }
findMode = true
HUD.show("/")
exitFindMode = ->
findMode = false
HUD.hide()
window.showHelpDialog = (html, fid) ->
return if (isShowingHelpDialog || !document.body || fid != frameId)
isShowingHelpDialog = true
container = document.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",
-> chrome.extension.sendRequest({ handler: "openOptionsPageInNewTab" })
false)
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)
#
# A heads-up-display (HUD) for showing Vimium page operations.
# Note: you cannot interact with the HUD until document.body is available.
#
HUD =
_tweenId: -1
_displayElement: null
_upgradeNotificationElement: null
# 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.
showForDuration: (text, duration) ->
HUD.show(text)
HUD._showForDurationTimerId = setTimeout((-> HUD.hide()), duration)
show: (text) ->
return unless HUD.enabled()
clearTimeout(HUD._showForDurationTimerId)
HUD.displayElement().innerHTML = text
clearInterval(HUD._tweenId)
HUD._tweenId = Tween.fade(HUD.displayElement(), 1.0, 150)
HUD.displayElement().style.display = ""
showUpgradeNotification: (version) ->
HUD.upgradeNotificationElement().innerHTML = "Vimium has been updated to
#{version}.x"
links = HUD.upgradeNotificationElement().getElementsByTagName("a")
links[0].addEventListener("click", HUD.onUpdateLinkClicked, false)
links[1].addEventListener "click", (event) ->
event.preventDefault()
HUD.onUpdateLinkClicked()
Tween.fade(HUD.upgradeNotificationElement(), 1.0, 150)
onUpdateLinkClicked: (event) ->
HUD.hideUpgradeNotification()
chrome.extension.sendRequest({ handler: "upgradeNotificationClosed" })
hideUpgradeNotification: (clickEvent) ->
Tween.fade(HUD.upgradeNotificationElement(), 0, 150,
-> HUD.upgradeNotificationElement().style.display = "none")
#
# Retrieves the HUD HTML element.
#
displayElement: ->
if (!HUD._displayElement)
HUD._displayElement = HUD.createHudElement()
# Keep this far enough to the right so that it doesn't collide with the "popups blocked" chrome HUD.
HUD._displayElement.style.right = "150px"
HUD._displayElement
upgradeNotificationElement: ->
if (!HUD._upgradeNotificationElement)
HUD._upgradeNotificationElement = HUD.createHudElement()
# Position this just to the left of our normal HUD.
HUD._upgradeNotificationElement.style.right = "315px"
HUD._upgradeNotificationElement
createHudElement: ->
element = document.createElement("div")
element.className = "vimiumReset vimiumHUD"
document.body.appendChild(element)
element
hide: (immediate) ->
clearInterval(HUD._tweenId)
if (immediate)
HUD.displayElement().style.display = "none"
else
HUD._tweenId = Tween.fade(HUD.displayElement(), 0, 150,
-> HUD.displayElement().style.display = "none")
isReady: -> document.body != null
# A preference which can be toggled in the Options page. */
enabled: -> !settings.get("hideHud")
Tween =
#
# Fades an element's alpha. Returns a timer ID which can be used to stop the tween via clearInterval.
#
fade: (element, toAlpha, duration, onComplete) ->
state = {}
state.duration = duration
state.startTime = (new Date()).getTime()
state.from = parseInt(element.style.opacity) || 0
state.to = toAlpha
state.onUpdate = (value) ->
element.style.opacity = value
if (value == state.to && onComplete)
onComplete()
state.timerId = setInterval((-> Tween.performTweenStep(state)), 50)
state.timerId
performTweenStep: (state) ->
elapsed = (new Date()).getTime() - state.startTime
if (elapsed >= state.duration)
clearInterval(state.timerId)
state.onUpdate(state.to)
else
value = (elapsed / state.duration) * (state.to - state.from) + state.from
state.onUpdate(value)
initializePreDomReady()
window.addEventListener("DOMContentLoaded", initializeOnDomReady)
window.onbeforeunload = ->
chrome.extension.sendRequest(
handler: "updateScrollPosition"
scrollX: window.scrollX
scrollY: window.scrollY)
root = exports ? window
root.settings = settings
root.HUD = HUD
root.handlerStack = handlerStack
root.frameId = frameId