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 = -> |
