From baccd7c5cef14480e21e41519e20ee19fa238655 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 3 Jan 2015 13:20:04 +0000 Subject: Modes; Fix various mode changes. --- content_scripts/mode.coffee | 92 +++++++++++++++------------------- content_scripts/mode_insert.coffee | 57 +++++++++++---------- content_scripts/mode_passkeys.coffee | 26 ++++------ content_scripts/vimium_frontend.coffee | 68 ++++++++++++++++--------- 4 files changed, 125 insertions(+), 118 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 8041f462..9e886a63 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -11,86 +11,74 @@ class Mode stopBubblingAndFalse: handlerStack.stopBubblingAndFalse # Default values. - name: "" # The name of this mode. - badge: "" # A badge to display on the popup when this mode is active. - keydown: "suppress" # A function, or "suppress", "bubble" or "pass"; see checkForBuiltInHandler(). - keypress: "suppress" # A function, or "suppress", "bubble" or "pass"; see checkForBuiltInHandler(). - keyup: "suppress" # A function, or "suppress", "bubble" or "pass"; see checkForBuiltInHandler(). + name: "" + badge: "" + keydown: (event) => @continueBubbling + keypress: (event) => @continueBubbling + keyup: (event) => @continueBubbling constructor: (options) -> + Mode.modes.unshift @ extend @, options @handlers = [] @handlers.push handlerStack.push - keydown: @checkForBuiltInHandler "keydown", @keydown - keypress: @checkForBuiltInHandler "keypress", @keypress - keyup: @checkForBuiltInHandler "keyup", @keyup - updateBadgeForMode: (badge) => @updateBadgeForMode badge - - Mode.modes.unshift @ - - # Allow the strings "suppress" and "pass" to be used as proxies for the built-in handlers. - checkForBuiltInHandler: (type, handler) -> - switch handler - when "suppress" then @generateHandler type, @suppressEvent - when "bubble" then @generateHandler type, @continueBubbling - when "pass" then @generateHandler type, @stopBubblingAndTrue - else handler - - # Generate a default handler which always always yields the same result; except Esc, which pops the current - # mode. - generateHandler: (type, result) -> - (event) => - return result unless type == "keydown" and KeyboardUtils.isEscape event - @exit() - @suppressEvent + keydown: @keydown + keypress: @keypress + keyup: @keyup + updateBadge: (badge) => handlerStack.alwaysContinueBubbling => @chooseBadge badge exit: -> handlerStack.remove handlerId for handlerId in @handlers Mode.modes = Mode.modes.filter (mode) => mode != @ Mode.updateBadge() - # Default updateBadgeForMode handler. This is overridden by sub-classes. The default is to install the - # current mode's badge, unless the bade is already set. - updateBadgeForMode: (badge) -> - handlerStack.alwaysContinueBubbling => badge.badge ||= @badge + # The badge is chosen by bubbling an "updateBadge" event down the handler stack allowing each mode the + # opportunity to choose a badge. chooseBadge, here, is the default: choose the current mode's badge unless + # one has already been chosen. This is overridden in sub-classes. + chooseBadge: (badge) -> + badge.badge ||= @badge - # Static method. Used externally and internally to initiate bubbling of an updateBadgeForMode event. - # Do not update the badge: - # - if this document does not have the focus, or - # - if the document's body is a frameset + # 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 has the focus, and + # the document's body isn't a frameset. @updateBadge: -> if document.hasFocus() unless document.body?.tagName.toLowerCase() == "frameset" badge = {badge: ""} - handlerStack.bubbleEvent "updateBadgeForMode", badge - Mode.sendBadge badge.badge - - # Static utility to update the browser-popup badge. - @sendBadge: (badge) -> - chrome.runtime.sendMessage({ handler: "setBadge", badge: badge }) + handlerStack.bubbleEvent "updateBadge", badge + chrome.runtime.sendMessage({ handler: "setBadge", badge: badge.badge }) - # Install a mode, call a function, and exit the mode again. + # Temporarily install a mode. @runIn: (mode, func) -> mode = new mode() func() mode.exit() -# A SingletonMode is a Mode of which there may be at most one instance of the same name (@singleton) active at -# any one time. New instances cancel previous instances on startup. +# A SingletonMode is a Mode of which there may be at most one instance (of @singleton) active at any one time. +# New instances cancel previous instances on startup. class SingletonMode extends Mode - constructor: (@singleton, options) -> - @cancel @singleton - super options - @instances: {} - cancel: (instance) -> - SingletonMode[instance].exit() if SingletonMode[instance] - exit: -> - delete SingletonMode[@instance] + delete SingletonMode[@singleton] super() + constructor: (@singleton, options={}) -> + SingletonMode[@singleton].exit() if SingletonMode[@singleton] + SingletonMode[@singleton] = @ + super options + +# MultiMode is a collection of modes which are installed or uninstalled together. +class MultiMode extends Mode + constructor: (modes...) -> + @modes = (new mode() for mode in modes) + super {name: "multimode"} + + exit: -> + mode.exit() for mode in modes + root = exports ? window root.Mode = Mode +root.SingletonMode = SingletonMode +root.MultiMode = MultiMode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index ed5d0023..6d7cdb89 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -1,6 +1,6 @@ class InsertMode extends Mode - isInsertMode: false + insertModeActive: false insertModeLock: null # Input or text elements are considered focusable and able to receieve their own keyboard events, and will @@ -14,34 +14,34 @@ class InsertMode extends Mode return true nodeName in ["textarea", "select"] - # Embedded elements like Flash and quicktime players can obtain focus but cannot be programmatically - # unfocused. + # Embedded elements like Flash and quicktime players can obtain focus. isEmbed: (element) -> element.nodeName?.toLowerCase() in ["embed", "object"] isFocusable: (element) -> - (@isEditable(element) or @isEmbed element) + @isEditable(element) or @isEmbed element # Check whether insert mode is active. Also, activate insert mode if the current element is content - # editable. - isActive: -> - return true if @isInsertMode + # editable (and the event is not suppressed). + isActiveOrActivate: (event) -> + return true if @insertModeActive + return false if event.suppressKeydownTrigger # Some sites (e.g. inbox.google.com) change the contentEditable attribute on the fly (see #1245); and # unfortunately, isEditable() is called *before* the change is made. Therefore, we need to re-check # whether the active element is contentEditable. @activate() if document.activeElement?.isContentEditable - @isInsertMode + @insertModeActive activate: (target=null) -> - unless @isInsertMode - @isInsertMode = true + unless @insertModeActive + @insertModeActive = true @insertModeLock = target @badge = "I" Mode.updateBadge() deactivate: -> - if @isInsertMode - @isInsertMode = false + if @insertModeActive + @insertModeActive = false @insertModeLock = null @badge = "" Mode.updateBadge() @@ -50,38 +50,41 @@ class InsertMode extends Mode super name: "insert" keydown: (event) => - return @continueBubbling if event.suppressInsertMode - return @continueBubbling unless @isActive() + return @continueBubbling unless @isActiveOrActivate event return @stopBubblingAndTrue unless KeyboardUtils.isEscape event - # We're now exiting insert mode. - if @isEditable(event.srcElement) or @isEmbed event.srcElement - # Remove the focus so the user can't just get himself back into insert mode by typing in the same input - # box. + # We're in insert mode, and now exiting. + if event.srcElement? and @isFocusable event.srcElement + # 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. event.srcElement.blur() @deactivate() @suppressEvent - keypress: => if @isInsertMode then @stopBubblingAndTrue else @continueBubbling - keyup: => if @isInsertMode then @stopBubblingAndTrue else @continueBubbling + keypress: => if @insertModeActive then @stopBubblingAndTrue else @continueBubbling + keyup: => if @insertModeActive then @stopBubblingAndTrue else @continueBubbling @handlers.push handlerStack.push focus: (event) => handlerStack.alwaysContinueBubbling => - if not @isInsertMode and @isFocusable event.target + if not @insertModeActive and @isFocusable event.target @activate event.target blur: (event) => handlerStack.alwaysContinueBubbling => - if @isInsertMode and event.target == @insertModeLock + if @insertModeActive and event.target == @insertModeLock @deactivate() - # We may already have been dropped into insert mode. So check. - Mode.updateBadge() + # We may already have focussed something, so check, so check. + @activate document.activeElement if document.activeElement and @isFocusable document.activeElement + + # Used to prevent keydown events from triggering insert mode (following find). + # FIXME(smblott) This is a hack. + @suppressKeydownTrigger: (event) -> + event.suppressKeydownTrigger = true -# Utility mode. # Activate this mode to prevent a focused, editable element from triggering insert mode. -class FocusMustNotTriggerInsertMode extends Mode +class InsertModeSuppressFocusTrigger extends Mode constructor: -> super() @handlers.push handlerStack.push @@ -89,4 +92,4 @@ class FocusMustNotTriggerInsertMode extends Mode root = exports ? window root.InsertMode = InsertMode -root.FocusMustNotTriggerInsertMode = FocusMustNotTriggerInsertMode +root.InsertModeSuppressFocusTrigger = InsertModeSuppressFocusTrigger diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee index c754e967..4c4d7d41 100644 --- a/content_scripts/mode_passkeys.coffee +++ b/content_scripts/mode_passkeys.coffee @@ -3,19 +3,8 @@ class PassKeysMode extends Mode keyQueue: "" passKeys: "" - # 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 passKey, then 'gt' and '99t' will - # neverthless be handled by vimium. - isPassKey: (keyChar) -> - not @keyQueue and 0 <= @passKeys.indexOf(keyChar) - - handlePassKeyEvent: (event) -> - for keyChar in [KeyboardUtils.getKeyChar(event), String.fromCharCode(event.charCode)] - return @stopBubblingAndTrue if keyChar and @isPassKey keyChar - @continueBubbling - - # This is called to set the pass-keys configuration and state with various types of request from various - # sources, so we handle several cases. + # This is called to set the passKeys configuration and state with various types of request from various + # sources, so we handle several cases here. # TODO(smblott) Rationalize this. configure: (request) -> if request.isEnabledForUrl? @@ -27,14 +16,21 @@ class PassKeysMode extends Mode if request.keyQueue? @keyQueue = request.keyQueue + # 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 + constructor: -> super name: "passkeys" keydown: (event) => @handlePassKeyEvent event keypress: (event) => @handlePassKeyEvent event - keyup: => @continueBubbling - updateBadgeForMode: (badge) -> + chooseBadge: (badge) -> @badge = if @passKeys and not @keyQueue then "P" else "" super badge diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 0f23af05..da479781 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -113,11 +113,9 @@ class NormalMode extends Mode keypress: onKeypress keyup: onKeyup - updateBadgeForMode: (badge) -> - handlerStack.alwaysContinueBubbling => - # Idea... Instead of an icon, we could show the keyQueue here (if it's non-empty). - super badge - badge.badge = "" unless isEnabledForUrl + chooseBadge: (badge) -> + super badge + badge.badge = "" unless isEnabledForUrl # # Complete initialization work that sould be done prior to DOMReady. @@ -136,8 +134,10 @@ initializePreDomReady = -> # Install passKeys and insert modes. These too are permanently on the stack (although not always active). # Note. There's no need to explicitly Mode.updateBadge(). The new InsertMode() updates the badge. + # Note. There's no need to explicitly Mode.updateBadge(). The new InsertMode() updates the badge. passKeysMode = new PassKeysMode() insertMode = new InsertMode() + Mode.updateBadge() checkIfEnabledForUrl() @@ -740,10 +740,10 @@ class FindMode extends Mode handleEscapeForFindMode() @exit() @suppressEvent - else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) + else if event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey handleDeleteForFindMode() @suppressEvent - else if (event.keyCode == keyCodes.enter) + else if event.keyCode == keyCodes.enter handleEnterForFindMode() @exit() @suppressEvent @@ -761,24 +761,44 @@ class FindMode extends Mode Mode.updateBadge() -# If find lands in an editable element, then "Esc" drops us into insert mode. -class PostFindMode extends Mode - constructor: (element) -> - super +# If find lands in an editable element then: +# - "Esc" drops us into insert mode. +# - Subsequent command keypresses should not cause us to drop into insert mode. +count = 0 +class PostFindMode extends SingletonMode + constructor: -> + element = document.activeElement + handleKeydownEscape = true + super PostFindMode, keydown: (event) => - @exit() - if (KeyboardUtils.isEscape(event)) - DomUtils.simulateSelect(document.activeElement) - insertMode.activate() + if handleKeydownEscape and KeyboardUtils.isEscape event + DomUtils.simulateSelect document.activeElement + insertMode.activate element + @exit() return @suppressEvent # we have "consumed" this event, so do not propagate - event.suppressInsertMode = true - return @continueBubbling - - elementCanTakeInput = document.activeElement && - DomUtils.isSelectable(document.activeElement) && - isDOMDescendant(findModeAnchorNode, document.activeElement) - elementCanTakeInput ||= document.activeElement?.isContentEditable - @exit() unless elementCanTakeInput + console.log "suppress", event + handleKeydownEscape = false + InsertMode.suppressKeydownTrigger event + # We can safely exit if element is contentEditable. Keystrokes will never cause us to drop into + # insert mode anyway. + @exit() if element.isContentEditable + @continueBubbling + keypress: => @continueBubbling + keyup: => @continueBubbling + + console.log ++count, "PostFindMode create" + canTakeInput = element and DomUtils.isSelectable(element) and isDOMDescendant findModeAnchorNode, element + canTakeInput ||= element?.isContentEditable + return @exit() unless canTakeInput + + @handlers.push handlerStack.push + DOMActive: (event) => @exit() + focus: (event) => @exit() + blur: (event) => @exit() + + exit: -> + console.log ++count, "exit PostFindMode" + super() performFindInPlace = -> cachedScrollX = window.scrollX @@ -807,7 +827,7 @@ executeFind = (query, options) -> HUD.hide(true) # ignore the selectionchange event generated by find() document.removeEventListener("selectionchange",restoreDefaultSelectionHighlight, true) - Mode.runIn FocusMustNotTriggerInsertMode, -> + Mode.runIn InsertModeSuppressFocusTrigger, -> result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false) setTimeout( -> document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true) -- cgit v1.2.3