diff options
Diffstat (limited to 'content_scripts')
| -rw-r--r-- | content_scripts/mode.coffee | 87 | ||||
| -rw-r--r-- | content_scripts/mode_find.coffee | 24 | ||||
| -rw-r--r-- | content_scripts/mode_insert.coffee | 38 | ||||
| -rw-r--r-- | content_scripts/mode_passkeys.coffee | 15 | ||||
| -rw-r--r-- | content_scripts/scroller.coffee | 15 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 54 |
6 files changed, 155 insertions, 78 deletions
diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 8e37ee36..0fcab675 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -38,18 +38,21 @@ # myMode.exit() # externally triggered. # -# For debug only; to be stripped out. +# For debug only. count = 0 class Mode - # If this is true, then we generate a trace of modes being activated and deactivated on the console. - @debug = true + # If Mode.debug is true, then we generate a trace of modes being activated and deactivated on the console, along + # with a list of the currently active modes. + debug: true + @modes: [] - # 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 = [] @@ -59,7 +62,8 @@ class Mode @name = @options.name || "anonymous" @count = ++count - console.log @count, "create:", @name if Mode.debug + @id = "#{@name}-#{@count}" + @logger "activate:", @id if @debug @push keydown: @options.keydown || null @@ -79,6 +83,7 @@ class Mode # Note. This handler ends up above the mode's own key handlers on the handler stack, so it takes # priority. @push + _name: "mode-#{@id}/exitOnEscape" "keydown": (event) => return @continueBubbling unless KeyboardUtils.isEscape event @exit event @@ -89,6 +94,7 @@ class Mode # loses the focus. if @options.exitOnBlur @push + _name: "mode-#{@id}/exitOnBlur" "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, @@ -97,6 +103,7 @@ class Mode @enabled = false @passKeys = "" @push + _name: "mode-#{@id}/registerStateChange" "registerStateChange": ({ enabled: enabled, passKeys: passKeys }) => @alwaysContinueBubbling => if enabled != @enabled or passKeys != @passKeys @@ -104,35 +111,43 @@ class Mode @passKeys = passKeys @registerStateChange?() - # If @options.trapAllKeyboardEvents is truthy, then it should be an element. All keyboard events on that - # element are suppressed *after* bubbling the event down the handler stack. This prevents such events - # from propagating to other extensions or the host page. - if @options.trapAllKeyboardEvents - @unshift - keydown: (event) => @alwaysContinueBubbling => - DomUtils.suppressPropagation event if event.srcElement == @options.trapAllKeyboardEvents - keypress: (event) => @alwaysContinueBubbling => - DomUtils.suppressEvent event if event.srcElement == @options.trapAllKeyboardEvents - keyup: (event) => @alwaysContinueBubbling => - DomUtils.suppressPropagation event if event.srcElement == @options.trapAllKeyboardEvents + # If @options.suppressPrintableEvents is truthy, then it should be an element. All printable keypress + # events on that element are suppressed, if necessary. They are suppressed *after* bubbling down the + # handler stack and finding no handler. This is used by PostFindMode to protect active, editable + # elements. + if @options.suppressPrintableEvents + @push + _name: "mode-#{@id}/suppressPrintableEvents" + keypress: (event) => + @alwaysContinueBubbling => + if event.srcElement == @options.suppressPrintableEvents + if KeyboardUtils.isPrintable(event) + event.vimium_suppress_event = true Mode.updateBadge() if @badge - # End of Mode.constructor(). + Mode.modes.push @ + @log() if @debug + # handlerStack.debugOn() + # End of Mode constructor. push: (handlers) -> + handlers._name ||= "mode-#{@id}" @handlers.push handlerStack.push handlers unshift: (handlers) -> - @handlers.unshift handlerStack.push handlers + handlers._name ||= "mode-#{@id}" + handlers._name += "/unshifted" + @handlers.push handlerStack.unshift handlers onExit: (handler) -> @exitHandlers.push handler exit: -> if @modeIsActive - console.log @count, "exit:", @name if Mode.debug + @logger "deactivate:", @id if @debug handler() for handler in @exitHandlers handlerStack.remove handlerId for handlerId in @handlers + Mode.modes = Mode.modes.filter (mode) => mode != @ Mode.updateBadge() @modeIsActive = false @@ -175,11 +190,25 @@ class Mode # 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] + if singletons[key] + @logger "singleton:", "deactivating #{singletons[key].id}" if @debug + singletons[key].exit() singletons[key] = @ @onExit => delete singletons[key] if singletons[key] == @ + # Debugging routines. + log: -> + if Mode.modes.length == 0 + @logger "It looks like debugging is not enabled in modes.coffee." + else + @logger "active modes (top to bottom), current: #{@id}" + for mode in Mode.modes[..].reverse() + @logger " ", mode.id + + logger: (args...) -> + handlerStack.log args... + # 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 choice of the other active modes. @@ -190,7 +219,11 @@ new class BadgeMode extends Mode name: "badge" trackState: true + # FIXME(smblott) BadgeMode is currently triggering and updateBadge event on every focus event. That's a + # lot, considerably more than is necessary. Really, it only needs to trigger when we change frame, or + # when we change tab. @push + _name: "mode-#{@id}/focus" "focus": => @alwaysContinueBubbling -> Mode.updateBadge() chooseBadge: (badge) -> @@ -200,5 +233,19 @@ new class BadgeMode extends Mode registerStateChange: -> Mode.updateBadge() +# KeySuppressor is a pseudo mode (near the bottom of the stack) which suppresses keyboard events tagged with +# the "vimium_suppress_event" property. This allows modes higher up in the stack to tag events for +# suppression, but only after verifying that no other mode (notably, normal mode) wants to handle the event. +# Note. We also create the the one-and-only instance, here. +new class KeySuppressor extends Mode + constructor: -> + super + name: "key-suppressor" + keydown: (event) => @handle event + keypress: (event) => @handle event + keyup: (event) => @handle event + + handle: (event) -> if event.vimium_suppress_event then @suppressEvent else @continueBubbling + root = exports ? window root.Mode = Mode diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 3b9f951e..91ae4507 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. -# 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 +# 1. Prevent keyboard events from dropping us unintentionally into insert mode. This is achieved by +# inheriting from InsertModeBlocker. +# 2. Prevent all printable keyboard events on the active element from propagating. This is achieved by setting the +# suppressPrintableEvents 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 # alternative. # 3. If the very-next keystroke is Escape, then drop immediately into insert mode. # @@ -16,10 +16,11 @@ 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 - trapAllKeyboardEvents: element + exitOnBlur: element + suppressPrintableEvents: element return @exit() unless element and findModeAnchorNode @@ -32,6 +33,7 @@ class PostFindMode extends InsertModeBlocker self = @ @push + _name: "mode-#{@id}/handle-escape" keydown: (event) -> if element == document.activeElement and KeyboardUtils.isEscape event self.exit() @@ -42,11 +44,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 b86b853c..5720c901 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -1,5 +1,6 @@ -# This mode is installed when insert mode is active. +# This mode is installed only when insert mode is active. It is a singleton, so a newly-activated instance +# displaces any active instance. class InsertMode extends Mode constructor: (options = {}) -> defaults = @@ -11,12 +12,15 @@ class InsertMode extends Mode keyup: (event) => @stopBubblingAndTrue exitOnEscape: true blurOnExit: true + targetElement: null - options = extend defaults, options - options.exitOnBlur = options.targetElement || null - super options + # If options.targetElement blurs, we exit. + options.exitOnBlur ||= options.targetElement + super extend defaults, options + triggerSuppressor.suppress() exit: (event = null) -> + triggerSuppressor.unsuppress() super() if @options.blurOnExit element = event?.srcElement @@ -32,8 +36,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 @@ -42,13 +46,14 @@ 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 @stopBubblingAndTrue @push + _name: "mode-#{@id}/activate-on-focus" focus: (event) => triggerSuppressor.unlessSuppressed => @alwaysContinueBubbling => @@ -56,7 +61,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 @@ -64,27 +69,30 @@ 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() @push + _name: "mode-#{@id}/bail-on-click" "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 diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee index 972dcad7..c4df06dc 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) - @continueBubbling + handleKeyChar: (keyChar) -> + @alwaysContinueBubbling => + event.vimium_suppress_normal_mode = true if keyChar and not @keyQueue and 0 <= @passKeys.indexOf keyChar 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/scroller.coffee b/content_scripts/scroller.coffee index 889dc042..6e2e1ffc 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -124,12 +124,15 @@ CoreScroller = @keyIsDown = false handlerStack.push + _name: 'scroller/track-key-status' keydown: (event) => - @keyIsDown = true - @lastEvent = event + handlerStack.alwaysContinueBubbling => + @keyIsDown = true + @lastEvent = event keyup: => - @keyIsDown = false - @time += 1 + handlerStack.alwaysContinueBubbling => + @keyIsDown = false + @time += 1 # Return true if CoreScroller would not initiate a new scroll right now. wouldNotInitiateScroll: -> @lastEvent?.repeat and @settings.get "smoothScroll" @@ -205,7 +208,9 @@ CoreScroller = # Scroller contains the two main scroll functions (scrollBy and scrollTo) which are exported to clients. Scroller = init: (frontendSettings) -> - handlerStack.push DOMActivate: -> activatedElement = event.target + handlerStack.push + _name: 'scroller/active-element' + DOMActivate: (event) -> handlerStack.alwaysContinueBubbling -> activatedElement = event.target CoreScroller.init frontendSettings # scroll the active element in :direction by :amount * :factor. diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index d91bb181..0da59f03 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 @@ -396,23 +396,29 @@ extend window, visibleInputs[selectedInputIndex].element.focus() @suppressEvent else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey - @exit event - @continueBubbling + mode = @exit event + if mode + # In @exit(), we just pushed a new mode (usually insert mode). Restart bubbling, so that the + # new mode can now see the event too. + # Exception: If the new mode exits on Escape, and this key event is Escape, then rebubbling the + # event will just cause the mode to exit immediately. So we suppress Escapes. + if mode.options.exitOnEscape and KeyboardUtils.isEscape event + @suppressEvent + else + @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 +461,8 @@ KeydownEvents = # Note that some keys will only register keydown events and not keystroke events, e.g. ESC. # -onKeypress = (event, extra) -> +onKeypress = (event) -> + return true if event.vimium_suppress_normal_mode keyChar = "" # Ignore modifier keys by themselves. @@ -465,23 +472,27 @@ onKeypress = (event, extra) -> # Enter insert mode when the user enables the native find interface. if (keyChar == "f" && KeyboardUtils.isPrimaryModifierKey(event)) enterInsertModeWithoutShowingIndicator() - return true + return handlerStack.stopBubblingAndTrue if (keyChar) if (findMode) handleKeyCharForFindMode(keyChar) DomUtils.suppressEvent(event) + return handlerStack.stopBubblingAndTrue else if (!isInsertMode() && !findMode) if (isPassKey keyChar) return handlerStack.stopBubblingAndTrue if currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar) DomUtils.suppressEvent(event) + keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) + return handlerStack.stopBubblingAndTrue keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) return true -onKeydown = (event, extra) -> +onKeydown = (event) -> + return true if event.vimium_suppress_normal_mode keyChar = "" # handle special keys, and normal input keys with modifiers being pressed. don't handle shiftKey alone (to @@ -520,37 +531,45 @@ onKeydown = (event, extra) -> exitInsertMode() DomUtils.suppressEvent event KeydownEvents.push event + return handlerStack.stopBubblingAndTrue else if (findMode) if (KeyboardUtils.isEscape(event)) handleEscapeForFindMode() DomUtils.suppressEvent event KeydownEvents.push event + return handlerStack.stopBubblingAndTrue else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) handleDeleteForFindMode() DomUtils.suppressEvent event KeydownEvents.push event + return handlerStack.stopBubblingAndTrue else if (event.keyCode == keyCodes.enter) handleEnterForFindMode() DomUtils.suppressEvent event KeydownEvents.push event + return handlerStack.stopBubblingAndTrue else if (!modifiers) DomUtils.suppressPropagation(event) KeydownEvents.push event + return handlerStack.stopBubblingAndTrue else if (isShowingHelpDialog && KeyboardUtils.isEscape(event)) hideHelpDialog() DomUtils.suppressEvent event KeydownEvents.push event + return handlerStack.stopBubblingAndTrue else if (!isInsertMode() && !findMode) if (keyChar) if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)) DomUtils.suppressEvent event KeydownEvents.push event + keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) + return handlerStack.stopBubblingAndTrue keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) @@ -572,12 +591,14 @@ onKeydown = (event, extra) -> isValidFirstKey(KeyboardUtils.getKeyChar(event)))) DomUtils.suppressPropagation(event) KeydownEvents.push event + return handlerStack.stopBubblingAndTrue return true onKeyup = (event) -> - DomUtils.suppressPropagation(event) if KeydownEvents.pop event - return true + return true unless KeydownEvents.pop event + DomUtils.suppressPropagation(event) + handlerStack.stopBubblingAndTrue checkIfEnabledForUrl = -> url = window.location.toString() @@ -776,6 +797,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 = -> |
