aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--content_scripts/vimium_frontend.coffee1036
-rw-r--r--content_scripts/vimium_frontend.js1183
3 files changed, 1037 insertions, 1183 deletions
diff --git a/.gitignore b/.gitignore
index 21f0225f..0d46f679 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@ background_scripts/completion.js
background_scripts/commands.js
background_scripts/settings.js
content_scripts/link_hints.js
+content_scripts/vimium_frontend.js
tests/completion_test.js
tests/test_helper.js
tests/utils_test.js
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
new file mode 100644
index 00000000..cd1da3e8
--- /dev/null
+++ b/content_scripts/vimium_frontend.coffee
@@ -0,0 +1,1036 @@
+#
+# 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".
+#
+getCurrentUrlHandlers = []; # function(url)
+
+insertModeLock = null
+findMode = false
+findModeQuery = { rawQuery: "" }
+findModeQueryHasResults = false
+findModeAnchorNode = null
+isShowingHelpDialog = false
+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
+linkHintCss = null
+activatedElement = null
+
+# 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"]
+ 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"]
+ 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()
+
+ chrome.extension.sendRequest { handler: "getLinkHintCss" }, (response) ->
+ linkHintCss = response.linkHintCss
+
+ refreshCompletionKeys()
+
+ # Send the key to the key handler in the background page.
+ keyPort = chrome.extension.connect({ name: "keyDown" })
+
+ chrome.extension.onRequest.addListener (request, sender, sendResponse) ->
+ if (request.name == "hideUpgradeNotification")
+ HUD.hideUpgradeNotification()
+ else if (request.name == "showUpgradeNotification" && isEnabledForUrl)
+ HUD.showUpgradeNotification(request.version)
+ else if (request.name == "showHelpDialog")
+ if (isShowingHelpDialog)
+ hideHelpDialog()
+ else
+ showHelpDialog(request.dialogHtml, request.frameId)
+ else if (request.name == "focusFrame")
+ if (frameId == request.frameId)
+ focusThisFrame(request.highlight)
+ else if (request.name == "refreshCompletionKeys")
+ refreshCompletionKeys(request)
+
+ # Free up the resources used by this open connection.
+ sendResponse({})
+
+ chrome.extension.onConnect.addListener (port, name) ->
+ if (port.name == "executePageCommand")
+ port.onMessage.addListener (args) ->
+ if (frameId == args.frameId)
+ if (args.passCountToFunction)
+ Utils.invokeCommandString(args.command, [args.count])
+ else
+ Utils.invokeCommandString(args.command) for i in [0...args.count]
+
+ refreshCompletionKeys(args)
+ else if (port.name == "getScrollPosition")
+ port.onMessage.addListener (args) ->
+ scrollPort = chrome.extension.connect({ name: "returnScrollPosition" })
+ scrollPort.postMessage
+ scrollX: window.scrollX,
+ scrollY: window.scrollY,
+ currentTab: args.currentTab
+ else if (port.name == "setScrollPosition")
+ port.onMessage.addListener (args) ->
+ if (args.scrollX > 0 || args.scrollY > 0)
+ DomUtils.documentReady(-> window.scrollBy(args.scrollX, args.scrollY))
+ else if (port.name == "returnCurrentTabUrl")
+ port.onMessage.addListener (args) ->
+ getCurrentUrlHandlers.pop()(args.url) if (getCurrentUrlHandlers.length > 0)
+ else if (port.name == "refreshCompletionKeys")
+ port.onMessage.addListener (args) -> refreshCompletionKeys(args.completionKeys)
+ else if (port.name == "getActiveState")
+ port.onMessage.addListener (args) -> port.postMessage({ enabled: isEnabledForUrl })
+ else if (port.name == "disableVimium")
+ port.onMessage.addListener (args) -> disableVimium()
+
+#
+# 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
+
+#
+# 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
+
+#
+# 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")))
+
+focusInput = (count) ->
+ results = DomUtils.evaluateXPath(textInputXPath, XPathResult.ORDERED_NODE_ITERATOR_TYPE)
+
+ lastInputBox
+ i = 0
+
+ while (i < count)
+ currentInputBox = results.iterateNext()
+ break unless currentInputBox
+ continue if (DomUtils.getVisibleClientRect(currentInputBox) == null)
+ lastInputBox = currentInputBox
+ i += 1
+
+ lastInputBox.focus() if lastInputBox
+
+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: ->
+ toggleViewSourceCallback = (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 })
+ getCurrentUrlHandlers.push(toggleViewSourceCallback)
+ getCurrentUrlPort = chrome.extension.connect({ name: "getCurrentTabUrl" })
+ getCurrentUrlPort.postMessage({})
+
+ 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
+ # getCurrentUrlHandlers.push(function (url) { Clipboard.copy(url); })
+ getCurrentUrlHandlers.push((url) -> chrome.extension.sendRequest({ handler: "copyToClipboard", data: url }))
+
+ # TODO(ilya): Convert to sendRequest.
+ getCurrentUrlPort = chrome.extension.connect({ name: "getCurrentTabUrl" })
+ getCurrentUrlPort.postMessage({})
+
+ HUD.showForDuration("Yanked URL", 1000)
+
+#
+# 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. <c-a> for control a.
+#
+# Note that some keys will only register keydown events and not keystroke events, e.g. ESC.
+#
+onKeypress = (event) ->
+ return unless 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)
+ suppressEvent(event)
+ else if (!isInsertMode() && !findMode)
+ if (currentCompletionKeys.indexOf(keyChar) != -1)
+ suppressEvent(event)
+
+ keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
+
+#
+# Called whenever we receive a key event. Each individual handler has the option to stop the event's
+# propagation by returning a falsy value.
+#
+bubbleEvent = (type, event) ->
+ for i in [(handlerStack.length - 1)..0]
+ # We need to check for existence of handler because the last function call may have caused the release of
+ # more than one handler.
+ if (handlerStack[i] && handlerStack[i][type] && !handlerStack[i][type](event))
+ suppressEvent(event)
+ return false
+ true
+
+suppressEvent = (event) ->
+ event.preventDefault()
+ event.stopPropagation()
+
+onKeydown = (event) ->
+ return unless 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()
+ suppressEvent(event)
+
+ else if (findMode)
+ if (KeyboardUtils.isEscape(event))
+ handleEscapeForFindMode()
+ suppressEvent(event)
+
+ else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey)
+ handleDeleteForFindMode()
+ suppressEvent(event)
+
+ else if (event.keyCode == keyCodes.enter)
+ handleEnterForFindMode()
+ 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)
+ suppressEvent(event)
+
+ keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
+
+ else if (KeyboardUtils.isEscape(event))
+ keyPort.postMessage({ keyChar:"<ESC>", 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 = () -> return unless 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 <ESC>.
+#
+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 = false
+ hasNoIgnoreCaseFlag = false
+ findModeQuery.parsedQuery = findModeQuery.rawQuery.replace /\\./g, (match) ->
+ switch (match)
+ when "\\r"
+ findModeQuery.isRegex = true
+ 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" + (findModeQuery.ignoreCase ? "i" : ""))
+ 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()
+
+# <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 = ->
+ 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 <esc> 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 && 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 <esc> immediately afterwards sends us into insert
+ # mode
+ elementCanTakeInput = DomUtils.isSelectable(document.activeElement) &&
+ isDOMDescendant(findModeAnchorNode, document.activeElement)
+ if (elementCanTakeInput)
+ handlerStack.push({
+ keydown: (event) ->
+ handlerStack.pop()
+ 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]
+ 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)
+
+ wordCount = (link) -> 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) ->
+ wcA = wordCount(a)
+ wcB = wordCount(b)
+ if (wcA == wcB) then a.originalIndex - b.originalIndex else wcA - wcB
+ )
+ .filter((a) -> wordCount(a) <= wordCount(candidateLinks[0]) + 1)
+
+ # try to get exact word matches first
+ for linkString in linkStrings
+ for candidateLink in candidateLinks
+ exactWordRegex = new RegExp("\\b" + linkString + "\\b", "i")
+ if (exactWordRegex.test(candidateLink.innerText))
+ followLink(candidateLink)
+ return true
+
+ for linkString in linkStrings
+ for candidateLink in candidateLinks
+ if (candidateLink.innerText.toLowerCase().indexOf(linkString) != -1)
+ followLink(candidateLink)
+ return true
+
+ false
+
+findAndFollowRel = (value) ->
+ relTags = ["link", "a", "area"]
+ for tag in relTags
+ elements = document.getElementsByTagName(relTag)
+ for element in elements
+ if (element.hasAttribute("rel") && element.rel == value)
+ followLink(element)
+ return true
+
+window.goPrevious = ->
+ previousPatterns = settings.get("previousPatterns") || ""
+ previousStrings = previousPatterns.split(",")
+ findAndFollowRel("prev") || findAndFollowLink(previousStrings)
+
+window.goNext = ->
+ nextPatterns = settings.get("nextPatterns") || ""
+ nextStrings = nextPatterns.split(",")
+ 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)
+ container.getElementsByClassName("optionsPage")[0].addEventListener("click",
+ -> chrome.extension.sendRequest({ handler: "openOptionsPageInNewTab" })
+ false)
+
+ # This is necessary because innerHTML does not evaluate javascript embedded in <script> tags.
+ scripts = Array.prototype.slice.call(container.getElementsByTagName("script"))
+ scripts.forEach((script) -> eval(script.text))
+
+hideHelpDialog = (clickEvent) ->
+ isShowingHelpDialog = false
+ helpDialog = document.getElementById("vimiumHelpDialogContainer")
+ if (helpDialog)
+ helpDialog.parentNode.removeChild(helpDialog)
+ if (clickEvent)
+ clickEvent.preventDefault()
+
+#
+# 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 " +
+ "<a class='vimiumReset' href='https://chrome.google.com/extensions/detail/dbepggeogbaibhgnhhndojpepiihcmeb'>" +
+ version + "</a>.<a class='vimiumReset close-button' href='#'>x</a>"
+ 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)
+
+#
+# Adds the given CSS to the page.
+#
+addCssToPage = (css, id) ->
+ head = document.getElementsByTagName("head")[0]
+ if (!head)
+ head = document.createElement("head")
+ document.documentElement.appendChild(head)
+ style = document.createElement("style")
+ style.id = id
+ style.type = "text/css"
+ style.appendChild(document.createTextNode(css))
+ head.appendChild(style)
+
+initializePreDomReady()
+window.addEventListener("DOMContentLoaded", initializeOnDomReady)
+
+window.onbeforeunload = ->
+ chrome.extension.sendRequest(
+ handler: "updateScrollPosition"
+ scrollX: window.scrollX
+ scrollY: window.scrollY)
+
+# TODO(philc): Export a more tighter, more coherent interface.
+root = exports ? window
+root.window = window
+root.settings = settings
+root.linkHintCss = linkHintCss
+root.addCssToPage = addCssToPage
+root.HUD = HUD
+root.handlerStack = handlerStack
diff --git a/content_scripts/vimium_frontend.js b/content_scripts/vimium_frontend.js
deleted file mode 100644
index d205ef1c..00000000
--- a/content_scripts/vimium_frontend.js
+++ /dev/null
@@ -1,1183 +0,0 @@
-/*
- * 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".
- */
-var getCurrentUrlHandlers = []; // function(url)
-
-var insertModeLock = null;
-var findMode = false;
-var findModeQuery = { rawQuery: "" };
-var findModeQueryHasResults = false;
-var findModeAnchorNode = null;
-var isShowingHelpDialog = false;
-var handlerStack = [];
-var keyPort;
-// Users can disable Vimium on URL patterns via the settings page.
-var isEnabledForUrl = true;
-// The user's operating system.
-var currentCompletionKeys;
-var validFirstKeys;
-var linkHintCss;
-var activatedElement;
-
-// 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.
-var textInputXPath = (function() {
- var textInputTypes = ["text", "search", "email", "url", "number", "password"];
- var inputElements = ["input[" +
- "(" + textInputTypes.map(function(type) {return '@type="' + type + '"'}).join(" or ") + "or not(@type))" +
- " and not(@disabled or @readonly)]",
- "textarea", "*[@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"];
- return 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.
- */
-var settings = {
- port: null,
- values: {},
- loadedValues: 0,
- valuesToLoad: ["scrollStepSize", "linkHintCharacters", "filterLinkHints", "hideHud", "previousPatterns",
- "nextPatterns", "findModeRawQuery"],
- isLoaded: false,
- eventListeners: {},
-
- init: function () {
- this.port = chrome.extension.connect({ name: "settings" });
- this.port.onMessage.addListener(this.receiveMessage);
- },
-
- get: function (key) { return this.values[key]; },
-
- set: function (key, value) {
- if (!this.port)
- this.init();
-
- this.values[key] = value;
- this.port.postMessage({ operation: "set", key: key, value: value });
- },
-
- load: function() {
- if (!this.port)
- this.init();
-
- for (var i in this.valuesToLoad) {
- this.port.postMessage({ operation: "get", key: this.valuesToLoad[i] });
- }
- },
-
- receiveMessage: function (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;
- var listener;
- while (listener = settings.eventListeners["load"].pop())
- listener();
- }
- },
-
- addEventListener: function(eventName, callback) {
- if (!(eventName in this.eventListeners))
- this.eventListeners[eventName] = [];
- this.eventListeners[eventName].push(callback);
- },
-
-};
-
-/*
- * Give this frame a unique id.
- */
-frameId = Math.floor(Math.random()*999999999)
-
-var hasModifiersRegex = /^<([amc]-)+.>/;
-
-/*
- * Complete initialization work that sould be done prior to DOMReady.
- */
-function initializePreDomReady() {
- settings.addEventListener("load", LinkHints.init.bind(LinkHints));
- settings.load();
-
- checkIfEnabledForUrl();
-
- chrome.extension.sendRequest({handler: "getLinkHintCss"}, function (response) {
- linkHintCss = response.linkHintCss;
- });
-
- refreshCompletionKeys();
-
- // Send the key to the key handler in the background page.
- keyPort = chrome.extension.connect({ name: "keyDown" });
-
- chrome.extension.onRequest.addListener(function(request, sender, sendResponse) {
- if (request.name == "hideUpgradeNotification") {
- HUD.hideUpgradeNotification();
- } else if (request.name == "showUpgradeNotification" && isEnabledForUrl) {
- HUD.showUpgradeNotification(request.version);
- } else if (request.name == "showHelpDialog") {
- if (isShowingHelpDialog)
- hideHelpDialog();
- else
- showHelpDialog(request.dialogHtml, request.frameId);
- } else if (request.name == "focusFrame") {
- if (frameId == request.frameId)
- focusThisFrame(request.highlight);
- } else if (request.name == "refreshCompletionKeys") {
- refreshCompletionKeys(request);
- }
- sendResponse({}); // Free up the resources used by this open connection.
- });
-
- chrome.extension.onConnect.addListener(function(port, name) {
- if (port.name == "executePageCommand") {
- port.onMessage.addListener(function(args) {
- if (frameId == args.frameId) {
- if (args.passCountToFunction) {
- Utils.invokeCommandString(args.command, [args.count]);
- } else {
- for (var i = 0; i < args.count; i++) { Utils.invokeCommandString(args.command); }
- }
- }
-
- refreshCompletionKeys(args);
- });
- }
- else if (port.name == "getScrollPosition") {
- port.onMessage.addListener(function(args) {
- var scrollPort = chrome.extension.connect({ name: "returnScrollPosition" });
- scrollPort.postMessage({
- scrollX: window.scrollX,
- scrollY: window.scrollY,
- currentTab: args.currentTab
- });
- });
- } else if (port.name == "setScrollPosition") {
- port.onMessage.addListener(function(args) {
- if (args.scrollX > 0 || args.scrollY > 0) {
- DomUtils.documentReady(function() { window.scrollBy(args.scrollX, args.scrollY); });
- }
- });
- } else if (port.name == "returnCurrentTabUrl") {
- port.onMessage.addListener(function(args) {
- if (getCurrentUrlHandlers.length > 0) { getCurrentUrlHandlers.pop()(args.url); }
- });
- } else if (port.name == "refreshCompletionKeys") {
- port.onMessage.addListener(function (args) {
- refreshCompletionKeys(args.completionKeys);
- });
- } else if (port.name == "getActiveState") {
- port.onMessage.addListener(function(args) {
- port.postMessage({ enabled: isEnabledForUrl });
- });
- } else if (port.name == "disableVimium") {
- port.onMessage.addListener(function(args) { disableVimium(); });
- }
- });
-}
-
-/*
- * This is called once the background page has told us that Vimium should be enabled for the current URL.
- */
-function 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.
- */
-function 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", function(e) {
- // settings may have changed since the frame last had focus
- settings.load();
- chrome.extension.sendRequest({ handler: "frameFocused", frameId: frameId });
-});
-
-/*
- * Called from the backend in order to change frame focus.
- */
-function focusThisFrame(shouldHighlight) {
- window.focus();
- if (document.body && shouldHighlight) {
- var borderWas = document.body.style.border;
- document.body.style.border = '5px solid yellow';
- setTimeout(function(){document.body.style.border = borderWas}, 200);
- }
-}
-
-/*
- * Initialization tasks that must wait for the document to be ready.
- */
-function initializeOnDomReady() {
- registerFrameIfSizeAvailable(window.top == window.self);
-
- if (isEnabledForUrl)
- enterInsertModeIfElementIsFocused();
-
- // 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?
-function 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(function () { registerFrameIfSizeAvailable(is_top); }, 100);
-}
-
-/*
- * Enters insert mode if the currently focused element in the DOM is focusable.
- */
-function enterInsertModeIfElementIsFocused() {
- if (document.activeElement && isEditable(document.activeElement) && !findMode)
- enterInsertModeWithoutShowingIndicator(document.activeElement);
-}
-
-function onDOMActivate(event) {
- activatedElement = event.target;
-}
-
-/**
- * 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.
- */
-function 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 // "y"
- window.scrollBy(0, amount);
- return;
- }
-
- // TODO refactor and put this together with the code in getVisibleClientRect
- function isRendered(element) {
- var computedStyle = window.getComputedStyle(element, null);
- return !(computedStyle.getPropertyValue('visibility') != 'visible' ||
- computedStyle.getPropertyValue('display') == 'none');
- }
-
- if (!activatedElement || !isRendered(activatedElement))
- activatedElement = document.body;
-
- scrollName = direction === "x" ? "scrollLeft" : "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) {
- var element = activatedElement;
- do {
- var oldScrollValue = element[scrollName];
- element[scrollName] += amount;
- var lastElement = element;
- // we may have an orphaned element. if so, just scroll the body element.
- element = element.parentElement || document.body;
- } while(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.
- var rect = activatedElement.getBoundingClientRect();
- if (rect.bottom < 0 || rect.top > window.innerHeight ||
- rect.right < 0 || rect.left > window.innerWidth)
- activatedElement = lastElement;
-}
-
-function scrollToBottom() { window.scrollTo(window.pageXOffset, document.body.scrollHeight); }
-function scrollToTop() { window.scrollTo(window.pageXOffset, 0); }
-function scrollToLeft() { window.scrollTo(0, window.pageYOffset); }
-function scrollToRight() { window.scrollTo(document.body.scrollWidth, window.pageYOffset); }
-function scrollUp() { scrollActivatedElementBy("y", -1 * settings.get("scrollStepSize")); }
-function scrollDown() { scrollActivatedElementBy("y", parseFloat(settings.get("scrollStepSize"))); }
-function scrollPageUp() { scrollActivatedElementBy("y", -1 * window.innerHeight / 2); }
-function scrollPageDown() { scrollActivatedElementBy("y", window.innerHeight / 2); }
-function scrollFullPageUp() { scrollActivatedElementBy("y", -window.innerHeight); }
-function scrollFullPageDown() { scrollActivatedElementBy("y", window.innerHeight); }
-function scrollLeft() { scrollActivatedElementBy("x", -1 * settings.get("scrollStepSize")); }
-function scrollRight() { scrollActivatedElementBy("x", parseFloat(settings.get("scrollStepSize"))); }
-
-function focusInput(count) {
- var results = DomUtils.evaluateXPath(textInputXPath, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
-
- var lastInputBox;
- var i = 0;
-
- while (i < count) {
- var currentInputBox = results.iterateNext();
- if (!currentInputBox) { break; }
-
- if (DomUtils.getVisibleClientRect(currentInputBox) === null)
- continue;
-
- lastInputBox = currentInputBox;
-
- i += 1;
- }
-
- if (lastInputBox) { lastInputBox.focus(); }
-}
-
-function reload() { window.location.reload(); }
-function goBack(count) { history.go(-count); }
-function goForward(count) { history.go(count); }
-
-function goUp(count) {
- var url = window.location.href;
- if (url[url.length-1] == '/')
- url = url.substring(0, url.length - 1);
-
- var 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('/');
- }
-}
-
-function toggleViewSource() {
- getCurrentUrlHandlers.push(toggleViewSourceCallback);
-
- var getCurrentUrlPort = chrome.extension.connect({ name: "getCurrentTabUrl" });
- getCurrentUrlPort.postMessage({});
-}
-
-function 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
- //getCurrentUrlHandlers.push(function (url) { Clipboard.copy(url); });
- getCurrentUrlHandlers.push(function (url) { chrome.extension.sendRequest({ handler: "copyToClipboard", data: url }); });
-
- // TODO(ilya): Convert to sendRequest.
- var getCurrentUrlPort = chrome.extension.connect({ name: "getCurrentTabUrl" });
- getCurrentUrlPort.postMessage({});
-
- HUD.showForDuration("Yanked URL", 1000);
-}
-
-function toggleViewSourceCallback(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});
-}
-
-/**
- * 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. <c-a> for control a.
- *
- * Note that some keys will only register keydown events and not keystroke events, e.g. ESC.
- */
-function onKeypress(event) {
- if (!bubbleEvent('keypress', event))
- return;
-
- var 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);
- suppressEvent(event);
- } else if (!isInsertMode() && !findMode) {
- if (currentCompletionKeys.indexOf(keyChar) != -1)
- suppressEvent(event);
-
- keyPort.postMessage({keyChar:keyChar, frameId:frameId});
- }
- }
- }
-}
-
-/**
- * Called whenever we receive a key event. Each individual handler has the option to stop the event's
- * propagation by returning a falsy value.
- */
-function bubbleEvent(type, event) {
- for (var i = handlerStack.length-1; i >= 0; i--) {
- // We need to check for existence of handler because the last function call may have caused the release of
- // more than one handler.
- if (handlerStack[i] && handlerStack[i][type] && !handlerStack[i][type](event)) {
- suppressEvent(event);
- return false;
- }
- }
- return true;
-}
-
-function suppressEvent(event) {
- event.preventDefault();
- event.stopPropagation();
-}
-
-function onKeydown(event) {
- if (!bubbleEvent('keydown', event))
- return;
-
- var 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);
-
- if (keyChar != "") { // Again, ignore just modifiers. Maybe this should replace the keyCode>31 condition.
- var 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 (var i in 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();
- suppressEvent(event);
- }
- }
- else if (findMode) {
- if (KeyboardUtils.isEscape(event)) {
- handleEscapeForFindMode();
- suppressEvent(event);
- }
- else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) {
- handleDeleteForFindMode();
- suppressEvent(event);
- }
- else if (event.keyCode == keyCodes.enter) {
- handleEnterForFindMode();
- 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)
- suppressEvent(event);
-
- keyPort.postMessage({keyChar:keyChar, frameId:frameId});
- }
- else if (KeyboardUtils.isEscape(event)) {
- keyPort.postMessage({keyChar:"<ESC>", 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 == "" && !isInsertMode() &&
- (currentCompletionKeys.indexOf(KeyboardUtils.getKeyChar(event)) != -1 ||
- isValidFirstKey(KeyboardUtils.getKeyChar(event))))
- event.stopPropagation();
-}
-
-function onKeyup() {
- if (!bubbleEvent('keyup', event))
- return;
-}
-
-function checkIfEnabledForUrl() {
- var url = window.location.toString();
-
- chrome.extension.sendRequest({ handler: "isEnabledForUrl", url: url }, function (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();
- });
-}
-
-function refreshCompletionKeys(response) {
- if (response) {
- currentCompletionKeys = response.completionKeys;
-
- if (response.validFirstKeys)
- validFirstKeys = response.validFirstKeys;
- }
- else {
- chrome.extension.sendRequest({ handler: "getCompletionKeys" }, refreshCompletionKeys);
- }
-}
-
-function isValidFirstKey(keyChar) {
- return validFirstKeys[keyChar] || /[1-9]/.test(keyChar);
-}
-
-function onFocusCapturePhase(event) {
- if (isFocusable(event.target) && !findMode)
- enterInsertModeWithoutShowingIndicator(event.target);
-}
-
-function 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.
- */
-function isFocusable(element) { return isEditable(element) || isEmbed(element); }
-
-/*
- * Embedded elements like Flash and quicktime players can obtain focus but cannot be programmatically
- * unfocused.
- */
-function isEmbed(element) { return ["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.
- */
-function isEditable(target) {
- if (target.isContentEditable)
- return true;
- var nodeName = target.nodeName.toLowerCase();
- // use a blacklist instead of a whitelist because new form controls are still being implemented for html5
- var noFocus = ["radio", "checkbox"];
- if (nodeName == "input" && noFocus.indexOf(target.type) == -1)
- return true;
- var focusableElements = ["textarea", "select"];
- return 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)
- */
-function 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 <ESC>.
- */
-function enterInsertModeWithoutShowingIndicator(target) { insertModeLock = target; }
-
-function exitInsertMode(target) {
- if (target === undefined || insertModeLock === target) {
- insertModeLock = null;
- HUD.hide();
- }
-}
-
-function isInsertMode() { return insertModeLock !== null; }
-
-// should be called whenever rawQuery is modified.
-function 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 = false;
- var hasNoIgnoreCaseFlag = false;
- findModeQuery.parsedQuery = findModeQuery.rawQuery.replace(/\\./g, function(match) {
- switch (match) {
- case "\\r":
- findModeQuery.isRegex = true;
- return '';
- case "\\I":
- hasNoIgnoreCaseFlag = true;
- return '';
- case "\\\\":
- return "\\";
- default:
- 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 {
- var pattern = new RegExp(findModeQuery.parsedQuery, "g" + (findModeQuery.ignoreCase ? "i" : ""));
- }
- catch (e) {
- // 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
- var text = document.body.innerText;
- findModeQuery.regexMatches = text.match(pattern);
- findModeQuery.activeRegexIndex = 0;
- }
-}
-
-function handleKeyCharForFindMode(keyChar) {
- findModeQuery.rawQuery += keyChar;
- updateFindModeQuery();
- performFindInPlace();
- showFindModeHUDForQuery();
-}
-
-function 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.
- var selection = window.getSelection();
- if (!selection.isCollapsed) {
- var range = window.getSelection().getRangeAt(0);
- window.getSelection().removeAllRanges();
- window.getSelection().addRange(range);
- }
- focusFoundLink() || selectFoundInputElement();
-}
-
-function handleDeleteForFindMode() {
- if (findModeQuery.rawQuery.length == 0) {
- exitFindMode();
- performFindInPlace();
- }
- else {
- findModeQuery.rawQuery = findModeQuery.rawQuery.substring(0, findModeQuery.rawQuery.length - 1);
- updateFindModeQuery();
- performFindInPlace();
- showFindModeHUDForQuery();
- }
-}
-
-// <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'
-function handleEnterForFindMode() {
- exitFindMode();
- focusFoundLink();
- document.body.classList.add("vimiumFindMode");
- settings.set("findModeRawQuery", findModeQuery.rawQuery);
-}
-
-function performFindInPlace() {
- var cachedScrollX = window.scrollX;
- var cachedScrollY = window.scrollY;
-
- var query = findModeQuery.isRegex ? getNextQueryFromRegexMatches(0) : 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'.
-function 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.
- var 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);
- var rv = window.find(query, options.caseSensitive, options.backwards, true, false, true, false);
- setTimeout(function() {
- document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true);
- }, 0);
-
- findMode = oldFindMode;
- // we need to save the anchor node here because <esc> seems to nullify it, regardless of whether we do
- // preventDefault()
- findModeAnchorNode = document.getSelection().anchorNode;
- return rv;
-}
-
-function restoreDefaultSelectionHighlight() {
- document.body.classList.remove("vimiumFindMode");
-}
-
-function focusFoundLink() {
- if (findModeQueryHasResults) {
- var link = getLinkFromSelection();
- if (link)
- link.focus();
- }
-}
-
-function isDOMDescendant(parent, child) {
- var node = child;
- while (node !== null) {
- if (node === parent)
- return true;
- node = node.parentNode;
- }
- return false;
-}
-
-function 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 && 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);
- }
-}
-
-function getNextQueryFromRegexMatches(stepSize) {
- if (!findModeQuery.regexMatches)
- return ""; // find()ing an empty query always returns false
-
- var totalMatches = findModeQuery.regexMatches.length;
- findModeQuery.activeRegexIndex += stepSize + totalMatches;
- findModeQuery.activeRegexIndex %= totalMatches;
-
- return findModeQuery.regexMatches[findModeQuery.activeRegexIndex];
-}
-
-function findAndFocus(backwards) {
- // check if the query has been changed by a script in another frame
- var mostRecentQuery = settings.get("findModeRawQuery") || "";
- if (mostRecentQuery !== findModeQuery.rawQuery) {
- findModeQuery.rawQuery = mostRecentQuery;
- updateFindModeQuery();
- }
-
- var query = findModeQuery.isRegex ? getNextQueryFromRegexMatches(backwards ? -1 : 1) :
- 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 <esc> immediately afterwards sends us into insert
- // mode
- var elementCanTakeInput = DomUtils.isSelectable(document.activeElement) &&
- isDOMDescendant(findModeAnchorNode, document.activeElement);
- if (elementCanTakeInput) {
- handlerStack.push({
- keydown: function(event) {
- handlerStack.pop();
- 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();
-}
-
-function performFind() { findAndFocus(); }
-
-function performBackwardsFind() { findAndFocus(true); }
-
-function getLinkFromSelection() {
- var node = window.getSelection().anchorNode;
- while (node && node !== document.body) {
- if (node.nodeName.toLowerCase() === 'a') return node;
- node = node.parentNode;
- }
- return null;
-}
-
-// used by the findAndFollow* functions.
-function 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.
- */
-function findAndFollowLink(linkStrings) {
- var linksXPath = DomUtils.makeXPath(["a", "*[@onclick or @role='link' or contains(@class, 'button')]"]);
- var links = DomUtils.evaluateXPath(linksXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
- var 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 (var i = links.snapshotLength - 1; i >= 0; i--) {
- var link = links.snapshotItem(i);
-
- // ensure link is visible (we don't mind if it is scrolled offscreen)
- var boundingClientRect = link.getBoundingClientRect();
- if (boundingClientRect.width == 0 || boundingClientRect.height == 0)
- continue;
- var computedStyle = window.getComputedStyle(link, null);
- if (computedStyle.getPropertyValue('visibility') != 'visible' ||
- computedStyle.getPropertyValue('display') == 'none')
- continue;
-
- var linkMatches = false;
- for (var j = 0; j < linkStrings.length; j++) {
- if (link.innerText.toLowerCase().indexOf(linkStrings[j]) !== -1) {
- linkMatches = true;
- break;
- }
- }
- if (!linkMatches) continue;
-
- candidateLinks.push(link);
- }
-
- if (candidateLinks.length === 0) return;
-
- function wordCount(link) { return 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(function(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(function(a,b) {
- var wcA = wordCount(a), wcB = wordCount(b);
- return wcA === wcB ? a.originalIndex - b.originalIndex : wcA - wcB;
- })
- .filter(function(a){return wordCount(a) <= wordCount(candidateLinks[0]) + 1});
-
- // try to get exact word matches first
- for (var i = 0; i < linkStrings.length; i++)
- for (var j = 0; j < candidateLinks.length; j++) {
- var exactWordRegex = new RegExp("\\b" + linkStrings[i] + "\\b", "i");
- if (exactWordRegex.test(candidateLinks[j].innerText)) {
- followLink(candidateLinks[j]);
- return true;
- }
- }
-
- for (var i = 0; i < linkStrings.length; i++)
- for (var j = 0; j < candidateLinks.length; j++) {
- if (candidateLinks[j].innerText.toLowerCase().indexOf(linkStrings[i]) !== -1) {
- followLink(candidateLinks[j]);
- return true;
- }
- }
-
- return false;
-}
-
-function findAndFollowRel(value) {
- var relTags = ['link', 'a', 'area'];
- for (i = 0; i < relTags.length; i++) {
- var elements = document.getElementsByTagName(relTags[i]);
- for (j = 0; j < elements.length; j++) {
- if (elements[j].hasAttribute('rel') && elements[j].rel == value) {
- followLink(elements[j]);
- return true;
- }
- }
- }
-}
-
-function goPrevious() {
- var previousPatterns = settings.get("previousPatterns") || "";
- var previousStrings = previousPatterns.split(",");
- findAndFollowRel('prev') || findAndFollowLink(previousStrings);
-}
-
-function goNext() {
- var nextPatterns = settings.get("nextPatterns") || "";
- var nextStrings = nextPatterns.split(",");
- findAndFollowRel('next') || findAndFollowLink(nextStrings);
-}
-
-function showFindModeHUDForQuery() {
- if (findModeQueryHasResults || findModeQuery.parsedQuery.length == 0)
- HUD.show("/" + findModeQuery.rawQuery);
- else
- HUD.show("/" + findModeQuery.rawQuery + " (No Matches)");
-}
-
-function enterFindMode() {
- findModeQuery = { rawQuery: "" };
- findMode = true;
- HUD.show("/");
-}
-
-function exitFindMode() {
- findMode = false;
- HUD.hide();
-}
-
-function showHelpDialog(html, fid) {
- if (isShowingHelpDialog || !document.body || fid != frameId)
- return;
- isShowingHelpDialog = true;
- var 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);
- container.getElementsByClassName("optionsPage")[0].addEventListener("click",
- function() { chrome.extension.sendRequest({ handler: "openOptionsPageInNewTab" }); }, false);
-
- // This is necessary because innerHTML does not evaluate javascript embedded in <script> tags.
- var scripts = Array.prototype.slice.call(container.getElementsByTagName("script"));
- scripts.forEach(function(script) { eval(script.text); });
-
-}
-
-function hideHelpDialog(clickEvent) {
- isShowingHelpDialog = false;
- var helpDialog = document.getElementById("vimiumHelpDialogContainer");
- if (helpDialog)
- helpDialog.parentNode.removeChild(helpDialog);
- if (clickEvent)
- clickEvent.preventDefault();
-}
-
-/*
- * 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: function(text, duration) {
- HUD.show(text);
- HUD._showForDurationTimerId = setTimeout(function() { HUD.hide(); }, duration);
- },
-
- show: function(text) {
- if (!HUD.enabled()) return;
- clearTimeout(HUD._showForDurationTimerId);
- HUD.displayElement().innerHTML = text;
- clearInterval(HUD._tweenId);
- HUD._tweenId = Tween.fade(HUD.displayElement(), 1.0, 150);
- HUD.displayElement().style.display = "";
- },
-
- showUpgradeNotification: function(version) {
- HUD.upgradeNotificationElement().innerHTML = "Vimium has been updated to " +
- "<a class='vimiumReset' href='https://chrome.google.com/extensions/detail/dbepggeogbaibhgnhhndojpepiihcmeb'>" +
- version + "</a>.<a class='vimiumReset close-button' href='#'>x</a>";
- var links = HUD.upgradeNotificationElement().getElementsByTagName("a");
- links[0].addEventListener("click", HUD.onUpdateLinkClicked, false);
- links[1].addEventListener("click", function(event) {
- event.preventDefault();
- HUD.onUpdateLinkClicked();
- });
- Tween.fade(HUD.upgradeNotificationElement(), 1.0, 150);
- },
-
- onUpdateLinkClicked: function(event) {
- HUD.hideUpgradeNotification();
- chrome.extension.sendRequest({ handler: "upgradeNotificationClosed" });
- },
-
- hideUpgradeNotification: function(clickEvent) {
- Tween.fade(HUD.upgradeNotificationElement(), 0, 150,
- function() { HUD.upgradeNotificationElement().style.display = "none"; });
- },
-
- /*
- * Retrieves the HUD HTML element.
- */
- displayElement: function() {
- 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";
- }
- return HUD._displayElement;
- },
-
- upgradeNotificationElement: function() {
- if (!HUD._upgradeNotificationElement) {
- HUD._upgradeNotificationElement = HUD.createHudElement();
- // Position this just to the left of our normal HUD.
- HUD._upgradeNotificationElement.style.right = "315px";
- }
- return HUD._upgradeNotificationElement;
- },
-
- createHudElement: function() {
- var element = document.createElement("div");
- element.className = "vimiumReset vimiumHUD";
- document.body.appendChild(element);
- return element;
- },
-
- hide: function(immediate) {
- clearInterval(HUD._tweenId);
- if (immediate)
- HUD.displayElement().style.display = "none";
- else
- HUD._tweenId = Tween.fade(HUD.displayElement(), 0, 150,
- function() { HUD.displayElement().style.display = "none"; });
- },
-
- isReady: function() { return document.body != null; },
-
- /* A preference which can be toggled in the Options page. */
- enabled: function() { return !settings.get("hideHud"); }
-
-};
-
-Tween = {
- /*
- * Fades an element's alpha. Returns a timer ID which can be used to stop the tween via clearInterval.
- */
- fade: function(element, toAlpha, duration, onComplete) {
- var state = {};
- state.duration = duration;
- state.startTime = (new Date()).getTime();
- state.from = parseInt(element.style.opacity) || 0;
- state.to = toAlpha;
- state.onUpdate = function(value) {
- element.style.opacity = value;
- if (value == state.to && onComplete)
- onComplete();
- };
- state.timerId = setInterval(function() { Tween.performTweenStep(state); }, 50);
- return state.timerId;
- },
-
- performTweenStep: function(state) {
- var elapsed = (new Date()).getTime() - state.startTime;
- if (elapsed >= state.duration) {
- clearInterval(state.timerId);
- state.onUpdate(state.to)
- } else {
- var value = (elapsed / state.duration) * (state.to - state.from) + state.from;
- state.onUpdate(value);
- }
- }
-};
-
-/*
- * Adds the given CSS to the page.
- */
-function addCssToPage(css, id) {
- var head = document.getElementsByTagName("head")[0];
- if (!head) {
- head = document.createElement("head");
- document.documentElement.appendChild(head);
- }
- var style = document.createElement("style");
- style.id = id;
- style.type = "text/css";
- style.appendChild(document.createTextNode(css));
- head.appendChild(style);
-}
-
-initializePreDomReady();
-window.addEventListener("DOMContentLoaded", initializeOnDomReady);
-
-window.onbeforeunload = function() {
- chrome.extension.sendRequest({ handler: "updateScrollPosition",
- scrollX: window.scrollX, scrollY: window.scrollY });
-}