aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--content_scripts/mode.coffee92
-rw-r--r--content_scripts/mode_insert.coffee57
-rw-r--r--content_scripts/mode_passkeys.coffee26
-rw-r--r--content_scripts/vimium_frontend.coffee68
4 files changed, 125 insertions, 118 deletions
diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee
index 8041f462..9e886a63 100644
--- a/content_scripts/mode.coffee
+++ b/content_scripts/mode.coffee
@@ -11,86 +11,74 @@ class Mode
stopBubblingAndFalse: handlerStack.stopBubblingAndFalse
# Default values.
- name: "" # The name of this mode.
- badge: "" # A badge to display on the popup when this mode is active.
- keydown: "suppress" # A function, or "suppress", "bubble" or "pass"; see checkForBuiltInHandler().
- keypress: "suppress" # A function, or "suppress", "bubble" or "pass"; see checkForBuiltInHandler().
- keyup: "suppress" # A function, or "suppress", "bubble" or "pass"; see checkForBuiltInHandler().
+ name: ""
+ badge: ""
+ keydown: (event) => @continueBubbling
+ keypress: (event) => @continueBubbling
+ keyup: (event) => @continueBubbling
constructor: (options) ->
+ Mode.modes.unshift @
extend @, options
@handlers = []
@handlers.push handlerStack.push
- keydown: @checkForBuiltInHandler "keydown", @keydown
- keypress: @checkForBuiltInHandler "keypress", @keypress
- keyup: @checkForBuiltInHandler "keyup", @keyup
- updateBadgeForMode: (badge) => @updateBadgeForMode badge
-
- Mode.modes.unshift @
-
- # Allow the strings "suppress" and "pass" to be used as proxies for the built-in handlers.
- checkForBuiltInHandler: (type, handler) ->
- switch handler
- when "suppress" then @generateHandler type, @suppressEvent
- when "bubble" then @generateHandler type, @continueBubbling
- when "pass" then @generateHandler type, @stopBubblingAndTrue
- else handler
-
- # Generate a default handler which always always yields the same result; except Esc, which pops the current
- # mode.
- generateHandler: (type, result) ->
- (event) =>
- return result unless type == "keydown" and KeyboardUtils.isEscape event
- @exit()
- @suppressEvent
+ keydown: @keydown
+ keypress: @keypress
+ keyup: @keyup
+ updateBadge: (badge) => handlerStack.alwaysContinueBubbling => @chooseBadge badge
exit: ->
handlerStack.remove handlerId for handlerId in @handlers
Mode.modes = Mode.modes.filter (mode) => mode != @
Mode.updateBadge()
- # Default updateBadgeForMode handler. This is overridden by sub-classes. The default is to install the
- # current mode's badge, unless the bade is already set.
- updateBadgeForMode: (badge) ->
- handlerStack.alwaysContinueBubbling => badge.badge ||= @badge
+ # The badge is chosen by bubbling an "updateBadge" event down the handler stack allowing each mode the
+ # opportunity to choose a badge. chooseBadge, here, is the default: choose the current mode's badge unless
+ # one has already been chosen. This is overridden in sub-classes.
+ chooseBadge: (badge) ->
+ badge.badge ||= @badge
- # Static method. Used externally and internally to initiate bubbling of an updateBadgeForMode event.
- # Do not update the badge:
- # - if this document does not have the focus, or
- # - if the document's body is a frameset
+ # 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 has the focus, and
+ # the document's body isn't a frameset.
@updateBadge: ->
if document.hasFocus()
unless document.body?.tagName.toLowerCase() == "frameset"
badge = {badge: ""}
- handlerStack.bubbleEvent "updateBadgeForMode", badge
- Mode.sendBadge badge.badge
-
- # Static utility to update the browser-popup badge.
- @sendBadge: (badge) ->
- chrome.runtime.sendMessage({ handler: "setBadge", badge: badge })
+ handlerStack.bubbleEvent "updateBadge", badge
+ chrome.runtime.sendMessage({ handler: "setBadge", badge: badge.badge })
- # Install a mode, call a function, and exit the mode again.
+ # Temporarily install a mode.
@runIn: (mode, func) ->
mode = new mode()
func()
mode.exit()
-# A SingletonMode is a Mode of which there may be at most one instance of the same name (@singleton) active at
-# any one time. New instances cancel previous instances on startup.
+# A SingletonMode is a Mode of which there may be at most one instance (of @singleton) active at any one time.
+# New instances cancel previous instances on startup.
class SingletonMode extends Mode
- constructor: (@singleton, options) ->
- @cancel @singleton
- super options
-
@instances: {}
- cancel: (instance) ->
- SingletonMode[instance].exit() if SingletonMode[instance]
-
exit: ->
- delete SingletonMode[@instance]
+ delete SingletonMode[@singleton]
super()
+ constructor: (@singleton, options={}) ->
+ SingletonMode[@singleton].exit() if SingletonMode[@singleton]
+ SingletonMode[@singleton] = @
+ super options
+
+# MultiMode is a collection of modes which are installed or uninstalled together.
+class MultiMode extends Mode
+ constructor: (modes...) ->
+ @modes = (new mode() for mode in modes)
+ super {name: "multimode"}
+
+ exit: ->
+ mode.exit() for mode in modes
+
root = exports ? window
root.Mode = Mode
+root.SingletonMode = SingletonMode
+root.MultiMode = MultiMode
diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee
index ed5d0023..6d7cdb89 100644
--- a/content_scripts/mode_insert.coffee
+++ b/content_scripts/mode_insert.coffee
@@ -1,6 +1,6 @@
class InsertMode extends Mode
- isInsertMode: false
+ insertModeActive: false
insertModeLock: null
# Input or text elements are considered focusable and able to receieve their own keyboard events, and will
@@ -14,34 +14,34 @@ class InsertMode extends Mode
return true
nodeName in ["textarea", "select"]
- # Embedded elements like Flash and quicktime players can obtain focus but cannot be programmatically
- # unfocused.
+ # Embedded elements like Flash and quicktime players can obtain focus.
isEmbed: (element) ->
element.nodeName?.toLowerCase() in ["embed", "object"]
isFocusable: (element) ->
- (@isEditable(element) or @isEmbed element)
+ @isEditable(element) or @isEmbed element
# Check whether insert mode is active. Also, activate insert mode if the current element is content
- # editable.
- isActive: ->
- return true if @isInsertMode
+ # editable (and the event is not suppressed).
+ isActiveOrActivate: (event) ->
+ return true if @insertModeActive
+ return false if event.suppressKeydownTrigger
# Some sites (e.g. inbox.google.com) change the contentEditable attribute on the fly (see #1245); and
# unfortunately, isEditable() is called *before* the change is made. Therefore, we need to re-check
# whether the active element is contentEditable.
@activate() if document.activeElement?.isContentEditable
- @isInsertMode
+ @insertModeActive
activate: (target=null) ->
- unless @isInsertMode
- @isInsertMode = true
+ unless @insertModeActive
+ @insertModeActive = true
@insertModeLock = target
@badge = "I"
Mode.updateBadge()
deactivate: ->
- if @isInsertMode
- @isInsertMode = false
+ if @insertModeActive
+ @insertModeActive = false
@insertModeLock = null
@badge = ""
Mode.updateBadge()
@@ -50,38 +50,41 @@ class InsertMode extends Mode
super
name: "insert"
keydown: (event) =>
- return @continueBubbling if event.suppressInsertMode
- return @continueBubbling unless @isActive()
+ return @continueBubbling unless @isActiveOrActivate event
return @stopBubblingAndTrue unless KeyboardUtils.isEscape event
- # We're now exiting insert mode.
- if @isEditable(event.srcElement) or @isEmbed event.srcElement
- # Remove the focus so the user can't just get himself back into insert mode by typing in the same input
- # box.
+ # We're in insert mode, and now exiting.
+ if event.srcElement? and @isFocusable event.srcElement
+ # 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.
event.srcElement.blur()
@deactivate()
@suppressEvent
- keypress: => if @isInsertMode then @stopBubblingAndTrue else @continueBubbling
- keyup: => if @isInsertMode then @stopBubblingAndTrue else @continueBubbling
+ keypress: => if @insertModeActive then @stopBubblingAndTrue else @continueBubbling
+ keyup: => if @insertModeActive then @stopBubblingAndTrue else @continueBubbling
@handlers.push handlerStack.push
focus: (event) =>
handlerStack.alwaysContinueBubbling =>
- if not @isInsertMode and @isFocusable event.target
+ if not @insertModeActive and @isFocusable event.target
@activate event.target
blur: (event) =>
handlerStack.alwaysContinueBubbling =>
- if @isInsertMode and event.target == @insertModeLock
+ if @insertModeActive and event.target == @insertModeLock
@deactivate()
- # We may already have been dropped into insert mode. So check.
- Mode.updateBadge()
+ # We may already have focussed something, so check, so check.
+ @activate document.activeElement if document.activeElement and @isFocusable document.activeElement
+
+ # Used to prevent keydown events from triggering insert mode (following find).
+ # FIXME(smblott) This is a hack.
+ @suppressKeydownTrigger: (event) ->
+ event.suppressKeydownTrigger = true
-# Utility mode.
# Activate this mode to prevent a focused, editable element from triggering insert mode.
-class FocusMustNotTriggerInsertMode extends Mode
+class InsertModeSuppressFocusTrigger extends Mode
constructor: ->
super()
@handlers.push handlerStack.push
@@ -89,4 +92,4 @@ class FocusMustNotTriggerInsertMode extends Mode
root = exports ? window
root.InsertMode = InsertMode
-root.FocusMustNotTriggerInsertMode = FocusMustNotTriggerInsertMode
+root.InsertModeSuppressFocusTrigger = InsertModeSuppressFocusTrigger
diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee
index c754e967..4c4d7d41 100644
--- a/content_scripts/mode_passkeys.coffee
+++ b/content_scripts/mode_passkeys.coffee
@@ -3,19 +3,8 @@ class PassKeysMode extends Mode
keyQueue: ""
passKeys: ""
- # 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 passKey, then 'gt' and '99t' will
- # neverthless be handled by vimium.
- isPassKey: (keyChar) ->
- not @keyQueue and 0 <= @passKeys.indexOf(keyChar)
-
- handlePassKeyEvent: (event) ->
- for keyChar in [KeyboardUtils.getKeyChar(event), String.fromCharCode(event.charCode)]
- return @stopBubblingAndTrue if keyChar and @isPassKey keyChar
- @continueBubbling
-
- # This is called to set the pass-keys configuration and state with various types of request from various
- # sources, so we handle several cases.
+ # This is called to set the passKeys configuration and state with various types of request from various
+ # sources, so we handle several cases here.
# TODO(smblott) Rationalize this.
configure: (request) ->
if request.isEnabledForUrl?
@@ -27,14 +16,21 @@ class PassKeysMode extends Mode
if request.keyQueue?
@keyQueue = request.keyQueue
+ # 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
+
constructor: ->
super
name: "passkeys"
keydown: (event) => @handlePassKeyEvent event
keypress: (event) => @handlePassKeyEvent event
- keyup: => @continueBubbling
- updateBadgeForMode: (badge) ->
+ chooseBadge: (badge) ->
@badge = if @passKeys and not @keyQueue then "P" else ""
super badge
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index 0f23af05..da479781 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -113,11 +113,9 @@ class NormalMode extends Mode
keypress: onKeypress
keyup: onKeyup
- updateBadgeForMode: (badge) ->
- handlerStack.alwaysContinueBubbling =>
- # Idea... Instead of an icon, we could show the keyQueue here (if it's non-empty).
- super badge
- badge.badge = "" unless isEnabledForUrl
+ chooseBadge: (badge) ->
+ super badge
+ badge.badge = "" unless isEnabledForUrl
#
# Complete initialization work that sould be done prior to DOMReady.
@@ -136,8 +134,10 @@ initializePreDomReady = ->
# Install passKeys and insert modes. These too are permanently on the stack (although not always active).
# Note. There's no need to explicitly Mode.updateBadge(). The new InsertMode() updates the badge.
+ # Note. There's no need to explicitly Mode.updateBadge(). The new InsertMode() updates the badge.
passKeysMode = new PassKeysMode()
insertMode = new InsertMode()
+ Mode.updateBadge()
checkIfEnabledForUrl()
@@ -740,10 +740,10 @@ class FindMode extends Mode
handleEscapeForFindMode()
@exit()
@suppressEvent
- else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey)
+ else if event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey
handleDeleteForFindMode()
@suppressEvent
- else if (event.keyCode == keyCodes.enter)
+ else if event.keyCode == keyCodes.enter
handleEnterForFindMode()
@exit()
@suppressEvent
@@ -761,24 +761,44 @@ class FindMode extends Mode
Mode.updateBadge()
-# If find lands in an editable element, then "Esc" drops us into insert mode.
-class PostFindMode extends Mode
- constructor: (element) ->
- super
+# If find lands in an editable element then:
+# - "Esc" drops us into insert mode.
+# - Subsequent command keypresses should not cause us to drop into insert mode.
+count = 0
+class PostFindMode extends SingletonMode
+ constructor: ->
+ element = document.activeElement
+ handleKeydownEscape = true
+ super PostFindMode,
keydown: (event) =>
- @exit()
- if (KeyboardUtils.isEscape(event))
- DomUtils.simulateSelect(document.activeElement)
- insertMode.activate()
+ if handleKeydownEscape and KeyboardUtils.isEscape event
+ DomUtils.simulateSelect document.activeElement
+ insertMode.activate element
+ @exit()
return @suppressEvent # we have "consumed" this event, so do not propagate
- event.suppressInsertMode = true
- return @continueBubbling
-
- elementCanTakeInput = document.activeElement &&
- DomUtils.isSelectable(document.activeElement) &&
- isDOMDescendant(findModeAnchorNode, document.activeElement)
- elementCanTakeInput ||= document.activeElement?.isContentEditable
- @exit() unless elementCanTakeInput
+ console.log "suppress", event
+ handleKeydownEscape = false
+ InsertMode.suppressKeydownTrigger event
+ # We can safely exit if element is contentEditable. Keystrokes will never cause us to drop into
+ # insert mode anyway.
+ @exit() if element.isContentEditable
+ @continueBubbling
+ keypress: => @continueBubbling
+ keyup: => @continueBubbling
+
+ console.log ++count, "PostFindMode create"
+ canTakeInput = element and DomUtils.isSelectable(element) and isDOMDescendant findModeAnchorNode, element
+ canTakeInput ||= element?.isContentEditable
+ return @exit() unless canTakeInput
+
+ @handlers.push handlerStack.push
+ DOMActive: (event) => @exit()
+ focus: (event) => @exit()
+ blur: (event) => @exit()
+
+ exit: ->
+ console.log ++count, "exit PostFindMode"
+ super()
performFindInPlace = ->
cachedScrollX = window.scrollX
@@ -807,7 +827,7 @@ executeFind = (query, options) ->
HUD.hide(true)
# ignore the selectionchange event generated by find()
document.removeEventListener("selectionchange",restoreDefaultSelectionHighlight, true)
- Mode.runIn FocusMustNotTriggerInsertMode, ->
+ Mode.runIn InsertModeSuppressFocusTrigger, ->
result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false)
setTimeout(
-> document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true)