aboutsummaryrefslogtreecommitdiffstats
path: root/content_scripts
diff options
context:
space:
mode:
authorStephen Blott2015-01-10 18:53:28 +0000
committerStephen Blott2015-01-10 18:53:28 +0000
commitd1c0a5d9bd1f67f2b32a993cfd62bc0b52c44185 (patch)
tree76eaa3b86be1c6ac130724168d0bf3bbbb6849c5 /content_scripts
parent93de6384632e3e682e02be4cb8cea160997de127 (diff)
parent80ad0bc3087a3bf00d61bdd6c9cf48e971e22480 (diff)
downloadvimium-d1c0a5d9bd1f67f2b32a993cfd62bc0b52c44185.tar.bz2
Merge branch 'modes-dev' into modes
Diffstat (limited to 'content_scripts')
-rw-r--r--content_scripts/mode.coffee87
-rw-r--r--content_scripts/mode_find.coffee24
-rw-r--r--content_scripts/mode_insert.coffee38
-rw-r--r--content_scripts/mode_passkeys.coffee15
-rw-r--r--content_scripts/scroller.coffee15
-rw-r--r--content_scripts/vimium_frontend.coffee54
6 files changed, 155 insertions, 78 deletions
diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee
index 8e37ee36..0fcab675 100644
--- a/content_scripts/mode.coffee
+++ b/content_scripts/mode.coffee
@@ -38,18 +38,21 @@
# myMode.exit() # externally triggered.
#
-# For debug only; to be stripped out.
+# For debug only.
count = 0
class Mode
- # If this is true, then we generate a trace of modes being activated and deactivated on the console.
- @debug = true
+ # If Mode.debug is true, then we generate a trace of modes being activated and deactivated on the console, along
+ # with a list of the currently active modes.
+ debug: true
+ @modes: []
- # Constants; readable shortcuts for event-handler return values.
+ # Constants; short, readable names for handlerStack event-handler return values.
continueBubbling: true
suppressEvent: false
stopBubblingAndTrue: handlerStack.stopBubblingAndTrue
stopBubblingAndFalse: handlerStack.stopBubblingAndFalse
+ restartBubbling: handlerStack.restartBubbling
constructor: (@options={}) ->
@handlers = []
@@ -59,7 +62,8 @@ class Mode
@name = @options.name || "anonymous"
@count = ++count
- console.log @count, "create:", @name if Mode.debug
+ @id = "#{@name}-#{@count}"
+ @logger "activate:", @id if @debug
@push
keydown: @options.keydown || null
@@ -79,6 +83,7 @@ class Mode
# Note. This handler ends up above the mode's own key handlers on the handler stack, so it takes
# priority.
@push
+ _name: "mode-#{@id}/exitOnEscape"
"keydown": (event) =>
return @continueBubbling unless KeyboardUtils.isEscape event
@exit event
@@ -89,6 +94,7 @@ class Mode
# loses the focus.
if @options.exitOnBlur
@push
+ _name: "mode-#{@id}/exitOnBlur"
"blur": (event) => @alwaysContinueBubbling => @exit() if event.srcElement == @options.exitOnBlur
# If @options.trackState is truthy, then the mode mainatins the current state in @enabled and @passKeys,
@@ -97,6 +103,7 @@ class Mode
@enabled = false
@passKeys = ""
@push
+ _name: "mode-#{@id}/registerStateChange"
"registerStateChange": ({ enabled: enabled, passKeys: passKeys }) =>
@alwaysContinueBubbling =>
if enabled != @enabled or passKeys != @passKeys
@@ -104,35 +111,43 @@ class Mode
@passKeys = passKeys
@registerStateChange?()
- # If @options.trapAllKeyboardEvents is truthy, then it should be an element. All keyboard events on that
- # element are suppressed *after* bubbling the event down the handler stack. This prevents such events
- # from propagating to other extensions or the host page.
- if @options.trapAllKeyboardEvents
- @unshift
- keydown: (event) => @alwaysContinueBubbling =>
- DomUtils.suppressPropagation event if event.srcElement == @options.trapAllKeyboardEvents
- keypress: (event) => @alwaysContinueBubbling =>
- DomUtils.suppressEvent event if event.srcElement == @options.trapAllKeyboardEvents
- keyup: (event) => @alwaysContinueBubbling =>
- DomUtils.suppressPropagation event if event.srcElement == @options.trapAllKeyboardEvents
+ # If @options.suppressPrintableEvents is truthy, then it should be an element. All printable keypress
+ # 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.
+ if @options.suppressPrintableEvents
+ @push
+ _name: "mode-#{@id}/suppressPrintableEvents"
+ keypress: (event) =>
+ @alwaysContinueBubbling =>
+ if event.srcElement == @options.suppressPrintableEvents
+ if KeyboardUtils.isPrintable(event)
+ event.vimium_suppress_event = true
Mode.updateBadge() if @badge
- # End of Mode.constructor().
+ Mode.modes.push @
+ @log() if @debug
+ # handlerStack.debugOn()
+ # End of Mode constructor.
push: (handlers) ->
+ handlers._name ||= "mode-#{@id}"
@handlers.push handlerStack.push handlers
unshift: (handlers) ->
- @handlers.unshift handlerStack.push handlers
+ handlers._name ||= "mode-#{@id}"
+ handlers._name += "/unshifted"
+ @handlers.push handlerStack.unshift handlers
onExit: (handler) ->
@exitHandlers.push handler
exit: ->
if @modeIsActive
- console.log @count, "exit:", @name if Mode.debug
+ @logger "deactivate:", @id if @debug
handler() for handler in @exitHandlers
handlerStack.remove handlerId for handlerId in @handlers
+ Mode.modes = Mode.modes.filter (mode) => mode != @
Mode.updateBadge()
@modeIsActive = false
@@ -175,11 +190,25 @@ class Mode
# suppress badge updates while exiting any existing active singleton. This prevents the badge from
# flickering in some cases.
Mode.badgeSuppressor.runSuppresed =>
- singletons[key].exit() if singletons[key]
+ if singletons[key]
+ @logger "singleton:", "deactivating #{singletons[key].id}" if @debug
+ singletons[key].exit()
singletons[key] = @
@onExit => delete singletons[key] if singletons[key] == @
+ # Debugging routines.
+ log: ->
+ if Mode.modes.length == 0
+ @logger "It looks like debugging is not enabled in modes.coffee."
+ else
+ @logger "active modes (top to bottom), current: #{@id}"
+ for mode in Mode.modes[..].reverse()
+ @logger " ", mode.id
+
+ logger: (args...) ->
+ handlerStack.log args...
+
# BadgeMode is a pseudo mode for triggering badge updates on focus changes and state updates. It sits at the
# bottom of the handler stack, and so it receives state changes *after* all other modes, and can override the
# badge choice of the other active modes.
@@ -190,7 +219,11 @@ new class BadgeMode extends Mode
name: "badge"
trackState: true
+ # FIXME(smblott) BadgeMode is currently triggering and updateBadge event on every focus event. That's a
+ # lot, considerably more than is necessary. Really, it only needs to trigger when we change frame, or
+ # when we change tab.
@push
+ _name: "mode-#{@id}/focus"
"focus": => @alwaysContinueBubbling -> Mode.updateBadge()
chooseBadge: (badge) ->
@@ -200,5 +233,19 @@ new class BadgeMode extends Mode
registerStateChange: ->
Mode.updateBadge()
+# KeySuppressor is a pseudo mode (near the bottom of the stack) which suppresses keyboard events tagged with
+# the "vimium_suppress_event" property. This allows modes higher up in the stack to tag events for
+# suppression, but only after verifying that no other mode (notably, normal mode) wants to handle the event.
+# Note. We also create the the one-and-only instance, here.
+new class KeySuppressor extends Mode
+ constructor: ->
+ super
+ name: "key-suppressor"
+ keydown: (event) => @handle event
+ keypress: (event) => @handle event
+ keyup: (event) => @handle event
+
+ handle: (event) -> if event.vimium_suppress_event then @suppressEvent else @continueBubbling
+
root = exports ? window
root.Mode = Mode
diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee
index 3b9f951e..91ae4507 100644
--- a/content_scripts/mode_find.coffee
+++ b/content_scripts/mode_find.coffee
@@ -2,11 +2,11 @@
# 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. Be an InsertModeBlocker. This prevents keyboard events from dropping us unintentionally into insert
-# mode. This is achieved by inheriting from InsertModeBlocker.
-# 2. Prevent all keyboard events on the active element from propagating. This is achieved by setting the
-# trapAllKeyboardEvents 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, although option 3 would be a reasonable
+# 1. Prevent keyboard events from dropping us unintentionally into insert mode. This is achieved by
+# inheriting from InsertModeBlocker.
+# 2. Prevent all printable keyboard 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, although Option 3 would be a reasonable
# alternative.
# 3. If the very-next keystroke is Escape, then drop immediately into insert mode.
#
@@ -16,10 +16,11 @@ class PostFindMode extends InsertModeBlocker
super
name: "post-find"
- # Be a singleton. That way, we don't have to keep track of any currently-active instance. Such an
- # instance is automatically deactivated when a new instance is created.
+ # 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
- trapAllKeyboardEvents: element
+ exitOnBlur: element
+ suppressPrintableEvents: element
return @exit() unless element and findModeAnchorNode
@@ -32,6 +33,7 @@ class PostFindMode extends InsertModeBlocker
self = @
@push
+ _name: "mode-#{@id}/handle-escape"
keydown: (event) ->
if element == document.activeElement and KeyboardUtils.isEscape event
self.exit()
@@ -42,11 +44,5 @@ class PostFindMode extends InsertModeBlocker
@remove()
true
- # Various ways in which we can leave PostFindMode.
- @push
- focus: (event) => @alwaysContinueBubbling => @exit()
- blur: (event) => @alwaysContinueBubbling => @exit()
- keydown: (event) => @alwaysContinueBubbling => @exit() if document.activeElement != element
-
root = exports ? window
root.PostFindMode = PostFindMode
diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee
index b86b853c..5720c901 100644
--- a/content_scripts/mode_insert.coffee
+++ b/content_scripts/mode_insert.coffee
@@ -1,5 +1,6 @@
-# This mode is installed when insert mode is active.
+# This mode is installed only when insert mode is active. It is a singleton, so a newly-activated instance
+# displaces any active instance.
class InsertMode extends Mode
constructor: (options = {}) ->
defaults =
@@ -11,12 +12,15 @@ class InsertMode extends Mode
keyup: (event) => @stopBubblingAndTrue
exitOnEscape: true
blurOnExit: true
+ targetElement: null
- options = extend defaults, options
- options.exitOnBlur = options.targetElement || null
- super options
+ # If options.targetElement blurs, we exit.
+ options.exitOnBlur ||= options.targetElement
+ super extend defaults, options
+ triggerSuppressor.suppress()
exit: (event = null) ->
+ triggerSuppressor.unsuppress()
super()
if @options.blurOnExit
element = event?.srcElement
@@ -32,8 +36,8 @@ class InsertMode extends Mode
# - On a keydown event in a contentEditable element.
# - When a focusable element receives the focus.
#
-# The trigger can be suppressed via triggerSuppressor; see InsertModeBlocker, below.
-# This mode is permanently installed fairly low down on the handler stack.
+# The trigger can be suppressed via triggerSuppressor; see InsertModeBlocker, below. This mode is permanently
+# installed (just above normal mode and passkeys mode) on the handler stack.
class InsertModeTrigger extends Mode
constructor: ->
super
@@ -42,13 +46,14 @@ class InsertModeTrigger extends Mode
triggerSuppressor.unlessSuppressed =>
# Some sites (e.g. inbox.google.com) change the contentEditable attribute on the fly (see #1245);
# and unfortunately, the focus event happens *before* the change is made. Therefore, we need to
- # check again whether the active element is contentEditable.
+ # check (on every keydown) whether the active element is contentEditable.
return @continueBubbling unless document.activeElement?.isContentEditable
new InsertMode
targetElement: document.activeElement
@stopBubblingAndTrue
@push
+ _name: "mode-#{@id}/activate-on-focus"
focus: (event) =>
triggerSuppressor.unlessSuppressed =>
@alwaysContinueBubbling =>
@@ -56,7 +61,7 @@ class InsertModeTrigger extends Mode
new InsertMode
targetElement: event.target
- # We may already have focussed an input, so check.
+ # We may have already focussed an input element, so check.
if document.activeElement and DomUtils.isEditable document.activeElement
new InsertMode
targetElement: document.activeElement
@@ -64,27 +69,30 @@ class InsertModeTrigger extends Mode
# Used by InsertModeBlocker to suppress InsertModeTrigger; see below.
triggerSuppressor = new Utils.Suppressor true # Note: true == @continueBubbling
-# Suppresses InsertModeTrigger. This is used by various modes (usually by inheritance) to prevent
+# Suppresses InsertModeTrigger. This is used by various modes (usually via inheritance) to prevent
# unintentionally dropping into insert mode on focusable elements.
class InsertModeBlocker extends Mode
constructor: (options = {}) ->
triggerSuppressor.suppress()
options.name ||= "insert-blocker"
+ # See "click" handler below for an explanation of options.onClickMode.
options.onClickMode ||= InsertMode
super options
@onExit -> triggerSuppressor.unsuppress()
@push
+ _name: "mode-#{@id}/bail-on-click"
"click": (event) =>
@alwaysContinueBubbling =>
- # The user knows best; so, if the user clicks on something, we get out of the way.
+ # The user knows best; so, if the user clicks on something, the insert-mode blocker gets out of the
+ # way.
@exit event
- # However, there's a corner case. If the active element is focusable, then we would have been in
- # insert mode had we not been blocking the trigger. Now, clicking on the element will not generate
- # a new focus event, so the insert-mode trigger will not fire. We have to handle this case
- # specially. @options.onClickMode is the mode to use.
+ # However, there's a corner case. If the active element is focusable, then, had we not been
+ # blocking the trigger, we would already have been in insert mode. Now, a click on that element
+ # will not generate a new focus event, so the insert-mode trigger will not fire. We have to handle
+ # this case specially. @options.onClickMode specifies the mode to use (by default, insert mode).
if document.activeElement and
- event.target == document.activeElement and DomUtils.isEditable document.activeElement
+ event.target == document.activeElement and DomUtils.isEditable document.activeElement
new @options.onClickMode
targetElement: document.activeElement
diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee
index 972dcad7..c4df06dc 100644
--- a/content_scripts/mode_passkeys.coffee
+++ b/content_scripts/mode_passkeys.coffee
@@ -3,24 +3,23 @@ class PassKeysMode extends Mode
constructor: ->
super
name: "passkeys"
- keydown: (event) => @handlePassKeyEvent event
- keypress: (event) => @handlePassKeyEvent event
trackState: true
+ keydown: (event) => @handleKeyChar KeyboardUtils.getKeyChar event
+ keypress: (event) => @handleKeyChar String.fromCharCode event.charCode
+ keyup: (event) => @handleKeyChar String.fromCharCode event.charCode
# Decide whether this event 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.
- handlePassKeyEvent: (event) ->
- for keyChar in [KeyboardUtils.getKeyChar(event), String.fromCharCode(event.charCode)]
- return @stopBubblingAndTrue if keyChar and not @keyQueue and 0 <= @passKeys.indexOf(keyChar)
- @continueBubbling
+ handleKeyChar: (keyChar) ->
+ @alwaysContinueBubbling =>
+ event.vimium_suppress_normal_mode = true if keyChar and not @keyQueue and 0 <= @passKeys.indexOf keyChar
configure: (request) ->
@keyQueue = request.keyQueue if request.keyQueue?
chooseBadge: (badge) ->
- @badge = if @passKeys and not @keyQueue then "P" else ""
- super badge
+ badge.badge ||= "P" if @passKeys and not @keyQueue
root = exports ? window
root.PassKeysMode = PassKeysMode
diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee
index 889dc042..6e2e1ffc 100644
--- a/content_scripts/scroller.coffee
+++ b/content_scripts/scroller.coffee
@@ -124,12 +124,15 @@ CoreScroller =
@keyIsDown = false
handlerStack.push
+ _name: 'scroller/track-key-status'
keydown: (event) =>
- @keyIsDown = true
- @lastEvent = event
+ handlerStack.alwaysContinueBubbling =>
+ @keyIsDown = true
+ @lastEvent = event
keyup: =>
- @keyIsDown = false
- @time += 1
+ handlerStack.alwaysContinueBubbling =>
+ @keyIsDown = false
+ @time += 1
# Return true if CoreScroller would not initiate a new scroll right now.
wouldNotInitiateScroll: -> @lastEvent?.repeat and @settings.get "smoothScroll"
@@ -205,7 +208,9 @@ CoreScroller =
# Scroller contains the two main scroll functions (scrollBy and scrollTo) which are exported to clients.
Scroller =
init: (frontendSettings) ->
- handlerStack.push DOMActivate: -> activatedElement = event.target
+ handlerStack.push
+ _name: 'scroller/active-element'
+ DOMActivate: (event) -> handlerStack.alwaysContinueBubbling -> activatedElement = event.target
CoreScroller.init frontendSettings
# scroll the active element in :direction by :amount * :factor.
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index d91bb181..0da59f03 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -384,8 +384,8 @@ extend window,
# shouldn't happen anyway. However, it does no harm to enforce it.
singleton: FocusSelector
targetMode: targetMode
- # For the InsertModeBlocker super-class (we'll always choose InsertMode on click). See comment in
- # InsertModeBlocker for an explanation of why this is needed.
+ # 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
keydown: (event) =>
if event.keyCode == KeyboardUtils.keyCodes.tab
@@ -396,23 +396,29 @@ extend window,
visibleInputs[selectedInputIndex].element.focus()
@suppressEvent
else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey
- @exit event
- @continueBubbling
+ 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
visibleInputs[selectedInputIndex].element.focus()
- if visibleInputs.length == 1
- @exit()
- else
- hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
+ return @exit() if visibleInputs.length == 1
+
+ hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint'
exit: ->
super()
DomUtils.removeElement hintContainingDiv
if document.activeElement == visibleInputs[selectedInputIndex].element
- # The InsertModeBlocker super-class handles the "click" case.
+ # The InsertModeBlocker super-class handles "click" events, so we should skip it here.
unless event?.type == "click"
- # In the legacy (and probably common) case, we're entering insert mode here. However, it could be
- # some other mode.
+ # In most cases, we're entering insert mode here. However, it could be some other mode.
new @options.targetMode
targetElement: document.activeElement
@@ -455,7 +461,8 @@ KeydownEvents =
# Note that some keys will only register keydown events and not keystroke events, e.g. ESC.
#
-onKeypress = (event, extra) ->
+onKeypress = (event) ->
+ return true if event.vimium_suppress_normal_mode
keyChar = ""
# Ignore modifier keys by themselves.
@@ -465,23 +472,27 @@ onKeypress = (event, extra) ->
# Enter insert mode when the user enables the native find interface.
if (keyChar == "f" && KeyboardUtils.isPrimaryModifierKey(event))
enterInsertModeWithoutShowingIndicator()
- return true
+ return handlerStack.stopBubblingAndTrue
if (keyChar)
if (findMode)
handleKeyCharForFindMode(keyChar)
DomUtils.suppressEvent(event)
+ return handlerStack.stopBubblingAndTrue
else if (!isInsertMode() && !findMode)
if (isPassKey keyChar)
return handlerStack.stopBubblingAndTrue
if currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)
DomUtils.suppressEvent(event)
+ keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
+ return handlerStack.stopBubblingAndTrue
keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
return true
-onKeydown = (event, extra) ->
+onKeydown = (event) ->
+ return true if event.vimium_suppress_normal_mode
keyChar = ""
# handle special keys, and normal input keys with modifiers being pressed. don't handle shiftKey alone (to
@@ -520,37 +531,45 @@ onKeydown = (event, extra) ->
exitInsertMode()
DomUtils.suppressEvent event
KeydownEvents.push event
+ return handlerStack.stopBubblingAndTrue
else if (findMode)
if (KeyboardUtils.isEscape(event))
handleEscapeForFindMode()
DomUtils.suppressEvent event
KeydownEvents.push event
+ return handlerStack.stopBubblingAndTrue
else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey)
handleDeleteForFindMode()
DomUtils.suppressEvent event
KeydownEvents.push event
+ return handlerStack.stopBubblingAndTrue
else if (event.keyCode == keyCodes.enter)
handleEnterForFindMode()
DomUtils.suppressEvent event
KeydownEvents.push event
+ return handlerStack.stopBubblingAndTrue
else if (!modifiers)
DomUtils.suppressPropagation(event)
KeydownEvents.push event
+ return handlerStack.stopBubblingAndTrue
else if (isShowingHelpDialog && KeyboardUtils.isEscape(event))
hideHelpDialog()
DomUtils.suppressEvent event
KeydownEvents.push event
+ return handlerStack.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 handlerStack.stopBubblingAndTrue
keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
@@ -572,12 +591,14 @@ onKeydown = (event, extra) ->
isValidFirstKey(KeyboardUtils.getKeyChar(event))))
DomUtils.suppressPropagation(event)
KeydownEvents.push event
+ return handlerStack.stopBubblingAndTrue
return true
onKeyup = (event) ->
- DomUtils.suppressPropagation(event) if KeydownEvents.pop event
- return true
+ return true unless KeydownEvents.pop event
+ DomUtils.suppressPropagation(event)
+ handlerStack.stopBubblingAndTrue
checkIfEnabledForUrl = ->
url = window.location.toString()
@@ -776,6 +797,7 @@ class FindMode extends InsertModeBlocker
super()
handleEscapeForFindMode() if event?.type == "keydown" and KeyboardUtils.isEscape event
handleEscapeForFindMode() if event?.type == "click"
+ # If event?.type == "click", then the InsertModeBlocker super-class will be dropping us into insert mode.
new PostFindMode findModeAnchorNode unless event?.type == "click"
performFindInPlace = ->