aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--content_scripts/mode.coffee201
-rw-r--r--content_scripts/mode_find.coffee19
-rw-r--r--content_scripts/mode_insert.coffee59
-rw-r--r--content_scripts/mode_passkeys.coffee17
-rw-r--r--content_scripts/mode_visual.coffee7
-rw-r--r--content_scripts/vimium_frontend.coffee13
-rw-r--r--lib/handler_stack.coffee23
7 files changed, 149 insertions, 190 deletions
diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee
index e9a4a621..92285b8c 100644
--- a/content_scripts/mode.coffee
+++ b/content_scripts/mode.coffee
@@ -1,50 +1,46 @@
-# Modes.
#
# A mode implements a number of keyboard event handlers which are pushed onto the handler stack when the mode
-# starts, and poped when the mode exits. The Mode base class takes as single argument options which can
-# define:
+# 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) for this mode.
-# Optional. Define a badge is the badge is constant. Otherwise, do not define a badge and override the
-# chooseBadge method instead. Or, if the mode *never* shows a badge, then do neither.
+# A badge (to appear on the browser popup).
+# Optional. Define a badge if the badge is constant. Otherwise, do not define a badge, but override
+# instead the chooseBadge method. 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.
#
-# Additional handlers associated with the mode can be added by using the push method. For example, if a mode
+# 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) => ....
-# Any such additional handlers are removed when the mode exits.
-#
-# New mode types are created by inheriting from Mode or one of its sub-classes. Some generic cub-classes are
-# provided below:
+# Any such handlers are removed when the mode is deactivated.
#
-# SingletonMode: ensures that at most one instance of the mode is active at any one time.
-# ExitOnBlur: exits the mode if the an indicated element loses the focus.
-# ExitOnEscapeMode: exits the mode on escape.
-# StateMode: tracks the current Vimium state in @enabled and @passKeys.
-#
-# To install and existing mode, use:
+# To activate a mode, use:
# myMode = new MyMode()
#
-# To remove a mode, use:
-# myMode.exit() # externally triggered.
+# Or (usually better) just:
+# new MyMode()
+# It is usually not necessary to retain a reference to the mode object.
+#
+# To deactivate a mode, use:
# @exit() # internally triggered (more common).
+# myMode.exit() # externally triggered.
#
# For debug only; to be stripped out.
count = 0
class Mode
- # Static.
- @modes: []
+ @debug = true
# Constants; readable shortcuts for event-handler return values.
continueBubbling: true
@@ -52,31 +48,62 @@ class Mode
stopBubblingAndTrue: handlerStack.stopBubblingAndTrue
stopBubblingAndFalse: handlerStack.stopBubblingAndFalse
- # Default values.
- name: ""
- badge: ""
- keydown: null # null will be ignored by handlerStack (so it's a safe default).
- keypress: null
- keyup: null
-
constructor: (options={}) ->
- Mode.modes.unshift @
- extend @, options
- @modeIsActive = true
- @count = ++count
- console.log @count, "create:", @name
-
+ @options = options
@handlers = []
@exitHandlers = []
+ @modeIsActive = true
+ @badge = options.badge || ""
+ @name = options.name || "anonymous"
+
+ @count = ++count
+ console.log @count, "create:", @name if Mode.debug
@push
- keydown: @keydown
- keypress: @keypress
- keyup: @keyup
+ keydown: options.keydown || null
+ keypress: options.keypress || null
+ keyup: options.keyup || null
updateBadge: (badge) => @alwaysContinueBubbling => @chooseBadge badge
+ # Some modes are singletons: there may be at most one instance active at any one time. A mode is a
+ # singleton if options.singleton is truthy. The value of options.singleton should be the key which is
+ # required to be unique. See PostFindMode for an example.
+ # New instances deactivate existing instances as they themselves are activated.
@registerSingleton options.singleton if options.singleton
+
+ # If options.exitOnEscape is truthy, then the mode will exit when the escape key is pressed. The
+ # triggering keyboard event will be passed to the mode's @exit() method.
+ if options.exitOnEscape
+ # Note. This handler ends up above the mode's own key handlers on the handler stack, so it takes
+ # priority.
+ @push
+ "keydown": (event) =>
+ return @continueBubbling unless KeyboardUtils.isEscape event
+ @exit event
+ DomUtils.suppressKeyupAfterEscape handlerStack
+ @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
+ "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,
+ # and calls @registerStateChange() (if defined) whenever the state changes.
+ if options.trackState
+ @enabled = false
+ @passKeys = ""
+ @push
+ "registerStateChange": ({enabled: enabled, passKeys: passKeys}) =>
+ @alwaysContinueBubbling =>
+ if enabled != @enabled or passKeys != @passKeys
+ @enabled = enabled
+ @passKeys = passKeys
+ @registerStateChange?()
+
Mode.updateBadge() if @badge
+ # End of Mode.constructor().
push: (handlers) ->
@handlers.push handlerStack.push handlers
@@ -86,10 +113,9 @@ class Mode
exit: ->
if @modeIsActive
- console.log @count, "exit:", @name
+ console.log @count, "exit:", @name if Mode.debug
handler() for handler in @exitHandlers
handlerStack.remove handlerId for handlerId in @handlers
- Mode.modes = Mode.modes.filter (mode) => mode != @
Mode.updateBadge()
@modeIsActive = false
@@ -111,97 +137,30 @@ class Mode
handler: "setBadge"
badge: badge.badge
- # Temporarily install a mode to call a function.
+ # Temporarily install a mode to protect a function call, then exit the mode. For example, temporarily
+ # install an InsertModeBlocker.
@runIn: (mode, func) ->
mode = new mode()
func()
mode.exit()
- # Some modes are singletons: there may be at most one instance active at any one time. A mode is a
- # singleton if options.singleton is truthy. The value of options.singleton should be the key which is
- # required to be unique. See PostFindMode for an example.
- @singletons: {}
- registerSingleton: (singleton) ->
- singletons = Mode.singletons
- singletons[singleton].exit() if singletons[singleton]
- singletons[singleton] = @
- @onExit =>
- delete singletons[singleton] if singletons[singleton] == @
-
-# A SingletonMode is a Mode of which there may be at most one instance (of @singleton) active at any one time.
-# New instances cancel previously-active instances on startup.
-class SingletonMode extends Mode
- @instances: {}
-
- exit: ->
- delete SingletonMode.instances[@singleton] if @singleton?
- super()
-
- constructor: (@singleton, options={}) ->
- if @singleton?
- SingletonMode.kill @singleton
- SingletonMode.instances[@singleton] = @
- super options
-
- # Static method. Return whether the indicated mode (singleton) is currently active or not.
- @isActive: (singleton) ->
- @instances[singleton]?
-
- # Static method. If there's a singleton instance active, then kill it.
- @kill: (singleton) ->
- SingletonMode.instances[singleton].exit() if SingletonMode.instances[singleton]
+ registerSingleton: do ->
+ singletons = {} # Static.
+ (key) ->
+ singletons[key].exit() if singletons[key]
+ singletons[key] = @
-# This mode exits when the user hits Esc.
-class ExitOnEscapeMode extends SingletonMode
- constructor: (singleton, options) ->
- super singleton, options
-
- # NOTE. This handler ends up above the mode's own key handlers on the handler stack, so it takes priority.
- @push
- "keydown": (event) =>
- return @continueBubbling unless KeyboardUtils.isEscape event
- @exit
- source: ExitOnEscapeMode
- event: event
- DomUtils.suppressKeyupAfterEscape handlerStack
- @suppressEvent
-
-# This mode exits when element (if defined) loses the focus.
-class ExitOnBlur extends ExitOnEscapeMode
- constructor: (element, singleton=null, options={}) ->
- super singleton, options
-
- if element?
- @push
- "blur": (event) => @alwaysContinueBubbling => @exit() if event.srcElement == element
-
-# The state mode tracks the enabled state in @enabled and @passKeys. It calls @registerStateChange() whenever
-# the state changes. The state is distributed by bubbling a "registerStateChange" event down the handler
-# stack.
-class StateMode extends Mode
- constructor: (options) ->
- @enabled = false
- @passKeys = ""
- super options
-
- @push
- "registerStateChange": ({enabled: enabled, passKeys: passKeys}) =>
- @alwaysContinueBubbling =>
- if enabled != @enabled or passKeys != @passKeys
- @enabled = enabled
- @passKeys = passKeys
- @registerStateChange()
-
- # Overridden by sub-classes.
- registerStateChange: ->
+ @onExit => delete singletons[key] if singletons[key] == @
-# BadgeMode is a psuedo mode for triggering badge updates on focus changes and state updates. It sits at the
+# 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 choices of all other modes.
-new class BadgeMode extends StateMode
+# badge choices of other modes.
+# Note. We also create the the one-and-only instance, here.
+new class BadgeMode extends Mode
constructor: (options) ->
super
name: "badge"
+ trackState: true
@push
"focus": => @alwaysContinueBubbling => Mode.updateBadge()
@@ -215,7 +174,3 @@ new class BadgeMode extends StateMode
root = exports ? window
root.Mode = Mode
-root.SingletonMode = SingletonMode
-root.ExitOnBlur = ExitOnBlur
-root.StateMode = StateMode
-root.ExitOnEscapeMode = ExitOnEscapeMode
diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee
index 18cb7b71..f9766e3a 100644
--- a/content_scripts/mode_find.coffee
+++ b/content_scripts/mode_find.coffee
@@ -3,26 +3,24 @@
# When we use find mode, the selection/focus can end up in a focusable/editable element. In this situation,
# PostFindMode handles two special cases:
# 1. Be an InsertModeBlocker. This prevents keyboard events from dropping us unintentionaly into insert
-# mode. Here, this is achieved by inheriting from InsertModeBlocker.
+# mode. This is achieved by inheriting from InsertModeBlocker.
# 2. If the very-next keystroke is Escape, then drop immediately into insert mode.
#
class PostFindMode extends InsertModeBlocker
constructor: (findModeAnchorNode) ->
- element = document.activeElement
-
super
name: "post-find"
singleton: PostFindMode
+ element = document.activeElement
return @exit() unless element and findModeAnchorNode
# Special cases only arise if the active element can take input. So, exit immediately if it cannot not.
canTakeInput = DomUtils.isSelectable(element) and DomUtils.isDOMDescendant findModeAnchorNode, element
canTakeInput ||= element.isContentEditable
- canTakeInput ||= findModeAnchorNode?.parentElement?.isContentEditable
+ canTakeInput ||= findModeAnchorNode.parentElement?.isContentEditable
return @exit() unless canTakeInput
- self = @
@push
keydown: (event) ->
if element == document.activeElement and KeyboardUtils.isEscape event
@@ -33,13 +31,18 @@ class PostFindMode extends InsertModeBlocker
@remove()
true
- # Install various ways in which we can leave this mode.
+ # Various ways in which we can leave PostFindMode.
@push
- DOMActive: (event) => @alwaysContinueBubbling => @exit()
- click: (event) => @alwaysContinueBubbling => @exit()
focus: (event) => @alwaysContinueBubbling => @exit()
blur: (event) => @alwaysContinueBubbling => @exit()
keydown: (event) => @alwaysContinueBubbling => @exit() if document.activeElement != element
+ # If element is selectable, then it's already focused. If the user clicks on it, then there's no new
+ # focus event, so InsertModeTrigger doesn't fire and we don't drop automatically into insert mode.
+ click: (event) =>
+ @alwaysContinueBubbling =>
+ new InsertMode event.target if DomUtils.isDOMDescendant element, event.target
+ @exit()
+
root = exports ? window
root.PostFindMode = PostFindMode
diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee
index 83d85fa7..b80a78ee 100644
--- a/content_scripts/mode_insert.coffee
+++ b/content_scripts/mode_insert.coffee
@@ -18,36 +18,35 @@ isFocusable =(element) ->
isEditable(element) or isEmbed element
# This mode is installed when insert mode is active.
-class InsertMode extends ExitOnBlur
- constructor: (@insertModeLock=null) ->
- super @insertModeLock, InsertMode,
+class InsertMode extends Mode
+ constructor: (@insertModeLock = null) ->
+ super
name: "insert"
badge: "I"
keydown: (event) => @stopBubblingAndTrue
keypress: (event) => @stopBubblingAndTrue
keyup: (event) => @stopBubblingAndTrue
+ singleton: InsertMode
+ exitOnEscape: true
+ exitOnBlur: @insertModeLock
- exit: (extra={}) ->
+ exit: (event = null) ->
super()
- if extra.source == ExitOnEscapeMode and extra.event?.srcElement?
- if isFocusable extra.event.srcElement
+ if @insertModeLock and event?.srcElement == @insertModeLock
+ if isFocusable @insertModeLock
# Remove the 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() 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.
- extra.event.srcElement.blur()
+ @insertModeLock.blur()
- # Static method. Return whether insert mode is currently active or not.
- @isActive: (singleton) -> SingletonMode.isActive InsertMode
+ # Static method. Check whether insert mode is currently active.
+ @isActive: (extra) -> extra?.insertModeIsActive
# Trigger insert mode:
# - On a keydown event in a contentEditable element.
# - When a focusable element receives the focus.
-# - When an editable activeElement is clicked. We cannot rely exclusively on focus events for triggering
-# insert mode. With find mode, an editable element can be active, but we're not in insert mode (see
-# PostFindMode), so no focus event will be generated. In this case, clicking on the element should
-# activate insert mode.
#
# This mode is permanently installed fairly low down on the handler stack.
class InsertModeTrigger extends Mode
@@ -55,7 +54,7 @@ class InsertModeTrigger extends Mode
super
name: "insert-trigger"
keydown: (event, extra) =>
- return @continueBubbling if InsertModeBlocker.isActive extra
+ return @continueBubbling if InsertModeTrigger.isDisabled extra
# 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.
@@ -66,31 +65,29 @@ class InsertModeTrigger extends Mode
@push
focus: (event, extra) =>
@alwaysContinueBubbling =>
- unless InsertMode.isActive() or InsertModeBlocker.isActive extra
- new InsertMode event.target if isFocusable event.target
+ return @continueBubbling if InsertModeTrigger.isDisabled extra
+ return if not isFocusable event.target
+ new InsertMode event.target
- click: (event, extra) =>
- @alwaysContinueBubbling =>
- # Do not check InsertModeBlocker.isActive() here. A user click overrides the blocker.
- unless InsertMode.isActive()
- if document.activeElement == event.target and isEditable event.target
- new InsertMode event.target
+ # We may already have focussed an input, so check.
+ new InsertMode document.activeElement if document.activeElement and isEditable document.activeElement
- # We may already have focussed something, so check.
- new InsertMode document.activeElement if document.activeElement and isFocusable document.activeElement
+ # Allow other modes to disable this trigger. Static.
+ @disable: (extra) -> extra.disableInsertModeTrigger = true
+ @isDisabled: (extra) -> extra?.disableInsertModeTrigger
-# Disables InsertModeTrigger. Used by find mode and findFocus to prevent unintentionally dropping into insert
-# mode on focusable elements.
+# Disables InsertModeTrigger. This is used by find mode and by findFocus to prevent unintentionally dropping
+# into insert mode on focusable elements.
class InsertModeBlocker extends Mode
- constructor: (options={}) ->
+ constructor: (options = {}) ->
options.name ||= "insert-blocker"
super options
@push
- "all": (event, extra) => @alwaysContinueBubbling => extra.isInsertModeBlockerActive = true
-
- # Static method. Return whether an insert-mode blocker is currently active or not.
- @isActive: (extra) -> extra?.isInsertModeBlockerActive
+ "focus": (event, extra) => @alwaysContinueBubbling -> InsertModeTrigger.disable extra
+ "keydown": (event, extra) => @alwaysContinueBubbling -> InsertModeTrigger.disable extra
+ "keypress": (event, extra) => @alwaysContinueBubbling -> InsertModeTrigger.disable extra
+ "keyup": (event, extra) => @alwaysContinueBubbling -> InsertModeTrigger.disable extra
root = exports ? window
root.InsertMode = InsertMode
diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee
index c8afed39..972dcad7 100644
--- a/content_scripts/mode_passkeys.coffee
+++ b/content_scripts/mode_passkeys.coffee
@@ -1,7 +1,11 @@
-class PassKeysMode extends StateMode
- configure: (request) ->
- @keyQueue = request.keyQueue if request.keyQueue?
+class PassKeysMode extends Mode
+ constructor: ->
+ super
+ name: "passkeys"
+ keydown: (event) => @handlePassKeyEvent event
+ keypress: (event) => @handlePassKeyEvent event
+ trackState: true
# 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
@@ -11,11 +15,8 @@ class PassKeysMode extends StateMode
return @stopBubblingAndTrue if keyChar and not @keyQueue and 0 <= @passKeys.indexOf(keyChar)
@continueBubbling
- constructor: ->
- super
- name: "passkeys"
- keydown: (event) => @handlePassKeyEvent event
- keypress: (event) => @handlePassKeyEvent event
+ configure: (request) ->
+ @keyQueue = request.keyQueue if request.keyQueue?
chooseBadge: (badge) ->
@badge = if @passKeys and not @keyQueue then "P" else ""
diff --git a/content_scripts/mode_visual.coffee b/content_scripts/mode_visual.coffee
index a9acf8be..2580106d 100644
--- a/content_scripts/mode_visual.coffee
+++ b/content_scripts/mode_visual.coffee
@@ -1,10 +1,11 @@
-# Note. ExitOnBlur extends extends ExitOnEscapeMode. So exit-on-escape is handled there.
-class VisualMode extends ExitOnBlur
+class VisualMode extends Mode
constructor: (element=null) ->
- super element, null,
+ super
name: "visual"
badge: "V"
+ exitOnEscape: true
+ exitOnBlur: element
keydown: (event) =>
return @suppressEvent
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index 193a1592..f0196c74 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -469,7 +469,7 @@ onKeypress = (event, extra) ->
keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
- if InsertModeBlocker.isActive extra
+ if InsertModeTrigger.isDisabled extra
# If PostFindMode is active, then we're blocking vimium's keystrokes from going into an input
# element. So we should also block other keystrokes (otherwise, it's weird). There's some controversy as
# to whether this is the right thing to do. See discussion in #1415.
@@ -568,7 +568,7 @@ onKeydown = (event, extra) ->
isValidFirstKey(KeyboardUtils.getKeyChar(event))))
DomUtils.suppressPropagation(event)
KeydownEvents.push event
- else if InsertModeBlocker.isActive extra
+ else if InsertModeTrigger.isDisabled extra
# If PostFindMode is active, then we're blocking vimium's keystrokes from going into an input
# element. So we should also block other keystrokes (otherwise, it's weird). There's some controversy as
# to whether this is the right thing to do. See discussion in #1415.
@@ -747,11 +747,12 @@ handleEnterForFindMode = ->
document.body.classList.add("vimiumFindMode")
settings.set("findModeRawQuery", findModeQuery.rawQuery)
-class FindMode extends ExitOnEscapeMode
+class FindMode extends Mode
constructor: ->
- super FindMode,
+ super
name: "find"
badge: "/"
+ exitOnEscape: true
keydown: (event) =>
if event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey
@@ -773,9 +774,9 @@ class FindMode extends ExitOnEscapeMode
keyup: (event) => @suppressEvent
- exit: (extra) ->
- handleEscapeForFindMode() if extra?.source == ExitOnEscapeMode
+ exit: (event) ->
super()
+ handleEscapeForFindMode() if event and KeyboardUtils.isEscape event
new PostFindMode findModeAnchorNode
performFindInPlace = ->
diff --git a/lib/handler_stack.coffee b/lib/handler_stack.coffee
index 9da0bc33..4d186341 100644
--- a/lib/handler_stack.coffee
+++ b/lib/handler_stack.coffee
@@ -15,11 +15,10 @@ class HandlerStack
@stopBubblingAndFalse = new Object()
# Adds a handler to the stack. Returns a unique ID for that handler that can be used to remove it later.
- # We use unshift (which is more expensive than push) so that bubbleEvent can just iterate over the stack in
- # the normal order.
push: (handler) ->
- @stack.unshift handler
handler.id = ++@counter
+ @stack.push handler
+ handler.id
# Called whenever we receive a key or other event. Each individual handler has the option to stop the
# event's propagation by returning a falsy value, or stop bubbling by returning @stopBubblingAndFalse or
@@ -27,8 +26,10 @@ class HandlerStack
bubbleEvent: (type, event) ->
# extra is passed to each handler. This allows handlers to pass information down the stack.
extra = {}
- for handler in @stack[..] # Take a copy of @stack, so that concurrent removes do not interfere.
- # We need to check whether the handler has been removed (handler.id == null).
+ # 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).
+ for handler in @stack[..].reverse()
+ # A handler may have been removed (handler.id == null).
if handler and handler.id
@currentId = handler.id
# A handler can register a handler for type "all", which will be invoked on all events. Such an "all"
@@ -44,12 +45,12 @@ class HandlerStack
true
remove: (id = @currentId) ->
- # This is more expense than splicing @stack, but better because splicing can interfere with concurrent
- # bubbleEvents.
- @stack = @stack.filter (handler) ->
- # Mark this handler as removed (so concurrent bubbleEvents will know not to invoke it).
- handler.id = null if handler.id == id
- handler?.id?
+ for i in [(@stack.length - 1)..0] by -1
+ handler = @stack[i]
+ if handler.id == id
+ handler.id = null
+ @stack.splice(i, 1)
+ break
# The handler stack handles chrome events (which may need to be suppressed) and internal (fake) events.
# This checks whether that the event at hand is a chrome event.