aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--content_scripts/mode.coffee24
-rw-r--r--content_scripts/mode_insert.coffee80
-rw-r--r--lib/utils.coffee18
3 files changed, 71 insertions, 51 deletions
diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee
index 160debc4..632d3e99 100644
--- a/content_scripts/mode.coffee
+++ b/content_scripts/mode.coffee
@@ -143,15 +143,19 @@ class Mode
# without having to be concerned with the result of the handler itself.
alwaysContinueBubbling: (func) -> handlerStack.alwaysContinueBubbling func
+ # User for sometimes suppressing badge updates.
+ @badgeSuppressor: new Utils.Suppressor()
+
# 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
+ @badgeSuppressor.unlessSuppressed ->
+ if document.hasFocus()
+ handlerStack.bubbleEvent "updateBadge", badge = {badge: ""}
+ chrome.runtime.sendMessage
+ handler: "setBadge"
+ badge: badge.badge
# Temporarily install a mode to protect a function call, then exit the mode. For example, temporarily
# install an InsertModeBlocker.
@@ -163,14 +167,18 @@ class Mode
registerSingleton: do ->
singletons = {} # Static.
(key) ->
- singletons[key].exit() if singletons[key]
+ # We're currently installing a new mode. So we'll be updating the badge shortly. Therefore, we can
+ # 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]
singletons[key] = @
@onExit => delete singletons[key] if singletons[key] == @
# 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 other modes.
+# badge choice of the other active modes.
# Note. We also create the the one-and-only instance, here.
new class BadgeMode extends Mode
constructor: (options) ->
@@ -179,7 +187,7 @@ new class BadgeMode extends Mode
trackState: true
@push
- "focus": => @alwaysContinueBubbling => Mode.updateBadge()
+ "focus": => @alwaysContinueBubbling -> Mode.updateBadge()
chooseBadge: (badge) ->
# If we're not enabled, then post an empty badge.
diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee
index 41d82add..5280aada 100644
--- a/content_scripts/mode_insert.coffee
+++ b/content_scripts/mode_insert.coffee
@@ -17,75 +17,69 @@ isEmbed =(element) ->
isFocusable =(element) ->
isEditable(element) or isEmbed element
+# This mode is installed when insert mode is active.
+class InsertMode extends Mode
+ constructor: (@insertModeLock = null) ->
+ super
+ name: "insert"
+ badge: "I"
+ singleton: InsertMode
+ keydown: (event) => @stopBubblingAndTrue
+ keypress: (event) => @stopBubblingAndTrue
+ keyup: (event) => @stopBubblingAndTrue
+ exitOnEscape: true
+ exitOnBlur: @insertModeLock
+
+ exit: (event = null) ->
+ super()
+ element = event?.srcElement
+ if element and isFocusable element
+ # 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.
+ element.blur()
+
# Automatically trigger insert 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.
class InsertModeTrigger extends Mode
constructor: ->
super
name: "insert-trigger"
keydown: (event) =>
- return @continueBubbling if InsertModeTrigger.isSuppressed()
- # 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.
- return @continueBubbling unless document.activeElement?.isContentEditable
- new InsertMode document.activeElement
- @stopBubblingAndTrue
+ 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.
+ return @continueBubbling unless document.activeElement?.isContentEditable
+ new InsertMode document.activeElement
+ @stopBubblingAndTrue
@push
focus: (event) =>
- @alwaysContinueBubbling =>
- return @continueBubbling if InsertModeTrigger.isSuppressed()
+ triggerSuppressor.unlessSuppressed =>
return if not isFocusable 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
- # Allow other modes (notably InsertModeBlocker, below) to suppress this trigger. All static.
- @suppressors: 0
- @isSuppressed: -> 0 < @suppressors
- @suppress: -> @suppressors += 1
- @unsuppress: -> @suppressors -= 1
+# Used by InsertModeBlocker to suppress InsertModeTrigger; see below.
+triggerSuppressor = new Utils.Suppressor true
# Suppresses InsertModeTrigger. This is used by various modes (usually by inheritance) to prevent
# unintentionally dropping into insert mode on focusable elements.
class InsertModeBlocker extends Mode
constructor: (options = {}) ->
- InsertModeTrigger.suppress()
+ triggerSuppressor.suppress()
options.name ||= "insert-blocker"
super options
-
- exit: ->
- super()
- InsertModeTrigger.unsuppress()
-
-# This mode is installed when insert mode is active.
-class InsertMode extends Mode
- constructor: (@insertModeLock = null) ->
- super
- name: "insert"
- badge: "I"
- singleton: InsertMode
- keydown: (event) => @stopBubblingAndTrue
- keypress: (event) => @stopBubblingAndTrue
- keyup: (event) => @stopBubblingAndTrue
- exitOnEscape: true
- exitOnBlur: @insertModeLock
-
- exit: (event = null) ->
- super()
- element = event?.srcElement
- if element and isFocusable element
- # 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.
- element.blur()
+ @onExit -> triggerSuppressor.unsuppress()
root = exports ? window
root.InsertMode = InsertMode
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 661f7e84..a7bfc440 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -152,6 +152,24 @@ Utils =
# locale-sensitive uppercase detection
hasUpperCase: (s) -> s.toLowerCase() != s
+ # Utility class. This can help different software components to interact without having to share much logic
+ # and/or state. See InsertModeTrigger for an example.
+ # suppressedResult is the value to be returned when a function call is suppressed.
+ Suppressor: class Suppressor
+ constructor: (@suppressedResult = null)->
+ @count = 0
+
+ suppress: -> @count += 1
+ unsuppress: -> @count -= 1
+
+ runSuppresed: (func) ->
+ @suppress()
+ func()
+ @unsuppress()
+
+ unlessSuppressed: (func) ->
+ if 0 < @count then @suppressedResult else func()
+
# This creates a new function out of an existing function, where the new function takes fewer arguments. This
# allows us to pass around functions instead of functions + a partial list of arguments.
Function::curry = ->