aboutsummaryrefslogtreecommitdiffstats
path: root/content_scripts/vimium_frontend.coffee
diff options
context:
space:
mode:
Diffstat (limited to 'content_scripts/vimium_frontend.coffee')
-rw-r--r--content_scripts/vimium_frontend.coffee257
1 files changed, 152 insertions, 105 deletions
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index a3ab051b..7121569a 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -4,9 +4,8 @@
# background page that we're in domReady and ready to accept normal commands by connectiong to a port named
# "domReady".
#
-window.handlerStack = new HandlerStack
-insertModeLock = null
+targetElement = null
findMode = false
findModeQuery = { rawQuery: "", matchCount: 0 }
findModeQueryHasResults = false
@@ -21,8 +20,8 @@ isEnabledForUrl = true
passKeys = null
keyQueue = null
# The user's operating system.
-currentCompletionKeys = null
-validFirstKeys = null
+currentCompletionKeys = ""
+validFirstKeys = ""
# 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
@@ -110,7 +109,21 @@ initializePreDomReady = ->
settings.addEventListener("load", LinkHints.init.bind(LinkHints))
settings.load()
- Scroller.init settings
+ class NormalMode extends Mode
+ constructor: ->
+ super
+ name: "normal"
+ keydown: (event) => onKeydown.call @, event
+ keypress: (event) => onKeypress.call @, event
+ keyup: (event) => onKeyup.call @, event
+
+ Scroller.init settings
+
+ # Install the permanent modes. The permanently-installed insert mode tracks focus/blur events, and
+ # activates/deactivates itself accordingly.
+ new NormalMode
+ new PassKeysMode
+ new InsertMode
checkIfEnabledForUrl()
@@ -136,9 +149,11 @@ initializePreDomReady = ->
getScrollPosition: -> scrollX: window.scrollX, scrollY: window.scrollY
setScrollPosition: (request) -> setScrollPosition request.scrollX, request.scrollY
executePageCommand: executePageCommand
- getActiveState: -> { enabled: isEnabledForUrl, passKeys: passKeys }
+ getActiveState: getActiveState
setState: setState
- currentKeyQueue: (request) -> keyQueue = request.keyQueue
+ currentKeyQueue: (request) ->
+ keyQueue = request.keyQueue
+ handlerStack.bubbleEvent "registerKeyQueue", { keyQueue: keyQueue }
chrome.runtime.onMessage.addListener (request, sender, sendResponse) ->
# In the options page, we will receive requests from both content and background scripts. ignore those
@@ -169,11 +184,8 @@ initializeWhenEnabled = (newPassKeys) ->
if (!installedListeners)
# Key event handlers fire on window before they do on document. Prefer window for key events so the page
# can't set handlers to grab the keys before us.
- installListener window, "keydown", onKeydown
- installListener window, "keypress", onKeypress
- installListener window, "keyup", onKeyup
- installListener document, "focus", onFocusCapturePhase
- installListener document, "blur", onBlurCapturePhase
+ for type in ["keydown", "keypress", "keyup", "click", "focus", "blur"]
+ do (type) -> installListener window, type, (event) -> handlerStack.bubbleEvent type, event
installListener document, "DOMActivate", onDOMActivate
enterInsertModeIfElementIsFocused()
installedListeners = true
@@ -182,6 +194,13 @@ setState = (request) ->
initializeWhenEnabled(request.passKeys) if request.enabled
isEnabledForUrl = request.enabled
passKeys = request.passKeys
+ handlerStack.bubbleEvent "registerStateChange",
+ enabled: request.enabled
+ passKeys: request.passKeys
+
+getActiveState = ->
+ Mode.updateBadge()
+ return { enabled: isEnabledForUrl, passKeys: passKeys }
#
# The backend needs to know which frame has focus.
@@ -305,6 +324,12 @@ extend window,
HUD.showForDuration("Yanked URL", 1000)
+ enterInsertMode: ->
+ new InsertMode global: true
+
+ enterVisualMode: =>
+ new VisualMode()
+
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.
@@ -321,10 +346,6 @@ extend window,
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"
@@ -337,33 +358,43 @@ extend window,
hint
- hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
-
- hintContainingDiv = DomUtils.addElementList(hints,
- { id: "vimiumInputMarkerContainer", className: "vimiumReset" })
+ new class FocusSelector extends Mode
+ constructor: ->
+ super
+ name: "focus-selector"
+ badge: "?"
+ # We share a singleton with PostFindMode. That way, a new FocusSelector displaces any existing
+ # PostFindMode.
+ singleton: PostFindMode
+ exitOnClick: true
+ keydown: (event) =>
+ if event.keyCode == KeyboardUtils.keyCodes.tab
+ hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint'
+ selectedInputIndex += hints.length + (if event.shiftKey then -1 else 1)
+ selectedInputIndex %= hints.length
+ hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
+ visibleInputs[selectedInputIndex].element.focus()
+ @suppressEvent
+ else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey
+ @exit()
+ @continueBubbling
+
+ @onExit -> DomUtils.removeElement hintContainingDiv
+ 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
+ if visibleInputs.length == 1
+ @exit()
+ else
+ hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
# Decide whether this keyChar should be passed to the underlying page.
# Keystrokes are *never* considered passKeys if the keyQueue is not empty. So, for example, if 't' is a
# passKey, then 'gt' and '99t' will neverthless be handled by vimium.
isPassKey = ( keyChar ) ->
+ return false # Disabled.
return !keyQueue and passKeys and 0 <= passKeys.indexOf(keyChar)
# Track which keydown events we have handled, so that we can subsequently suppress the corresponding keyup
@@ -397,9 +428,8 @@ KeydownEvents =
#
# Note that some keys will only register keydown events and not keystroke events, e.g. ESC.
#
+# @/this, here, is the the normal-mode Mode object.
onKeypress = (event) ->
- return unless handlerStack.bubbleEvent('keypress', event)
-
keyChar = ""
# Ignore modifier keys by themselves.
@@ -409,23 +439,27 @@ onKeypress = (event) ->
# Enter insert mode when the user enables the native find interface.
if (keyChar == "f" && KeyboardUtils.isPrimaryModifierKey(event))
enterInsertModeWithoutShowingIndicator()
- return
+ return @stopBubblingAndTrue
if (keyChar)
if (findMode)
handleKeyCharForFindMode(keyChar)
DomUtils.suppressEvent(event)
+ return @stopBubblingAndTrue
else if (!isInsertMode() && !findMode)
if (isPassKey keyChar)
- return undefined
- if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar))
+ return @stopBubblingAndTrue
+ if currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)
DomUtils.suppressEvent(event)
+ keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
+ return @stopBubblingAndTrue
keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
-onKeydown = (event) ->
- return unless handlerStack.bubbleEvent('keydown', event)
+ return @continueBubbling
+# @/this, here, is the the normal-mode Mode object.
+onKeydown = (event) ->
keyChar = ""
# handle special keys, and normal input keys with modifiers being pressed. don't handle shiftKey alone (to
@@ -464,37 +498,45 @@ onKeydown = (event) ->
exitInsertMode()
DomUtils.suppressEvent event
KeydownEvents.push event
+ return @stopBubblingAndTrue
else if (findMode)
if (KeyboardUtils.isEscape(event))
handleEscapeForFindMode()
DomUtils.suppressEvent event
KeydownEvents.push event
+ return @stopBubblingAndTrue
else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey)
handleDeleteForFindMode()
DomUtils.suppressEvent event
KeydownEvents.push event
+ return @stopBubblingAndTrue
else if (event.keyCode == keyCodes.enter)
handleEnterForFindMode()
DomUtils.suppressEvent event
KeydownEvents.push event
+ return @stopBubblingAndTrue
else if (!modifiers)
DomUtils.suppressPropagation(event)
KeydownEvents.push event
+ return @stopBubblingAndTrue
else if (isShowingHelpDialog && KeyboardUtils.isEscape(event))
hideHelpDialog()
DomUtils.suppressEvent event
KeydownEvents.push event
+ return @stopBubblingAndTrue
else if (!isInsertMode() && !findMode)
if (keyChar)
if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar))
DomUtils.suppressEvent event
KeydownEvents.push event
+ keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
+ return @stopBubblingAndTrue
keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
@@ -516,13 +558,15 @@ onKeydown = (event) ->
isValidFirstKey(KeyboardUtils.getKeyChar(event))))
DomUtils.suppressPropagation(event)
KeydownEvents.push event
+ return @stopBubblingAndTrue
-onKeyup = (event) ->
- handledKeydown = KeydownEvents.pop event
- return unless handlerStack.bubbleEvent("keyup", event)
+ return @continueBubbling
- # Don't propagate the keyup to the underlying page if Vimium has handled it. See #733.
- DomUtils.suppressPropagation(event) if handledKeydown
+# @/this, here, is the the normal-mode Mode object.
+onKeyup = (event) ->
+ return @continueBubbling unless KeydownEvents.pop event
+ DomUtils.suppressPropagation(event)
+ @stopBubblingAndTrue
checkIfEnabledForUrl = ->
url = window.location.toString()
@@ -534,8 +578,12 @@ checkIfEnabledForUrl = ->
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()
+ handlerStack.bubbleEvent "registerStateChange",
+ enabled: response.isEnabledForUrl
+ passKeys: response.passKeys
-refreshCompletionKeys = (response) ->
+# Exported to window, but only for DOM tests.
+window.refreshCompletionKeys = (response) ->
if (response)
currentCompletionKeys = response.completionKeys
@@ -583,35 +631,21 @@ isEditable = (target) ->
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
+# when the last editable element that came into focus -- which targetElement points to -- has been blurred.
+# If insert mode is entered manually (via pressing 'i'), then we set targetElement to 'undefined', and only
# leave insert mode when the user presses <ESC>.
# Note. This returns the truthiness of target, which is required by isInsertMode.
#
-enterInsertModeWithoutShowingIndicator = (target) -> insertModeLock = target
+enterInsertModeWithoutShowingIndicator = (target) ->
+ return # Disabled.
exitInsertMode = (target) ->
- if (target == undefined || insertModeLock == target)
- insertModeLock = null
- HUD.hide()
+ return # Disabled.
isInsertMode = ->
- return true if insertModeLock != null
- # Some sites (e.g. inbox.google.com) change the contentEditable attribute on the fly (see #1245); and
- # unfortunately, isEditable() is called *before* the change is made. Therefore, we need to re-check whether
- # the active element is contentEditable.
- document.activeElement and document.activeElement.isContentEditable and
- enterInsertModeWithoutShowingIndicator document.activeElement
+ return false # Disabled.
# should be called whenever rawQuery is modified.
updateFindModeQuery = ->
@@ -701,6 +735,41 @@ handleEnterForFindMode = ->
document.body.classList.add("vimiumFindMode")
settings.set("findModeRawQuery", findModeQuery.rawQuery)
+class FindMode extends Mode
+ constructor: ->
+ super
+ name: "find"
+ badge: "/"
+ exitOnEscape: true
+ exitOnClick: true
+
+ keydown: (event) =>
+ if event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey
+ handleDeleteForFindMode()
+ @suppressEvent
+ else if event.keyCode == keyCodes.enter
+ handleEnterForFindMode()
+ @exit()
+ @suppressEvent
+ else
+ DomUtils.suppressPropagation(event)
+ handlerStack.stopBubblingAndFalse
+
+ keypress: (event) ->
+ handlerStack.neverContinueBubbling ->
+ if event.keyCode > 31
+ keyChar = String.fromCharCode event.charCode
+ handleKeyCharForFindMode keyChar if keyChar
+
+ keyup: (event) => @suppressEvent
+
+ exit: (event) ->
+ super()
+ handleEscapeForFindMode() if event?.type == "keydown" and KeyboardUtils.isEscape event
+ handleEscapeForFindMode() if event?.type == "click"
+ if findModeQueryHasResults and event?.type != "click"
+ new PostFindMode
+
performFindInPlace = ->
cachedScrollX = window.scrollX
cachedScrollY = window.scrollY
@@ -719,13 +788,9 @@ performFindInPlace = ->
# :options is an optional dict. valid parameters are 'caseSensitive' and 'backwards'.
executeFind = (query, options) ->
+ result = null
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
@@ -737,7 +802,13 @@ executeFind = (query, options) ->
-> document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true)
0)
- findMode = oldFindMode
+ # We are either in normal mode ("n"), or find mode ("/"). We are not in insert mode. Nevertheless, if a
+ # previous find landed in an editable element, then that element may still be activated. In this case, we
+ # don't want to leave it behind (see #1412).
+ if document.activeElement and DomUtils.isEditable document.activeElement
+ if not DomUtils.isSelected document.activeElement
+ document.activeElement.blur()
+
# we need to save the anchor node here because <esc> seems to nullify it, regardless of whether we do
# preventDefault()
findModeAnchorNode = document.getSelection().anchorNode
@@ -750,13 +821,6 @@ focusFoundLink = ->
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.
@@ -764,7 +828,7 @@ selectFoundInputElement = ->
# 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.isDOMDescendant(findModeAnchorNode, document.activeElement))
DomUtils.simulateSelect(document.activeElement)
# the element has already received focus via find(), so invoke insert mode manually
enterInsertModeWithoutShowingIndicator(document.activeElement)
@@ -795,27 +859,11 @@ findAndFocus = (backwards) ->
findModeQueryHasResults =
executeFind(query, { backwards: backwards, caseSensitive: !findModeQuery.ignoreCase })
- if (!findModeQueryHasResults)
+ if findModeQueryHasResults
+ focusFoundLink()
+ new PostFindMode() if findModeQueryHasResults
+ else
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 = 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()
@@ -930,11 +978,10 @@ showFindModeHUDForQuery = ->
window.enterFindMode = ->
findModeQuery = { rawQuery: "" }
- findMode = true
HUD.show("/")
+ new FindMode()
exitFindMode = ->
- findMode = false
HUD.hide()
window.showHelpDialog = (html, fid) ->