diff options
| -rw-r--r-- | content_scripts/link_hints.coffee | 37 | ||||
| -rw-r--r-- | content_scripts/mode.coffee | 21 | ||||
| -rw-r--r-- | content_scripts/mode_find.coffee | 58 | ||||
| -rw-r--r-- | content_scripts/mode_insert.coffee | 69 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 40 |
5 files changed, 112 insertions, 113 deletions
diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index b0feea8c..5e95ef99 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -8,9 +8,10 @@ # In 'filter' mode, our link hints are numbers, and the user can narrow down the range of possibilities by # typing the text of the link itself. # -# The "name" property here is a short-form name to appear in the link-hints mode name. Debugging only. The +# The "name" property below is a short-form name to appear in the link-hints mode name. Debugging only. The # key appears in the mode's badge. # NOTE(smblott) The use of keys in badges is experimental. It may prove too noisy. +# OPEN_IN_CURRENT_TAB = { name: "curr-tab", key: "" } OPEN_IN_NEW_BG_TAB = { name: "bg-tab", key: "B" } OPEN_IN_NEW_FG_TAB = { name: "fg-tab", key: "F" } @@ -65,21 +66,13 @@ LinkHints = @hintMarkerContainingDiv = DomUtils.addElementList(hintMarkers, { id: "vimiumHintMarkerContainer", className: "vimiumReset" }) - @handlerMode = - new class HintMode extends Mode - constructor: -> - super - name: "hint/#{mode.name}" - badge: "#{mode.key}?" - exitOnEscape: true - keydown: (event) -> LinkHints.onKeyDownInMode hintMarkers, event - # trap all other keyboard events - keypress: => @stopBubblingAndFalse - keyup: => @stopBubblingAndFalse - - exit: (delay, callback) => - LinkHints.deactivateMode delay, callback - super() + @hintMode = new Mode + name: "hint/#{mode.name}" + badge: "#{mode.key}?" + keydown: @onKeyDownInMode.bind(this, hintMarkers), + # trap all key events + keypress: -> false + keyup: -> false setOpenLinkMode: (@mode) -> if @mode is OPEN_IN_NEW_BG_TAB or @mode is OPEN_IN_NEW_FG_TAB or @mode is OPEN_WITH_QUEUE @@ -278,14 +271,13 @@ LinkHints = # TODO(philc): Ignore keys that have modifiers. if (KeyboardUtils.isEscape(event)) - # TODO(smblott). Now unreachable. Clean up. Left like this for now to keep the diff clean. - @handlerMode.exit() + @deactivateMode() else keyResult = @getMarkerMatcher().matchHintsByKey(hintMarkers, event) linksMatched = keyResult.linksMatched delay = keyResult.delay ? 0 if (linksMatched.length == 0) - @handlerMode.exit() + @deactivateMode() else if (linksMatched.length == 1) @activateLink(linksMatched[0], delay) else @@ -303,7 +295,7 @@ LinkHints = clickEl = matchedLink.clickableItem if (DomUtils.isSelectable(clickEl)) DomUtils.simulateSelect(clickEl) - @handlerMode.exit delay, -> LinkHints.delayMode = false + @deactivateMode(delay, -> LinkHints.delayMode = false) else # TODO figure out which other input elements should not receive focus if (clickEl.nodeName.toLowerCase() == "input" && clickEl.type != "button") @@ -311,11 +303,11 @@ LinkHints = DomUtils.flashRect(matchedLink.rect) @linkActivator(clickEl) if @mode is OPEN_WITH_QUEUE - @handlerMode.exit delay, -> + @deactivateMode delay, -> LinkHints.delayMode = false LinkHints.activateModeWithQueue() else - @handlerMode.exit delay, -> LinkHints.delayMode = false + @deactivateMode(delay, -> LinkHints.delayMode = false) # # Shows the marker, highlighting matchingCharCount characters. @@ -342,6 +334,7 @@ LinkHints = if (LinkHints.hintMarkerContainingDiv) DomUtils.removeElement LinkHints.hintMarkerContainingDiv LinkHints.hintMarkerContainingDiv = null + @hintMode.exit() HUD.hide() @isActive = false diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 98d3df80..ebb3e8bc 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -119,30 +119,9 @@ class Mode @passKeys = passKeys @registerStateChange?() - # If @options.suppressPrintableEvents is truthy, then it should be an element. All printable keyboard - # 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. Note, this handler is installed with unshift (not push), so it ends is installed at the - # *bottom* of the handler stack, and sees keyboard events only after other modes (notably, normal mode) - # have not handled them. - if @options.suppressPrintableEvents - do => - handler = (event) => - if event.srcElement == @options.suppressPrintableEvents and KeyboardUtils.isPrintable event - @suppressEvent - else - @continueBubbling - - @unshift - _name: "mode-#{@id}/suppressPrintableEvents" - keydown: handler - keypress: handler - keyup: handler - Mode.updateBadge() if @badge Mode.modes.push @ @logStack() if @debug - # handlerStack.debugOn() # End of Mode constructor. push: (handlers) -> diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index bf6e7f5b..08bc9e5d 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -2,33 +2,33 @@ # 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. Prevent keyboard events from dropping us unintentionally into insert mode. This is achieved by... -# 2. Prevent all printable keypress 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. +# 1. Prevent keyboard events from dropping us unintentionally into insert mode. +# 2. Prevent all printable keypress events on the active element from propagating beyond normal mode. See +# #1415. This implements Option 2 from there. # 3. If the very-next keystroke is Escape, then drop immediately into insert mode. # class PostFindMode extends Mode constructor: (findModeAnchorNode) -> element = document.activeElement - initialSelection = window.getSelection().toString() super name: "post-find" + badge: "N" # Pretend to be normal mode (because we don't want the insert-mode badge). # 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 - suppressPrintableEvents: element - # If the selection changes (e.g. via paste, or the arrow keys), then the user is interacting with the - # element, so get out of the way and activate insert mode. This implements 5c (without the input - # listener) as discussed in #1415. - keyup: => + exitOnClick: true + keydown: (event) -> InsertMode.suppressEvent event + keypress: (event) -> InsertMode.suppressEvent event + keyup: (event) => @alwaysContinueBubbling => - if window.getSelection().toString() != initialSelection + if document.getSelection().type != "Range" + # If the selection is no longer a range, then the user is interacting with the element, so get out + # of the way and stop suppressing insert mode. See discussion of Option 5c from #1415. @exit() - new InsertMode - targetElement: element + else + InsertMode.suppressEvent event return @exit() unless element and findModeAnchorNode @@ -36,21 +36,37 @@ class PostFindMode extends Mode # cannot. canTakeInput = DomUtils.isSelectable(element) and DomUtils.isDOMDescendant findModeAnchorNode, element canTakeInput ||= element.isContentEditable - canTakeInput ||= findModeAnchorNode.parentElement?.isContentEditable + canTakeInput ||= findModeAnchorNode.parentElement?.isContentEditable # FIXME(smblott) This is too specific. return @exit() unless canTakeInput + # If the very-next keydown is Esc, drop immediately into insert mode. self = @ @push _name: "mode-#{@id}/handle-escape" keydown: (event) -> - if element == document.activeElement and KeyboardUtils.isEscape event - self.exit() - new InsertMode - targetElement: element + if document.activeElement == element and KeyboardUtils.isEscape event DomUtils.suppressKeyupAfterEscape handlerStack - return false - @remove() - true + self.exit() + false # Suppress event. + else + @remove() + true # Continue bubbling. + + # Prevent printable keyboard events from propagating to to the page; see Option 2 from #1415. + do => + handler = (event) => + if event.srcElement == element and KeyboardUtils.isPrintable event + @suppressEvent + else + @continueBubbling + + # Note. We use unshift here, instead of push; therefore we see events *after* normal mode, and so only + # unmapped keys. + @unshift + _name: "mode-#{@id}/suppressPrintableEvents" + keydown: handler + keypress: handler + keyup: handler root = exports ? window root.PostFindMode = PostFindMode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 678e35cc..4be9c589 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -3,32 +3,67 @@ class InsertMode extends Mode constructor: (options = {}) -> defaults = name: "insert" - exitOnEscape: true - keydown: (event) => @handler event - keypress: (event) => @handler event - keyup: (event) => @handler event + keydown: (event) => @handleKeydownEvent event + keypress: (event) => @handleKeyEvent event + keyup: (event) => @handleKeyEvent event super extend defaults, options + @insertModeLock = if options.targetElement? then options.targetElement else null @push - "blur": => @exit() + "blur": => @alwaysContinueBubbling => + if DomUtils.isFocusable event.target + @exit event.target + Mode.updateBadge() + "focus": (event) => @alwaysContinueBubbling => + @insertModeLock = event.target if DomUtils.isFocusable event.target - active: -> - document.activeElement and DomUtils.isFocusable document.activeElement + if @insertModeLock == null + # We may already have focused an input element, so check. + @insertModeLock = event.target if document.activeElement and DomUtils.isFocusable document.activeElement - handler: (event) -> - if @active() then @stopBubblingAndTrue else @continueBubbling + isActive: -> + return true if @insertModeLock != null + # Some sites (e.g. inbox.google.com) change the contentEditable property on the fly (see #1245); and + # unfortunately, the focus event fires *before* the change. Therefore, we need to re-check whether the + # active element is contentEditable. + @insertModeLock = document.activeElement if document.activeElement?.isContentEditable + @insertModeLock != null - exit: () -> - document.activeElement.blur() if @active() - if @options.permanentInsertMode - # We don't really exit if we're permanently installed. - Mode.updateBadge() - else - super() + handleKeydownEvent: (event) -> + return @continueBubbling if event == InsertMode.suppressedEvent or not @isActive() + return @stopBubblingAndTrue unless KeyboardUtils.isEscape event + DomUtils.suppressKeyupAfterEscape handlerStack + if DomUtils.isFocusable event.srcElement + # Remove 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() etc. 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. + event.srcElement.blur() + @exit() + Mode.updateBadge() + @suppressEvent + + # Handles keypress and keyup events. + handleKeyEvent: (event) -> + if @isActive() and event != InsertMode.suppressedEvent then @stopBubblingAndTrue else @continueBubbling + + exit: (target) -> + if target == undefined or target == @insertModeLock + if @options.targetElement? + super() + else + # If @options.targetElement isn't set, then this is the permanently-installed instance from the front + # end. So, we don't actually exit; instead, we just reset ourselves. + @insertModeLock = null chooseBadge: (badge) -> - badge.badge ||= "I" if @active() + badge.badge ||= "I" if @isActive() + + # Static stuff to allow PostFindMode to suppress insert mode. + @suppressedEvent: null + @suppressEvent: (event) -> @suppressedEvent = event root = exports ? window root.InsertMode = InsertMode diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index f2e0cb2a..00d90e81 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -6,6 +6,7 @@ # passKeysMode = null +insertMode = null targetElement = null findMode = false findModeQuery = { rawQuery: "", matchCount: 0 } @@ -127,8 +128,7 @@ initializePreDomReady = -> new NormalMode() Scroller.init settings passKeysMode = new PassKeysMode() - new InsertMode - permanentInsertMode: true + insertMode = new InsertMode() checkIfEnabledForUrl() @@ -337,11 +337,10 @@ extend window, enterVisualMode: => new VisualMode() - focusInput: (count, targetMode = InsertMode) -> + focusInput: (count) -> # Focus the first input element on the page, and create overlays to highlight all the input elements, with # the currently-focused element highlighted specially. Tabbing will shift focus to the next input element. # Pressing any other key will remove the overlays and the special tab behavior. - # targetMode is the mode we want to enter. resultSet = DomUtils.evaluateXPath(textInputXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) visibleInputs = for i in [0...resultSet.snapshotLength] by 1 @@ -375,13 +374,7 @@ extend window, super name: "focus-selector" badge: "?" - # Be a singleton. It doesn't make any sense to have two instances active at the same time; and that - # shouldn't happen anyway. However, it does no harm to enforce it. - singleton: FocusSelector - targetMode: targetMode - # 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 + exitOnClick: true keydown: (event) => if event.keyCode == KeyboardUtils.keyCodes.tab hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint' @@ -391,16 +384,8 @@ extend window, visibleInputs[selectedInputIndex].element.focus() @suppressEvent else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey - 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 + @exit() + @continueBubbling visibleInputs[selectedInputIndex].element.focus() return @exit() if visibleInputs.length == 1 @@ -408,14 +393,8 @@ extend window, hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' exit: -> - super() DomUtils.removeElement hintContainingDiv - if document.activeElement == visibleInputs[selectedInputIndex].element - # The InsertModeBlocker super-class handles "click" events, so we should skip it here. - unless event?.type == "click" - # In most cases, we're entering insert mode here. However, it could be some other mode. - new @options.targetMode - targetElement: document.activeElement + super() # Decide whether this keyChar 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 @@ -823,10 +802,7 @@ executeFind = (query, options) -> HUD.hide(true) # ignore the selectionchange event generated by find() document.removeEventListener("selectionchange",restoreDefaultSelectionHighlight, true) - handlerId = handlerStack.push - focus: -> handlerStack.stopBubblingAndTrue - result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false) - handlerStack.remove + result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false) setTimeout( -> document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true) 0) |
