diff options
| -rw-r--r-- | content_scripts/mode.coffee | 201 | ||||
| -rw-r--r-- | content_scripts/mode_find.coffee | 19 | ||||
| -rw-r--r-- | content_scripts/mode_insert.coffee | 59 | ||||
| -rw-r--r-- | content_scripts/mode_passkeys.coffee | 17 | ||||
| -rw-r--r-- | content_scripts/mode_visual.coffee | 7 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 13 | ||||
| -rw-r--r-- | lib/handler_stack.coffee | 23 | 
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. | 
