diff options
| author | Stephen Blott | 2015-01-10 00:00:11 +0000 |
|---|---|---|
| committer | Stephen Blott | 2015-01-10 07:23:47 +0000 |
| commit | ac90db47aa2671cd663cc6a9cdf783dc30a582e9 (patch) | |
| tree | a80cafd3af5c43ac20620e3c8d9dabd0addd9b7b | |
| parent | d97e7786cb04dbbe5cae8e4b86e25437f66eb799 (diff) | |
| download | vimium-ac90db47aa2671cd663cc6a9cdf783dc30a582e9.tar.bz2 | |
Modes; more changes...
- Better comments.
- Strip unnecessary handlers for leaving post-find mode.
- Simplify passKeys.
- focusInput now re-bubbles its triggering keydown event.
| -rw-r--r-- | content_scripts/mode.coffee | 3 | ||||
| -rw-r--r-- | content_scripts/mode_find.coffee | 17 | ||||
| -rw-r--r-- | content_scripts/mode_insert.coffee | 53 | ||||
| -rw-r--r-- | content_scripts/mode_passkeys.coffee | 13 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 25 | ||||
| -rw-r--r-- | lib/handler_stack.coffee | 37 |
6 files changed, 74 insertions, 74 deletions
diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index b6cb5fae..37f3a8c2 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -45,11 +45,12 @@ class Mode # If this is true, then we generate a trace of modes being activated and deactivated on the console. @debug = true - # Constants; readable shortcuts for event-handler return values. + # Constants; short, readable names for handlerStack event-handler return values. continueBubbling: true suppressEvent: false stopBubblingAndTrue: handlerStack.stopBubblingAndTrue stopBubblingAndFalse: handlerStack.stopBubblingAndFalse + restartBubbling: handlerStack.restartBubbling constructor: (@options={}) -> @handlers = [] diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 3b9f951e..d63b3319 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -2,11 +2,11 @@ # When we use find mode, the selection/focus can end up in a focusable/editable element. In this situation, # special considerations apply. We implement three special cases: -# 1. Be an InsertModeBlocker. This prevents keyboard events from dropping us unintentionally into insert -# mode. This is achieved by inheriting from InsertModeBlocker. +# 1. Prevent keyboard events from dropping us unintentionally into insert mode. This is achieved by +# inheriting from InsertModeBlocker. # 2. Prevent all keyboard events on the active element from propagating. This is achieved by setting the # trapAllKeyboardEvents option. There's some controversy as to whether this is the right thing to do. -# See discussion in #1415. This implements option 2 from there, although option 3 would be a reasonable +# See discussion in #1415. This implements Option 2 from there, although Option 3 would be a reasonable # alternative. # 3. If the very-next keystroke is Escape, then drop immediately into insert mode. # @@ -16,9 +16,10 @@ class PostFindMode extends InsertModeBlocker super name: "post-find" - # Be a singleton. That way, we don't have to keep track of any currently-active instance. Such an - # instance is automatically deactivated when a new instance is created. + # Be a singleton. That way, we don't have to keep track of any currently-active instance. Any active + # instance is automatically deactivated when a new instance is activated. singleton: PostFindMode + exitOnBlur: element trapAllKeyboardEvents: element return @exit() unless element and findModeAnchorNode @@ -42,11 +43,5 @@ class PostFindMode extends InsertModeBlocker @remove() true - # Various ways in which we can leave PostFindMode. - @push - focus: (event) => @alwaysContinueBubbling => @exit() - blur: (event) => @alwaysContinueBubbling => @exit() - keydown: (event) => @alwaysContinueBubbling => @exit() if document.activeElement != element - root = exports ? window root.PostFindMode = PostFindMode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 9a2d5ce1..144b0be6 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -1,5 +1,5 @@ -# This mode is installed when insert mode is active. +# This mode is installed only when insert mode is active. class InsertMode extends Mode constructor: (options = {}) -> defaults = @@ -11,10 +11,10 @@ class InsertMode extends Mode keyup: (event) => @stopBubblingAndTrue exitOnEscape: true blurOnExit: true + targetElement: null - options = extend defaults, options - options.exitOnBlur = options.targetElement || null - super options + options.exitOnBlur ||= options.targetElement + super extend defaults, options triggerSuppressor.suppress() exit: (event = null) -> @@ -34,8 +34,8 @@ class InsertMode extends 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. +# The trigger can be suppressed via triggerSuppressor; see InsertModeBlocker, below. This mode is permanently +# installed (just above normal mode and passkeys mode) on the handler stack. class InsertModeTrigger extends Mode constructor: -> super @@ -44,7 +44,7 @@ class InsertModeTrigger extends Mode 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. + # check (on every keydown) whether the active element is contentEditable. return @continueBubbling unless document.activeElement?.isContentEditable new InsertMode targetElement: document.activeElement @@ -58,7 +58,7 @@ class InsertModeTrigger extends Mode new InsertMode targetElement: event.target - # We may already have focussed an input, so check. + # We may have already focussed an input element, so check. if document.activeElement and DomUtils.isEditable document.activeElement new InsertMode targetElement: document.activeElement @@ -66,12 +66,13 @@ class InsertModeTrigger extends Mode # Used by InsertModeBlocker to suppress InsertModeTrigger; see below. triggerSuppressor = new Utils.Suppressor true # Note: true == @continueBubbling -# Suppresses InsertModeTrigger. This is used by various modes (usually by inheritance) to prevent +# Suppresses InsertModeTrigger. This is used by various modes (usually via inheritance) to prevent # unintentionally dropping into insert mode on focusable elements. class InsertModeBlocker extends Mode constructor: (options = {}) -> triggerSuppressor.suppress() options.name ||= "insert-blocker" + # See "click" handler below for an explanation of options.onClickMode. options.onClickMode ||= InsertMode super options @onExit -> triggerSuppressor.unsuppress() @@ -79,27 +80,29 @@ class InsertModeBlocker extends Mode @push "click": (event) => @alwaysContinueBubbling => - # The user knows best; so, if the user clicks on something, we get out of the way. + # The user knows best; so, if the user clicks on something, the insert-mode blocker gets out of the + # way. @exit event - # However, there's a corner case. If the active element is focusable, then we would have been in - # insert mode had we not been blocking the trigger. Now, clicking on the element will not generate - # a new focus event, so the insert-mode trigger will not fire. We have to handle this case - # specially. @options.onClickMode is the mode to use. + # However, there's a corner case. If the active element is focusable, then, had we not been + # blocking the trigger, we would already have been in insert mode. Now, a click on that element + # will not generate a new focus event, so the insert-mode trigger will not fire. We have to handle + # this case specially. @options.onClickMode specifies the mode to use (by default, insert mode). if document.activeElement and - event.target == document.activeElement and DomUtils.isEditable document.activeElement + event.target == document.activeElement and DomUtils.isEditable document.activeElement new @options.onClickMode targetElement: document.activeElement # There's some unfortunate feature interaction with chrome's content editable handling. If the selection is # content editable and a descendant of the active element, then chrome focuses it on any unsuppressed keyboard -# events. This has the unfortunate effect of dropping us unintentally into insert mode. See #1415. -# This mode sits near the bottom of the handler stack and suppresses keyboard events if: +# event. This has the unfortunate effect of dropping us unintentally into insert mode. See #1415. +# A single instance of this mode sits near the bottom of the handler stack and suppresses keyboard events if: # - they haven't been handled by any other mode (so not by normal mode, passkeys mode, insert mode, and so -# on), and +# on), # - the selection is content editable, and # - the selection is a descendant of the active element. # This should rarely fire, typically only on fudged keypresses in normal mode. And, even then, only in the -# circumstances outlined above. So it shouldn't normally block other extensions or the page itself from +# circumstances outlined above. So, we shouldn't usually be blocking keyboard events for other extensions or +# the page itself. # handling keyboard events. new class ContentEditableTrap extends Mode constructor: -> @@ -109,17 +112,15 @@ new class ContentEditableTrap extends Mode keypress: (event) => @handle => @suppressEvent keyup: (event) => @handle => @suppressEvent - # True if the selection is content editable and a descendant of the active element. In this situation, - # chrome unilaterally focuses the element containing the anchor, dropping us into insert mode. + handle: (func) -> if @isContentEditableFocused() then func() else @continueBubbling + + # True if the selection is content editable and a descendant of the active element. isContentEditableFocused: -> element = document.getSelection()?.anchorNode?.parentElement - return element?.isContentEditable? and - document.activeElement? and + return element?.isContentEditable and + document.activeElement and DomUtils.isDOMDescendant document.activeElement, element - handle: (func) -> - if @isContentEditableFocused() then func() else @continueBubbling - root = exports ? window root.InsertMode = InsertMode root.InsertModeTrigger = InsertModeTrigger diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee index 972dcad7..112e14ed 100644 --- a/content_scripts/mode_passkeys.coffee +++ b/content_scripts/mode_passkeys.coffee @@ -3,24 +3,23 @@ class PassKeysMode extends Mode constructor: -> super name: "passkeys" - keydown: (event) => @handlePassKeyEvent event - keypress: (event) => @handlePassKeyEvent event trackState: true + keydown: (event) => @handleKeyChar KeyboardUtils.getKeyChar event + keypress: (event) => @handleKeyChar String.fromCharCode event.charCode + keyup: (event) => @handleKeyChar String.fromCharCode event.charCode # 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) + handleKeyChar: (keyChar) -> + return @stopBubblingAndTrue if keyChar and not @keyQueue and 0 <= @passKeys.indexOf keyChar @continueBubbling configure: (request) -> @keyQueue = request.keyQueue if request.keyQueue? chooseBadge: (badge) -> - @badge = if @passKeys and not @keyQueue then "P" else "" - super badge + badge.badge ||= "P" if @passKeys and not @keyQueue root = exports ? window root.PassKeysMode = PassKeysMode diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 97fbc56f..a9bf30a3 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -384,8 +384,8 @@ extend window, # shouldn't happen anyway. However, it does no harm to enforce it. singleton: FocusSelector targetMode: targetMode - # For the InsertModeBlocker super-class (we'll always choose InsertMode on click). See comment in - # InsertModeBlocker for an explanation of why this is needed. + # Set the target mode for when/if the active element is clicked. Usually, the target is insert + # mode. See comment in InsertModeBlocker for an explanation of why this is needed. onClickMode: targetMode keydown: (event) => if event.keyCode == KeyboardUtils.keyCodes.tab @@ -397,22 +397,22 @@ extend window, @suppressEvent else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey @exit event - @continueBubbling + # In @exit(), we just pushed a new mode (usually insert mode). Restart bubbling, so that the + # new mode can now see the event too. + @restartBubbling visibleInputs[selectedInputIndex].element.focus() - if visibleInputs.length == 1 - @exit() - else - hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' + return @exit() if visibleInputs.length == 1 + + hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' exit: -> super() DomUtils.removeElement hintContainingDiv if document.activeElement == visibleInputs[selectedInputIndex].element - # The InsertModeBlocker super-class handles the "click" case. + # The InsertModeBlocker super-class handles "click" events, so we should skip it here. unless event?.type == "click" - # In the legacy (and probably common) case, we're entering insert mode here. However, it could be - # some other mode. + # In most cases, we're entering insert mode here. However, it could be some other mode. new @options.targetMode targetElement: document.activeElement @@ -455,7 +455,7 @@ KeydownEvents = # Note that some keys will only register keydown events and not keystroke events, e.g. ESC. # -onKeypress = (event, extra) -> +onKeypress = (event) -> keyChar = "" # Ignore modifier keys by themselves. @@ -484,7 +484,7 @@ onKeypress = (event, extra) -> return true -onKeydown = (event, extra) -> +onKeydown = (event) -> keyChar = "" # handle special keys, and normal input keys with modifiers being pressed. don't handle shiftKey alone (to @@ -789,6 +789,7 @@ class FindMode extends InsertModeBlocker super() handleEscapeForFindMode() if event?.type == "keydown" and KeyboardUtils.isEscape event handleEscapeForFindMode() if event?.type == "click" + # If event?.type == "click", then the InsertModeBlocker super-class will be dropping us into insert mode. new PostFindMode findModeAnchorNode unless event?.type == "click" performFindInPlace = -> diff --git a/lib/handler_stack.coffee b/lib/handler_stack.coffee index 97e189c5..44c7538b 100644 --- a/lib/handler_stack.coffee +++ b/lib/handler_stack.coffee @@ -14,6 +14,11 @@ class HandlerStack # processing should take place. @stopBubblingAndFalse = new Object() + # A handler should return this value to indicate that bubbling should be restarted. Typically, this is + # used when, while bubbling an event, a new mode is pushed onto the stack. See `focusInput` for an + # example. + @restartBubbling = new Object() + # Adds a handler to the top of the stack. Returns a unique ID for that handler that can be used to remove it # later. push: (handler) -> @@ -30,30 +35,26 @@ class HandlerStack # event's propagation by returning a falsy value, or stop bubbling by returning @stopBubblingAndFalse or # @stopBubblingAndTrue. bubbleEvent: (type, event) -> - # extra is passed to each handler. This allows handlers to pass information down the stack. - extra = {} - # 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). + # We take a copy of the array in order to avoid interference from concurrent removes (for example, to + # avoid calling the same handler twice, because elements have been spliced out of the array by remove). for handler in @stack[..].reverse() - # A handler may have been removed (handler.id == null). - if handler and handler.id + # A handler may have been removed (handler.id == null), so check. + if handler?.id and handler[type] @currentId = handler.id - # A handler can register a handler for type "all", which will be invoked on all events. Such an "all" - # handler will be invoked first. - for func in [ handler.all, handler[type] ] - if func - passThrough = func.call @, event, extra - if not passThrough - DomUtils.suppressEvent(event) if @isChromeEvent event - return false - return true if passThrough == @stopBubblingAndTrue - return false if passThrough == @stopBubblingAndFalse + result = handler[type].call @, event + if not result + DomUtils.suppressEvent(event) if @isChromeEvent event + return false + return true if result == @stopBubblingAndTrue + return false if result == @stopBubblingAndFalse + return @bubbleEvent type, event if result == @restartBubbling true remove: (id = @currentId) -> for i in [(@stack.length - 1)..0] by -1 handler = @stack[i] if handler.id == id + # Mark the handler as removed. handler.id = null @stack.splice(i, 1) break @@ -63,7 +64,9 @@ class HandlerStack isChromeEvent: (event) -> event?.preventDefault? or event?.stopImmediatePropagation? - # Convenience wrappers. + # Convenience wrappers. Handlers must return an approriate value. These are wrappers which handlers can + # use to always return the same value. This then means that the handler itself can be implemented without + # regard to its return value. alwaysContinueBubbling: (handler) -> handler() true |
