aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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
-rw-r--r--lib/dom_utils.coffee1
-rw-r--r--lib/handler_stack.coffee71
-rw-r--r--lib/keyboard_utils.coffee6
9 files changed, 215 insertions, 96 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 = ->
diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee
index 9d7ca867..322188b3 100644
--- a/lib/dom_utils.coffee
+++ b/lib/dom_utils.coffee
@@ -206,6 +206,7 @@ DomUtils =
# Suppress the next keyup event for Escape.
suppressKeyupAfterEscape: (handlerStack) ->
handlerStack.push
+ _name: "dom_utils/suppressKeyupAfterEscape"
keyup: (event) ->
return true unless KeyboardUtils.isEscape event
@remove()
diff --git a/lib/handler_stack.coffee b/lib/handler_stack.coffee
index 97e189c5..22d04941 100644
--- a/lib/handler_stack.coffee
+++ b/lib/handler_stack.coffee
@@ -3,6 +3,8 @@ root = exports ? window
class HandlerStack
constructor: ->
+ @debug = false
+ @eventNumber = 0
@stack = []
@counter = 0
@@ -14,11 +16,18 @@ class HandlerStack
# processing should take place.
@stopBubblingAndFalse = new Object()
+ # A handler should return this value to indicate that bubbling should be restarted. Typically, this is
+ # used when, while bubbling an event, a new mode is pushed onto the stack. See `focusInput` for an
+ # example.
+ @restartBubbling = new Object()
+
# Adds a handler to the top of the stack. Returns a unique ID for that handler that can be used to remove it
# later.
push: (handler) ->
- @stack.push handler
handler.id = ++@counter
+ handler._name ||= "anon-#{@counter}"
+ @stack.push handler
+ handler.id
# Adds a handler to the bottom of the stack. Returns a unique ID for that handler that can be used to remove
# it later.
@@ -30,30 +39,28 @@ class HandlerStack
# event's propagation by returning a falsy value, or stop bubbling by returning @stopBubblingAndFalse or
# @stopBubblingAndTrue.
bubbleEvent: (type, event) ->
- # extra is passed to each handler. This allows handlers to pass information down the stack.
- extra = {}
- # We take a copy of the array, here, in order to avoid interference from concurrent removes (for example,
- # to avoid calling the same handler twice).
+ @eventNumber += 1
+ # We take a copy of the array in order to avoid interference from concurrent removes (for example, to
+ # avoid calling the same handler twice, because elements have been spliced out of the array by remove).
for handler in @stack[..].reverse()
- # A handler may have been removed (handler.id == null).
- if handler and handler.id
+ # A handler may have been removed (handler.id == null), so check.
+ if handler?.id and handler[type]
@currentId = handler.id
- # A handler can register a handler for type "all", which will be invoked on all events. Such an "all"
- # handler will be invoked first.
- for func in [ handler.all, handler[type] ]
- if func
- passThrough = func.call @, event, extra
- if not passThrough
- DomUtils.suppressEvent(event) if @isChromeEvent event
- return false
- return true if passThrough == @stopBubblingAndTrue
- return false if passThrough == @stopBubblingAndFalse
+ result = handler[type].call @, event
+ @logResult type, event, handler, result if @debug
+ if not result
+ DomUtils.suppressEvent(event) if @isChromeEvent event
+ return false
+ return true if result == @stopBubblingAndTrue
+ return false if result == @stopBubblingAndFalse
+ return @bubbleEvent type, event if result == @restartBubbling
true
remove: (id = @currentId) ->
for i in [(@stack.length - 1)..0] by -1
handler = @stack[i]
if handler.id == id
+ # Mark the handler as removed.
handler.id = null
@stack.splice(i, 1)
break
@@ -63,7 +70,9 @@ class HandlerStack
isChromeEvent: (event) ->
event?.preventDefault? or event?.stopImmediatePropagation?
- # Convenience wrappers.
+ # Convenience wrappers. Handlers must return an approriate value. These are wrappers which handlers can
+ # use to always return the same value. This then means that the handler itself can be implemented without
+ # regard to its return value.
alwaysContinueBubbling: (handler) ->
handler()
true
@@ -72,5 +81,31 @@ class HandlerStack
handler()
false
+ # Debugging.
+ debugOn: -> @debug = true
+ debugOff: -> @debug = false
+
+ logResult: (type, event, handler, result) ->
+ # FIXME(smblott). Badge updating is too noisy, so we filter it out. However, we do need to look at how
+ # many badge update events are happening. It seems to be more than necessary.
+ return if type == "updateBadge"
+ label =
+ switch result
+ when @stopBubblingAndTrue then "stop/true"
+ when @stopBubblingAndFalse then "stop/false"
+ when @restartBubbling then "rebubble"
+ when true then "continue"
+ label ||= if result then "continue/truthy" else "suppress"
+ @log @eventNumber, type, handler._name, label
+
+ logRecords: []
+ log: (args...) ->
+ line = args.join " "
+ @logRecords.push line
+ console.log line
+
+ clipLog: ->
+ Clipboard.copy logRecords.join "\n"
+
root.HandlerStack = HandlerStack
root.handlerStack = new HandlerStack
diff --git a/lib/keyboard_utils.coffee b/lib/keyboard_utils.coffee
index d2a843f9..30d99656 100644
--- a/lib/keyboard_utils.coffee
+++ b/lib/keyboard_utils.coffee
@@ -55,6 +55,12 @@ KeyboardUtils =
# c-[ is mapped to ESC in Vim by default.
(event.keyCode == @keyCodes.ESC) || (event.ctrlKey && @getKeyChar(event) == '[')
+ # TODO. This is probably a poor way of detecting printable characters. However, it shouldn't incorrectly
+ # identify any of chrome's own keyboard shortcuts as printable.
+ isPrintable: (event) ->
+ return false if event.metaKey or event.ctrlKey or event.altKey
+ @getKeyChar(event)?.length == 1
+
KeyboardUtils.init()
root = exports ? window