aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--content_scripts/link_hints.coffee37
-rw-r--r--content_scripts/mode.coffee21
-rw-r--r--content_scripts/mode_find.coffee58
-rw-r--r--content_scripts/mode_insert.coffee69
-rw-r--r--content_scripts/vimium_frontend.coffee40
5 files changed, 112 insertions, 113 deletions
diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee
index b0feea8c..5e95ef99 100644
--- a/content_scripts/link_hints.coffee
+++ b/content_scripts/link_hints.coffee
@@ -8,9 +8,10 @@
# In 'filter' mode, our link hints are numbers, and the user can narrow down the range of possibilities by
# typing the text of the link itself.
#
-# The "name" property here is a short-form name to appear in the link-hints mode name. Debugging only. The
+# The "name" property below is a short-form name to appear in the link-hints mode name. Debugging only. The
# key appears in the mode's badge.
# NOTE(smblott) The use of keys in badges is experimental. It may prove too noisy.
+#
OPEN_IN_CURRENT_TAB = { name: "curr-tab", key: "" }
OPEN_IN_NEW_BG_TAB = { name: "bg-tab", key: "B" }
OPEN_IN_NEW_FG_TAB = { name: "fg-tab", key: "F" }
@@ -65,21 +66,13 @@ LinkHints =
@hintMarkerContainingDiv = DomUtils.addElementList(hintMarkers,
{ id: "vimiumHintMarkerContainer", className: "vimiumReset" })
- @handlerMode =
- new class HintMode extends Mode
- constructor: ->
- super
- name: "hint/#{mode.name}"
- badge: "#{mode.key}?"
- exitOnEscape: true
- keydown: (event) -> LinkHints.onKeyDownInMode hintMarkers, event
- # trap all other keyboard events
- keypress: => @stopBubblingAndFalse
- keyup: => @stopBubblingAndFalse
-
- exit: (delay, callback) =>
- LinkHints.deactivateMode delay, callback
- super()
+ @hintMode = new Mode
+ name: "hint/#{mode.name}"
+ badge: "#{mode.key}?"
+ keydown: @onKeyDownInMode.bind(this, hintMarkers),
+ # trap all key events
+ keypress: -> false
+ keyup: -> false
setOpenLinkMode: (@mode) ->
if @mode is OPEN_IN_NEW_BG_TAB or @mode is OPEN_IN_NEW_FG_TAB or @mode is OPEN_WITH_QUEUE
@@ -278,14 +271,13 @@ LinkHints =
# TODO(philc): Ignore keys that have modifiers.
if (KeyboardUtils.isEscape(event))
- # TODO(smblott). Now unreachable. Clean up. Left like this for now to keep the diff clean.
- @handlerMode.exit()
+ @deactivateMode()
else
keyResult = @getMarkerMatcher().matchHintsByKey(hintMarkers, event)
linksMatched = keyResult.linksMatched
delay = keyResult.delay ? 0
if (linksMatched.length == 0)
- @handlerMode.exit()
+ @deactivateMode()
else if (linksMatched.length == 1)
@activateLink(linksMatched[0], delay)
else
@@ -303,7 +295,7 @@ LinkHints =
clickEl = matchedLink.clickableItem
if (DomUtils.isSelectable(clickEl))
DomUtils.simulateSelect(clickEl)
- @handlerMode.exit delay, -> LinkHints.delayMode = false
+ @deactivateMode(delay, -> LinkHints.delayMode = false)
else
# TODO figure out which other input elements should not receive focus
if (clickEl.nodeName.toLowerCase() == "input" && clickEl.type != "button")
@@ -311,11 +303,11 @@ LinkHints =
DomUtils.flashRect(matchedLink.rect)
@linkActivator(clickEl)
if @mode is OPEN_WITH_QUEUE
- @handlerMode.exit delay, ->
+ @deactivateMode delay, ->
LinkHints.delayMode = false
LinkHints.activateModeWithQueue()
else
- @handlerMode.exit delay, -> LinkHints.delayMode = false
+ @deactivateMode(delay, -> LinkHints.delayMode = false)
#
# Shows the marker, highlighting matchingCharCount characters.
@@ -342,6 +334,7 @@ LinkHints =
if (LinkHints.hintMarkerContainingDiv)
DomUtils.removeElement LinkHints.hintMarkerContainingDiv
LinkHints.hintMarkerContainingDiv = null
+ @hintMode.exit()
HUD.hide()
@isActive = false
diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee
index 98d3df80..ebb3e8bc 100644
--- a/content_scripts/mode.coffee
+++ b/content_scripts/mode.coffee
@@ -119,30 +119,9 @@ class Mode
@passKeys = passKeys
@registerStateChange?()
- # If @options.suppressPrintableEvents is truthy, then it should be an element. All printable keyboard
- # events on that element are suppressed, if necessary. They are suppressed *after* bubbling down the
- # handler stack and finding no handler. This is used by PostFindMode to protect active, editable
- # elements. Note, this handler is installed with unshift (not push), so it ends is installed at the
- # *bottom* of the handler stack, and sees keyboard events only after other modes (notably, normal mode)
- # have not handled them.
- if @options.suppressPrintableEvents
- do =>
- handler = (event) =>
- if event.srcElement == @options.suppressPrintableEvents and KeyboardUtils.isPrintable event
- @suppressEvent
- else
- @continueBubbling
-
- @unshift
- _name: "mode-#{@id}/suppressPrintableEvents"
- keydown: handler
- keypress: handler
- keyup: handler
-
Mode.updateBadge() if @badge
Mode.modes.push @
@logStack() if @debug
- # handlerStack.debugOn()
# End of Mode constructor.
push: (handlers) ->
diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee
index bf6e7f5b..08bc9e5d 100644
--- a/content_scripts/mode_find.coffee
+++ b/content_scripts/mode_find.coffee
@@ -2,33 +2,33 @@
# When we use find mode, the selection/focus can end up in a focusable/editable element. In this situation,
# special considerations apply. We implement three special cases:
-# 1. Prevent keyboard events from dropping us unintentionally into insert mode. This is achieved by...
-# 2. Prevent all printable keypress events on the active element from propagating. This is achieved by setting the
-# suppressPrintableEvents option. There's some controversy as to whether this is the right thing to do.
-# See discussion in #1415. This implements Option 2 from there.
+# 1. Prevent keyboard events from dropping us unintentionally into insert mode.
+# 2. Prevent all printable keypress events on the active element from propagating beyond normal mode. See
+# #1415. This implements Option 2 from there.
# 3. If the very-next keystroke is Escape, then drop immediately into insert mode.
#
class PostFindMode extends Mode
constructor: (findModeAnchorNode) ->
element = document.activeElement
- initialSelection = window.getSelection().toString()
super
name: "post-find"
+ badge: "N" # Pretend to be normal mode (because we don't want the insert-mode badge).
# Be a singleton. That way, we don't have to keep track of any currently-active instance. Any active
# instance is automatically deactivated when a new instance is activated.
singleton: PostFindMode
exitOnBlur: element
- suppressPrintableEvents: element
- # If the selection changes (e.g. via paste, or the arrow keys), then the user is interacting with the
- # element, so get out of the way and activate insert mode. This implements 5c (without the input
- # listener) as discussed in #1415.
- keyup: =>
+ exitOnClick: true
+ keydown: (event) -> InsertMode.suppressEvent event
+ keypress: (event) -> InsertMode.suppressEvent event
+ keyup: (event) =>
@alwaysContinueBubbling =>
- if window.getSelection().toString() != initialSelection
+ if document.getSelection().type != "Range"
+ # If the selection is no longer a range, then the user is interacting with the element, so get out
+ # of the way and stop suppressing insert mode. See discussion of Option 5c from #1415.
@exit()
- new InsertMode
- targetElement: element
+ else
+ InsertMode.suppressEvent event
return @exit() unless element and findModeAnchorNode
@@ -36,21 +36,37 @@ class PostFindMode extends Mode
# cannot.
canTakeInput = DomUtils.isSelectable(element) and DomUtils.isDOMDescendant findModeAnchorNode, element
canTakeInput ||= element.isContentEditable
- canTakeInput ||= findModeAnchorNode.parentElement?.isContentEditable
+ canTakeInput ||= findModeAnchorNode.parentElement?.isContentEditable # FIXME(smblott) This is too specific.
return @exit() unless canTakeInput
+ # If the very-next keydown is Esc, drop immediately into insert mode.
self = @
@push
_name: "mode-#{@id}/handle-escape"
keydown: (event) ->
- if element == document.activeElement and KeyboardUtils.isEscape event
- self.exit()
- new InsertMode
- targetElement: element
+ if document.activeElement == element and KeyboardUtils.isEscape event
DomUtils.suppressKeyupAfterEscape handlerStack
- return false
- @remove()
- true
+ self.exit()
+ false # Suppress event.
+ else
+ @remove()
+ true # Continue bubbling.
+
+ # Prevent printable keyboard events from propagating to to the page; see Option 2 from #1415.
+ do =>
+ handler = (event) =>
+ if event.srcElement == element and KeyboardUtils.isPrintable event
+ @suppressEvent
+ else
+ @continueBubbling
+
+ # Note. We use unshift here, instead of push; therefore we see events *after* normal mode, and so only
+ # unmapped keys.
+ @unshift
+ _name: "mode-#{@id}/suppressPrintableEvents"
+ keydown: handler
+ keypress: handler
+ keyup: handler
root = exports ? window
root.PostFindMode = PostFindMode
diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee
index 678e35cc..4be9c589 100644
--- a/content_scripts/mode_insert.coffee
+++ b/content_scripts/mode_insert.coffee
@@ -3,32 +3,67 @@ class InsertMode extends Mode
constructor: (options = {}) ->
defaults =
name: "insert"
- exitOnEscape: true
- keydown: (event) => @handler event
- keypress: (event) => @handler event
- keyup: (event) => @handler event
+ keydown: (event) => @handleKeydownEvent event
+ keypress: (event) => @handleKeyEvent event
+ keyup: (event) => @handleKeyEvent event
super extend defaults, options
+ @insertModeLock = if options.targetElement? then options.targetElement else null
@push
- "blur": => @exit()
+ "blur": => @alwaysContinueBubbling =>
+ if DomUtils.isFocusable event.target
+ @exit event.target
+ Mode.updateBadge()
+ "focus": (event) => @alwaysContinueBubbling =>
+ @insertModeLock = event.target if DomUtils.isFocusable event.target
- active: ->
- document.activeElement and DomUtils.isFocusable document.activeElement
+ if @insertModeLock == null
+ # We may already have focused an input element, so check.
+ @insertModeLock = event.target if document.activeElement and DomUtils.isFocusable document.activeElement
- handler: (event) ->
- if @active() then @stopBubblingAndTrue else @continueBubbling
+ isActive: ->
+ return true if @insertModeLock != null
+ # Some sites (e.g. inbox.google.com) change the contentEditable property on the fly (see #1245); and
+ # unfortunately, the focus event fires *before* the change. Therefore, we need to re-check whether the
+ # active element is contentEditable.
+ @insertModeLock = document.activeElement if document.activeElement?.isContentEditable
+ @insertModeLock != null
- exit: () ->
- document.activeElement.blur() if @active()
- if @options.permanentInsertMode
- # We don't really exit if we're permanently installed.
- Mode.updateBadge()
- else
- super()
+ handleKeydownEvent: (event) ->
+ return @continueBubbling if event == InsertMode.suppressedEvent or not @isActive()
+ return @stopBubblingAndTrue unless KeyboardUtils.isEscape event
+ DomUtils.suppressKeyupAfterEscape handlerStack
+ if DomUtils.isFocusable event.srcElement
+ # Remove focus so the user can't just get himself back into insert mode by typing in the same input
+ # box.
+ # NOTE(smblott, 2014/12/22) Including embeds for .blur() etc. here is experimental. It appears to be
+ # the right thing to do for most common use cases. However, it could also cripple flash-based sites and
+ # games. See discussion in #1211 and #1194.
+ event.srcElement.blur()
+ @exit()
+ Mode.updateBadge()
+ @suppressEvent
+
+ # Handles keypress and keyup events.
+ handleKeyEvent: (event) ->
+ if @isActive() and event != InsertMode.suppressedEvent then @stopBubblingAndTrue else @continueBubbling
+
+ exit: (target) ->
+ if target == undefined or target == @insertModeLock
+ if @options.targetElement?
+ super()
+ else
+ # If @options.targetElement isn't set, then this is the permanently-installed instance from the front
+ # end. So, we don't actually exit; instead, we just reset ourselves.
+ @insertModeLock = null
chooseBadge: (badge) ->
- badge.badge ||= "I" if @active()
+ badge.badge ||= "I" if @isActive()
+
+ # Static stuff to allow PostFindMode to suppress insert mode.
+ @suppressedEvent: null
+ @suppressEvent: (event) -> @suppressedEvent = event
root = exports ? window
root.InsertMode = InsertMode
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index f2e0cb2a..00d90e81 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -6,6 +6,7 @@
#
passKeysMode = null
+insertMode = null
targetElement = null
findMode = false
findModeQuery = { rawQuery: "", matchCount: 0 }
@@ -127,8 +128,7 @@ initializePreDomReady = ->
new NormalMode()
Scroller.init settings
passKeysMode = new PassKeysMode()
- new InsertMode
- permanentInsertMode: true
+ insertMode = new InsertMode()
checkIfEnabledForUrl()
@@ -337,11 +337,10 @@ extend window,
enterVisualMode: =>
new VisualMode()
- focusInput: (count, targetMode = InsertMode) ->
+ focusInput: (count) ->
# Focus the first input element on the page, and create overlays to highlight all the input elements, with
# the currently-focused element highlighted specially. Tabbing will shift focus to the next input element.
# Pressing any other key will remove the overlays and the special tab behavior.
- # targetMode is the mode we want to enter.
resultSet = DomUtils.evaluateXPath(textInputXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE)
visibleInputs =
for i in [0...resultSet.snapshotLength] by 1
@@ -375,13 +374,7 @@ extend window,
super
name: "focus-selector"
badge: "?"
- # Be a singleton. It doesn't make any sense to have two instances active at the same time; and that
- # shouldn't happen anyway. However, it does no harm to enforce it.
- singleton: FocusSelector
- targetMode: targetMode
- # Set the target mode for when/if the active element is clicked. Usually, the target is insert
- # mode. See comment in InsertModeBlocker for an explanation of why this is needed.
- onClickMode: targetMode
+ exitOnClick: true
keydown: (event) =>
if event.keyCode == KeyboardUtils.keyCodes.tab
hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint'
@@ -391,16 +384,8 @@ extend window,
visibleInputs[selectedInputIndex].element.focus()
@suppressEvent
else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey
- mode = @exit event
- if mode
- # In @exit(), we just pushed a new mode (usually insert mode). Restart bubbling, so that the
- # new mode can now see the event too.
- # Exception: If the new mode exits on Escape, and this key event is Escape, then rebubbling the
- # event will just cause the mode to exit immediately. So we suppress Escapes.
- if mode.options.exitOnEscape and KeyboardUtils.isEscape event
- @suppressEvent
- else
- @restartBubbling
+ @exit()
+ @continueBubbling
visibleInputs[selectedInputIndex].element.focus()
return @exit() if visibleInputs.length == 1
@@ -408,14 +393,8 @@ extend window,
hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
exit: ->
- super()
DomUtils.removeElement hintContainingDiv
- if document.activeElement == visibleInputs[selectedInputIndex].element
- # The InsertModeBlocker super-class handles "click" events, so we should skip it here.
- unless event?.type == "click"
- # In most cases, we're entering insert mode here. However, it could be some other mode.
- new @options.targetMode
- targetElement: document.activeElement
+ super()
# 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
@@ -823,10 +802,7 @@ executeFind = (query, options) ->
HUD.hide(true)
# ignore the selectionchange event generated by find()
document.removeEventListener("selectionchange",restoreDefaultSelectionHighlight, true)
- handlerId = handlerStack.push
- focus: -> handlerStack.stopBubblingAndTrue
- result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false)
- handlerStack.remove
+ result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false)
setTimeout(
-> document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true)
0)