aboutsummaryrefslogtreecommitdiffstats
path: root/content_scripts/mode.coffee
diff options
context:
space:
mode:
authorStephen Blott2015-01-18 10:39:09 +0000
committerStephen Blott2015-01-18 10:39:09 +0000
commita1edae57e2847c2b6ffcae60ea8c9c16216e4692 (patch)
tree30ff186038028f9d0c0d5cc08d572ca56dda8819 /content_scripts/mode.coffee
parent8c9e429074580ea20aba662ee430d87bd73ebc4b (diff)
parent5d087c89917e21872711b7b908fcdd3c7e9e7f17 (diff)
downloadvimium-a1edae57e2847c2b6ffcae60ea8c9c16216e4692.tar.bz2
Merge pull request #1413 from smblott-github/modes
A modal-browsing framework
Diffstat (limited to 'content_scripts/mode.coffee')
-rw-r--r--content_scripts/mode.coffee202
1 files changed, 202 insertions, 0 deletions
diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee
new file mode 100644
index 00000000..acc3978e
--- /dev/null
+++ b/content_scripts/mode.coffee
@@ -0,0 +1,202 @@
+#
+# A mode implements a number of keyboard (and possibly other) event handlers which are pushed onto the handler
+# stack when the mode is activated, and popped off when it is deactivated. The Mode class constructor takes a
+# single argument "options" which can define (amongst other things):
+#
+# name:
+# A name for this mode.
+#
+# badge:
+# A badge (to appear on the browser popup).
+# Optional. Define a badge if the badge is constant; for example, in find mode the badge is always "/".
+# Otherwise, do not define a badge, but instead override the updateBadge method; for example, in passkeys
+# mode, the badge may be "P" or "", depending on the configuration state. Or, if the mode *never* shows a
+# badge, then do neither.
+#
+# keydown:
+# keypress:
+# keyup:
+# Key handlers. Optional: provide these as required. The default is to continue bubbling all key events.
+#
+# Further options are described in the constructor, below.
+#
+# Additional handlers associated with a mode can be added by using the push method. For example, if a mode
+# responds to "focus" events, then push an additional handler:
+# @push
+# "focus": (event) => ....
+# Such handlers are removed when the mode is deactivated.
+#
+# The following events can be handled:
+# keydown, keypress, keyup, click, focus and blur
+
+# Debug only.
+count = 0
+
+class Mode
+ # If Mode.debug is true, then we generate a trace of modes being activated and deactivated on the console.
+ debug: false
+ @modes: []
+
+ # Constants; short, readable names for the return values expected by handlerStack.bubbleEvent.
+ continueBubbling: true
+ suppressEvent: false
+ stopBubblingAndTrue: handlerStack.stopBubblingAndTrue
+ stopBubblingAndFalse: handlerStack.stopBubblingAndFalse
+ restartBubbling: handlerStack.restartBubbling
+
+ constructor: (@options = {}) ->
+ @handlers = []
+ @exitHandlers = []
+ @modeIsActive = true
+ @badge = @options.badge || ""
+ @name = @options.name || "anonymous"
+
+ @count = ++count
+ @id = "#{@name}-#{@count}"
+ @log "activate:", @id
+
+ @push
+ keydown: @options.keydown || null
+ keypress: @options.keypress || null
+ keyup: @options.keyup || null
+ updateBadge: (badge) => @alwaysContinueBubbling => @updateBadge badge
+
+ # If @options.exitOnEscape is truthy, then the mode will exit when the escape key is pressed.
+ if @options.exitOnEscape
+ # 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
+ DomUtils.suppressKeyupAfterEscape handlerStack
+ @exit event, event.srcElement
+ @suppressEvent
+
+ # If @options.exitOnBlur is truthy, then it should be an element. The mode will exit when that element
+ # loses the focus.
+ if @options.exitOnBlur
+ @push
+ _name: "mode-#{@id}/exitOnBlur"
+ "blur": (event) => @alwaysContinueBubbling => @exit() if event.target == @options.exitOnBlur
+
+ # If @options.exitOnClick is truthy, then the mode will exit on any click event.
+ if @options.exitOnClick
+ @push
+ _name: "mode-#{@id}/exitOnClick"
+ "click": (event) => @alwaysContinueBubbling => @exit event
+
+ # Some modes are singletons: there may be at most one instance active at any time. A mode is a singleton
+ # if @options.singleton is truthy. The value of @options.singleton should be the key which is intended to
+ # be unique. New instances deactivate existing instances with the same key.
+ if @options.singleton
+ do =>
+ singletons = Mode.singletons ||= {}
+ key = @options.singleton
+ @onExit => delete singletons[key] if singletons[key] == @
+ if singletons[key]
+ @log "singleton:", "deactivating #{singletons[key].id}"
+ singletons[key].exit()
+ singletons[key] = @
+
+ # If @options.trackState is truthy, then the mode mainatins the current state in @enabled and @passKeys,
+ # and calls @registerStateChange() (if defined) whenever the state changes. The mode also tracks the
+ # current keyQueue in @keyQueue.
+ if @options.trackState
+ @enabled = false
+ @passKeys = ""
+ @keyQueue = ""
+ @push
+ _name: "mode-#{@id}/registerStateChange"
+ registerStateChange: ({ enabled: enabled, passKeys: passKeys }) => @alwaysContinueBubbling =>
+ if enabled != @enabled or passKeys != @passKeys
+ @enabled = enabled
+ @passKeys = passKeys
+ @registerStateChange?()
+ registerKeyQueue: ({ keyQueue: keyQueue }) => @alwaysContinueBubbling => @keyQueue = keyQueue
+
+ Mode.modes.push @
+ Mode.updateBadge()
+ @logModes()
+ # End of Mode constructor.
+
+ push: (handlers) ->
+ handlers._name ||= "mode-#{@id}"
+ @handlers.push handlerStack.push handlers
+
+ unshift: (handlers) ->
+ handlers._name ||= "mode-#{@id}"
+ @handlers.push handlerStack.unshift handlers
+
+ onExit: (handler) ->
+ @exitHandlers.push handler
+
+ exit: ->
+ if @modeIsActive
+ @log "deactivate:", @id
+ handler() for handler in @exitHandlers
+ handlerStack.remove handlerId for handlerId in @handlers
+ Mode.modes = Mode.modes.filter (mode) => mode != @
+ Mode.updateBadge()
+ @modeIsActive = false
+
+ # The badge is chosen by bubbling an "updateBadge" event down the handler stack allowing each mode the
+ # opportunity to choose a badge. This is overridden in sub-classes.
+ updateBadge: (badge) ->
+ badge.badge ||= @badge
+
+ # Shorthand for an otherwise long name. This wraps a handler with an arbitrary return value, and always
+ # yields @continueBubbling instead. This simplifies handlers if they always continue bubbling (a common
+ # case), because they do not need to be concerned with the value they yield.
+ alwaysContinueBubbling: handlerStack.alwaysContinueBubbling
+
+ # Static method. Used externally and internally to initiate bubbling of an updateBadge event and to send
+ # the resulting badge to the background page. We only update the badge if this document (hence this frame)
+ # has the focus.
+ @updateBadge: ->
+ if document.hasFocus()
+ handlerStack.bubbleEvent "updateBadge", badge = badge: ""
+ chrome.runtime.sendMessage
+ handler: "setBadge"
+ badge: badge.badge
+
+ # Debugging routines.
+ logModes: ->
+ if @debug
+ @log "active modes (top to bottom):"
+ @log " ", mode.id for mode in Mode.modes[..].reverse()
+
+ log: (args...) ->
+ console.log args... if @debug
+
+ # Return the must-recently activated mode (only used in tests).
+ @top: ->
+ @modes[@modes.length-1]
+
+# 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 modes. We create the the one-and-only instance here.
+new class BadgeMode extends Mode
+ constructor: () ->
+ super
+ name: "badge"
+ trackState: true
+
+ # FIXME(smblott) BadgeMode is currently triggering an updateBadge event on every focus event. That's a
+ # lot, considerably more than 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()
+
+ updateBadge: (badge) ->
+ # If we're not enabled, then post an empty badge.
+ badge.badge = "" unless @enabled
+
+ # When the registerStateChange event bubbles to the bottom of the stack, all modes have been notified. So
+ # it's now time to update the badge.
+ registerStateChange: ->
+ Mode.updateBadge()
+
+root = exports ? window
+root.Mode = Mode