diff options
| -rw-r--r-- | content_scripts/mode.coffee | 24 | ||||
| -rw-r--r-- | content_scripts/mode_insert.coffee | 80 | ||||
| -rw-r--r-- | lib/utils.coffee | 18 | 
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 = ->  | 
