From f2b428b4fe1eecd66ee95513da779470f7c621aa Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Wed, 31 Dec 2014 19:04:26 +0000 Subject: Modes proof-of-concept. --- content_scripts/mode.coffee | 12 ++++++++++++ content_scripts/vimium_frontend.coffee | 22 +++++++++------------- 2 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 content_scripts/mode.coffee (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee new file mode 100644 index 00000000..f7bf9e69 --- /dev/null +++ b/content_scripts/mode.coffee @@ -0,0 +1,12 @@ +root = exports ? window + +class root.Mode + constructor: (onKeydown, onKeypress, onKeyup, @popModeCallback) -> + @handlerId = handlerStack.push + keydown: onKeydown + keypress: onKeypress + keyup: onKeyup + + popMode: -> + handlerStack.remove @handlerId + @popModeCallback() diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index ae275f0c..5f8b050f 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -4,7 +4,6 @@ # background page that we're in domReady and ready to accept normal commands by connectiong to a port named # "domReady". # -window.handlerStack = new HandlerStack insertModeLock = null findMode = false @@ -110,6 +109,8 @@ initializePreDomReady = -> settings.addEventListener("load", LinkHints.init.bind(LinkHints)) settings.load() + nf = -> true + new Mode(onKeydown, onKeypress, onKeyup, nf) Scroller.init settings checkIfEnabledForUrl() @@ -169,9 +170,11 @@ initializeWhenEnabled = (newPassKeys) -> if (!installedListeners) # Key event handlers fire on window before they do on document. Prefer window for key events so the page # can't set handlers to grab the keys before us. - installListener window, "keydown", onKeydown - installListener window, "keypress", onKeypress - installListener window, "keyup", onKeyup + for type in ["keydown", "keypress", "keyup"] + do (type) -> + installListener window, type, (event) -> + console.log type + handlerStack.bubbleEvent type, event installListener document, "focus", onFocusCapturePhase installListener document, "blur", onBlurCapturePhase installListener document, "DOMActivate", onDOMActivate @@ -398,8 +401,6 @@ KeydownEvents = # Note that some keys will only register keydown events and not keystroke events, e.g. ESC. # onKeypress = (event) -> - return unless handlerStack.bubbleEvent('keypress', event) - keyChar = "" # Ignore modifier keys by themselves. @@ -424,8 +425,7 @@ onKeypress = (event) -> keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) onKeydown = (event) -> - return unless handlerStack.bubbleEvent('keydown', event) - + console.log "onKeydown" keyChar = "" # handle special keys, and normal input keys with modifiers being pressed. don't handle shiftKey alone (to @@ -518,11 +518,7 @@ onKeydown = (event) -> KeydownEvents.push event onKeyup = (event) -> - handledKeydown = KeydownEvents.pop event - return unless handlerStack.bubbleEvent("keyup", event) - - # Don't propagate the keyup to the underlying page if Vimium has handled it. See #733. - DomUtils.suppressPropagation(event) if handledKeydown + DomUtils.suppressPropagation(event) if KeydownEvents.pop event checkIfEnabledForUrl = -> url = window.location.toString() -- cgit v1.2.3 From acefe43cef5a216cb2504e85799699c359b6b4d8 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Wed, 31 Dec 2014 20:52:27 +0000 Subject: Modes; incorporate three test modes. As a proof of concept, this incorporates normal mode, passkeys mode and insert mode. --- content_scripts/mode.coffee | 78 +++++++++++++++++++++--- content_scripts/vimium_frontend.coffee | 106 +++++++++++++++++++++++---------- 2 files changed, 145 insertions(+), 39 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index f7bf9e69..e4b6017c 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -1,12 +1,74 @@ -root = exports ? window -class root.Mode - constructor: (onKeydown, onKeypress, onKeyup, @popModeCallback) -> +class Mode + # Static members. + @modes: [] + @current: -> Mode.modes[0] + @suppressPropagation = false + @propagate = true + + # 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" or "pass"; the latter are replaced with suitable functions. + keypress: "suppress" # A function, or "suppress" or "pass"; the latter are replaced with suitable functions. + keyup: "suppress" # A function, or "suppress" or "pass"; the latter are replaced with suitable functions. + onDeactivate: -> # Called when leaving this mode. + onReactivate: -> # Called when this mode is reactivated. + + constructor: (options) -> + extend @, options + @handlerId = handlerStack.push - keydown: onKeydown - keypress: onKeypress - keyup: onKeyup + keydown: @checkForBuiltInHandler "keydown", @keydown + keypress: @checkForBuiltInHandler "keypress", @keypress + keyup: @checkForBuiltInHandler "keyup", @keyup + reactivateMode: => + @onReactivate() + Mode.setBadge() + return Mode.suppressPropagation + + Mode.modes.unshift @ + Mode.setBadge() + + # Allow the strings "suppress" and "pass" to be used as proxies for the built-in handlers. + checkForBuiltInHandler: (type, handler) -> + switch handler + when "suppress" then @generateSuppressPropagation type + when "pass" then @generatePassThrough type + else handler - popMode: -> + # Generate a default handler which always passes through; except Esc, which pops the current mode. + generatePassThrough: (type) -> + me = @ + (event) -> + if type == "keydown" and KeyboardUtils.isEscape event + me.popMode event + return Mode.suppressPropagation + handlerStack.passThrough + + # Generate a default handler which always suppresses propagation; except Esc, which pops the current mode. + generateSuppressPropagation: (type) -> + handler = @generatePassThrough type + (event) -> handler(event) and Mode.suppressPropagation # Always falsy. + + # Leave the current mode; event may or may not be provide. It is the responsibility of the creator of this + # object to know whether or not an event will be provided. Bubble a "reactivateMode" event to notify the + # now-active mode that it is once again top dog. + popMode: (event) -> + Mode.modes = Mode.modes.filter (mode) => mode != @ handlerStack.remove @handlerId - @popModeCallback() + @onDeactivate event + handlerStack.bubbleEvent "reactivateMode", event + + # Set the badge on the browser popup to indicate the current mode; static method. + @setBadge: -> + badge = Mode.getBadge() + chrome.runtime.sendMessage({ handler: "setBadge", badge: badge }) + + # Static convenience methods. + @is: (mode) -> Mode.current()?.name == mode + @getBadge: -> Mode.current()?.badge || "" + @isInsert: -> Mode.is "insert" + +root = exports ? window +root.Mode = Mode diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 5f8b050f..969e9209 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -20,8 +20,8 @@ isEnabledForUrl = true passKeys = null keyQueue = null # The user's operating system. -currentCompletionKeys = null -validFirstKeys = null +currentCompletionKeys = "" +validFirstKeys = "" # The types in that we consider for focusInput command. Right now this is recalculated in # each content script. Alternatively we could calculate it once in the background page and use a request to @@ -109,10 +109,30 @@ initializePreDomReady = -> settings.addEventListener("load", LinkHints.init.bind(LinkHints)) settings.load() - nf = -> true - new Mode(onKeydown, onKeypress, onKeyup, nf) + # Install normal mode. This will be at the bottom of both the mode stack and the handler stack, and is never + # deactivated. + new Mode + name: "normal" + keydown: onKeydown + keypress: onKeypress + keyup: onKeyup + + # Initialize the scroller. The scroller installs key handlers, and these will be next on the handler stack, + # immediately above normal mode. Scroller.init settings + handlePassKeyEvent = (event) -> + for keyChar in [ KeyboardUtils.getKeyChar(event), String.fromCharCode(event.charCode) ] + return handlerStack.passThrough if keyChar and isPassKey keyChar + true + + # Install passKeys mode. This mode is never deactivated. + new Mode + name: "passkeys" + keydown: handlePassKeyEvent + keypress: handlePassKeyEvent + keyup: -> true # Allow event to propagate. + checkIfEnabledForUrl() refreshCompletionKeys() @@ -137,7 +157,7 @@ initializePreDomReady = -> getScrollPosition: -> scrollX: window.scrollX, scrollY: window.scrollY setScrollPosition: (request) -> setScrollPosition request.scrollX, request.scrollY executePageCommand: executePageCommand - getActiveState: -> { enabled: isEnabledForUrl, passKeys: passKeys } + getActiveState: -> { enabled: isEnabledForUrl, passKeys: passKeys, badge: Mode.getBadge() } setState: setState currentKeyQueue: (request) -> keyQueue = request.keyQueue @@ -171,10 +191,7 @@ initializeWhenEnabled = (newPassKeys) -> # Key event handlers fire on window before they do on document. Prefer window for key events so the page # can't set handlers to grab the keys before us. for type in ["keydown", "keypress", "keyup"] - do (type) -> - installListener window, type, (event) -> - console.log type - handlerStack.bubbleEvent type, event + do (type) -> installListener window, type, (event) -> handlerStack.bubbleEvent type, event installListener document, "focus", onFocusCapturePhase installListener document, "blur", onBlurCapturePhase installListener document, "DOMActivate", onDOMActivate @@ -192,7 +209,7 @@ setState = (request) -> window.addEventListener "focus", -> # settings may have changed since the frame last had focus settings.load() - chrome.runtime.sendMessage({ handler: "frameFocused", frameId: frameId }) + chrome.runtime.sendMessage({ handler: "frameFocused", frameId: frameId, badge: Mode.getBadge() }) # # Initialization tasks that must wait for the document to be ready. @@ -410,22 +427,24 @@ onKeypress = (event) -> # Enter insert mode when the user enables the native find interface. if (keyChar == "f" && KeyboardUtils.isPrimaryModifierKey(event)) enterInsertModeWithoutShowingIndicator() - return + return Mode.propagate if (keyChar) if (findMode) handleKeyCharForFindMode(keyChar) - DomUtils.suppressEvent(event) + return Mode.suppressPropagation else if (!isInsertMode() && !findMode) if (isPassKey keyChar) - return undefined + return Mode.propagate if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)) - DomUtils.suppressEvent(event) + keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) + return Mode.suppressPropagation keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) + return Mode.propagate + onKeydown = (event) -> - console.log "onKeydown" keyChar = "" # handle special keys, and normal input keys with modifiers being pressed. don't handle shiftKey alone (to @@ -463,38 +482,39 @@ onKeydown = (event) -> event.srcElement.blur() exitInsertMode() DomUtils.suppressEvent event - handledKeydownEvents.push event + KeydownEvents.push event else if (findMode) if (KeyboardUtils.isEscape(event)) handleEscapeForFindMode() - DomUtils.suppressEvent event KeydownEvents.push event + return Mode.suppressPropagation else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) handleDeleteForFindMode() - DomUtils.suppressEvent event KeydownEvents.push event + return Mode.suppressPropagation else if (event.keyCode == keyCodes.enter) handleEnterForFindMode() - DomUtils.suppressEvent event KeydownEvents.push event + return Mode.suppressPropagation else if (!modifiers) - DomUtils.suppressPropagation(event) KeydownEvents.push event + return Mode.suppressPropagation else if (isShowingHelpDialog && KeyboardUtils.isEscape(event)) hideHelpDialog() - DomUtils.suppressEvent event KeydownEvents.push event + return Mode.suppressPropagation 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 Mode.suppressPropagation keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) @@ -502,7 +522,7 @@ onKeydown = (event) -> keyPort.postMessage({ keyChar:"", frameId:frameId }) else if isPassKey KeyboardUtils.getKeyChar(event) - return undefined + return Mode.propagate # Added to prevent propagating this event to other listeners if it's one that'll trigger a Vimium command. # The goal is to avoid the scenario where Google Instant Search uses every keydown event to dump us @@ -514,11 +534,14 @@ onKeydown = (event) -> if (keyChar == "" && !isInsertMode() && (currentCompletionKeys.indexOf(KeyboardUtils.getKeyChar(event)) != -1 || isValidFirstKey(KeyboardUtils.getKeyChar(event)))) - DomUtils.suppressPropagation(event) + # Suppress chrome propagation of this event, but drop through, and continue handler-stack processing. + DomUtils.suppressPropagation event KeydownEvents.push event + return Mode.propagate + onKeyup = (event) -> - DomUtils.suppressPropagation(event) if KeydownEvents.pop event + if KeydownEvents.pop event then Mode.suppressPropagation else Mode.propagate checkIfEnabledForUrl = -> url = window.location.toString() @@ -584,7 +607,7 @@ isEditable = (target) -> # window.enterInsertMode = (target) -> enterInsertModeWithoutShowingIndicator(target) - HUD.show("Insert mode") + # HUD.show("Insert mode") # With this proof-of-concept, visual feedback is given via badges on the browser popup. # # We cannot count on 'focus' and 'blur' events to happen sequentially. For example, if blurring element A @@ -594,15 +617,36 @@ window.enterInsertMode = (target) -> # leave insert mode when the user presses . # Note. This returns the truthiness of target, which is required by isInsertMode. # -enterInsertModeWithoutShowingIndicator = (target) -> insertModeLock = target +enterInsertModeWithoutShowingIndicator = (target) -> + insertModeLock = target + unless Mode.isInsert() + # Install insert-mode handler. Hereafter, all key events will be passed directly to the underlying page. + # The current isInsertMode logic in the normal-mode handlers is now redundant.. + new Mode + name: "insert" + badge: "I" + keydown: "pass" + keypress: "pass" + keyup: "pass" + onDeactivate: (event) -> + if isEditable(event.srcElement) or isEmbed(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() + insertModeLock = null + HUD.hide() exitInsertMode = (target) -> - if (target == undefined || insertModeLock == target) - insertModeLock = null - HUD.hide() + # This assumes that, if insert mode is active at all, then it *must* be the current mode. That is, we + # cannot enter any other mode from insert mode. + if Mode.isInsert() and (target == null or target == insertModeLock) + Mode.popMode() isInsertMode = -> - return true if insertModeLock != null + return true if Mode.isInsert() # 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. -- cgit v1.2.3 From aed5e2b5e1015a2e581edadbc5dd2d1b5a2719f4 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 1 Jan 2015 09:57:06 +0000 Subject: Modes; minor changes. --- content_scripts/vimium_frontend.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'content_scripts') diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 969e9209..f7ae3a76 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -618,8 +618,8 @@ window.enterInsertMode = (target) -> # Note. This returns the truthiness of target, which is required by isInsertMode. # enterInsertModeWithoutShowingIndicator = (target) -> - insertModeLock = target unless Mode.isInsert() + insertModeLock = target # Install insert-mode handler. Hereafter, all key events will be passed directly to the underlying page. # The current isInsertMode logic in the normal-mode handlers is now redundant.. new Mode -- cgit v1.2.3 From 2d047e7ee7e77a02ccb29658ada953a092cee20a Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 1 Jan 2015 12:02:16 +0000 Subject: Modes; implement insert mode. --- content_scripts/mode.coffee | 29 +++++--------- content_scripts/mode_insert.coffee | 70 ++++++++++++++++++++++++++++++++++ content_scripts/vimium_frontend.coffee | 55 ++++++++------------------ 3 files changed, 96 insertions(+), 58 deletions(-) create mode 100644 content_scripts/mode_insert.coffee (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index e4b6017c..88938e79 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -3,6 +3,8 @@ class Mode # Static members. @modes: [] @current: -> Mode.modes[0] + + # Constants. Static. @suppressPropagation = false @propagate = true @@ -12,8 +14,6 @@ class Mode keydown: "suppress" # A function, or "suppress" or "pass"; the latter are replaced with suitable functions. keypress: "suppress" # A function, or "suppress" or "pass"; the latter are replaced with suitable functions. keyup: "suppress" # A function, or "suppress" or "pass"; the latter are replaced with suitable functions. - onDeactivate: -> # Called when leaving this mode. - onReactivate: -> # Called when this mode is reactivated. constructor: (options) -> extend @, options @@ -22,10 +22,6 @@ class Mode keydown: @checkForBuiltInHandler "keydown", @keydown keypress: @checkForBuiltInHandler "keypress", @keypress keyup: @checkForBuiltInHandler "keyup", @keyup - reactivateMode: => - @onReactivate() - Mode.setBadge() - return Mode.suppressPropagation Mode.modes.unshift @ Mode.setBadge() @@ -37,12 +33,12 @@ class Mode when "pass" then @generatePassThrough type else handler - # Generate a default handler which always passes through; except Esc, which pops the current mode. + # Generate a default handler which always passes through to the underlying page; except Esc, which pops the + # current mode. generatePassThrough: (type) -> - me = @ - (event) -> + (event) => if type == "keydown" and KeyboardUtils.isEscape event - me.popMode event + @exit() return Mode.suppressPropagation handlerStack.passThrough @@ -51,19 +47,14 @@ class Mode handler = @generatePassThrough type (event) -> handler(event) and Mode.suppressPropagation # Always falsy. - # Leave the current mode; event may or may not be provide. It is the responsibility of the creator of this - # object to know whether or not an event will be provided. Bubble a "reactivateMode" event to notify the - # now-active mode that it is once again top dog. - popMode: (event) -> - Mode.modes = Mode.modes.filter (mode) => mode != @ + exit: -> handlerStack.remove @handlerId - @onDeactivate event - handlerStack.bubbleEvent "reactivateMode", event + Mode.modes = Mode.modes.filter (mode) => mode != @ + Mode.setBadge() # Set the badge on the browser popup to indicate the current mode; static method. @setBadge: -> - badge = Mode.getBadge() - chrome.runtime.sendMessage({ handler: "setBadge", badge: badge }) + chrome.runtime.sendMessage({ handler: "setBadge", badge: Mode.getBadge() }) # Static convenience methods. @is: (mode) -> Mode.current()?.name == mode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee new file mode 100644 index 00000000..4a1d4349 --- /dev/null +++ b/content_scripts/mode_insert.coffee @@ -0,0 +1,70 @@ + +class InsertMode extends Mode + userActivated: false + + # Input or text elements are considered focusable and able to receieve their own keyboard events, and will + # enter insert mode if focused. Also note that the "contentEditable" attribute can be set on any element + # which makes it a rich text editor, like the notes on jjot.com. + isEditable: (element) -> + return true if element.isContentEditable + nodeName = element.nodeName?.toLowerCase() + # Use a blacklist instead of a whitelist because new form controls are still being implemented for html5. + if nodeName == "input" and element.type and not element.type in ["radio", "checkbox"] + return true + nodeName in ["textarea", "select"] + + # Embedded elements like Flash and quicktime players can obtain focus but cannot be programmatically + # unfocused. + isEmbed: (element) -> + element.nodeName?.toLowerCase() in ["embed", "object"] + + canEditElement: (element) -> + element and (@isEditable(element) or @isEmbed element) + + isActive: -> + @userActivated or @canEditElement document.activeElement + + generateKeyHandler: (type) -> + (event) => + return Mode.propagate unless @isActive() + return handlerStack.passThrough unless type == "keydown" and KeyboardUtils.isEscape event + # We're now exiting insert mode. + if @canEditElement 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() + @userActivated = false + @updateBadge() + Mode.suppressPropagation + + pickBadge: -> + if @isActive() then "I" else "" + + updateBadge: -> + badge = @badge + @badge = @pickBadge() + Mode.setBadge() if badge != @badge + Mode.propagate + + activate: -> + @userActivated = true + @updateBadge() + + constructor: -> + super + name: "insert" + badge: @pickBadge() + keydown: @generateKeyHandler "keydown" + keypress: @generateKeyHandler "keypress" + keyup: @generateKeyHandler "keyup" + + handlerStack.push + DOMActivate: => @updateBadge() + focus: => @updateBadge() + blur: => @updateBadge() + +root = exports ? window +root.InsertMode = InsertMode diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index f7ae3a76..6480d511 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -5,6 +5,7 @@ # "domReady". # +insertMode = null insertModeLock = null findMode = false findModeQuery = { rawQuery: "", matchCount: 0 } @@ -133,6 +134,9 @@ initializePreDomReady = -> keypress: handlePassKeyEvent keyup: -> true # Allow event to propagate. + # Install insert mode. + insertMode = new InsertMode() + checkIfEnabledForUrl() refreshCompletionKeys() @@ -192,9 +196,11 @@ initializeWhenEnabled = (newPassKeys) -> # can't set handlers to grab the keys before us. for type in ["keydown", "keypress", "keyup"] do (type) -> installListener window, type, (event) -> handlerStack.bubbleEvent type, event - installListener document, "focus", onFocusCapturePhase + # installListener document, "focus", onFocusCapturePhase # No longer needed. installListener document, "blur", onBlurCapturePhase installListener document, "DOMActivate", onDOMActivate + installListener document, "focus", onFocus + installListener document, "blur", onBlur enterInsertModeIfElementIsFocused() installedListeners = true @@ -244,6 +250,8 @@ enterInsertModeIfElementIsFocused = -> enterInsertModeWithoutShowingIndicator(document.activeElement) onDOMActivate = (event) -> handlerStack.bubbleEvent 'DOMActivate', event +onFocus = (event) -> handlerStack.bubbleEvent 'focus', event +onBlur = (event) -> handlerStack.bubbleEvent 'blur', event executePageCommand = (request) -> return unless frameId == request.frameId @@ -325,6 +333,9 @@ extend window, HUD.showForDuration("Yanked URL", 1000) + enterInsertMode: -> + insertMode?.activate() + 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. @@ -601,14 +612,6 @@ isEditable = (target) -> focusableElements = ["textarea", "select"] focusableElements.indexOf(nodeName) >= 0 -# -# Enters insert mode and show an "Insert mode" message. Showing the UI is only useful when entering insert -# mode manually by pressing "i". In most cases we do not show any UI (enterInsertModeWithoutShowingIndicator) -# -window.enterInsertMode = (target) -> - enterInsertModeWithoutShowingIndicator(target) - # HUD.show("Insert mode") # With this proof-of-concept, visual feedback is given via badges on the browser popup. - # # We cannot count on 'focus' and 'blur' events to happen sequentially. For example, if blurring element A # causes element B to come into focus, we may get "B focus" before "A blur". Thus we only leave insert mode @@ -618,40 +621,13 @@ window.enterInsertMode = (target) -> # Note. This returns the truthiness of target, which is required by isInsertMode. # enterInsertModeWithoutShowingIndicator = (target) -> - unless Mode.isInsert() - insertModeLock = target - # Install insert-mode handler. Hereafter, all key events will be passed directly to the underlying page. - # The current isInsertMode logic in the normal-mode handlers is now redundant.. - new Mode - name: "insert" - badge: "I" - keydown: "pass" - keypress: "pass" - keyup: "pass" - onDeactivate: (event) -> - if isEditable(event.srcElement) or isEmbed(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() - insertModeLock = null - HUD.hide() + return # Disabled. exitInsertMode = (target) -> - # This assumes that, if insert mode is active at all, then it *must* be the current mode. That is, we - # cannot enter any other mode from insert mode. - if Mode.isInsert() and (target == null or target == insertModeLock) - Mode.popMode() + return # Disabled. isInsertMode = -> - return true if Mode.isInsert() - # 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. - document.activeElement and document.activeElement.isContentEditable and - enterInsertModeWithoutShowingIndicator document.activeElement + return false # Disabled. # should be called whenever rawQuery is modified. updateFindModeQuery = -> @@ -705,6 +681,7 @@ updateFindModeQuery = -> findModeQuery.matchCount = text.match(pattern)?.length handleKeyCharForFindMode = (keyChar) -> + console.log "xxxxxxxxxxxxxxx" findModeQuery.rawQuery += keyChar updateFindModeQuery() performFindInPlace() -- cgit v1.2.3 From c783b653e185166009ba0cdf94c6fdbb442d7f39 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 1 Jan 2015 17:40:31 +0000 Subject: Modes; revert and modify normal-mode key handling. --- content_scripts/vimium_frontend.coffee | 37 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 19 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 6480d511..b40e9735 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -428,6 +428,7 @@ KeydownEvents = # # Note that some keys will only register keydown events and not keystroke events, e.g. ESC. # + onKeypress = (event) -> keyChar = "" @@ -438,22 +439,21 @@ onKeypress = (event) -> # Enter insert mode when the user enables the native find interface. if (keyChar == "f" && KeyboardUtils.isPrimaryModifierKey(event)) enterInsertModeWithoutShowingIndicator() - return Mode.propagate + return true if (keyChar) if (findMode) handleKeyCharForFindMode(keyChar) - return Mode.suppressPropagation + DomUtils.suppressEvent(event) else if (!isInsertMode() && !findMode) if (isPassKey keyChar) - return Mode.propagate + return handlerStack.passThrough if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)) - keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) - return Mode.suppressPropagation + DomUtils.suppressEvent(event) keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) - return Mode.propagate + return true onKeydown = (event) -> keyChar = "" @@ -493,39 +493,38 @@ onKeydown = (event) -> event.srcElement.blur() exitInsertMode() DomUtils.suppressEvent event - KeydownEvents.push event + handledKeydownEvents.push event else if (findMode) if (KeyboardUtils.isEscape(event)) handleEscapeForFindMode() + DomUtils.suppressEvent event KeydownEvents.push event - return Mode.suppressPropagation else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) handleDeleteForFindMode() + DomUtils.suppressEvent event KeydownEvents.push event - return Mode.suppressPropagation else if (event.keyCode == keyCodes.enter) handleEnterForFindMode() + DomUtils.suppressEvent event KeydownEvents.push event - return Mode.suppressPropagation else if (!modifiers) + DomUtils.suppressPropagation(event) KeydownEvents.push event - return Mode.suppressPropagation else if (isShowingHelpDialog && KeyboardUtils.isEscape(event)) hideHelpDialog() + DomUtils.suppressEvent event KeydownEvents.push event - return Mode.suppressPropagation 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 Mode.suppressPropagation keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) @@ -533,7 +532,7 @@ onKeydown = (event) -> keyPort.postMessage({ keyChar:"", frameId:frameId }) else if isPassKey KeyboardUtils.getKeyChar(event) - return Mode.propagate + return undefined # Added to prevent propagating this event to other listeners if it's one that'll trigger a Vimium command. # The goal is to avoid the scenario where Google Instant Search uses every keydown event to dump us @@ -545,14 +544,14 @@ onKeydown = (event) -> if (keyChar == "" && !isInsertMode() && (currentCompletionKeys.indexOf(KeyboardUtils.getKeyChar(event)) != -1 || isValidFirstKey(KeyboardUtils.getKeyChar(event)))) - # Suppress chrome propagation of this event, but drop through, and continue handler-stack processing. - DomUtils.suppressPropagation event + DomUtils.suppressPropagation(event) KeydownEvents.push event - return Mode.propagate + return true onKeyup = (event) -> - if KeydownEvents.pop event then Mode.suppressPropagation else Mode.propagate + DomUtils.suppressPropagation(event) if KeydownEvents.pop event + return true checkIfEnabledForUrl = -> url = window.location.toString() -- cgit v1.2.3 From a321ca5e1a335f0b04714fa8ea00c2bac8febb86 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 2 Jan 2015 08:11:22 +0000 Subject: Modes; flesh out passkeys mode. --- content_scripts/mode_passkeys.coffee | 32 ++++++++++++++++++++++++++++++++ content_scripts/vimium_frontend.coffee | 20 +++++++++----------- 2 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 content_scripts/mode_passkeys.coffee (limited to 'content_scripts') diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee new file mode 100644 index 00000000..7a0249ad --- /dev/null +++ b/content_scripts/mode_passkeys.coffee @@ -0,0 +1,32 @@ + +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)] + # A key is passed through to the underlying page by returning handlerStack.passThrough. + return handlerStack.passThrough if keyChar and @isPassKey keyChar + true + + setState: (response) -> + if response.isEnabledForUrl? + @passKeys = (response.isEnabledForUrl and response.passKeys) or "" + if response.keyQueue? + @keyQueue = response.keyQueue + + constructor: -> + super + name: "passkeys" + keydown: (event) => @handlePassKeyEvent event + keypress: (event) => @handlePassKeyEvent event + keyup: -> true # Allow event to propagate. + +root = exports ? window +root.PassKeysMode = PassKeysMode diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index b40e9735..409cf96b 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -6,6 +6,7 @@ # insertMode = null +passKeysMode = null insertModeLock = null findMode = false findModeQuery = { rawQuery: "", matchCount: 0 } @@ -110,7 +111,7 @@ initializePreDomReady = -> settings.addEventListener("load", LinkHints.init.bind(LinkHints)) settings.load() - # Install normal mode. This will be at the bottom of both the mode stack and the handler stack, and is never + # Install normal mode. This is at the bottom of both the mode stack and the handler stack, and is never # deactivated. new Mode name: "normal" @@ -118,7 +119,7 @@ initializePreDomReady = -> keypress: onKeypress keyup: onKeyup - # Initialize the scroller. The scroller installs key handlers, and these will be next on the handler stack, + # Initialize the scroller. The scroller install a key handler, and this is next on the handler stack, # immediately above normal mode. Scroller.init settings @@ -127,14 +128,8 @@ initializePreDomReady = -> return handlerStack.passThrough if keyChar and isPassKey keyChar true - # Install passKeys mode. This mode is never deactivated. - new Mode - name: "passkeys" - keydown: handlePassKeyEvent - keypress: handlePassKeyEvent - keyup: -> true # Allow event to propagate. - - # Install insert mode. + # Install passKeys and insert modes. These too are permanently on the stack (although not always active). + passKeysMode = new PassKeysMode() insertMode = new InsertMode() checkIfEnabledForUrl() @@ -163,7 +158,7 @@ initializePreDomReady = -> executePageCommand: executePageCommand getActiveState: -> { enabled: isEnabledForUrl, passKeys: passKeys, badge: Mode.getBadge() } setState: setState - currentKeyQueue: (request) -> keyQueue = request.keyQueue + currentKeyQueue: (request) -> passKeysMode.setState request chrome.runtime.onMessage.addListener (request, sender, sendResponse) -> # In the options page, we will receive requests from both content and background scripts. ignore those @@ -208,6 +203,7 @@ setState = (request) -> initializeWhenEnabled(request.passKeys) if request.enabled isEnabledForUrl = request.enabled passKeys = request.passKeys + passKeysMode.setState request # # The backend needs to know which frame has focus. @@ -395,6 +391,7 @@ extend window, # 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 ) -> + return false # Diabled. return !keyQueue and passKeys and 0 <= passKeys.indexOf(keyChar) # Track which keydown events we have handled, so that we can subsequently suppress the corresponding keyup @@ -557,6 +554,7 @@ checkIfEnabledForUrl = -> url = window.location.toString() chrome.runtime.sendMessage { handler: "isEnabledForUrl", url: url }, (response) -> + passKeysMode.setState response isEnabledForUrl = response.isEnabledForUrl if (isEnabledForUrl) initializeWhenEnabled(response.passKeys) -- cgit v1.2.3 From b5535bc5a1b44c12cff62bac601a8d6ec7e04a6c Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 2 Jan 2015 08:23:12 +0000 Subject: Modes; better name for handlerStack.passDirectlyToPage. --- content_scripts/mode.coffee | 2 +- content_scripts/mode_insert.coffee | 2 +- content_scripts/mode_passkeys.coffee | 4 ++-- content_scripts/vimium_frontend.coffee | 7 +------ 4 files changed, 5 insertions(+), 10 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 88938e79..82dbf74a 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -40,7 +40,7 @@ class Mode if type == "keydown" and KeyboardUtils.isEscape event @exit() return Mode.suppressPropagation - handlerStack.passThrough + handlerStack.passDirectlyToPage # Generate a default handler which always suppresses propagation; except Esc, which pops the current mode. generateSuppressPropagation: (type) -> diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 4a1d4349..4ef490c9 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -27,7 +27,7 @@ class InsertMode extends Mode generateKeyHandler: (type) -> (event) => return Mode.propagate unless @isActive() - return handlerStack.passThrough unless type == "keydown" and KeyboardUtils.isEscape event + return handlerStack.passDirectlyToPage unless type == "keydown" and KeyboardUtils.isEscape event # We're now exiting insert mode. if @canEditElement event.srcElement # Remove the focus so the user can't just get himself back into insert mode by typing in the same input diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee index 7a0249ad..ce9f25d2 100644 --- a/content_scripts/mode_passkeys.coffee +++ b/content_scripts/mode_passkeys.coffee @@ -11,8 +11,8 @@ class PassKeysMode extends Mode handlePassKeyEvent: (event) -> for keyChar in [KeyboardUtils.getKeyChar(event), String.fromCharCode(event.charCode)] - # A key is passed through to the underlying page by returning handlerStack.passThrough. - return handlerStack.passThrough if keyChar and @isPassKey keyChar + # A key is passed through to the underlying page by returning handlerStack.passDirectlyToPage. + return handlerStack.passDirectlyToPage if keyChar and @isPassKey keyChar true setState: (response) -> diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 409cf96b..59404247 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -123,11 +123,6 @@ initializePreDomReady = -> # immediately above normal mode. Scroller.init settings - handlePassKeyEvent = (event) -> - for keyChar in [ KeyboardUtils.getKeyChar(event), String.fromCharCode(event.charCode) ] - return handlerStack.passThrough if keyChar and isPassKey keyChar - true - # Install passKeys and insert modes. These too are permanently on the stack (although not always active). passKeysMode = new PassKeysMode() insertMode = new InsertMode() @@ -444,7 +439,7 @@ onKeypress = (event) -> DomUtils.suppressEvent(event) else if (!isInsertMode() && !findMode) if (isPassKey keyChar) - return handlerStack.passThrough + return handlerStack.passDirectlyToPage if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)) DomUtils.suppressEvent(event) -- cgit v1.2.3 From 20ebbf3de2384738af916a441470d74a5aca14a3 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 2 Jan 2015 10:24:39 +0000 Subject: Modes; rework badge handling and fix passkeys mode. --- content_scripts/mode.coffee | 30 +++++++++++++-------- content_scripts/mode_insert.coffee | 49 ++++++++++++++++++++-------------- content_scripts/mode_passkeys.coffee | 25 ++++++++++++----- content_scripts/vimium_frontend.coffee | 16 ++++++++--- 4 files changed, 79 insertions(+), 41 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 82dbf74a..468f587a 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -18,13 +18,14 @@ class Mode constructor: (options) -> extend @, options - @handlerId = handlerStack.push + @handlers = [] + @handlers.push handlerStack.push keydown: @checkForBuiltInHandler "keydown", @keydown keypress: @checkForBuiltInHandler "keypress", @keypress keyup: @checkForBuiltInHandler "keyup", @keyup + updateBadgeForMode: (badge) => @updateBadgeForMode badge Mode.modes.unshift @ - Mode.setBadge() # Allow the strings "suppress" and "pass" to be used as proxies for the built-in handlers. checkForBuiltInHandler: (type, handler) -> @@ -48,18 +49,25 @@ class Mode (event) -> handler(event) and Mode.suppressPropagation # Always falsy. exit: -> - handlerStack.remove @handlerId + handlerStack.remove handlerId for handlerId in @handlers Mode.modes = Mode.modes.filter (mode) => mode != @ - Mode.setBadge() + Mode.updateBadge() - # Set the badge on the browser popup to indicate the current mode; static method. - @setBadge: -> - chrome.runtime.sendMessage({ handler: "setBadge", badge: Mode.getBadge() }) + # 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) -> + badge.badge ||= @badge + Mode.propagate - # Static convenience methods. - @is: (mode) -> Mode.current()?.name == mode - @getBadge: -> Mode.current()?.badge || "" - @isInsert: -> Mode.is "insert" + # Static method. Used externally and internally to initiate bubbling of an updateBadgeForMode event. + @updateBadge: -> + 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 }) root = exports ? window root.Mode = Mode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 4ef490c9..e68bf6ab 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -1,6 +1,6 @@ class InsertMode extends Mode - userActivated: false + isInsertMode: false # Input or text elements are considered focusable and able to receieve their own keyboard events, and will # enter insert mode if focused. Also note that the "contentEditable" attribute can be set on any element @@ -11,7 +11,7 @@ class InsertMode extends Mode # Use a blacklist instead of a whitelist because new form controls are still being implemented for html5. if nodeName == "input" and element.type and not element.type in ["radio", "checkbox"] return true - nodeName in ["textarea", "select"] + nodeName in ["textarea", "select"] # Embedded elements like Flash and quicktime players can obtain focus but cannot be programmatically # unfocused. @@ -21,8 +21,12 @@ class InsertMode extends Mode canEditElement: (element) -> element and (@isEditable(element) or @isEmbed element) + # Check whether insert mode is active. Also, activate insert mode if the current element is editable. isActive: -> - @userActivated or @canEditElement document.activeElement + return true if @isInsertMode + # FIXME(smblott). Is there a way to (safely) cache the results of these @canEditElement() calls? + @activate() if @canEditElement document.activeElement + @isInsertMode generateKeyHandler: (type) -> (event) => @@ -36,35 +40,40 @@ class InsertMode extends Mode # 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() - @userActivated = false - @updateBadge() + @isInsertMode = false + Mode.updateBadge() Mode.suppressPropagation - pickBadge: -> - if @isActive() then "I" else "" + activate: -> + @isInsertMode = true + Mode.updateBadge() - updateBadge: -> - badge = @badge - @badge = @pickBadge() - Mode.setBadge() if badge != @badge - Mode.propagate + # Override (and re-use) updateBadgeForMode() from Mode.updateBadgeForMode(). Use insert-mode badge only if + # we're active and no mode higher in stack has already inserted a badge. + updateBadgeForMode: (badge) -> + @badge = if @isActive() then "I" else "" + super badge - activate: -> - @userActivated = true - @updateBadge() + checkModeState: -> + previousState = @isInsertMode + if @isActive() != previousState + Mode.updateBadge() constructor: -> super name: "insert" - badge: @pickBadge() + badge: "I" keydown: @generateKeyHandler "keydown" keypress: @generateKeyHandler "keypress" keyup: @generateKeyHandler "keyup" - handlerStack.push - DOMActivate: => @updateBadge() - focus: => @updateBadge() - blur: => @updateBadge() + @handlers.push handlerStack.push + DOMActivate: => @checkModeState() + focus: => @checkModeState() + blur: => @checkModeState() + + # We may already have been dropped into insert mode. So check. + Mode.updateBadge() root = exports ? window root.InsertMode = InsertMode diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee index ce9f25d2..82f7596b 100644 --- a/content_scripts/mode_passkeys.coffee +++ b/content_scripts/mode_passkeys.coffee @@ -13,20 +13,31 @@ class PassKeysMode extends Mode for keyChar in [KeyboardUtils.getKeyChar(event), String.fromCharCode(event.charCode)] # A key is passed through to the underlying page by returning handlerStack.passDirectlyToPage. return handlerStack.passDirectlyToPage if keyChar and @isPassKey keyChar - true + Mode.propagate - setState: (response) -> - if response.isEnabledForUrl? - @passKeys = (response.isEnabledForUrl and response.passKeys) or "" - if response.keyQueue? - @keyQueue = response.keyQueue + # This is called to set the pass-keys state with various types of request from various sources, so we handle + # all of these. + # TODO(smblott) Rationalize this. + setState: (request) -> + if request.isEnabledForUrl? + @passKeys = (request.isEnabledForUrl and request.passKeys) or "" + if request.enabled? + @passKeys = (request.enabled and request.passKeys) or "" + if request.keyQueue? + @keyQueue = request.keyQueue + Mode.updateBadge() constructor: -> super name: "passkeys" keydown: (event) => @handlePassKeyEvent event keypress: (event) => @handlePassKeyEvent event - keyup: -> true # Allow event to propagate. + keyup: -> Mode.propagate + + # Overriding and re-using updateBadgeForMode() from Mode.updateBadgeForMode(). + updateBadgeForMode: (badge) -> + @badge = if @passKeys and not @keyQueue then "P" else "" + super badge root = exports ? window root.PassKeysMode = PassKeysMode diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 59404247..3ce4169d 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -115,15 +115,22 @@ initializePreDomReady = -> # deactivated. new Mode name: "normal" + badge: "N" keydown: onKeydown keypress: onKeypress keyup: onKeyup + # Overriding updateBadgeForMode() from Mode.updateBadgeForMode(). + updateBadgeForMode: (badge) -> + badge.badge ||= @badge + badge.badge = "" unless isEnabledForUrl + # Initialize the scroller. The scroller install a key handler, and this is next on the handler stack, # immediately above normal mode. Scroller.init settings # 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. passKeysMode = new PassKeysMode() insertMode = new InsertMode() @@ -151,7 +158,9 @@ initializePreDomReady = -> getScrollPosition: -> scrollX: window.scrollX, scrollY: window.scrollY setScrollPosition: (request) -> setScrollPosition request.scrollX, request.scrollY executePageCommand: executePageCommand - getActiveState: -> { enabled: isEnabledForUrl, passKeys: passKeys, badge: Mode.getBadge() } + getActiveState: -> + Mode.updateBadge() + return { enabled: isEnabledForUrl, passKeys: passKeys } setState: setState currentKeyQueue: (request) -> passKeysMode.setState request @@ -206,7 +215,7 @@ setState = (request) -> window.addEventListener "focus", -> # settings may have changed since the frame last had focus settings.load() - chrome.runtime.sendMessage({ handler: "frameFocused", frameId: frameId, badge: Mode.getBadge() }) + chrome.runtime.sendMessage({ handler: "frameFocused", frameId: frameId }) # # Initialization tasks that must wait for the document to be ready. @@ -549,13 +558,14 @@ checkIfEnabledForUrl = -> url = window.location.toString() chrome.runtime.sendMessage { handler: "isEnabledForUrl", url: url }, (response) -> - passKeysMode.setState response isEnabledForUrl = response.isEnabledForUrl if (isEnabledForUrl) initializeWhenEnabled(response.passKeys) else if (HUD.isReady()) # Quickly hide any HUD we might already be showing, e.g. if we entered insert mode on page load. HUD.hide() + passKeysMode.setState response + Mode.updateBadge() refreshCompletionKeys = (response) -> if (response) -- cgit v1.2.3 From 6d471844497ff35b83296d7da34830288696f029 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 2 Jan 2015 11:09:25 +0000 Subject: Modes; temporary hack to fix find mode. --- content_scripts/mode_passkeys.coffee | 3 +++ content_scripts/vimium_frontend.coffee | 12 +++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee index 82f7596b..6320c698 100644 --- a/content_scripts/mode_passkeys.coffee +++ b/content_scripts/mode_passkeys.coffee @@ -7,6 +7,9 @@ class PassKeysMode extends Mode # 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) -> + # FIXME(smblott). Temporary hack: attach findMode to the window (so passKeysMode can see it). This will be + # fixed when find mode is rationalized or #1401 is merged. + return false if window.findMode not @keyQueue and 0 <= @passKeys.indexOf(keyChar) handlePassKeyEvent: (event) -> diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 3ce4169d..09e775a6 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -8,7 +8,9 @@ insertMode = null passKeysMode = null insertModeLock = null -findMode = false +# FIXME(smblott). Temporary hack: attach findMode to the window (so passKeysMode can see it). This will be +# fixed when find mode is rationalized or #1401 is merged. +window.findMode = false findModeQuery = { rawQuery: "", matchCount: 0 } findModeQueryHasResults = false findModeAnchorNode = null @@ -743,7 +745,7 @@ executeFind = (query, options) -> # rather hacky, but this is our way of signalling to the insertMode listener not to react to the focus # changes that find() induces. oldFindMode = findMode - findMode = true + window.findMode = true # Same hack, see comment at window.findMode definition. document.body.classList.add("vimiumFindMode") @@ -756,7 +758,7 @@ executeFind = (query, options) -> -> document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true) 0) - findMode = oldFindMode + window.findMode = oldFindMode # Same hack, see comment at window.findMode definition. # we need to save the anchor node here because seems to nullify it, regardless of whether we do # preventDefault() findModeAnchorNode = document.getSelection().anchorNode @@ -949,11 +951,11 @@ showFindModeHUDForQuery = -> window.enterFindMode = -> findModeQuery = { rawQuery: "" } - findMode = true + window.findMode = true # Same hack, see comment at window.findMode definition. HUD.show("/") exitFindMode = -> - findMode = false + window.findMode = false # Same hack, see comment at window.findMode definition. HUD.hide() window.showHelpDialog = (html, fid) -> -- cgit v1.2.3 From 298ee34b1c90b0203a74a2d158858428475bfd95 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 2 Jan 2015 11:47:42 +0000 Subject: Modes; fix insert mode. --- content_scripts/mode_insert.coffee | 39 +++++++++++++++++++--------------- content_scripts/vimium_frontend.coffee | 1 + 2 files changed, 23 insertions(+), 17 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index e68bf6ab..c8b05217 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -9,7 +9,7 @@ class InsertMode extends Mode return true if element.isContentEditable nodeName = element.nodeName?.toLowerCase() # Use a blacklist instead of a whitelist because new form controls are still being implemented for html5. - if nodeName == "input" and element.type and not element.type in ["radio", "checkbox"] + if nodeName == "input" and element.type not in ["radio", "checkbox"] return true nodeName in ["textarea", "select"] @@ -18,14 +18,17 @@ class InsertMode extends Mode isEmbed: (element) -> element.nodeName?.toLowerCase() in ["embed", "object"] - canEditElement: (element) -> - element and (@isEditable(element) or @isEmbed element) + isFocusable: (element) -> + (@isEditable(element) or @isEmbed element) - # Check whether insert mode is active. Also, activate insert mode if the current element is editable. + # Check whether insert mode is active. Also, activate insert mode if the current element is content + # editable. isActive: -> return true if @isInsertMode - # FIXME(smblott). Is there a way to (safely) cache the results of these @canEditElement() calls? - @activate() if @canEditElement document.activeElement + # 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 generateKeyHandler: (type) -> @@ -33,7 +36,7 @@ class InsertMode extends Mode return Mode.propagate unless @isActive() return handlerStack.passDirectlyToPage unless type == "keydown" and KeyboardUtils.isEscape event # We're now exiting insert mode. - if @canEditElement event.srcElement + 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. # NOTE(smblott, 2014/12/22) Including embeds for .blur() here is experimental. It appears to be the @@ -45,8 +48,9 @@ class InsertMode extends Mode Mode.suppressPropagation activate: -> - @isInsertMode = true - Mode.updateBadge() + unless @isInsertMode + @isInsertMode = true + Mode.updateBadge() # Override (and re-use) updateBadgeForMode() from Mode.updateBadgeForMode(). Use insert-mode badge only if # we're active and no mode higher in stack has already inserted a badge. @@ -54,11 +58,6 @@ class InsertMode extends Mode @badge = if @isActive() then "I" else "" super badge - checkModeState: -> - previousState = @isInsertMode - if @isActive() != previousState - Mode.updateBadge() - constructor: -> super name: "insert" @@ -68,9 +67,15 @@ class InsertMode extends Mode keyup: @generateKeyHandler "keyup" @handlers.push handlerStack.push - DOMActivate: => @checkModeState() - focus: => @checkModeState() - blur: => @checkModeState() + focus: (event) => + handlerStack.alwaysPropagate => + if not @isInsertMode and @isFocusable event.target + @activate() + blur: (event) => + handlerStack.alwaysPropagate => + if @isInsertMode and @isFocusable event.target + @isInsertMode = false + Mode.updateBadge() # We may already have been dropped into insert mode. So check. Mode.updateBadge() diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 09e775a6..da1f5de1 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -126,6 +126,7 @@ initializePreDomReady = -> updateBadgeForMode: (badge) -> badge.badge ||= @badge badge.badge = "" unless isEnabledForUrl + Mode.propagate # Not really necessary, but makes intention clear and does no harm. # Initialize the scroller. The scroller install a key handler, and this is next on the handler stack, # immediately above normal mode. -- cgit v1.2.3 From b179d80ac9c35eb85de3995e4c4fb7dc9945ed75 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 2 Jan 2015 13:22:31 +0000 Subject: Modes; fix badges. --- content_scripts/mode.coffee | 3 +-- content_scripts/mode_insert.coffee | 7 +++---- content_scripts/mode_passkeys.coffee | 7 ++++--- content_scripts/vimium_frontend.coffee | 7 +++---- 4 files changed, 11 insertions(+), 13 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 468f587a..9016caa2 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -56,8 +56,7 @@ class Mode # 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) -> - badge.badge ||= @badge - Mode.propagate + handlerStack.alwaysPropagate => badge.badge ||= @badge # Static method. Used externally and internally to initiate bubbling of an updateBadgeForMode event. @updateBadge: -> diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index c8b05217..f37cf1ad 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -52,11 +52,10 @@ class InsertMode extends Mode @isInsertMode = true Mode.updateBadge() - # Override (and re-use) updateBadgeForMode() from Mode.updateBadgeForMode(). Use insert-mode badge only if - # we're active and no mode higher in stack has already inserted a badge. + # Override updateBadgeForMode() from Mode.updateBadgeForMode(). updateBadgeForMode: (badge) -> - @badge = if @isActive() then "I" else "" - super badge + handlerStack.alwaysPropagate => + super badge if @isActive() constructor: -> super diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee index 6320c698..bb4518ae 100644 --- a/content_scripts/mode_passkeys.coffee +++ b/content_scripts/mode_passkeys.coffee @@ -33,14 +33,15 @@ class PassKeysMode extends Mode constructor: -> super name: "passkeys" + badge: "P" keydown: (event) => @handlePassKeyEvent event keypress: (event) => @handlePassKeyEvent event keyup: -> Mode.propagate - # Overriding and re-using updateBadgeForMode() from Mode.updateBadgeForMode(). + # Overriding updateBadgeForMode() from Mode.updateBadgeForMode(). updateBadgeForMode: (badge) -> - @badge = if @passKeys and not @keyQueue then "P" else "" - super badge + handlerStack.alwaysPropagate => + super badge 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 da1f5de1..6d63b24a 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -124,9 +124,9 @@ initializePreDomReady = -> # Overriding updateBadgeForMode() from Mode.updateBadgeForMode(). updateBadgeForMode: (badge) -> - badge.badge ||= @badge - badge.badge = "" unless isEnabledForUrl - Mode.propagate # Not really necessary, but makes intention clear and does no harm. + handlerStack.alwaysPropagate => + badge.badge ||= @badge + badge.badge = "" unless isEnabledForUrl # Initialize the scroller. The scroller install a key handler, and this is next on the handler stack, # immediately above normal mode. @@ -686,7 +686,6 @@ updateFindModeQuery = -> findModeQuery.matchCount = text.match(pattern)?.length handleKeyCharForFindMode = (keyChar) -> - console.log "xxxxxxxxxxxxxxx" findModeQuery.rawQuery += keyChar updateFindModeQuery() performFindInPlace() -- cgit v1.2.3 From 425eb0dd84c1d3bf3eb854bda68140db8c46cb7a Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 2 Jan 2015 14:21:20 +0000 Subject: Modes; better frame handling. --- content_scripts/mode.coffee | 11 ++++++++--- content_scripts/vimium_frontend.coffee | 3 ++- 2 files changed, 10 insertions(+), 4 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 9016caa2..7ca818b4 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -59,10 +59,15 @@ class Mode handlerStack.alwaysPropagate => 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 @updateBadge: -> - badge = {badge: ""} - handlerStack.bubbleEvent "updateBadgeForMode", badge - Mode.sendBadge badge.badge + 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) -> diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 6d63b24a..7ce3e988 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -201,7 +201,7 @@ initializeWhenEnabled = (newPassKeys) -> # installListener document, "focus", onFocusCapturePhase # No longer needed. installListener document, "blur", onBlurCapturePhase installListener document, "DOMActivate", onDOMActivate - installListener document, "focus", onFocus + installListener document, "focusin", onFocus installListener document, "blur", onBlur enterInsertModeIfElementIsFocused() installedListeners = true @@ -281,6 +281,7 @@ window.focusThisFrame = (shouldHighlight) -> chrome.runtime.sendMessage({ handler: "nextFrame", frameId: frameId }) return window.focus() + Mode.updateBadge() if (document.body && shouldHighlight) borderWas = document.body.style.border document.body.style.border = '5px solid yellow' -- cgit v1.2.3 From d4c43d8f9095325b41544ad7811cc131c1b186f1 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 2 Jan 2015 14:33:18 +0000 Subject: Modes; show keyqueue in badge. --- content_scripts/vimium_frontend.coffee | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 7ce3e988..fb6199bf 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -125,7 +125,7 @@ initializePreDomReady = -> # Overriding updateBadgeForMode() from Mode.updateBadgeForMode(). updateBadgeForMode: (badge) -> handlerStack.alwaysPropagate => - badge.badge ||= @badge + badge.badge ||= if keyQueue then keyQueue else @badge badge.badge = "" unless isEnabledForUrl # Initialize the scroller. The scroller install a key handler, and this is next on the handler stack, @@ -165,7 +165,9 @@ initializePreDomReady = -> Mode.updateBadge() return { enabled: isEnabledForUrl, passKeys: passKeys } setState: setState - currentKeyQueue: (request) -> passKeysMode.setState request + currentKeyQueue: (request) -> + keyQueue = request.keyQueue + passKeysMode.setState request chrome.runtime.onMessage.addListener (request, sender, sendResponse) -> # In the options page, we will receive requests from both content and background scripts. ignore those -- cgit v1.2.3 From b7d5e25e353010505db7754e97d4387c8aa6b8fc Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 2 Jan 2015 15:01:32 +0000 Subject: Modes; simplify badge handling. --- content_scripts/mode_insert.coffee | 28 +++++++++++++--------------- content_scripts/mode_passkeys.coffee | 7 +------ content_scripts/vimium_frontend.coffee | 32 ++++++++++++++++++-------------- 3 files changed, 32 insertions(+), 35 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index f37cf1ad..9504edfd 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -31,6 +31,17 @@ class InsertMode extends Mode @activate() if document.activeElement?.isContentEditable @isInsertMode + activate: -> + unless @isInsertMode + @isInsertMode = true + @badge = "I" + Mode.updateBadge() + + deactivate: -> + @isInsertMode = false + @badge = "" + Mode.updateBadge() + generateKeyHandler: (type) -> (event) => return Mode.propagate unless @isActive() @@ -43,24 +54,12 @@ class InsertMode extends Mode # 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() - @isInsertMode = false - Mode.updateBadge() + @deactivate() Mode.suppressPropagation - activate: -> - unless @isInsertMode - @isInsertMode = true - Mode.updateBadge() - - # Override updateBadgeForMode() from Mode.updateBadgeForMode(). - updateBadgeForMode: (badge) -> - handlerStack.alwaysPropagate => - super badge if @isActive() - constructor: -> super name: "insert" - badge: "I" keydown: @generateKeyHandler "keydown" keypress: @generateKeyHandler "keypress" keyup: @generateKeyHandler "keyup" @@ -73,8 +72,7 @@ class InsertMode extends Mode blur: (event) => handlerStack.alwaysPropagate => if @isInsertMode and @isFocusable event.target - @isInsertMode = false - Mode.updateBadge() + @deactivate() # We may already have been dropped into insert mode. So check. Mode.updateBadge() diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee index bb4518ae..a953deca 100644 --- a/content_scripts/mode_passkeys.coffee +++ b/content_scripts/mode_passkeys.coffee @@ -28,20 +28,15 @@ class PassKeysMode extends Mode @passKeys = (request.enabled and request.passKeys) or "" if request.keyQueue? @keyQueue = request.keyQueue + @badge = if @passKeys and not @keyQueue then "P" else "" Mode.updateBadge() constructor: -> super name: "passkeys" - badge: "P" keydown: (event) => @handlePassKeyEvent event keypress: (event) => @handlePassKeyEvent event keyup: -> Mode.propagate - # Overriding updateBadgeForMode() from Mode.updateBadgeForMode(). - updateBadgeForMode: (badge) -> - handlerStack.alwaysPropagate => - super badge 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 fb6199bf..2df2e226 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -106,6 +106,21 @@ frameId = Math.floor(Math.random()*999999999) hasModifiersRegex = /^<([amc]-)+.>/ +class NormalMode extends Mode + constructor: -> + super + name: "normal" + badge: "N" + keydown: onKeydown + keypress: onKeypress + keyup: onKeyup + + updateBadgeForMode: (badge) -> + handlerStack.alwaysPropagate => + # Idea... Instead of an icon, we could show the keyQueue here (if it's non-empty). + super badge + badge.badge = "" unless isEnabledForUrl + # # Complete initialization work that sould be done prior to DOMReady. # @@ -115,20 +130,9 @@ initializePreDomReady = -> # Install normal mode. This is at the bottom of both the mode stack and the handler stack, and is never # deactivated. - new Mode - name: "normal" - badge: "N" - keydown: onKeydown - keypress: onKeypress - keyup: onKeyup - - # Overriding updateBadgeForMode() from Mode.updateBadgeForMode(). - updateBadgeForMode: (badge) -> - handlerStack.alwaysPropagate => - badge.badge ||= if keyQueue then keyQueue else @badge - badge.badge = "" unless isEnabledForUrl - - # Initialize the scroller. The scroller install a key handler, and this is next on the handler stack, + new NormalMode() + + # Initialize the scroller. The scroller installs a key handler, and this is next on the handler stack, # immediately above normal mode. Scroller.init settings -- cgit v1.2.3 From 2d8c478e8086abf80b206d0fd8abc488a035b5cd Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 2 Jan 2015 15:59:58 +0000 Subject: Modes; incorporate find mode. --- content_scripts/mode.coffee | 16 ++++++---- content_scripts/mode_passkeys.coffee | 11 +++---- content_scripts/vimium_frontend.coffee | 54 +++++++++++++++++++++++++++------- 3 files changed, 60 insertions(+), 21 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 7ca818b4..a2a8b8b0 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -9,11 +9,11 @@ class Mode @propagate = true # 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" or "pass"; the latter are replaced with suitable functions. - keypress: "suppress" # A function, or "suppress" or "pass"; the latter are replaced with suitable functions. - keyup: "suppress" # A function, or "suppress" or "pass"; the latter are replaced with suitable functions. + name: "" # The name of this mode. + badge: "" # A badge to display on the popup when this mode is active. + keydown: "pass" # A function, or "suppress" or "pass"; the latter are replaced with suitable handlers. + keypress: "pass" # A function, or "suppress" or "pass"; the latter are replaced with suitable handlers. + keyup: "pass" # A function, or "suppress" or "pass"; the latter are replaced with suitable handlers. constructor: (options) -> extend @, options @@ -73,5 +73,11 @@ class Mode @sendBadge: (badge) -> chrome.runtime.sendMessage({ handler: "setBadge", badge: badge }) + # Install a mode, call a function, and exit the mode again. + @runIn: (mode, func) -> + mode = new mode() + func() + mode.exit() + root = exports ? window root.Mode = Mode diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee index a953deca..9e922104 100644 --- a/content_scripts/mode_passkeys.coffee +++ b/content_scripts/mode_passkeys.coffee @@ -7,9 +7,6 @@ class PassKeysMode extends Mode # 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) -> - # FIXME(smblott). Temporary hack: attach findMode to the window (so passKeysMode can see it). This will be - # fixed when find mode is rationalized or #1401 is merged. - return false if window.findMode not @keyQueue and 0 <= @passKeys.indexOf(keyChar) handlePassKeyEvent: (event) -> @@ -24,12 +21,12 @@ class PassKeysMode extends Mode setState: (request) -> if request.isEnabledForUrl? @passKeys = (request.isEnabledForUrl and request.passKeys) or "" + Mode.updateBadge() if request.enabled? @passKeys = (request.enabled and request.passKeys) or "" + Mode.updateBadge() if request.keyQueue? @keyQueue = request.keyQueue - @badge = if @passKeys and not @keyQueue then "P" else "" - Mode.updateBadge() constructor: -> super @@ -38,5 +35,9 @@ class PassKeysMode extends Mode keypress: (event) => @handlePassKeyEvent event keyup: -> Mode.propagate + updateBadgeForMode: (badge) -> + @badge = if @passKeys and not @keyQueue then "P" else "" + super badge + root = exports ? window root.PassKeysMode = PassKeysMode diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 2df2e226..c0f98d85 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -8,9 +8,7 @@ insertMode = null passKeysMode = null insertModeLock = null -# FIXME(smblott). Temporary hack: attach findMode to the window (so passKeysMode can see it). This will be -# fixed when find mode is rationalized or #1401 is merged. -window.findMode = false +findMode = false findModeQuery = { rawQuery: "", matchCount: 0 } findModeQueryHasResults = false findModeAnchorNode = null @@ -729,6 +727,42 @@ handleEnterForFindMode = -> document.body.classList.add("vimiumFindMode") settings.set("findModeRawQuery", findModeQuery.rawQuery) +class FindMode extends Mode + constructor: -> + super + name: "find" + badge: "F" + + keydown: (event) => + if KeyboardUtils.isEscape event + handleEscapeForFindMode() + @exit() + Mode.suppressPropagation + else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) + handleDeleteForFindMode() + Mode.suppressPropagation + else if (event.keyCode == keyCodes.enter) + handleEnterForFindMode() + @exit() + Mode.suppressPropagation + else + DomUtils.suppressPropagation(event) + handlerStack.eventConsumed + + keypress: (event) -> + handlerStack.neverPropagate -> + if event.keyCode > 31 + keyChar = String.fromCharCode event.charCode + handleKeyCharForFindMode keyChar if keyChar + + keyup: (event) -> handlerStack.neverPropagate -> false + + # Prevent insert mode from detecting a focused editable element. + @handlers.push handlerStack.push + focus: (event) -> handlerStack.neverPropagate (event) -> + + Mode.updateBadge() + performFindInPlace = -> cachedScrollX = window.scrollX cachedScrollY = window.scrollY @@ -747,25 +781,21 @@ performFindInPlace = -> # :options is an optional dict. valid parameters are 'caseSensitive' and 'backwards'. executeFind = (query, options) -> + result = null options = options || {} - # rather hacky, but this is our way of signalling to the insertMode listener not to react to the focus - # changes that find() induces. - oldFindMode = findMode - window.findMode = true # Same hack, see comment at window.findMode definition. - document.body.classList.add("vimiumFindMode") # prevent find from matching its own search query in the HUD HUD.hide(true) # ignore the selectionchange event generated by find() document.removeEventListener("selectionchange",restoreDefaultSelectionHighlight, true) - result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false) + Mode.runIn FindMode, -> + result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false) setTimeout( -> document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true) 0) - window.findMode = oldFindMode # Same hack, see comment at window.findMode definition. # we need to save the anchor node here because seems to nullify it, regardless of whether we do # preventDefault() findModeAnchorNode = document.getSelection().anchorNode @@ -839,6 +869,7 @@ findAndFocus = (backwards) -> if (KeyboardUtils.isEscape(event)) DomUtils.simulateSelect(document.activeElement) enterInsertModeWithoutShowingIndicator(document.activeElement) + insertMode.activate() return false # we have "consumed" this event, so do not propagate return true }) @@ -958,8 +989,9 @@ showFindModeHUDForQuery = -> window.enterFindMode = -> findModeQuery = { rawQuery: "" } - window.findMode = true # Same hack, see comment at window.findMode definition. + # window.findMode = true # Same hack, see comment at window.findMode definition. HUD.show("/") + new FindMode() exitFindMode = -> window.findMode = false # Same hack, see comment at window.findMode definition. -- cgit v1.2.3 From 072bb424d16e6faba243dcf1ab247494cbf8c9ee Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 2 Jan 2015 17:57:19 +0000 Subject: Modes; better constant naming. --- content_scripts/mode.coffee | 41 ++++++++++++++++------------------ content_scripts/mode_insert.coffee | 6 ++--- content_scripts/mode_passkeys.coffee | 11 +++++---- content_scripts/vimium_frontend.coffee | 10 ++++----- 4 files changed, 32 insertions(+), 36 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index a2a8b8b0..24c50561 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -4,16 +4,18 @@ class Mode @modes: [] @current: -> Mode.modes[0] - # Constants. Static. - @suppressPropagation = false - @propagate = true + # Constants; readable shortcuts for event-handler return values. + continueBubbling: true + suppressEvent: false + stopBubblingAndTrue: handlerStack.stopBubblingAndTrue + 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: "pass" # A function, or "suppress" or "pass"; the latter are replaced with suitable handlers. - keypress: "pass" # A function, or "suppress" or "pass"; the latter are replaced with suitable handlers. - keyup: "pass" # A function, or "suppress" or "pass"; the latter are replaced with suitable handlers. + 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(). constructor: (options) -> extend @, options @@ -30,23 +32,18 @@ class Mode # Allow the strings "suppress" and "pass" to be used as proxies for the built-in handlers. checkForBuiltInHandler: (type, handler) -> switch handler - when "suppress" then @generateSuppressPropagation type - when "pass" then @generatePassThrough type + 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 passes through to the underlying page; except Esc, which pops the - # current mode. - generatePassThrough: (type) -> + # Generate a default handler which always always yields the same result; except Esc, which pops the current + # mode. + generateHandler: (type, result) -> (event) => - if type == "keydown" and KeyboardUtils.isEscape event - @exit() - return Mode.suppressPropagation - handlerStack.passDirectlyToPage - - # Generate a default handler which always suppresses propagation; except Esc, which pops the current mode. - generateSuppressPropagation: (type) -> - handler = @generatePassThrough type - (event) -> handler(event) and Mode.suppressPropagation # Always falsy. + return result unless type == "keydown" and KeyboardUtils.isEscape event + @exit() + @suppressEvent exit: -> handlerStack.remove handlerId for handlerId in @handlers diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 9504edfd..ccd93870 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -44,8 +44,8 @@ class InsertMode extends Mode generateKeyHandler: (type) -> (event) => - return Mode.propagate unless @isActive() - return handlerStack.passDirectlyToPage unless type == "keydown" and KeyboardUtils.isEscape event + return @continueBubbling unless @isActive() + return @stopBubblingAndTrue unless type == "keydown" and 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 @@ -55,7 +55,7 @@ class InsertMode extends Mode # games. See discussion in #1211 and #1194. event.srcElement.blur() @deactivate() - Mode.suppressPropagation + @suppressEvent constructor: -> super diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee index 9e922104..c7c2c9b7 100644 --- a/content_scripts/mode_passkeys.coffee +++ b/content_scripts/mode_passkeys.coffee @@ -11,12 +11,11 @@ class PassKeysMode extends Mode handlePassKeyEvent: (event) -> for keyChar in [KeyboardUtils.getKeyChar(event), String.fromCharCode(event.charCode)] - # A key is passed through to the underlying page by returning handlerStack.passDirectlyToPage. - return handlerStack.passDirectlyToPage if keyChar and @isPassKey keyChar - Mode.propagate + return @stopBubblingAndTrue if keyChar and @isPassKey keyChar + @continueBubbling - # This is called to set the pass-keys state with various types of request from various sources, so we handle - # all of these. + # This is called to set the pass-keys configuration and state with various types of request from various + # sources, so we handle several cases. # TODO(smblott) Rationalize this. setState: (request) -> if request.isEnabledForUrl? @@ -33,7 +32,7 @@ class PassKeysMode extends Mode name: "passkeys" keydown: (event) => @handlePassKeyEvent event keypress: (event) => @handlePassKeyEvent event - keyup: -> Mode.propagate + keyup: => @continueBubbling updateBadgeForMode: (badge) -> @badge = if @passKeys and not @keyQueue then "P" else "" diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index c0f98d85..7950bd42 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -456,7 +456,7 @@ onKeypress = (event) -> DomUtils.suppressEvent(event) else if (!isInsertMode() && !findMode) if (isPassKey keyChar) - return handlerStack.passDirectlyToPage + return handlerStack.stopBubblingAndTrue if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)) DomUtils.suppressEvent(event) @@ -737,17 +737,17 @@ class FindMode extends Mode if KeyboardUtils.isEscape event handleEscapeForFindMode() @exit() - Mode.suppressPropagation + @suppressEvent else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) handleDeleteForFindMode() - Mode.suppressPropagation + @suppressEvent else if (event.keyCode == keyCodes.enter) handleEnterForFindMode() @exit() - Mode.suppressPropagation + @suppressEvent else DomUtils.suppressPropagation(event) - handlerStack.eventConsumed + handlerStack.stopBubblingAndFalse keypress: (event) -> handlerStack.neverPropagate -> -- cgit v1.2.3 From 5ef737632663dc5403cb6439db2df24fcc29af3b Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 3 Jan 2015 05:26:48 +0000 Subject: Modes; tidy up find mode. --- content_scripts/mode_passkeys.coffee | 2 +- content_scripts/vimium_frontend.coffee | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee index c7c2c9b7..c754e967 100644 --- a/content_scripts/mode_passkeys.coffee +++ b/content_scripts/mode_passkeys.coffee @@ -17,7 +17,7 @@ class PassKeysMode extends Mode # This is called to set the pass-keys configuration and state with various types of request from various # sources, so we handle several cases. # TODO(smblott) Rationalize this. - setState: (request) -> + configure: (request) -> if request.isEnabledForUrl? @passKeys = (request.isEnabledForUrl and request.passKeys) or "" Mode.updateBadge() diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 7950bd42..a9b318c6 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -169,7 +169,7 @@ initializePreDomReady = -> setState: setState currentKeyQueue: (request) -> keyQueue = request.keyQueue - passKeysMode.setState request + passKeysMode.configure request chrome.runtime.onMessage.addListener (request, sender, sendResponse) -> # In the options page, we will receive requests from both content and background scripts. ignore those @@ -214,7 +214,7 @@ setState = (request) -> initializeWhenEnabled(request.passKeys) if request.enabled isEnabledForUrl = request.enabled passKeys = request.passKeys - passKeysMode.setState request + passKeysMode.configure request # # The backend needs to know which frame has focus. @@ -572,7 +572,7 @@ checkIfEnabledForUrl = -> else if (HUD.isReady()) # Quickly hide any HUD we might already be showing, e.g. if we entered insert mode on page load. HUD.hide() - passKeysMode.setState response + passKeysMode.configure response Mode.updateBadge() refreshCompletionKeys = (response) -> @@ -728,10 +728,10 @@ handleEnterForFindMode = -> settings.set("findModeRawQuery", findModeQuery.rawQuery) class FindMode extends Mode - constructor: -> + constructor: (badge="F") -> super name: "find" - badge: "F" + badge: badge keydown: (event) => if KeyboardUtils.isEscape event @@ -763,6 +763,9 @@ class FindMode extends Mode Mode.updateBadge() +class FindModeWithoutBadge extends FindMode + constructor: -> super "" + performFindInPlace = -> cachedScrollX = window.scrollX cachedScrollY = window.scrollY @@ -790,7 +793,7 @@ executeFind = (query, options) -> HUD.hide(true) # ignore the selectionchange event generated by find() document.removeEventListener("selectionchange",restoreDefaultSelectionHighlight, true) - Mode.runIn FindMode, -> + Mode.runIn FindModeWithoutBadge, -> result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false) setTimeout( -> document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true) -- cgit v1.2.3 From a71da0e9aff6a4f89724f5a15a022790e23d5049 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 3 Jan 2015 05:45:10 +0000 Subject: Modes; refactor insert mode. --- content_scripts/mode_insert.coffee | 49 +++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 24 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index ccd93870..34fad926 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -1,6 +1,7 @@ class InsertMode extends Mode isInsertMode: false + insertModeLock: null # Input or text elements are considered focusable and able to receieve their own keyboard events, and will # enter insert mode if focused. Also note that the "contentEditable" attribute can be set on any element @@ -31,47 +32,47 @@ class InsertMode extends Mode @activate() if document.activeElement?.isContentEditable @isInsertMode - activate: -> + activate: (target=null) -> unless @isInsertMode @isInsertMode = true + @insertModeLock = target @badge = "I" Mode.updateBadge() deactivate: -> - @isInsertMode = false - @badge = "" - Mode.updateBadge() - - generateKeyHandler: (type) -> - (event) => - return @continueBubbling unless @isActive() - return @stopBubblingAndTrue unless type == "keydown" and 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. - # 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 + if @isInsertMode + @isInsertMode = false + @insertModeLock = null + @badge = "" + Mode.updateBadge() constructor: -> super name: "insert" - keydown: @generateKeyHandler "keydown" - keypress: @generateKeyHandler "keypress" - keyup: @generateKeyHandler "keyup" + keydown: (event) => + return @continueBubbling unless @isActive() + 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. + # 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 @handlers.push handlerStack.push focus: (event) => handlerStack.alwaysPropagate => if not @isInsertMode and @isFocusable event.target - @activate() + @activate event.target blur: (event) => handlerStack.alwaysPropagate => - if @isInsertMode and @isFocusable event.target + if @isInsertMode and event.target == @insertModeLock @deactivate() # We may already have been dropped into insert mode. So check. -- cgit v1.2.3 From 103fcde7c51fe83bc9c58fc28c3c11ce6a791f0f Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 3 Jan 2015 05:58:09 +0000 Subject: Modes; more renaming and refactoring. --- content_scripts/mode.coffee | 2 +- content_scripts/mode_insert.coffee | 13 +++++++++++-- content_scripts/vimium_frontend.coffee | 15 ++++----------- 3 files changed, 16 insertions(+), 14 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 24c50561..f411d29b 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -53,7 +53,7 @@ class Mode # 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.alwaysPropagate => badge.badge ||= @badge + handlerStack.alwaysContinueBubbling => badge.badge ||= @badge # Static method. Used externally and internally to initiate bubbling of an updateBadgeForMode event. # Do not update the badge: diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 34fad926..64aaa445 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -67,16 +67,25 @@ class InsertMode extends Mode @handlers.push handlerStack.push focus: (event) => - handlerStack.alwaysPropagate => + handlerStack.alwaysContinueBubbling => if not @isInsertMode and @isFocusable event.target @activate event.target blur: (event) => - handlerStack.alwaysPropagate => + handlerStack.alwaysContinueBubbling => if @isInsertMode and event.target == @insertModeLock @deactivate() # We may already have been dropped into insert mode. So check. Mode.updateBadge() +# Utility mode. +# Activate this mode to prevent a focused, editable element from triggering insert mode. +class FocusMustNotTriggerInsertMode extends Mode + constructor: -> + super() + @handlers.push handlerStack.push + focus: => @suppressEvent + root = exports ? window root.InsertMode = InsertMode +root.FocusMustNotTriggerInsertMode = FocusMustNotTriggerInsertMode diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index a9b318c6..a252c878 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -114,7 +114,7 @@ class NormalMode extends Mode keyup: onKeyup updateBadgeForMode: (badge) -> - handlerStack.alwaysPropagate => + handlerStack.alwaysContinueBubbling => # Idea... Instead of an icon, we could show the keyQueue here (if it's non-empty). super badge badge.badge = "" unless isEnabledForUrl @@ -750,22 +750,15 @@ class FindMode extends Mode handlerStack.stopBubblingAndFalse keypress: (event) -> - handlerStack.neverPropagate -> + handlerStack.neverContinueBubbling -> if event.keyCode > 31 keyChar = String.fromCharCode event.charCode handleKeyCharForFindMode keyChar if keyChar - keyup: (event) -> handlerStack.neverPropagate -> false - - # Prevent insert mode from detecting a focused editable element. - @handlers.push handlerStack.push - focus: (event) -> handlerStack.neverPropagate (event) -> + keyup: (event) => @suppressEvent Mode.updateBadge() -class FindModeWithoutBadge extends FindMode - constructor: -> super "" - performFindInPlace = -> cachedScrollX = window.scrollX cachedScrollY = window.scrollY @@ -793,7 +786,7 @@ executeFind = (query, options) -> HUD.hide(true) # ignore the selectionchange event generated by find() document.removeEventListener("selectionchange",restoreDefaultSelectionHighlight, true) - Mode.runIn FindModeWithoutBadge, -> + Mode.runIn FocusMustNotTriggerInsertMode, -> result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false) setTimeout( -> document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true) -- cgit v1.2.3 From 734d16a457c0f18edcf3c951752f5e16ed2199e9 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 3 Jan 2015 07:29:35 +0000 Subject: Modes; fix #1410. --- content_scripts/mode_insert.coffee | 1 + content_scripts/vimium_frontend.coffee | 36 +++++++++++++++++++++------------- 2 files changed, 23 insertions(+), 14 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 64aaa445..ed5d0023 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -50,6 +50,7 @@ class InsertMode extends Mode super name: "insert" keydown: (event) => + return @continueBubbling if event.suppressInsertMode return @continueBubbling unless @isActive() return @stopBubblingAndTrue unless KeyboardUtils.isEscape event # We're now exiting insert mode. diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index a252c878..0f23af05 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -726,6 +726,8 @@ handleEnterForFindMode = -> focusFoundLink() document.body.classList.add("vimiumFindMode") settings.set("findModeRawQuery", findModeQuery.rawQuery) + # If we have found an input element, the pressing immediately afterwards sends us into insert mode. + new PostFindMode() class FindMode extends Mode constructor: (badge="F") -> @@ -759,6 +761,25 @@ 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 + keydown: (event) => + @exit() + if (KeyboardUtils.isEscape(event)) + DomUtils.simulateSelect(document.activeElement) + insertMode.activate() + 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 + performFindInPlace = -> cachedScrollX = window.scrollX cachedScrollY = window.scrollY @@ -855,20 +876,7 @@ findAndFocus = (backwards) -> # if we have found an input element via 'n', pressing immediately afterwards sends us into insert # mode - elementCanTakeInput = document.activeElement && - DomUtils.isSelectable(document.activeElement) && - isDOMDescendant(findModeAnchorNode, document.activeElement) - if (elementCanTakeInput) - handlerStack.push({ - keydown: (event) -> - @remove() - if (KeyboardUtils.isEscape(event)) - DomUtils.simulateSelect(document.activeElement) - enterInsertModeWithoutShowingIndicator(document.activeElement) - insertMode.activate() - return false # we have "consumed" this event, so do not propagate - return true - }) + new PostFindMode() focusFoundLink() -- cgit v1.2.3 From 00573389c63cebb42c225e10786aeb05e72fab39 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 3 Jan 2015 10:02:07 +0000 Subject: Modes; add SingletonMode. --- content_scripts/mode.coffee | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index f411d29b..8041f462 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -76,5 +76,21 @@ class 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. +class SingletonMode extends Mode + constructor: (@singleton, options) -> + @cancel @singleton + super options + + @instances: {} + + cancel: (instance) -> + SingletonMode[instance].exit() if SingletonMode[instance] + + exit: -> + delete SingletonMode[@instance] + super() + root = exports ? window root.Mode = Mode -- cgit v1.2.3 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 From 7889b3c2c68354d377c31121d6fb94f528e0454c Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 3 Jan 2015 15:10:06 +0000 Subject: Modes; simplify PostFindMode. --- content_scripts/mode.coffee | 5 ++ content_scripts/mode_insert.coffee | 2 +- content_scripts/vimium_frontend.coffee | 87 ++++++++++++++++++++-------------- 3 files changed, 58 insertions(+), 36 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 9e886a63..9126a824 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -1,4 +1,6 @@ +count = 0 + class Mode # Static members. @modes: [] @@ -20,6 +22,8 @@ class Mode constructor: (options) -> Mode.modes.unshift @ extend @, options + @count = ++count + console.log @count, "create:", @name @handlers = [] @handlers.push handlerStack.push @@ -29,6 +33,7 @@ class Mode updateBadge: (badge) => handlerStack.alwaysContinueBubbling => @chooseBadge badge exit: -> + console.log @count, "exit:", @name handlerStack.remove handlerId for handlerId in @handlers Mode.modes = Mode.modes.filter (mode) => mode != @ Mode.updateBadge() diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 6d7cdb89..cffb8735 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -86,7 +86,7 @@ class InsertMode extends Mode # Activate this mode to prevent a focused, editable element from triggering insert mode. class InsertModeSuppressFocusTrigger extends Mode constructor: -> - super() + super {name: "suppress-focus-trigger"} @handlers.push handlerStack.push focus: => @suppressEvent diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index da479781..e20dafc4 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -200,13 +200,9 @@ initializeWhenEnabled = (newPassKeys) -> if (!installedListeners) # Key event handlers fire on window before they do on document. Prefer window for key events so the page # can't set handlers to grab the keys before us. - for type in ["keydown", "keypress", "keyup"] + for type in ["keydown", "keypress", "keyup", "click", "focus", "blur"] do (type) -> installListener window, type, (event) -> handlerStack.bubbleEvent type, event - # installListener document, "focus", onFocusCapturePhase # No longer needed. - installListener document, "blur", onBlurCapturePhase installListener document, "DOMActivate", onDOMActivate - installListener document, "focusin", onFocus - installListener document, "blur", onBlur enterInsertModeIfElementIsFocused() installedListeners = true @@ -360,6 +356,13 @@ extend window, selectedInputIndex = Math.min(count - 1, visibleInputs.length - 1) + # We need to make sure that the following .focus() actually does generate a "focus" event. We need such + # an event: + # - to trigger insert mode, and + # - to kick any PostFindMode listeners out of the way. + # Unfortunately, if the element is already focused (as may happen following a find), then no "focus" event + # is generated. So, here, we first generate a psuedo "focus" event. + PostFindMode.fakeFocus visibleInputs[selectedInputIndex].element visibleInputs[selectedInputIndex].element.focus() return if visibleInputs.length == 1 @@ -761,44 +764,56 @@ class FindMode extends Mode Mode.updateBadge() -# 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 +# Handle various special cases which arise when find finds a match within a focusable element. class PostFindMode extends SingletonMode constructor: -> element = document.activeElement - handleKeydownEscape = true - super PostFindMode, - keydown: (event) => - 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 - 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" + + # Special cases only arise if the active element is focusable. So, exit immediately if it is not. canTakeInput = element and DomUtils.isSelectable(element) and isDOMDescendant findModeAnchorNode, element canTakeInput ||= element?.isContentEditable - return @exit() unless canTakeInput + return unless canTakeInput + + super PostFindMode, {name: "post-find-mode"} + if element.isContentEditable + # Prevent InsertMode from activating on keydown. + @handlers.push handlerStack.push + keydown: (event) => + InsertMode.suppressKeydownTrigger event + @continueBubbling + + # If the next key is Esc, then drop into insert mode. + @handlers.push handlerStack.push + keydown: (event) -> + @remove() + return true unless KeyboardUtils.isEscape event + DomUtils.simulateSelect document.activeElement + insertMode.activate element + return false + + # We can stop watching on any change of focus or user click. + # FIXME(smblott). This is broken. If there is a text area, and the text area is focused with + # find, then clicking within that text area does *not* generate a useful event, and therefore does not + # disable PostFindMode mode, and therefore does not allow us to enter insert mode. @handlers.push handlerStack.push - DOMActive: (event) => @exit() - focus: (event) => @exit() - blur: (event) => @exit() + DOMActive: (event) => handlerStack.alwaysContinueBubbling => + console.log "ACTIVATE" + @exit() + click: (event) => + handlerStack.alwaysContinueBubbling => + console.log "CLICK" + @exit() + focus: (event) => handlerStack.alwaysContinueBubbling => + console.log "FOCUS" + @exit() + blur: (event) => + console.log "BLUR" + handlerStack.alwaysContinueBubbling => @exit() - exit: -> - console.log ++count, "exit PostFindMode" - super() + # This removes any PostFindMode modes on the stack and triggers a "focus" event for InsertMode. + @fakeFocus: (element) -> + handlerStack.bubbleEvent "focus", {target: element} performFindInPlace = -> cachedScrollX = window.scrollX @@ -1015,7 +1030,9 @@ window.enterFindMode = -> findModeQuery = { rawQuery: "" } # window.findMode = true # Same hack, see comment at window.findMode definition. HUD.show("/") + console.log "aaa" new FindMode() + console.log "bbb" exitFindMode = -> window.findMode = false # Same hack, see comment at window.findMode definition. -- cgit v1.2.3 From 21e4636c7898709e2cbece8cd85cc2ae4d2ce790 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 3 Jan 2015 16:26:06 +0000 Subject: Modes; add template for visual mode. --- content_scripts/mode_visual.coffee | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 content_scripts/mode_visual.coffee (limited to 'content_scripts') diff --git a/content_scripts/mode_visual.coffee b/content_scripts/mode_visual.coffee new file mode 100644 index 00000000..8c5f8d51 --- /dev/null +++ b/content_scripts/mode_visual.coffee @@ -0,0 +1,21 @@ + +# Use new VisualMode() to enter visual mode. +# Use @exit() to leave visual mode. + +class VisualMode extends Mode + constructor: -> + super + name: "Visual" + badge: "V" + + keydown: (event) => + return Mode.suppressEvent + + keypress: (event) => + return Mode.suppressEvent + + keyup: (event) => + return Mode.suppressEvent + +root = exports ? window +root.VisualMode = VisualMode -- cgit v1.2.3 From 762b17344a1d12aa58c5df2f3eef452175dc0166 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 3 Jan 2015 16:30:32 +0000 Subject: Modes; visual-mode template. Visual mode command has been create: bound to `v`, of course. The template is in mode_visual.coffee. It shouldn't really be necessary to make changes outside of there. Let me know if you have any issues. --- content_scripts/mode_visual.coffee | 9 ++++++--- content_scripts/vimium_frontend.coffee | 3 +++ 2 files changed, 9 insertions(+), 3 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_visual.coffee b/content_scripts/mode_visual.coffee index 8c5f8d51..f88e20fe 100644 --- a/content_scripts/mode_visual.coffee +++ b/content_scripts/mode_visual.coffee @@ -1,7 +1,4 @@ -# Use new VisualMode() to enter visual mode. -# Use @exit() to leave visual mode. - class VisualMode extends Mode constructor: -> super @@ -9,6 +6,10 @@ class VisualMode extends Mode badge: "V" keydown: (event) => + if KeyboardUtils.isEscape event + @exit() + return Mode.suppressEvent + return Mode.suppressEvent keypress: (event) => @@ -17,5 +18,7 @@ class VisualMode extends Mode keyup: (event) => return Mode.suppressEvent + Mode.updateBadge() + root = exports ? window root.VisualMode = VisualMode diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index e20dafc4..e0ce03d5 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -340,6 +340,9 @@ extend window, enterInsertMode: -> insertMode?.activate() + enterVisualMode: => + new VisualMode() + 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. -- cgit v1.2.3 From c67586154625065804d14c0fb8897447391f45b4 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 3 Jan 2015 17:03:52 +0000 Subject: Modes; visual-mode, add comment. --- content_scripts/mode_visual.coffee | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_visual.coffee b/content_scripts/mode_visual.coffee index f88e20fe..67f485a0 100644 --- a/content_scripts/mode_visual.coffee +++ b/content_scripts/mode_visual.coffee @@ -1,6 +1,10 @@ class VisualMode extends Mode - constructor: -> + + # Proposal... The visual selection must stay within element. This will become relevant if we ever get so + # far as implementing a vim-like editing mode for text areas/content editable. + # + constructor: (element=document.body) -> super name: "Visual" badge: "V" -- cgit v1.2.3 From 6a817e575d55daec146203e7ef8929fdfd81bace Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 3 Jan 2015 17:16:43 +0000 Subject: Modes; fix SingletonMode.. --- content_scripts/mode.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 9126a824..a19c3df0 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -66,12 +66,12 @@ class SingletonMode extends Mode @instances: {} exit: -> - delete SingletonMode[@singleton] + delete SingletonMode.instances[@singleton] super() constructor: (@singleton, options={}) -> - SingletonMode[@singleton].exit() if SingletonMode[@singleton] - SingletonMode[@singleton] = @ + SingletonMode.instances[@singleton].exit() if SingletonMode.instances[@singleton] + SingletonMode.instances[@singleton] = @ super options # MultiMode is a collection of modes which are installed or uninstalled together. -- cgit v1.2.3 From 7537736a0898f736d689092b9bd350886f327ab0 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 3 Jan 2015 18:32:47 +0000 Subject: Modes; add ConstrainedMode. --- content_scripts/mode.coffee | 19 +++++++++++++++++++ content_scripts/mode_visual.coffee | 6 +++--- 2 files changed, 22 insertions(+), 3 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index a19c3df0..e4e2679d 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -83,7 +83,26 @@ class MultiMode extends Mode exit: -> mode.exit() for mode in modes +# When the user clicks anywhere outside of the given element, the mode is exited. +class ConstrainedMode extends Mode + constructor: (@element, options) -> + options.name = if options.name? then "constrained-#{options.name}" else "constrained" + super options + + @handlers.push handlerStack.push + "click": (event) => + @exit() unless @isDOMDescendant @element, event.srcElement + @continueBubbling + + isDOMDescendant: (parent, child) -> + node = child + while (node != null) + return true if (node == parent) + node = node.parentNode + false + root = exports ? window root.Mode = Mode root.SingletonMode = SingletonMode root.MultiMode = MultiMode +root.ConstrainedMode = ConstrainedMode diff --git a/content_scripts/mode_visual.coffee b/content_scripts/mode_visual.coffee index 67f485a0..07530e94 100644 --- a/content_scripts/mode_visual.coffee +++ b/content_scripts/mode_visual.coffee @@ -1,12 +1,12 @@ -class VisualMode extends Mode +class VisualMode extends ConstrainedMode # Proposal... The visual selection must stay within element. This will become relevant if we ever get so # far as implementing a vim-like editing mode for text areas/content editable. # constructor: (element=document.body) -> - super - name: "Visual" + super element, + name: "visual" badge: "V" keydown: (event) => -- cgit v1.2.3 From 2f5d2fd957117c52a4d395bc43cad61103b72b32 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 3 Jan 2015 18:55:59 +0000 Subject: Modes; PostFindMode documents its event. --- content_scripts/vimium_frontend.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'content_scripts') diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index e0ce03d5..d1ada884 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -816,7 +816,7 @@ class PostFindMode extends SingletonMode # This removes any PostFindMode modes on the stack and triggers a "focus" event for InsertMode. @fakeFocus: (element) -> - handlerStack.bubbleEvent "focus", {target: element} + handlerStack.bubbleEvent "focus", {target: element, note: "generated by PostFindMode.fakeFocus()"} performFindInPlace = -> cachedScrollX = window.scrollX -- cgit v1.2.3 From f165fb17bfacf5c7c1b51566a0a8583609d3acf0 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 3 Jan 2015 19:00:49 +0000 Subject: Modes; comment out some unused code. --- content_scripts/mode.coffee | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index e4e2679d..01427914 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -74,14 +74,14 @@ class SingletonMode extends Mode SingletonMode.instances[@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 +# # 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 # When the user clicks anywhere outside of the given element, the mode is exited. class ConstrainedMode extends Mode @@ -101,6 +101,18 @@ class ConstrainedMode extends Mode node = node.parentNode false +# # The mode exits when the user hits Esc. +# class ExitOnEscapeMode extends Mode +# constructor: (options) -> +# super options +# +# # This handler ends up above the mode's own handlers on the handler stack, so it takes priority. +# @handlers.push handlerStack.push +# "keydown": (event) => +# return @continueBubbling unless KeyboardUtils.isEscape event +# @exit() +# @suppressEvent + root = exports ? window root.Mode = Mode root.SingletonMode = SingletonMode -- cgit v1.2.3 From 615f8a79f91f1d868465a6dae903c6710103515f Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 4 Jan 2015 07:21:05 +0000 Subject: Modes; update badge on focus change. --- content_scripts/mode.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 01427914..64001eaa 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -60,6 +60,10 @@ class Mode func() mode.exit() +# We need to detect when the focused frame/tab changes, and update the badge. +handlerStack.push + "focus": -> handlerStack.alwaysContinueBubbling -> Mode.updateBadge() + # 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 @@ -116,5 +120,4 @@ class ConstrainedMode extends Mode root = exports ? window root.Mode = Mode root.SingletonMode = SingletonMode -root.MultiMode = MultiMode root.ConstrainedMode = ConstrainedMode -- cgit v1.2.3 From 9ae4b6c10d53153929d905f28bc7de57c0ba6dfe Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 4 Jan 2015 09:29:36 +0000 Subject: Modes; various improvements. - Add StateMode. - PasskeysMode is a StateMode. - BadgeUpdateMode is a StateMode. - Improve badge handling. - Add push method to Mode. - Document how modes work. - Cache badge on background page to reduce the number of updates. - Remove badge restriction on document.body?.tagName.toLowerCase() == "frameset". - Add ExitOnEscape mode, use it for ConstrainedMode and FindMode. - Move PostFindMode to its own file. --- content_scripts/mode.coffee | 155 +++++++++++++++++++++++++-------- content_scripts/mode_find.coffee | 59 +++++++++++++ content_scripts/mode_insert.coffee | 18 +++- content_scripts/mode_passkeys.coffee | 17 +--- content_scripts/mode_visual.coffee | 8 +- content_scripts/vimium_frontend.coffee | 106 +++++----------------- 6 files changed, 216 insertions(+), 147 deletions(-) create mode 100644 content_scripts/mode_find.coffee (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 64001eaa..10b7bb2a 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -1,4 +1,43 @@ - +# Modes. +# +# A mode implements a number of event handlers which are pushed onto the handler stack when the mode starts, +# and poped when the mode exits. The Mode base takes as single argument options which can defined: +# +# 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 set a badge and override the +# chooseBadge method instead. Or, if the mode *never* shows a bade, 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 +# 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: +# SingletonMode: ensures that at most one instance of the mode should be active at any time. +# ConstrainedMode: exits the mode if the user clicks outside of the given element. +# ExitOnEscapeMode: exits the mode if the user types Esc. +# StateMode: tracks the current Vimium state in @enabled and @passKeys. +# +# To install and existing mode, use: +# myMode = new MyMode() +# +# To remove a mode, use: +# myMode.exit() # externally triggered. +# @exit() # internally triggered (more common). +# + +# Debug only; to be stripped out. count = 0 class Mode @@ -15,23 +54,26 @@ class Mode # Default values. name: "" badge: "" - keydown: (event) => @continueBubbling - keypress: (event) => @continueBubbling - keyup: (event) => @continueBubbling + keydown: null + keypress: null + keyup: null - constructor: (options) -> + constructor: (options={}) -> Mode.modes.unshift @ extend @, options @count = ++count console.log @count, "create:", @name @handlers = [] - @handlers.push handlerStack.push + @push keydown: @keydown keypress: @keypress keyup: @keyup updateBadge: (badge) => handlerStack.alwaysContinueBubbling => @chooseBadge badge + push: (handlers) -> + @handlers.push handlerStack.push handlers + exit: -> console.log @count, "exit:", @name handlerStack.remove handlerId for handlerId in @handlers @@ -45,14 +87,13 @@ class Mode badge.badge ||= @badge # 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. + # the resulting badge to the background page. We only update the badge if this document has the focus. @updateBadge: -> if document.hasFocus() - unless document.body?.tagName.toLowerCase() == "frameset" - badge = {badge: ""} - handlerStack.bubbleEvent "updateBadge", badge - chrome.runtime.sendMessage({ handler: "setBadge", badge: badge.badge }) + handlerStack.bubbleEvent "updateBadge", badge = {badge: ""} + chrome.runtime.sendMessage + handler: "setBadge" + badge: badge.badge # Temporarily install a mode. @runIn: (mode, func) -> @@ -60,10 +101,6 @@ class Mode func() mode.exit() -# We need to detect when the focused frame/tab changes, and update the badge. -handlerStack.push - "focus": -> handlerStack.alwaysContinueBubbling -> Mode.updateBadge() - # 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 @@ -74,26 +111,35 @@ class SingletonMode extends Mode super() constructor: (@singleton, options={}) -> - SingletonMode.instances[@singleton].exit() if SingletonMode.instances[@singleton] + SingletonMode.kill @singleton SingletonMode.instances[@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 + # Static method. If there's a singleton instance running, then kill it. + @kill: (singleton) -> + SingletonMode.instances[singleton].exit() if SingletonMode.instances[singleton] + +# The mode exits when the user hits Esc. +class ExitOnEscapeMode extends Mode + constructor: (options) -> + super options + + # 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 + @suppressEvent # When the user clicks anywhere outside of the given element, the mode is exited. -class ConstrainedMode extends Mode +class ConstrainedMode extends ExitOnEscapeMode constructor: (@element, options) -> options.name = if options.name? then "constrained-#{options.name}" else "constrained" super options - @handlers.push handlerStack.push + @push "click": (event) => @exit() unless @isDOMDescendant @element, event.srcElement @continueBubbling @@ -105,19 +151,52 @@ class ConstrainedMode extends Mode node = node.parentNode false -# # The mode exits when the user hits Esc. -# class ExitOnEscapeMode extends Mode -# constructor: (options) -> -# super options -# -# # This handler ends up above the mode's own handlers on the handler stack, so it takes priority. -# @handlers.push handlerStack.push -# "keydown": (event) => -# return @continueBubbling unless KeyboardUtils.isEscape event -# @exit() -# @suppressEvent +# The state mode tracks the enabled state in @enabled and @passKeys, and its initialized state in +# @initialized. It calls @registerStateChange() whenever the state changes. +class StateMode extends Mode + constructor: (options) -> + @stateInitialized = false + @enabled = false + @passKeys = "" + super options + + @push + "registerStateChange": ({enabled: enabled, passKeys: passKeys}) => + handlerStack.alwaysContinueBubbling => + if enabled != @enabled or passKeys != @passKeys or not @stateInitialized + @stateInitialized = true + @enabled = enabled + @passKeys = passKeys + @registerStateChange() + + # Overridden by sub-classes. + registerStateChange: -> + +# BadgeMode is a psuedo mode for managing 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. +class BadgeMode extends StateMode + constructor: (options) -> + options.name ||= "badge" + super options + + @push + "focus": => + handlerStack.alwaysContinueBubbling => + Mode.updateBadge() + + chooseBadge: (badge) -> + # If we're not enabled, then post an empty badge (so, no badge at all). + badge.badge = "" unless @enabled + + registerStateChange: -> + Mode.updateBadge() + +# Install a single BadgeMode instance. +new BadgeMode {} root = exports ? window root.Mode = Mode root.SingletonMode = SingletonMode root.ConstrainedMode = ConstrainedMode +root.StateMode = StateMode +root.ExitOnEscapeMode = ExitOnEscapeMode diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee new file mode 100644 index 00000000..d6380682 --- /dev/null +++ b/content_scripts/mode_find.coffee @@ -0,0 +1,59 @@ +# NOTE(smblott). Ultimately, all of the FindMode-related code should be moved to this file. + +# When we use find mode, the selection/focus can end up in a focusable/editable element. Subsequent keyboard +# events could drop us into insert mode, which is a bad user experience. The PostFindMode mode is installed +# after find events to prevent this. +# +# PostFindMode also maps Esc (on the next keystroke) to immediately drop into insert mode. +class PostFindMode extends SingletonMode + constructor: (insertMode, findModeAnchorNode) -> + element = document.activeElement + return unless element + + # Special cases only arise if the active element is focusable. So, exit immediately if it is not. + canTakeInput = DomUtils.isSelectable(element) and DomUtils.isDOMDescendant findModeAnchorNode, element + canTakeInput ||= element?.isContentEditable + return unless canTakeInput + + super PostFindMode, + name: "post-find" + + # If the very next key is Esc, then drop straight into insert mode. + do => + self = @ + @push + keydown: (event) -> + @remove() + if element == document.activeElement and KeyboardUtils.isEscape event + self.exit() + # NOTE(smblott). The legacy code (2015/1/4) uses DomUtils.simulateSelect() here. But this moves + # the selection. It's better to leave the selection where it is. + insertMode.activate element + return false + true + + if element.isContentEditable + # Prevent InsertMode from activating on keydown. + @push + keydown: (event) -> handlerStack.alwaysContinueBubbling -> InsertMode.suppressKeydownTrigger event + + # Install various ways in which we can leave this mode. + @push + DOMActive: (event) => handlerStack.alwaysContinueBubbling => @exit() + click: (event) => handlerStack.alwaysContinueBubbling => @exit() + focus: (event) => handlerStack.alwaysContinueBubbling => @exit() + blur: (event) => handlerStack.alwaysContinueBubbling => @exit() + keydown: (event) => handlerStack.alwaysContinueBubbling => @exit() if document.activeElement != element + + # There's feature interference between PostFindMode, InsertMode and focusInput. PostFindMode prevents + # InsertMode from triggering on keyboard events. And FindMode prevents InsertMode from triggering on focus + # events. This means that an input element can already be focused, but InsertMode is not active. When that + # element is then (again) focused by focusInput, no new focus event is generated, so we don't drop into + # InsertMode as expected. + # This hack fixes this. + @exitModeAndEnterInsert: (element) -> + SingletonMode.kill PostFindMode + insertMode.activate element + +root = exports ? window +root.PostFindMode = PostFindMode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index cffb8735..5a0ac9eb 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -46,6 +46,18 @@ class InsertMode extends Mode @badge = "" Mode.updateBadge() + exit: (event) -> + if event?.source == ExitOnEscapeMode + element = event?.event?.srcElement + if element? and @isFocusable element + # 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. + element.blur() + @deactivate() + constructor: -> super name: "insert" @@ -65,7 +77,7 @@ class InsertMode extends Mode keypress: => if @insertModeActive then @stopBubblingAndTrue else @continueBubbling keyup: => if @insertModeActive then @stopBubblingAndTrue else @continueBubbling - @handlers.push handlerStack.push + @push focus: (event) => handlerStack.alwaysContinueBubbling => if not @insertModeActive and @isFocusable event.target @@ -86,8 +98,8 @@ class InsertMode extends Mode # Activate this mode to prevent a focused, editable element from triggering insert mode. class InsertModeSuppressFocusTrigger extends Mode constructor: -> - super {name: "suppress-focus-trigger"} - @handlers.push handlerStack.push + super {name: "suppress-insert-mode-focus-trigger"} + @push focus: => @suppressEvent root = exports ? window diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee index 4c4d7d41..c8afed39 100644 --- a/content_scripts/mode_passkeys.coffee +++ b/content_scripts/mode_passkeys.coffee @@ -1,20 +1,7 @@ -class PassKeysMode extends Mode - keyQueue: "" - passKeys: "" - - # 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. +class PassKeysMode extends StateMode configure: (request) -> - if request.isEnabledForUrl? - @passKeys = (request.isEnabledForUrl and request.passKeys) or "" - Mode.updateBadge() - if request.enabled? - @passKeys = (request.enabled and request.passKeys) or "" - Mode.updateBadge() - if request.keyQueue? - @keyQueue = request.keyQueue + @keyQueue = request.keyQueue if 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 diff --git a/content_scripts/mode_visual.coffee b/content_scripts/mode_visual.coffee index 07530e94..b07d784e 100644 --- a/content_scripts/mode_visual.coffee +++ b/content_scripts/mode_visual.coffee @@ -1,19 +1,13 @@ +# Note. ConstrainedMode extends extends ExitOnEscapeMode. So exit-on-escape is handled there. class VisualMode extends ConstrainedMode - # Proposal... The visual selection must stay within element. This will become relevant if we ever get so - # far as implementing a vim-like editing mode for text areas/content editable. - # constructor: (element=document.body) -> super element, name: "visual" badge: "V" keydown: (event) => - if KeyboardUtils.isEscape event - @exit() - return Mode.suppressEvent - return Mode.suppressEvent keypress: (event) => diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index d1ada884..9d539956 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -133,8 +133,6 @@ initializePreDomReady = -> Scroller.init settings # 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() @@ -163,9 +161,7 @@ initializePreDomReady = -> getScrollPosition: -> scrollX: window.scrollX, scrollY: window.scrollY setScrollPosition: (request) -> setScrollPosition request.scrollX, request.scrollY executePageCommand: executePageCommand - getActiveState: -> - Mode.updateBadge() - return { enabled: isEnabledForUrl, passKeys: passKeys } + getActiveState: getActiveState setState: setState currentKeyQueue: (request) -> keyQueue = request.keyQueue @@ -210,7 +206,13 @@ setState = (request) -> initializeWhenEnabled(request.passKeys) if request.enabled isEnabledForUrl = request.enabled passKeys = request.passKeys - passKeysMode.configure request + handlerStack.bubbleEvent "registerStateChange", + enabled: request.enabled + passKeys: request.passKeys + +getActiveState = -> + Mode.updateBadge() + return { enabled: isEnabledForUrl, passKeys: passKeys } # # The backend needs to know which frame has focus. @@ -281,7 +283,6 @@ window.focusThisFrame = (shouldHighlight) -> chrome.runtime.sendMessage({ handler: "nextFrame", frameId: frameId }) return window.focus() - Mode.updateBadge() if (document.body && shouldHighlight) borderWas = document.body.style.border document.body.style.border = '5px solid yellow' @@ -359,13 +360,9 @@ extend window, selectedInputIndex = Math.min(count - 1, visibleInputs.length - 1) - # We need to make sure that the following .focus() actually does generate a "focus" event. We need such - # an event: - # - to trigger insert mode, and - # - to kick any PostFindMode listeners out of the way. - # Unfortunately, if the element is already focused (as may happen following a find), then no "focus" event - # is generated. So, here, we first generate a psuedo "focus" event. - PostFindMode.fakeFocus visibleInputs[selectedInputIndex].element + # See the definition of PostFindMode.exitModeAndEnterInsert for an explanation of why this is needed. + PostFindMode.exitModeAndEnterInsert visibleInputs[selectedInputIndex].element + visibleInputs[selectedInputIndex].element.focus() return if visibleInputs.length == 1 @@ -578,8 +575,9 @@ checkIfEnabledForUrl = -> else if (HUD.isReady()) # Quickly hide any HUD we might already be showing, e.g. if we entered insert mode on page load. HUD.hide() - passKeysMode.configure response - Mode.updateBadge() + handlerStack.bubbleEvent "registerStateChange", + enabled: response.isEnabledForUrl + passKeys: response.passKeys refreshCompletionKeys = (response) -> if (response) @@ -733,20 +731,16 @@ handleEnterForFindMode = -> document.body.classList.add("vimiumFindMode") settings.set("findModeRawQuery", findModeQuery.rawQuery) # If we have found an input element, the pressing immediately afterwards sends us into insert mode. - new PostFindMode() + new PostFindMode insertMode, findModeAnchorNode -class FindMode extends Mode +class FindMode extends ExitOnEscapeMode constructor: (badge="F") -> super name: "find" badge: badge keydown: (event) => - if KeyboardUtils.isEscape event - handleEscapeForFindMode() - @exit() - @suppressEvent - else if event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey + if event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey handleDeleteForFindMode() @suppressEvent else if event.keyCode == keyCodes.enter @@ -767,56 +761,9 @@ class FindMode extends Mode Mode.updateBadge() -# Handle various special cases which arise when find finds a match within a focusable element. -class PostFindMode extends SingletonMode - constructor: -> - element = document.activeElement - - # Special cases only arise if the active element is focusable. So, exit immediately if it is not. - canTakeInput = element and DomUtils.isSelectable(element) and isDOMDescendant findModeAnchorNode, element - canTakeInput ||= element?.isContentEditable - return unless canTakeInput - - super PostFindMode, {name: "post-find-mode"} - - if element.isContentEditable - # Prevent InsertMode from activating on keydown. - @handlers.push handlerStack.push - keydown: (event) => - InsertMode.suppressKeydownTrigger event - @continueBubbling - - # If the next key is Esc, then drop into insert mode. - @handlers.push handlerStack.push - keydown: (event) -> - @remove() - return true unless KeyboardUtils.isEscape event - DomUtils.simulateSelect document.activeElement - insertMode.activate element - return false - - # We can stop watching on any change of focus or user click. - # FIXME(smblott). This is broken. If there is a text area, and the text area is focused with - # find, then clicking within that text area does *not* generate a useful event, and therefore does not - # disable PostFindMode mode, and therefore does not allow us to enter insert mode. - @handlers.push handlerStack.push - DOMActive: (event) => handlerStack.alwaysContinueBubbling => - console.log "ACTIVATE" - @exit() - click: (event) => - handlerStack.alwaysContinueBubbling => - console.log "CLICK" - @exit() - focus: (event) => handlerStack.alwaysContinueBubbling => - console.log "FOCUS" - @exit() - blur: (event) => - console.log "BLUR" - handlerStack.alwaysContinueBubbling => @exit() - - # This removes any PostFindMode modes on the stack and triggers a "focus" event for InsertMode. - @fakeFocus: (element) -> - handlerStack.bubbleEvent "focus", {target: element, note: "generated by PostFindMode.fakeFocus()"} + exit: (event) -> + handleEscapeForFindMode() if event?.source == ExitOnEscapeMode + super() performFindInPlace = -> cachedScrollX = window.scrollX @@ -863,13 +810,6 @@ focusFoundLink = -> link = getLinkFromSelection() link.focus() if link -isDOMDescendant = (parent, child) -> - node = child - while (node != null) - return true if (node == parent) - node = node.parentNode - false - selectFoundInputElement = -> # if the found text is in an input element, getSelection().anchorNode will be null, so we use activeElement # instead. however, since the last focused element might not be the one currently pointed to by find (e.g. @@ -877,7 +817,7 @@ selectFoundInputElement = -> # heuristic of checking that the last anchor node is an ancestor of our element. if (findModeQueryHasResults && document.activeElement && DomUtils.isSelectable(document.activeElement) && - isDOMDescendant(findModeAnchorNode, document.activeElement)) + DomUtils.isDOMDescendant(findModeAnchorNode, document.activeElement)) DomUtils.simulateSelect(document.activeElement) # the element has already received focus via find(), so invoke insert mode manually enterInsertModeWithoutShowingIndicator(document.activeElement) @@ -914,7 +854,7 @@ findAndFocus = (backwards) -> # if we have found an input element via 'n', pressing immediately afterwards sends us into insert # mode - new PostFindMode() + new PostFindMode insertMode, findModeAnchorNode focusFoundLink() @@ -1033,9 +973,7 @@ window.enterFindMode = -> findModeQuery = { rawQuery: "" } # window.findMode = true # Same hack, see comment at window.findMode definition. HUD.show("/") - console.log "aaa" new FindMode() - console.log "bbb" exitFindMode = -> window.findMode = false # Same hack, see comment at window.findMode definition. -- cgit v1.2.3 From a5adf7c06128cc963a09acc9960bab1117b55d1a Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 4 Jan 2015 13:11:46 +0000 Subject: Modes; fix move of find to its own file. --- content_scripts/mode_find.coffee | 23 +++++++++-------------- content_scripts/vimium_frontend.coffee | 2 +- 2 files changed, 10 insertions(+), 15 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index d6380682..d6d1ff33 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -19,18 +19,13 @@ class PostFindMode extends SingletonMode name: "post-find" # If the very next key is Esc, then drop straight into insert mode. - do => - self = @ - @push - keydown: (event) -> - @remove() - if element == document.activeElement and KeyboardUtils.isEscape event - self.exit() - # NOTE(smblott). The legacy code (2015/1/4) uses DomUtils.simulateSelect() here. But this moves - # the selection. It's better to leave the selection where it is. - insertMode.activate element - return false - true + @push + keydown: (event) -> + @remove() + if element == document.activeElement and KeyboardUtils.isEscape event + PostFindMode.exitModeAndEnterInsert insertMode, element + return false + true if element.isContentEditable # Prevent InsertMode from activating on keydown. @@ -51,9 +46,9 @@ class PostFindMode extends SingletonMode # element is then (again) focused by focusInput, no new focus event is generated, so we don't drop into # InsertMode as expected. # This hack fixes this. - @exitModeAndEnterInsert: (element) -> + @exitModeAndEnterInsert: (insertMode, element) -> SingletonMode.kill PostFindMode - insertMode.activate element + insertMode.activate insertMode, element root = exports ? window root.PostFindMode = PostFindMode diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 9d539956..75b4172f 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -361,7 +361,7 @@ extend window, selectedInputIndex = Math.min(count - 1, visibleInputs.length - 1) # See the definition of PostFindMode.exitModeAndEnterInsert for an explanation of why this is needed. - PostFindMode.exitModeAndEnterInsert visibleInputs[selectedInputIndex].element + PostFindMode.exitModeAndEnterInsert insertMode, visibleInputs[selectedInputIndex].element visibleInputs[selectedInputIndex].element.focus() -- cgit v1.2.3 From 7e4fdac07ffb59c438a17c2c88051064aaab16b5 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 4 Jan 2015 14:54:04 +0000 Subject: Revise handler stack implementation. The old implementation: - Wasn't actually checking whether handlers had been removed before calling them. - Could end up calling the same handler twice (if a handler was removed further down the stack, and the stack elements moved due the resulting splice. Solution: - Mark elements as removed and check. Set their ids to null. - Don't splice stack. Also, optimisation: - Removing the element at the top of the stack is still O(1). - In Modes, reverse handlers before removing (so, more likely to hit the optimisation above). For the record, the stable stack length at the moment seems to be about 10-12 elements. --- content_scripts/mode.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 10b7bb2a..76b65a12 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -76,7 +76,9 @@ class Mode exit: -> console.log @count, "exit:", @name - handlerStack.remove handlerId for handlerId in @handlers + # We reverse @handlers, here. That way, handlers are popped in the opposite order to that in which they + # were pushed. + handlerStack.remove handlerId for handlerId in @handlers.reverse() Mode.modes = Mode.modes.filter (mode) => mode != @ Mode.updateBadge() -- cgit v1.2.3 From 73f66f25e6b8e5b5b8456074ad4fa79ba1d3ca4d Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 4 Jan 2015 16:19:14 +0000 Subject: Modes; revise InsertMode as two classes. --- content_scripts/mode.coffee | 47 +++++---- content_scripts/mode_find.coffee | 48 ++++----- content_scripts/mode_insert.coffee | 171 ++++++++++++++++----------------- content_scripts/vimium_frontend.coffee | 35 ++++--- 4 files changed, 143 insertions(+), 158 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 76b65a12..96fc9b0c 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -61,6 +61,7 @@ class Mode constructor: (options={}) -> Mode.modes.unshift @ extend @, options + @modeIsActive = true @count = ++count console.log @count, "create:", @name @@ -75,12 +76,14 @@ class Mode @handlers.push handlerStack.push handlers exit: -> - console.log @count, "exit:", @name - # We reverse @handlers, here. That way, handlers are popped in the opposite order to that in which they - # were pushed. - handlerStack.remove handlerId for handlerId in @handlers.reverse() - Mode.modes = Mode.modes.filter (mode) => mode != @ - Mode.updateBadge() + if @modeIsActive + console.log @count, "exit:", @name + # We reverse @handlers, here. That way, handlers are popped in the opposite order to that in which they + # were pushed. + handlerStack.remove handlerId for handlerId in @handlers.reverse() + Mode.modes = Mode.modes.filter (mode) => mode != @ + Mode.updateBadge() + @modeIsActive = false # 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 @@ -122,9 +125,9 @@ class SingletonMode extends Mode SingletonMode.instances[singleton].exit() if SingletonMode.instances[singleton] # The mode exits when the user hits Esc. -class ExitOnEscapeMode extends Mode - constructor: (options) -> - super options +class ExitOnEscapeMode extends SingletonMode + constructor: (singleton, options) -> + super singleton, options # This handler ends up above the mode's own key handlers on the handler stack, so it takes priority. @push @@ -135,23 +138,17 @@ class ExitOnEscapeMode extends Mode event: event @suppressEvent -# When the user clicks anywhere outside of the given element, the mode is exited. +# When @element loses the focus. class ConstrainedMode extends ExitOnEscapeMode - constructor: (@element, options) -> - options.name = if options.name? then "constrained-#{options.name}" else "constrained" - super options - - @push - "click": (event) => - @exit() unless @isDOMDescendant @element, event.srcElement - @continueBubbling - - isDOMDescendant: (parent, child) -> - node = child - while (node != null) - return true if (node == parent) - node = node.parentNode - false + constructor: (@element, singleton, options) -> + super singleton, options + + if @element + @element.focus() + @push + "blur": (event) => + handlerStack.alwaysContinueBubbling => + @exit() if event.srcElement == @element # The state mode tracks the enabled state in @enabled and @passKeys, and its initialized state in # @initialized. It calls @registerStateChange() whenever the state changes. diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index d6d1ff33..795e7a14 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -1,37 +1,35 @@ # NOTE(smblott). Ultimately, all of the FindMode-related code should be moved to this file. -# When we use find mode, the selection/focus can end up in a focusable/editable element. Subsequent keyboard -# events could drop us into insert mode, which is a bad user experience. The PostFindMode mode is installed -# after find events to prevent this. +# 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. Suppress InsertModeTrigger. This presents keyboard events from dropping us unintentionaly into insert +# mode. Here, this is achieved by inheriting PostFindMode from InsertModeBlocker. +# 2. If the very-next keystroke is Escape, then drop immediately into insert mode. # -# PostFindMode also maps Esc (on the next keystroke) to immediately drop into insert mode. -class PostFindMode extends SingletonMode - constructor: (insertMode, findModeAnchorNode) -> +class PostFindMode extends InsertModeBlocker + constructor: (findModeAnchorNode) -> element = document.activeElement - return unless element + + super PostFindMode, element, + name: "post-find" + + return @exit() unless element and findModeAnchorNode # Special cases only arise if the active element is focusable. So, exit immediately if it is not. canTakeInput = DomUtils.isSelectable(element) and DomUtils.isDOMDescendant findModeAnchorNode, element - canTakeInput ||= element?.isContentEditable - return unless canTakeInput - - super PostFindMode, - name: "post-find" + canTakeInput ||= element.isContentEditable + return @exit() unless canTakeInput - # If the very next key is Esc, then drop straight into insert mode. + self = @ @push keydown: (event) -> - @remove() if element == document.activeElement and KeyboardUtils.isEscape event - PostFindMode.exitModeAndEnterInsert insertMode, element + self.exit() + new InsertMode element return false + @remove() true - if element.isContentEditable - # Prevent InsertMode from activating on keydown. - @push - keydown: (event) -> handlerStack.alwaysContinueBubbling -> InsertMode.suppressKeydownTrigger event - # Install various ways in which we can leave this mode. @push DOMActive: (event) => handlerStack.alwaysContinueBubbling => @exit() @@ -40,15 +38,5 @@ class PostFindMode extends SingletonMode blur: (event) => handlerStack.alwaysContinueBubbling => @exit() keydown: (event) => handlerStack.alwaysContinueBubbling => @exit() if document.activeElement != element - # There's feature interference between PostFindMode, InsertMode and focusInput. PostFindMode prevents - # InsertMode from triggering on keyboard events. And FindMode prevents InsertMode from triggering on focus - # events. This means that an input element can already be focused, but InsertMode is not active. When that - # element is then (again) focused by focusInput, no new focus event is generated, so we don't drop into - # InsertMode as expected. - # This hack fixes this. - @exitModeAndEnterInsert: (insertMode, element) -> - SingletonMode.kill PostFindMode - insertMode.activate insertMode, element - root = exports ? window root.PostFindMode = PostFindMode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 5a0ac9eb..32994aef 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -1,107 +1,102 @@ -class InsertMode extends Mode - insertModeActive: false - insertModeLock: null - - # Input or text elements are considered focusable and able to receieve their own keyboard events, and will - # enter insert mode if focused. Also note that the "contentEditable" attribute can be set on any element - # which makes it a rich text editor, like the notes on jjot.com. - isEditable: (element) -> - return true if element.isContentEditable - nodeName = element.nodeName?.toLowerCase() - # Use a blacklist instead of a whitelist because new form controls are still being implemented for html5. - if nodeName == "input" and element.type not in ["radio", "checkbox"] - return true - nodeName in ["textarea", "select"] - - # 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 - - # Check whether insert mode is active. Also, activate insert mode if the current element is content - # 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 - @insertModeActive - - activate: (target=null) -> - unless @insertModeActive - @insertModeActive = true - @insertModeLock = target - @badge = "I" - Mode.updateBadge() - - deactivate: -> - if @insertModeActive - @insertModeActive = false - @insertModeLock = null - @badge = "" - Mode.updateBadge() - - exit: (event) -> - if event?.source == ExitOnEscapeMode - element = event?.event?.srcElement - if element? and @isFocusable element +# Input or text elements are considered focusable and able to receieve their own keyboard events, and will +# enter insert mode if focused. Also note that the "contentEditable" attribute can be set on any element +# which makes it a rich text editor, like the notes on jjot.com. +isEditable =(element) -> + return true if element.isContentEditable + nodeName = element.nodeName?.toLowerCase() + # Use a blacklist instead of a whitelist because new form controls are still being implemented for html5. + if nodeName == "input" and element.type not in ["radio", "checkbox"] + return true + nodeName in ["textarea", "select"] + +# 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 + +class InsertMode extends ConstrainedMode + + constructor: (@insertModeLock=null) -> + super @insertModeLock, InsertMode, + name: "insert" + badge: "I" + keydown: (event) => @stopBubblingAndTrue + keypress: (event) => @stopBubblingAndTrue + keyup: (event) => @stopBubblingAndTrue + + @push + focus: (event, extra) => + handlerStack.alwaysContinueBubbling => + # Inform InsertModeTrigger that InsertMode is already active. + extra.insertModeActive = true + + Mode.updateBadge() + + exit: (event=null) -> + if event?.source == ExitOnEscapeMode and event?.event?.srcElement? + element = event.event.srcElement + if isFocusable element # 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. element.blur() - @deactivate() + super() +# Trigger insert mode: +# - On keydown event in a contentEditable element. +# - When a focusable element receives the focus. +# Can be suppressed by setting extra.suppressInsertModeTrigger. +class InsertModeTrigger extends Mode constructor: -> super - name: "insert" - keydown: (event) => - return @continueBubbling unless @isActiveOrActivate event - return @stopBubblingAndTrue unless KeyboardUtils.isEscape event - # 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 @insertModeActive then @stopBubblingAndTrue else @continueBubbling - keyup: => if @insertModeActive then @stopBubblingAndTrue else @continueBubbling + name: "insert-trigger" + keydown: (event, extra) => + handlerStack.alwaysContinueBubbling => + unless extra.suppressInsertModeTrigger? + # 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 check + # whether the active element is contentEditable. + new InsertMode() if document.activeElement?.isContentEditable @push - focus: (event) => - handlerStack.alwaysContinueBubbling => - if not @insertModeActive and @isFocusable event.target - @activate event.target - blur: (event) => + focus: (event, extra) => handlerStack.alwaysContinueBubbling => - if @insertModeActive and event.target == @insertModeLock - @deactivate() + unless extra.suppressInsertModeTrigger? + new InsertMode event.target if isFocusable event.target - # We may already have focussed something, so check, so check. - @activate document.activeElement if document.activeElement and @isFocusable document.activeElement + # We may already have focussed something, so check. + new InsertMode 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 + @suppress: (extra) -> + extra.suppressInsertModeTrigger = true -# Activate this mode to prevent a focused, editable element from triggering insert mode. -class InsertModeSuppressFocusTrigger extends Mode - constructor: -> - super {name: "suppress-insert-mode-focus-trigger"} - @push - focus: => @suppressEvent +# Disables InsertModeTrigger. Used by find mode to prevent unintentionally dropping into insert mode on +# focusable elements. +# If @element is provided, then don't block focus events, and block keydown events only on the indicated +# element. +class InsertModeBlocker extends SingletonMode + constructor: (singleton=InsertModeBlocker, @element=null, options={}) -> + options.name ||= "insert-blocker" + super singleton, options + + unless @element? + @push + focus: (event, extra) => + handlerStack.alwaysContinueBubbling => + InsertModeTrigger.suppress extra + + if @element?.isContentEditable + @push + keydown: (event, extra) => + handlerStack.alwaysContinueBubbling => + InsertModeTrigger.suppress extra if event.srcElement == @element root = exports ? window root.InsertMode = InsertMode -root.InsertModeSuppressFocusTrigger = InsertModeSuppressFocusTrigger +root.InsertModeTrigger = InsertModeTrigger +root.InsertModeBlocker = InsertModeBlocker diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 75b4172f..299cdcf2 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -134,7 +134,7 @@ initializePreDomReady = -> # Install passKeys and insert modes. These too are permanently on the stack (although not always active). passKeysMode = new PassKeysMode() - insertMode = new InsertMode() + new InsertModeTrigger() Mode.updateBadge() checkIfEnabledForUrl() @@ -339,12 +339,14 @@ extend window, HUD.showForDuration("Yanked URL", 1000) enterInsertMode: -> - insertMode?.activate() + new InsertMode() enterVisualMode: => new VisualMode() focusInput: (count) -> + SingletonMode.kill PostFindMode + # 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. @@ -360,10 +362,14 @@ extend window, selectedInputIndex = Math.min(count - 1, visibleInputs.length - 1) - # See the definition of PostFindMode.exitModeAndEnterInsert for an explanation of why this is needed. - PostFindMode.exitModeAndEnterInsert insertMode, visibleInputs[selectedInputIndex].element - - visibleInputs[selectedInputIndex].element.focus() + # There's feature interference between PostFindMode, InsertMode and focusInput. PostFindMode prevents + # InsertMode from triggering on focus events. Therefore, an input element can already be focused, but + # InsertMode is not active. When that element is then (again) focused by focusInput, below, no new focus + # event is generated, so we don't drop into InsertMode as expected. + # Therefore we blur() the element before focussing it. + element = visibleInputs[selectedInputIndex].element + element.blur() if document.activeElement == element + element.focus() return if visibleInputs.length == 1 @@ -730,14 +736,12 @@ handleEnterForFindMode = -> focusFoundLink() document.body.classList.add("vimiumFindMode") settings.set("findModeRawQuery", findModeQuery.rawQuery) - # If we have found an input element, the pressing immediately afterwards sends us into insert mode. - new PostFindMode insertMode, findModeAnchorNode class FindMode extends ExitOnEscapeMode - constructor: (badge="F") -> - super + constructor: -> + super FindMode, name: "find" - badge: badge + badge: "/" keydown: (event) => if event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey @@ -761,9 +765,10 @@ class FindMode extends ExitOnEscapeMode Mode.updateBadge() - exit: (event) -> - handleEscapeForFindMode() if event?.source == ExitOnEscapeMode + exit: (extra) -> + handleEscapeForFindMode() if extra?.source == ExitOnEscapeMode super() + new PostFindMode findModeAnchorNode performFindInPlace = -> cachedScrollX = window.scrollX @@ -792,7 +797,7 @@ executeFind = (query, options) -> HUD.hide(true) # ignore the selectionchange event generated by find() document.removeEventListener("selectionchange",restoreDefaultSelectionHighlight, true) - Mode.runIn InsertModeSuppressFocusTrigger, -> + Mode.runIn InsertModeBlocker, -> result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false) setTimeout( -> document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true) @@ -854,7 +859,7 @@ findAndFocus = (backwards) -> # if we have found an input element via 'n', pressing immediately afterwards sends us into insert # mode - new PostFindMode insertMode, findModeAnchorNode + new PostFindMode findModeAnchorNode focusFoundLink() -- cgit v1.2.3 From 308e4007814443955fa77a793da3cd2ec274686c Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 5 Jan 2015 13:20:19 +0000 Subject: Modes; fix findFocus. --- content_scripts/mode_find.coffee | 3 +- content_scripts/mode_insert.coffee | 6 --- content_scripts/vimium_frontend.coffee | 70 +++++++++++++++++++++++----------- 3 files changed, 48 insertions(+), 31 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 795e7a14..8b458770 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -10,8 +10,7 @@ class PostFindMode extends InsertModeBlocker constructor: (findModeAnchorNode) -> element = document.activeElement - super PostFindMode, element, - name: "post-find" + super PostFindMode, element, {name: "post-find"} return @exit() unless element and findModeAnchorNode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 32994aef..bead43f8 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -27,12 +27,6 @@ class InsertMode extends ConstrainedMode keypress: (event) => @stopBubblingAndTrue keyup: (event) => @stopBubblingAndTrue - @push - focus: (event, extra) => - handlerStack.alwaysContinueBubbling => - # Inform InsertModeTrigger that InsertMode is already active. - extra.insertModeActive = true - Mode.updateBadge() exit: (event=null) -> diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 299cdcf2..cd717d5e 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -5,7 +5,6 @@ # "domReady". # -insertMode = null passKeysMode = null insertModeLock = null findMode = false @@ -362,15 +361,13 @@ extend window, selectedInputIndex = Math.min(count - 1, visibleInputs.length - 1) - # There's feature interference between PostFindMode, InsertMode and focusInput. PostFindMode prevents - # InsertMode from triggering on focus events. Therefore, an input element can already be focused, but - # InsertMode is not active. When that element is then (again) focused by focusInput, below, no new focus - # event is generated, so we don't drop into InsertMode as expected. - # Therefore we blur() the element before focussing it. element = visibleInputs[selectedInputIndex].element - element.blur() if document.activeElement == element element.focus() + # If PostFindMode is was previously active, then element may already have had the focus. In this case, + # focus() does not generate a "focus" event. So we now force insert mode. + new InsertMode element + return if visibleInputs.length == 1 hints = for tuple in visibleInputs @@ -390,23 +387,50 @@ extend window, hintContainingDiv = DomUtils.addElementList(hints, { id: "vimiumInputMarkerContainer", className: "vimiumReset" }) - handlerStack.push keydown: (event) -> - if event.keyCode == KeyboardUtils.keyCodes.tab - hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint' - if event.shiftKey - if --selectedInputIndex == -1 - selectedInputIndex = hints.length - 1 - else - if ++selectedInputIndex == hints.length - selectedInputIndex = 0 - hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' - visibleInputs[selectedInputIndex].element.focus() - else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey - DomUtils.removeElement hintContainingDiv - @remove() - return true + class FocusSelector extends InsertModeBlocker + constructor: -> + super InsertModeBlocker, null, + name: "focus-selector" + badge: "?" + keydown: (event) => + if event.keyCode == KeyboardUtils.keyCodes.tab + hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint' + if event.shiftKey + if --selectedInputIndex == -1 + selectedInputIndex = hints.length - 1 + else + if ++selectedInputIndex == hints.length + selectedInputIndex = 0 + hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' + element = visibleInputs[selectedInputIndex].element + element.focus() + false + else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey + DomUtils.removeElement hintContainingDiv + @exit() + new InsertMode element + return true + keypress: (event) -> false + keyup: (event) -> false + + new FocusSelector() + + # handlerStack.push keydown: (event) -> + # if event.keyCode == KeyboardUtils.keyCodes.tab + # hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint' + # if event.shiftKey + # if --selectedInputIndex == -1 + # selectedInputIndex = hints.length - 1 + # else + # if ++selectedInputIndex == hints.length + # selectedInputIndex = 0 + # hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' + # visibleInputs[selectedInputIndex].element.focus() + # else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey + # DomUtils.removeElement hintContainingDiv + # @remove() + # return true - false # 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 -- cgit v1.2.3 From edd52393cc9897a74d2ea94001cafe55dec09433 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 5 Jan 2015 13:26:04 +0000 Subject: Modes; simplify badge logic. --- content_scripts/mode.coffee | 2 ++ content_scripts/mode_insert.coffee | 2 -- content_scripts/mode_visual.coffee | 4 +--- content_scripts/vimium_frontend.coffee | 2 -- 4 files changed, 3 insertions(+), 7 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 96fc9b0c..30136315 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -72,6 +72,8 @@ class Mode keyup: @keyup updateBadge: (badge) => handlerStack.alwaysContinueBubbling => @chooseBadge badge + Mode.updateBadge() if @badge + push: (handlers) -> @handlers.push handlerStack.push handlers diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index bead43f8..d289ed86 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -27,8 +27,6 @@ class InsertMode extends ConstrainedMode keypress: (event) => @stopBubblingAndTrue keyup: (event) => @stopBubblingAndTrue - Mode.updateBadge() - exit: (event=null) -> if event?.source == ExitOnEscapeMode and event?.event?.srcElement? element = event.event.srcElement diff --git a/content_scripts/mode_visual.coffee b/content_scripts/mode_visual.coffee index b07d784e..6ef8ed4e 100644 --- a/content_scripts/mode_visual.coffee +++ b/content_scripts/mode_visual.coffee @@ -3,7 +3,7 @@ class VisualMode extends ConstrainedMode constructor: (element=document.body) -> - super element, + super element, VisualMode, name: "visual" badge: "V" @@ -16,7 +16,5 @@ class VisualMode extends ConstrainedMode keyup: (event) => return Mode.suppressEvent - Mode.updateBadge() - root = exports ? window root.VisualMode = VisualMode diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index cd717d5e..99463fbc 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -787,8 +787,6 @@ class FindMode extends ExitOnEscapeMode keyup: (event) => @suppressEvent - Mode.updateBadge() - exit: (extra) -> handleEscapeForFindMode() if extra?.source == ExitOnEscapeMode super() -- cgit v1.2.3 From f2cc3b3e870e3c0c6946a675b7971e128bf9e824 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 5 Jan 2015 13:33:55 +0000 Subject: Modes; minor tweeks. --- content_scripts/mode.coffee | 10 +++++----- content_scripts/vimium_frontend.coffee | 24 +++++++++--------------- 2 files changed, 14 insertions(+), 20 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 30136315..e5795190 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -140,17 +140,17 @@ class ExitOnEscapeMode extends SingletonMode event: event @suppressEvent -# When @element loses the focus. +# Exit mode when @constrainingElement (if defined) loses the focus. class ConstrainedMode extends ExitOnEscapeMode - constructor: (@element, singleton, options) -> + constructor: (@constrainingElement, singleton, options) -> super singleton, options - if @element - @element.focus() + if @constrainingElement + @constrainingElement.focus() @push "blur": (event) => handlerStack.alwaysContinueBubbling => - @exit() if event.srcElement == @element + @exit() if event.srcElement == @constrainingElement # The state mode tracks the enabled state in @enabled and @passKeys, and its initialized state in # @initialized. It calls @registerStateChange() whenever the state changes. diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 99463fbc..fd55348d 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -344,8 +344,6 @@ extend window, new VisualMode() focusInput: (count) -> - SingletonMode.kill PostFindMode - # 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. @@ -361,14 +359,14 @@ extend window, selectedInputIndex = Math.min(count - 1, visibleInputs.length - 1) - element = visibleInputs[selectedInputIndex].element - element.focus() - - # If PostFindMode is was previously active, then element may already have had the focus. In this case, - # focus() does not generate a "focus" event. So we now force insert mode. - new InsertMode element + element = visibleInputs[selectedInputIndex].element.focus() - return if visibleInputs.length == 1 + if visibleInputs.length == 1 + # If PostFindMode is was previously active, then element may already have had the focus. In this case, + # focus(), above, will not have generated a "focus" event to trigger insert mode. So we force insert + # mode now. + new InsertMode visibleInputs[selectedInputIndex].element + return hints = for tuple in visibleInputs hint = document.createElement("div") @@ -402,16 +400,12 @@ extend window, if ++selectedInputIndex == hints.length selectedInputIndex = 0 hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' - element = visibleInputs[selectedInputIndex].element - element.focus() + visibleInputs[selectedInputIndex].element.focus() false else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey DomUtils.removeElement hintContainingDiv @exit() - new InsertMode element - return true - keypress: (event) -> false - keyup: (event) -> false + new InsertMode visibleInputs[selectedInputIndex].element new FocusSelector() -- cgit v1.2.3 From 94586418ffd92246551c26ff00f0d80b0e2289fa Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 5 Jan 2015 14:25:08 +0000 Subject: Modes; more minor tweeks. --- content_scripts/mode.coffee | 93 ++++++++++++++++------------------ content_scripts/mode_find.coffee | 14 ++--- content_scripts/mode_insert.coffee | 33 ++++++------ content_scripts/mode_visual.coffee | 7 ++- content_scripts/vimium_frontend.coffee | 39 +++----------- 5 files changed, 80 insertions(+), 106 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index e5795190..adc5439d 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -1,15 +1,16 @@ # Modes. # -# A mode implements a number of event handlers which are pushed onto the handler stack when the mode starts, -# and poped when the mode exits. The Mode base takes as single argument options which can defined: +# 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: # # 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 set a badge and override the -# chooseBadge method instead. Or, if the mode *never* shows a bade, then do neither. +# 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. # # keydown: # keypress: @@ -24,9 +25,10 @@ # # New mode types are created by inheriting from Mode or one of its sub-classes. Some generic cub-classes are # provided below: -# SingletonMode: ensures that at most one instance of the mode should be active at any time. -# ConstrainedMode: exits the mode if the user clicks outside of the given element. -# ExitOnEscapeMode: exits the mode if the user types Esc. +# +# SingletonMode: ensures that at most one instance of the mode is active at any one time. +# ConstrainedMode: 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: @@ -37,13 +39,12 @@ # @exit() # internally triggered (more common). # -# Debug only; to be stripped out. +# For debug only; to be stripped out. count = 0 class Mode - # Static members. + # Static. @modes: [] - @current: -> Mode.modes[0] # Constants; readable shortcuts for event-handler return values. continueBubbling: true @@ -54,7 +55,7 @@ class Mode # Default values. name: "" badge: "" - keydown: null + keydown: null # null will be ignored by handlerStack (so it's a safe default). keypress: null keyup: null @@ -70,7 +71,7 @@ class Mode keydown: @keydown keypress: @keypress keyup: @keyup - updateBadge: (badge) => handlerStack.alwaysContinueBubbling => @chooseBadge badge + updateBadge: (badge) => @alwaysContinueBubbling => @chooseBadge badge Mode.updateBadge() if @badge @@ -80,21 +81,22 @@ class Mode exit: -> if @modeIsActive console.log @count, "exit:", @name - # We reverse @handlers, here. That way, handlers are popped in the opposite order to that in which they - # were pushed. - handlerStack.remove handlerId for handlerId in @handlers.reverse() + handlerStack.remove handlerId for handlerId in @handlers Mode.modes = Mode.modes.filter (mode) => mode != @ Mode.updateBadge() @modeIsActive = false # 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. + # opportunity to choose a badge. chooseBadge, here, is the default. It is overridden in sub-classes. chooseBadge: (badge) -> badge.badge ||= @badge + # Shorthand for a long name. + alwaysContinueBubbling: (func) -> handlerStack.alwaysContinueBubbling func + # 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. + # the resulting badge to the background page. We only update the badge if this document (hence this frame) + # has the focus. @updateBadge: -> if document.hasFocus() handlerStack.bubbleEvent "updateBadge", badge = {badge: ""} @@ -102,36 +104,37 @@ class Mode handler: "setBadge" badge: badge.badge - # Temporarily install a mode. + # Temporarily install a mode to call a function. @runIn: (mode, func) -> mode = new mode() func() mode.exit() # 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. +# New instances cancel previously-active instances on startup. class SingletonMode extends Mode @instances: {} exit: -> - delete SingletonMode.instances[@singleton] + delete SingletonMode.instances[@singleton] if @singleton? super() constructor: (@singleton, options={}) -> - SingletonMode.kill @singleton - SingletonMode.instances[@singleton] = @ + if @singleton? + SingletonMode.kill @singleton + SingletonMode.instances[@singleton] = @ super options - # Static method. If there's a singleton instance running, then kill it. + # Static method. If there's a singleton instance active, then kill it. @kill: (singleton) -> SingletonMode.instances[singleton].exit() if SingletonMode.instances[singleton] -# The mode exits when the user hits Esc. +# This mode exits when the user hits Esc. class ExitOnEscapeMode extends SingletonMode constructor: (singleton, options) -> super singleton, options - # This handler ends up above the mode's own key handlers on the handler stack, so it takes priority. + # 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 @@ -140,7 +143,7 @@ class ExitOnEscapeMode extends SingletonMode event: event @suppressEvent -# Exit mode when @constrainingElement (if defined) loses the focus. +# This mode exits when @constrainingElement (if defined) loses the focus. class ConstrainedMode extends ExitOnEscapeMode constructor: (@constrainingElement, singleton, options) -> super singleton, options @@ -148,24 +151,22 @@ class ConstrainedMode extends ExitOnEscapeMode if @constrainingElement @constrainingElement.focus() @push - "blur": (event) => - handlerStack.alwaysContinueBubbling => - @exit() if event.srcElement == @constrainingElement + "blur": (event) => @alwaysContinueBubbling => + @exit() if event.srcElement == @constrainingElement -# The state mode tracks the enabled state in @enabled and @passKeys, and its initialized state in -# @initialized. It calls @registerStateChange() whenever the state changes. +# 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) -> - @stateInitialized = false @enabled = false @passKeys = "" super options @push "registerStateChange": ({enabled: enabled, passKeys: passKeys}) => - handlerStack.alwaysContinueBubbling => - if enabled != @enabled or passKeys != @passKeys or not @stateInitialized - @stateInitialized = true + @alwaysContinueBubbling => + if enabled != @enabled or passKeys != @passKeys @enabled = enabled @passKeys = passKeys @registerStateChange() @@ -173,28 +174,24 @@ class StateMode extends Mode # Overridden by sub-classes. registerStateChange: -> -# BadgeMode is a psuedo mode for managing 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. -class BadgeMode extends StateMode +# BadgeMode is a psuedo 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 constructor: (options) -> - options.name ||= "badge" - super options + super + name: "badge" @push - "focus": => - handlerStack.alwaysContinueBubbling => - Mode.updateBadge() + "focus": => @alwaysContinueBubbling => Mode.updateBadge() chooseBadge: (badge) -> - # If we're not enabled, then post an empty badge (so, no badge at all). + # If we're not enabled, then post an empty badge. badge.badge = "" unless @enabled registerStateChange: -> Mode.updateBadge() -# Install a single BadgeMode instance. -new BadgeMode {} - root = exports ? window root.Mode = Mode root.SingletonMode = SingletonMode diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 8b458770..0937c510 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -2,8 +2,8 @@ # 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. Suppress InsertModeTrigger. This presents keyboard events from dropping us unintentionaly into insert -# mode. Here, this is achieved by inheriting PostFindMode from InsertModeBlocker. +# 1. Suppress InsertModeTrigger. This prevents keyboard events from dropping us unintentionaly into insert +# mode. Here, 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 @@ -31,11 +31,11 @@ class PostFindMode extends InsertModeBlocker # Install various ways in which we can leave this mode. @push - DOMActive: (event) => handlerStack.alwaysContinueBubbling => @exit() - click: (event) => handlerStack.alwaysContinueBubbling => @exit() - focus: (event) => handlerStack.alwaysContinueBubbling => @exit() - blur: (event) => handlerStack.alwaysContinueBubbling => @exit() - keydown: (event) => handlerStack.alwaysContinueBubbling => @exit() if document.activeElement != element + DOMActive: (event) => @alwaysContinueBubbling => @exit() + click: (event) => @alwaysContinueBubbling => @exit() + 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 d289ed86..51ef3d8b 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -17,8 +17,8 @@ isEmbed =(element) -> isFocusable =(element) -> isEditable(element) or isEmbed element +# This mode is installed when insert mode is active. class InsertMode extends ConstrainedMode - constructor: (@insertModeLock=null) -> super @insertModeLock, InsertMode, name: "insert" @@ -27,9 +27,9 @@ class InsertMode extends ConstrainedMode keypress: (event) => @stopBubblingAndTrue keyup: (event) => @stopBubblingAndTrue - exit: (event=null) -> - if event?.source == ExitOnEscapeMode and event?.event?.srcElement? - element = event.event.srcElement + exit: (extra=null) -> + if extra?.source == ExitOnEscapeMode and extra?.event?.srcElement? + element = extra.event.srcElement if isFocusable element # Remove the focus so the user can't just get himself back into insert mode by typing in the same # input box. @@ -40,24 +40,26 @@ class InsertMode extends ConstrainedMode super() # Trigger insert mode: -# - On keydown event in a contentEditable element. +# - On a keydown event in a contentEditable element. # - When a focusable element receives the focus. # Can be suppressed by setting extra.suppressInsertModeTrigger. +# +# This mode is permanently installed fairly low down on the handler stack. class InsertModeTrigger extends Mode constructor: -> super name: "insert-trigger" keydown: (event, extra) => - handlerStack.alwaysContinueBubbling => + @alwaysContinueBubbling => unless extra.suppressInsertModeTrigger? - # 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 check - # whether the active element is contentEditable. + # 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. new InsertMode() if document.activeElement?.isContentEditable @push focus: (event, extra) => - handlerStack.alwaysContinueBubbling => + @alwaysContinueBubbling => unless extra.suppressInsertModeTrigger? new InsertMode event.target if isFocusable event.target @@ -67,10 +69,9 @@ class InsertModeTrigger extends Mode @suppress: (extra) -> extra.suppressInsertModeTrigger = true -# Disables InsertModeTrigger. Used by find mode to prevent unintentionally dropping into insert mode on -# focusable elements. -# If @element is provided, then don't block focus events, and block keydown events only on the indicated -# element. +# Disables InsertModeTrigger. Used by find mode and findFocus to prevent unintentionally dropping into insert +# mode on focusable elements. +# If @element is provided, then don't suppress focus events, and suppress keydown events only on @element. class InsertModeBlocker extends SingletonMode constructor: (singleton=InsertModeBlocker, @element=null, options={}) -> options.name ||= "insert-blocker" @@ -79,13 +80,13 @@ class InsertModeBlocker extends SingletonMode unless @element? @push focus: (event, extra) => - handlerStack.alwaysContinueBubbling => + @alwaysContinueBubbling => InsertModeTrigger.suppress extra if @element?.isContentEditable @push keydown: (event, extra) => - handlerStack.alwaysContinueBubbling => + @alwaysContinueBubbling => InsertModeTrigger.suppress extra if event.srcElement == @element root = exports ? window diff --git a/content_scripts/mode_visual.coffee b/content_scripts/mode_visual.coffee index 6ef8ed4e..bc4e0901 100644 --- a/content_scripts/mode_visual.coffee +++ b/content_scripts/mode_visual.coffee @@ -1,20 +1,19 @@ # Note. ConstrainedMode extends extends ExitOnEscapeMode. So exit-on-escape is handled there. class VisualMode extends ConstrainedMode - constructor: (element=document.body) -> super element, VisualMode, name: "visual" badge: "V" keydown: (event) => - return Mode.suppressEvent + return @suppressEvent keypress: (event) => - return Mode.suppressEvent + return @suppressEvent keyup: (event) => - return Mode.suppressEvent + return @suppressEvent root = exports ? window root.VisualMode = VisualMode diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index fd55348d..82beb90c 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -359,12 +359,12 @@ extend window, selectedInputIndex = Math.min(count - 1, visibleInputs.length - 1) - element = visibleInputs[selectedInputIndex].element.focus() + # If PostFindMode is active, then the target element may already have the focus, in which case, .focus() + # will not generate a "focus" event to trigger insert mode. So we handle insert mode manually, here. + Mode.runIn InsertModeBlocker, -> + visibleInputs[selectedInputIndex].element.focus() if visibleInputs.length == 1 - # If PostFindMode is was previously active, then element may already have had the focus. In this case, - # focus(), above, will not have generated a "focus" event to trigger insert mode. So we force insert - # mode now. new InsertMode visibleInputs[selectedInputIndex].element return @@ -385,7 +385,7 @@ extend window, hintContainingDiv = DomUtils.addElementList(hints, { id: "vimiumInputMarkerContainer", className: "vimiumReset" }) - class FocusSelector extends InsertModeBlocker + new class FocusSelector extends InsertModeBlocker constructor: -> super InsertModeBlocker, null, name: "focus-selector" @@ -393,39 +393,16 @@ extend window, keydown: (event) => if event.keyCode == KeyboardUtils.keyCodes.tab hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint' - if event.shiftKey - if --selectedInputIndex == -1 - selectedInputIndex = hints.length - 1 - else - if ++selectedInputIndex == hints.length - selectedInputIndex = 0 + selectedInputIndex += hints.length + (if event.shiftKey then -1 else 1) + selectedInputIndex %= hints.length hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' visibleInputs[selectedInputIndex].element.focus() false else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey - DomUtils.removeElement hintContainingDiv @exit() + DomUtils.removeElement hintContainingDiv new InsertMode visibleInputs[selectedInputIndex].element - new FocusSelector() - - # handlerStack.push keydown: (event) -> - # if event.keyCode == KeyboardUtils.keyCodes.tab - # hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint' - # if event.shiftKey - # if --selectedInputIndex == -1 - # selectedInputIndex = hints.length - 1 - # else - # if ++selectedInputIndex == hints.length - # selectedInputIndex = 0 - # hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' - # visibleInputs[selectedInputIndex].element.focus() - # else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey - # DomUtils.removeElement hintContainingDiv - # @remove() - # return true - - # 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. -- cgit v1.2.3 From ff2aaea99ef2852aa607d47c9fc6387099c92da1 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 6 Jan 2015 05:01:59 +0000 Subject: Modes; simplify focusInput. --- content_scripts/vimium_frontend.coffee | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 82beb90c..1c562216 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -359,15 +359,6 @@ extend window, selectedInputIndex = Math.min(count - 1, visibleInputs.length - 1) - # If PostFindMode is active, then the target element may already have the focus, in which case, .focus() - # will not generate a "focus" event to trigger insert mode. So we handle insert mode manually, here. - Mode.runIn InsertModeBlocker, -> - visibleInputs[selectedInputIndex].element.focus() - - if visibleInputs.length == 1 - new InsertMode visibleInputs[selectedInputIndex].element - return - hints = for tuple in visibleInputs hint = document.createElement("div") hint.className = "vimiumReset internalVimiumInputHint vimiumInputHint" @@ -400,8 +391,15 @@ extend window, false else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey @exit() - DomUtils.removeElement hintContainingDiv - new InsertMode visibleInputs[selectedInputIndex].element + + visibleInputs[selectedInputIndex].element.focus() + @exit() if visibleInputs.length == 1 + + exit: -> + DomUtils.removeElement hintContainingDiv + new InsertMode visibleInputs[selectedInputIndex].element + super() + @continueBubbling # 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 -- cgit v1.2.3 From b1d14573539ea6006fa8506ff44d2a58f5ac1211 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 6 Jan 2015 07:38:06 +0000 Subject: On find, possibly .blur() active element. Fixes #1412. --- content_scripts/vimium_frontend.coffee | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'content_scripts') diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 8e235c7e..ea3791dd 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -391,15 +391,16 @@ extend window, false else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey @exit() + @continueBubbling visibleInputs[selectedInputIndex].element.focus() @exit() if visibleInputs.length == 1 exit: -> DomUtils.removeElement hintContainingDiv + visibleInputs[selectedInputIndex].element.focus() new InsertMode visibleInputs[selectedInputIndex].element super() - @continueBubbling # 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 @@ -797,6 +798,12 @@ executeFind = (query, options) -> # we need to save the anchor node here because seems to nullify it, regardless of whether we do # preventDefault() findModeAnchorNode = document.getSelection().anchorNode + + # If the anchor node is outside of the active element, then blur the active element. We don't want to leave + # behind an inappropriate active element. This fixes #1412. + if document.activeElement and not DomUtils.isDOMDescendant findModeAnchorNode, document.activeElement + document.activeElement.blur() + result restoreDefaultSelectionHighlight = -> document.body.classList.remove("vimiumFindMode") -- cgit v1.2.3 From 3594bad00aec580bc837e2b2cc6d4051da149da0 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 6 Jan 2015 09:42:28 +0000 Subject: Modes; rework insert blocker. Fix bug whereby clicking on the focused element does not activate insert mode. This bug is also present (though harder to fix) in master. --- content_scripts/mode.coffee | 4 ++++ content_scripts/mode_insert.coffee | 36 ++++++++++++++++++++++++------------ 2 files changed, 28 insertions(+), 12 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index adc5439d..b3cca56c 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -125,6 +125,10 @@ class SingletonMode extends Mode 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] diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 51ef3d8b..2b0ebb45 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -27,17 +27,19 @@ class InsertMode extends ConstrainedMode keypress: (event) => @stopBubblingAndTrue keyup: (event) => @stopBubblingAndTrue - exit: (extra=null) -> - if extra?.source == ExitOnEscapeMode and extra?.event?.srcElement? - element = extra.event.srcElement - if isFocusable element + exit: (extra={}) -> + super() + if extra.source == ExitOnEscapeMode and extra.event?.srcElement? + if isFocusable extra.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. - element.blur() - super() + extra.event.srcElement.blur() + + # Static method. Return whether insert mode is currently active or not. + @isActive: (singleton) -> SingletonMode.isActive InsertMode # Trigger insert mode: # - On a keydown event in a contentEditable element. @@ -55,13 +57,25 @@ class InsertModeTrigger extends Mode # 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. - new InsertMode() if document.activeElement?.isContentEditable + # NOTE. There's no need to check InsertMode.isActive() here since, if insert mode *is* active, + # then we wouldn't be receiving this keyboard event. + new InsertMode document.activeElement if document.activeElement?.isContentEditable @push focus: (event, extra) => @alwaysContinueBubbling => - unless extra.suppressInsertModeTrigger? - new InsertMode event.target if isFocusable event.target + unless InsertMode.isActive() + unless extra.suppressInsertModeTrigger? + new InsertMode event.target if isFocusable event.target + + click: (event, extra) => + @alwaysContinueBubbling => + unless InsertMode.isActive() + # 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), and no focus + # event will be generated. In this case, clicking on the element should activate insert mode. + if document.activeElement == event.target and isEditable event.target + new InsertMode event.target # We may already have focussed something, so check. new InsertMode document.activeElement if document.activeElement and isFocusable document.activeElement @@ -79,9 +93,7 @@ class InsertModeBlocker extends SingletonMode unless @element? @push - focus: (event, extra) => - @alwaysContinueBubbling => - InsertModeTrigger.suppress extra + focus: (event, extra) => @alwaysContinueBubbling => InsertModeTrigger.suppress extra if @element?.isContentEditable @push -- cgit v1.2.3 From b0f56ca439af45b62b23efd8c19c3838945f21f4 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 6 Jan 2015 10:52:13 +0000 Subject: Modes; yet more minor tweeks. --- content_scripts/mode.coffee | 6 ++++++ content_scripts/mode_find.coffee | 3 ++- content_scripts/mode_insert.coffee | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index b3cca56c..b81a4ede 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -67,6 +67,8 @@ class Mode console.log @count, "create:", @name @handlers = [] + @exitHandlers = [] + @push keydown: @keydown keypress: @keypress @@ -78,9 +80,13 @@ class Mode push: (handlers) -> @handlers.push handlerStack.push handlers + onExit: (handler) -> + @exitHandlers.push handler + exit: -> if @modeIsActive console.log @count, "exit:", @name + handler() for handler in @exitHandlers handlerStack.remove handlerId for handlerId in @handlers Mode.modes = Mode.modes.filter (mode) => mode != @ Mode.updateBadge() diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 0937c510..85cdc6c5 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -10,7 +10,8 @@ class PostFindMode extends InsertModeBlocker constructor: (findModeAnchorNode) -> element = document.activeElement - super PostFindMode, element, {name: "post-find"} + super element, + name: "post-find" return @exit() unless element and findModeAnchorNode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 2b0ebb45..24442928 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -87,9 +87,9 @@ class InsertModeTrigger extends Mode # mode on focusable elements. # If @element is provided, then don't suppress focus events, and suppress keydown events only on @element. class InsertModeBlocker extends SingletonMode - constructor: (singleton=InsertModeBlocker, @element=null, options={}) -> + constructor: (@element=null, options={}) -> options.name ||= "insert-blocker" - super singleton, options + super InsertModeBlocker, options unless @element? @push -- cgit v1.2.3 From f19f21f7114c6cdc2c62b69e0f6dafac68fd84a0 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 6 Jan 2015 12:04:19 +0000 Subject: Mode; simplify InsertModeBlocker logic. --- content_scripts/mode.coffee | 11 ++++----- content_scripts/mode_find.coffee | 15 ++++++++---- content_scripts/mode_insert.coffee | 42 ++++++++++++++-------------------- content_scripts/vimium_frontend.coffee | 2 +- 4 files changed, 33 insertions(+), 37 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index b81a4ede..df833c51 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -153,16 +153,15 @@ class ExitOnEscapeMode extends SingletonMode event: event @suppressEvent -# This mode exits when @constrainingElement (if defined) loses the focus. +# This mode exits when element (if defined) loses the focus. class ConstrainedMode extends ExitOnEscapeMode - constructor: (@constrainingElement, singleton, options) -> + constructor: (element, singleton=null, options={}) -> super singleton, options - if @constrainingElement - @constrainingElement.focus() + if element?.focus? + element.focus() @push - "blur": (event) => @alwaysContinueBubbling => - @exit() if event.srcElement == @constrainingElement + "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 diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 85cdc6c5..9a0086a1 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -32,11 +32,16 @@ class PostFindMode extends InsertModeBlocker # Install various ways in which we can leave this mode. @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 + DOMActive: (event, extra) => @alwaysContinueBubbling => @exit extra + click: (event, extra) => @alwaysContinueBubbling => @exit extra + focus: (event, extra) => @alwaysContinueBubbling => @exit extra + blur: (event, extra) => @alwaysContinueBubbling => @exit extra + keydown: (event, extra) => @alwaysContinueBubbling => @exit extra if document.activeElement != element + + # Inform handlers further down the stack that PostFindMode exited on this event. + exit: (extra) -> + extra.postFindModeExited = true if extra + super() root = exports ? window root.PostFindMode = PostFindMode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 24442928..c6f9d5b1 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -44,7 +44,10 @@ class InsertMode extends ConstrainedMode # Trigger insert mode: # - On a keydown event in a contentEditable element. # - When a focusable element receives the focus. -# Can be suppressed by setting extra.suppressInsertModeTrigger. +# - 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), and no focus event will be generated. In this case, clicking on the element should +# activate insert mode (even if the insert-mode blocker is active). # # This mode is permanently installed fairly low down on the handler stack. class InsertModeTrigger extends Mode @@ -53,53 +56,42 @@ class InsertModeTrigger extends Mode name: "insert-trigger" keydown: (event, extra) => @alwaysContinueBubbling => - unless extra.suppressInsertModeTrigger? + unless InsertModeBlocker.isActive() # 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. - # NOTE. There's no need to check InsertMode.isActive() here since, if insert mode *is* active, - # then we wouldn't be receiving this keyboard event. new InsertMode document.activeElement if document.activeElement?.isContentEditable @push focus: (event, extra) => @alwaysContinueBubbling => - unless InsertMode.isActive() - unless extra.suppressInsertModeTrigger? - new InsertMode event.target if isFocusable event.target + unless InsertMode.isActive() or InsertModeBlocker.isActive() + new InsertMode event.target if isFocusable event.target click: (event, extra) => @alwaysContinueBubbling => unless InsertMode.isActive() - # 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), and no focus - # event will be generated. In this case, clicking on the element should activate insert mode. - if document.activeElement == event.target and isEditable event.target - new InsertMode event.target + # We cannot check InsertModeBlocker.isActive(). PostFindMode exits on clicks, so will already have + # gone. So, instead, it sets an extra we can check. + if extra?.postFindModeExited + if document.activeElement == event.target and isEditable event.target + new InsertMode event.target # We may already have focussed something, so check. new InsertMode document.activeElement if document.activeElement and isFocusable document.activeElement - @suppress: (extra) -> - extra.suppressInsertModeTrigger = true - # Disables InsertModeTrigger. Used by find mode and findFocus to prevent unintentionally dropping into insert # mode on focusable elements. -# If @element is provided, then don't suppress focus events, and suppress keydown events only on @element. class InsertModeBlocker extends SingletonMode - constructor: (@element=null, options={}) -> + constructor: (element, options={}) -> options.name ||= "insert-blocker" super InsertModeBlocker, options - unless @element? - @push - focus: (event, extra) => @alwaysContinueBubbling => InsertModeTrigger.suppress extra + @push + "blur": (event) => @alwaysContinueBubbling => @exit() if element? and event.srcElement == element - if @element?.isContentEditable - @push - keydown: (event, extra) => - @alwaysContinueBubbling => - InsertModeTrigger.suppress extra if event.srcElement == @element + # Static method. Return whether the insert-mode blocker is currently active or not. + @isActive: (singleton) -> SingletonMode.isActive InsertModeBlocker root = exports ? window root.InsertMode = InsertMode diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index ea3791dd..77738f59 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -378,7 +378,7 @@ extend window, new class FocusSelector extends InsertModeBlocker constructor: -> - super InsertModeBlocker, null, + super null, name: "focus-selector" badge: "?" keydown: (event) => -- cgit v1.2.3 From 849f1caeeabefbbc0233c9ebc69c41146fe01898 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 6 Jan 2015 12:19:06 +0000 Subject: Modes; minor tweeks. --- content_scripts/mode_visual.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_visual.coffee b/content_scripts/mode_visual.coffee index bc4e0901..09335057 100644 --- a/content_scripts/mode_visual.coffee +++ b/content_scripts/mode_visual.coffee @@ -1,8 +1,8 @@ # Note. ConstrainedMode extends extends ExitOnEscapeMode. So exit-on-escape is handled there. class VisualMode extends ConstrainedMode - constructor: (element=document.body) -> - super element, VisualMode, + constructor: (element=null) -> + super element, null, name: "visual" badge: "V" -- cgit v1.2.3 From ff88d1707647253cb3b93d3dc79831dc72063feb Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 6 Jan 2015 12:43:22 +0000 Subject: Modes; fix non-vimium keys in PostFindMode. --- content_scripts/vimium_frontend.coffee | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 77738f59..07b430ba 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -460,7 +460,10 @@ onKeypress = (event) -> else if (!isInsertMode() && !findMode) if (isPassKey keyChar) return handlerStack.stopBubblingAndTrue - if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)) + if currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar) or + # 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). + InsertModeBlocker.isActive() DomUtils.suppressEvent(event) keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) @@ -555,7 +558,10 @@ onKeydown = (event) -> # TOOD(ilya): Revisit this. Not sure it's the absolute best approach. if (keyChar == "" && !isInsertMode() && (currentCompletionKeys.indexOf(KeyboardUtils.getKeyChar(event)) != -1 || - isValidFirstKey(KeyboardUtils.getKeyChar(event)))) + isValidFirstKey(KeyboardUtils.getKeyChar(event)) || + # 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). + InsertModeBlocker.isActive())) DomUtils.suppressPropagation(event) KeydownEvents.push event -- cgit v1.2.3 From faf5b35ab501b8c4edc25f484c0b76ee38319f1f Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 6 Jan 2015 13:23:26 +0000 Subject: Modes; simplify PostFindMode. --- content_scripts/mode_find.coffee | 19 +++++++------------ content_scripts/mode_insert.coffee | 11 ++++------- 2 files changed, 11 insertions(+), 19 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 9a0086a1..0bcb0eb1 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -2,8 +2,8 @@ # 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. Suppress InsertModeTrigger. This prevents keyboard events from dropping us unintentionaly into insert -# mode. Here, this is achieved by inheriting from InsertModeBlocker. +# 1. Be an InsertModeBlocker. This prevents keyboard events from dropping us unintentionaly into insert +# mode. Here, 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 @@ -32,16 +32,11 @@ class PostFindMode extends InsertModeBlocker # Install various ways in which we can leave this mode. @push - DOMActive: (event, extra) => @alwaysContinueBubbling => @exit extra - click: (event, extra) => @alwaysContinueBubbling => @exit extra - focus: (event, extra) => @alwaysContinueBubbling => @exit extra - blur: (event, extra) => @alwaysContinueBubbling => @exit extra - keydown: (event, extra) => @alwaysContinueBubbling => @exit extra if document.activeElement != element - - # Inform handlers further down the stack that PostFindMode exited on this event. - exit: (extra) -> - extra.postFindModeExited = true if extra - super() + DOMActive: (event) => @alwaysContinueBubbling => @exit() + click: (event) => @alwaysContinueBubbling => @exit() + 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 c6f9d5b1..c340c559 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -46,8 +46,8 @@ class InsertMode extends ConstrainedMode # - 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), and no focus event will be generated. In this case, clicking on the element should -# activate insert mode (even if the insert-mode blocker is active). +# 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 @@ -71,11 +71,8 @@ class InsertModeTrigger extends Mode click: (event, extra) => @alwaysContinueBubbling => unless InsertMode.isActive() - # We cannot check InsertModeBlocker.isActive(). PostFindMode exits on clicks, so will already have - # gone. So, instead, it sets an extra we can check. - if extra?.postFindModeExited - if document.activeElement == event.target and isEditable event.target - new InsertMode event.target + if document.activeElement == event.target and isEditable event.target + new InsertMode event.target # We may already have focussed something, so check. new InsertMode document.activeElement if document.activeElement and isFocusable document.activeElement -- cgit v1.2.3 From 486dce36493836461de7e9ae73721230ad69a1b5 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 6 Jan 2015 13:32:34 +0000 Subject: Modes; fix PostFindMode for contentEditable. --- content_scripts/mode_find.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 0bcb0eb1..e11b2e0f 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -15,9 +15,10 @@ class PostFindMode extends InsertModeBlocker return @exit() unless element and findModeAnchorNode - # Special cases only arise if the active element is focusable. So, exit immediately if it is not. + # 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 return @exit() unless canTakeInput self = @ -- cgit v1.2.3 From 914d689f8b4414dd65ed70b7b5ff86973fe8994a Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 6 Jan 2015 13:56:48 +0000 Subject: Modes; also fix #1415 for and the like. --- content_scripts/vimium_frontend.coffee | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 07b430ba..24cc25c3 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -460,14 +460,16 @@ onKeypress = (event) -> else if (!isInsertMode() && !findMode) if (isPassKey keyChar) return handlerStack.stopBubblingAndTrue - if currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar) or - # 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). - InsertModeBlocker.isActive() + if currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar) DomUtils.suppressEvent(event) keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) + if InsertModeBlocker.isActive() + # 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). + DomUtils.suppressEvent(event) + return true onKeydown = (event) -> @@ -558,10 +560,12 @@ onKeydown = (event) -> # TOOD(ilya): Revisit this. Not sure it's the absolute best approach. if (keyChar == "" && !isInsertMode() && (currentCompletionKeys.indexOf(KeyboardUtils.getKeyChar(event)) != -1 || - isValidFirstKey(KeyboardUtils.getKeyChar(event)) || - # 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). - InsertModeBlocker.isActive())) + isValidFirstKey(KeyboardUtils.getKeyChar(event)))) + DomUtils.suppressPropagation(event) + KeydownEvents.push event + else if InsertModeBlocker.isActive() + # 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). DomUtils.suppressPropagation(event) KeydownEvents.push event -- cgit v1.2.3 From c585331efc1b3c446f0f315a8904fbd9658b1cce Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 6 Jan 2015 16:19:07 +0000 Subject: Modes; when exiting on Escape, also grab keyup event. Fixes #1416. --- content_scripts/mode.coffee | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index df833c51..5a7bead5 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -151,6 +151,11 @@ class ExitOnEscapeMode extends SingletonMode @exit source: ExitOnEscapeMode event: event + # Suppress the corresponding keyup event too. + handlerStack.push + keyup: (event) -> + @remove() if KeyboardUtils.isEscape event + @suppressEvent @suppressEvent # This mode exits when element (if defined) loses the focus. -- cgit v1.2.3 From f01c57d2f25b4e66000e8812d5bbc247d53e6bce Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 6 Jan 2015 16:41:43 +0000 Subject: Modes; when exiting on Escape, also grab keyup event. Also fix post insert. --- content_scripts/mode.coffee | 6 +----- content_scripts/mode_find.coffee | 1 + 2 files changed, 2 insertions(+), 5 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 5a7bead5..857eb140 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -151,11 +151,7 @@ class ExitOnEscapeMode extends SingletonMode @exit source: ExitOnEscapeMode event: event - # Suppress the corresponding keyup event too. - handlerStack.push - keyup: (event) -> - @remove() if KeyboardUtils.isEscape event - @suppressEvent + DomUtils.suppressKeyupAfterEscape handlerStack @suppressEvent # This mode exits when element (if defined) loses the focus. diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index e11b2e0f..44d50608 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -27,6 +27,7 @@ class PostFindMode extends InsertModeBlocker if element == document.activeElement and KeyboardUtils.isEscape event self.exit() new InsertMode element + DomUtils.suppressKeyupAfterEscape handlerStack return false @remove() true -- cgit v1.2.3 From a7fcfd9a663e2d81a86e5e49e54162399ccb5e6b Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Wed, 7 Jan 2015 06:09:31 +0000 Subject: Modes; rename ConstrainedMode as ExitOnBlur. --- content_scripts/mode.coffee | 9 ++++----- content_scripts/mode_insert.coffee | 2 +- content_scripts/mode_visual.coffee | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 857eb140..ff75460f 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -27,7 +27,7 @@ # provided below: # # SingletonMode: ensures that at most one instance of the mode is active at any one time. -# ConstrainedMode: exits the mode if the an indicated element loses the focus. +# 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. # @@ -155,12 +155,11 @@ class ExitOnEscapeMode extends SingletonMode @suppressEvent # This mode exits when element (if defined) loses the focus. -class ConstrainedMode extends ExitOnEscapeMode +class ExitOnBlur extends ExitOnEscapeMode constructor: (element, singleton=null, options={}) -> super singleton, options - if element?.focus? - element.focus() + if element? @push "blur": (event) => @alwaysContinueBubbling => @exit() if event.srcElement == element @@ -205,6 +204,6 @@ new class BadgeMode extends StateMode root = exports ? window root.Mode = Mode root.SingletonMode = SingletonMode -root.ConstrainedMode = ConstrainedMode +root.ExitOnBlur = ExitOnBlur root.StateMode = StateMode root.ExitOnEscapeMode = ExitOnEscapeMode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index c340c559..960b42f8 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -18,7 +18,7 @@ isFocusable =(element) -> isEditable(element) or isEmbed element # This mode is installed when insert mode is active. -class InsertMode extends ConstrainedMode +class InsertMode extends ExitOnBlur constructor: (@insertModeLock=null) -> super @insertModeLock, InsertMode, name: "insert" diff --git a/content_scripts/mode_visual.coffee b/content_scripts/mode_visual.coffee index 09335057..a9acf8be 100644 --- a/content_scripts/mode_visual.coffee +++ b/content_scripts/mode_visual.coffee @@ -1,6 +1,6 @@ -# Note. ConstrainedMode extends extends ExitOnEscapeMode. So exit-on-escape is handled there. -class VisualMode extends ConstrainedMode +# Note. ExitOnBlur extends extends ExitOnEscapeMode. So exit-on-escape is handled there. +class VisualMode extends ExitOnBlur constructor: (element=null) -> super element, null, name: "visual" -- cgit v1.2.3 From 04ac4c64c9634d9f81035ff7e9db537f39b42f3c Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Wed, 7 Jan 2015 09:57:03 +0000 Subject: Modes; rework Singletons, InsertModeBlocker and HandlerStack. This begins work on addressing @philc's comments in #1413. That work is nevertheless not yet complete. --- content_scripts/mode.coffee | 12 ++++++++++++ content_scripts/mode_find.coffee | 3 ++- content_scripts/mode_insert.coffee | 28 +++++++++++++++------------- content_scripts/vimium_frontend.coffee | 22 ++++++++++++++-------- 4 files changed, 43 insertions(+), 22 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index ff75460f..e9a4a621 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -75,6 +75,7 @@ class Mode keyup: @keyup updateBadge: (badge) => @alwaysContinueBubbling => @chooseBadge badge + @registerSingleton options.singleton if options.singleton Mode.updateBadge() if @badge push: (handlers) -> @@ -116,6 +117,17 @@ class 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 diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 44d50608..18cb7b71 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -10,8 +10,9 @@ class PostFindMode extends InsertModeBlocker constructor: (findModeAnchorNode) -> element = document.activeElement - super element, + super name: "post-find" + singleton: PostFindMode return @exit() unless element and findModeAnchorNode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 960b42f8..83d85fa7 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -55,21 +55,23 @@ class InsertModeTrigger extends Mode super name: "insert-trigger" keydown: (event, extra) => - @alwaysContinueBubbling => - unless InsertModeBlocker.isActive() - # 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. - new InsertMode document.activeElement if document.activeElement?.isContentEditable + return @continueBubbling if InsertModeBlocker.isActive 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. + return @continueBubbling unless document.activeElement?.isContentEditable + new InsertMode document.activeElement + @stopBubblingAndTrue @push focus: (event, extra) => @alwaysContinueBubbling => - unless InsertMode.isActive() or InsertModeBlocker.isActive() + unless InsertMode.isActive() or InsertModeBlocker.isActive extra new InsertMode event.target if isFocusable 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 @@ -79,16 +81,16 @@ class InsertModeTrigger extends Mode # Disables InsertModeTrigger. Used by find mode and findFocus to prevent unintentionally dropping into insert # mode on focusable elements. -class InsertModeBlocker extends SingletonMode - constructor: (element, options={}) -> +class InsertModeBlocker extends Mode + constructor: (options={}) -> options.name ||= "insert-blocker" - super InsertModeBlocker, options + super options @push - "blur": (event) => @alwaysContinueBubbling => @exit() if element? and event.srcElement == element + "all": (event, extra) => @alwaysContinueBubbling => extra.isInsertModeBlockerActive = true - # Static method. Return whether the insert-mode blocker is currently active or not. - @isActive: (singleton) -> SingletonMode.isActive InsertModeBlocker + # Static method. Return whether an insert-mode blocker is currently active or not. + @isActive: (extra) -> extra?.isInsertModeBlockerActive root = exports ? window root.InsertMode = InsertMode diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 24cc25c3..193a1592 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -378,9 +378,10 @@ extend window, new class FocusSelector extends InsertModeBlocker constructor: -> - super null, + super name: "focus-selector" badge: "?" + singleton: FocusSelector keydown: (event) => if event.keyCode == KeyboardUtils.keyCodes.tab hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint' @@ -388,11 +389,14 @@ extend window, selectedInputIndex %= hints.length hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' visibleInputs[selectedInputIndex].element.focus() - false + @suppressEvent else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey @exit() @continueBubbling + # TODO. InsertModeBlocker is no longer a singleton. Need to make this a singleton. Fix once class + # hierarchy is removed. + visibleInputs[selectedInputIndex].element.focus() @exit() if visibleInputs.length == 1 @@ -441,7 +445,7 @@ KeydownEvents = # Note that some keys will only register keydown events and not keystroke events, e.g. ESC. # -onKeypress = (event) -> +onKeypress = (event, extra) -> keyChar = "" # Ignore modifier keys by themselves. @@ -465,14 +469,15 @@ onKeypress = (event) -> keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) - if InsertModeBlocker.isActive() + if InsertModeBlocker.isActive 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). + # 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. DomUtils.suppressEvent(event) return true -onKeydown = (event) -> +onKeydown = (event, extra) -> keyChar = "" # handle special keys, and normal input keys with modifiers being pressed. don't handle shiftKey alone (to @@ -563,9 +568,10 @@ onKeydown = (event) -> isValidFirstKey(KeyboardUtils.getKeyChar(event)))) DomUtils.suppressPropagation(event) KeydownEvents.push event - else if InsertModeBlocker.isActive() + else if InsertModeBlocker.isActive 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). + # 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. DomUtils.suppressPropagation(event) KeydownEvents.push event -- cgit v1.2.3 From 0429da577097bd7d30d12901fcc74385e44d83f4 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Wed, 7 Jan 2015 11:29:24 +0000 Subject: Modes; Continue incorporation of comments in #1413. - Slight rework of HandlerStack. - Remove classs ExitOnEscape and ExitOnBlur - Rework InsertMode, plus trigger and blocker. - Remove StateMode. - Do no mixin options. - Lots of tidy up (including set a debug variable to Mode). --- content_scripts/mode.coffee | 201 +++++++++++++-------------------- content_scripts/mode_find.coffee | 19 ++-- content_scripts/mode_insert.coffee | 59 +++++----- content_scripts/mode_passkeys.coffee | 17 +-- content_scripts/mode_visual.coffee | 7 +- content_scripts/vimium_frontend.coffee | 13 ++- 6 files changed, 137 insertions(+), 179 deletions(-) (limited to 'content_scripts') 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 = -> -- cgit v1.2.3 From 8cbe2df33d8af3845801bfaacf3b58adab9916cb Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Wed, 7 Jan 2015 19:00:24 +0000 Subject: Modes; minor changes. --- content_scripts/vimium_frontend.coffee | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index f0196c74..3f36a5cd 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -371,10 +371,9 @@ extend window, hint - hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' - - hintContainingDiv = DomUtils.addElementList(hints, - { id: "vimiumInputMarkerContainer", className: "vimiumReset" }) + hintContainingDiv = DomUtils.addElementList hints, + id: "vimiumInputMarkerContainer" + className: "vimiumReset" new class FocusSelector extends InsertModeBlocker constructor: -> @@ -394,11 +393,8 @@ extend window, @exit() @continueBubbling - # TODO. InsertModeBlocker is no longer a singleton. Need to make this a singleton. Fix once class - # hierarchy is removed. - - visibleInputs[selectedInputIndex].element.focus() @exit() if visibleInputs.length == 1 + hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' exit: -> DomUtils.removeElement hintContainingDiv @@ -991,12 +987,10 @@ showFindModeHUDForQuery = -> window.enterFindMode = -> findModeQuery = { rawQuery: "" } - # window.findMode = true # Same hack, see comment at window.findMode definition. HUD.show("/") new FindMode() exitFindMode = -> - window.findMode = false # Same hack, see comment at window.findMode definition. HUD.hide() window.showHelpDialog = (html, fid) -> -- cgit v1.2.3 From 7c886d32cca6c0540a9ec6247eb1617b8f1db86a Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 8 Jan 2015 07:20:55 +0000 Subject: Modes; refactor PostFindMode key handling. In particular, this refactors the handling of non-vimium key events in PostFindMode. This implements option 2 from #1415. However, #1415 is not resolved, and option 3 remains a viable option. --- content_scripts/mode.coffee | 18 +++++++++++++++++- content_scripts/mode_find.coffee | 22 +++++++++++++++------- content_scripts/vimium_frontend.coffee | 12 ------------ 3 files changed, 32 insertions(+), 20 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 92285b8c..160debc4 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -102,12 +102,27 @@ 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 + Mode.updateBadge() if @badge # End of Mode.constructor(). push: (handlers) -> @handlers.push handlerStack.push handlers + unshift: (handlers) -> + @handlers.unshift handlerStack.push handlers + onExit: (handler) -> @exitHandlers.push handler @@ -124,7 +139,8 @@ class Mode chooseBadge: (badge) -> badge.badge ||= @badge - # Shorthand for a long name. + # Shorthand for an otherwise long name. This allow us to write handlers which always yield the same value, + # without having to be concerned with the result of the handler itself. alwaysContinueBubbling: (func) -> handlerStack.alwaysContinueBubbling func # Static method. Used externally and internally to initiate bubbling of an updateBadge event and to send diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index f9766e3a..837606f3 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -1,26 +1,33 @@ # NOTE(smblott). Ultimately, all of the FindMode-related code should be moved to this file. # 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 +# PostFindMode handles 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. If the very-next keystroke is Escape, then drop immediately into insert mode. +# 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 +# alternative. +# 3. 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 + trapAllKeyboardEvents: element - 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. + # Special cases only arise if the active element can take input. So, exit immediately if it cannot. canTakeInput = DomUtils.isSelectable(element) and DomUtils.isDOMDescendant findModeAnchorNode, element canTakeInput ||= element.isContentEditable canTakeInput ||= findModeAnchorNode.parentElement?.isContentEditable return @exit() unless canTakeInput + self = @ @push keydown: (event) -> if element == document.activeElement and KeyboardUtils.isEscape event @@ -38,10 +45,11 @@ class PostFindMode extends InsertModeBlocker 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. + # focus event, so InsertModeTrigger doesn't fire and we don't drop automatically into insert mode. So + # we have to handle this case separately. click: (event) => @alwaysContinueBubbling => - new InsertMode event.target if DomUtils.isDOMDescendant element, event.target + new InsertMode element if DomUtils.isDOMDescendant element, event.target @exit() root = exports ? window diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 3f36a5cd..07b4fe4b 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -465,12 +465,6 @@ onKeypress = (event, extra) -> keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) - 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. - DomUtils.suppressEvent(event) - return true onKeydown = (event, extra) -> @@ -564,12 +558,6 @@ onKeydown = (event, extra) -> isValidFirstKey(KeyboardUtils.getKeyChar(event)))) DomUtils.suppressPropagation(event) KeydownEvents.push event - 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. - DomUtils.suppressPropagation(event) - KeydownEvents.push event return true -- cgit v1.2.3 From 5d199e6c786bb2874f7ecb700d505e7b2d70d982 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 8 Jan 2015 07:44:53 +0000 Subject: Modes; refactor and simplify. - Insert mode trigger and blocker. - Better comments for PostFindMode. - Better comments for FocusSelector. - Make insert mode consistent with master. --- content_scripts/mode_find.coffee | 7 ++- content_scripts/mode_insert.coffee | 82 +++++++++++++++++----------------- content_scripts/vimium_frontend.coffee | 2 + 3 files changed, 47 insertions(+), 44 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 837606f3..2ef74a89 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -1,7 +1,7 @@ # NOTE(smblott). Ultimately, all of the FindMode-related code should be moved to this file. # When we use find mode, the selection/focus can end up in a focusable/editable element. In this situation, -# PostFindMode handles three special cases: +# 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 @@ -16,12 +16,15 @@ 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. singleton: PostFindMode trapAllKeyboardEvents: element return @exit() unless element and findModeAnchorNode - # Special cases only arise if the active element can take input. So, exit immediately if it cannot. + # Special considerations only arise if the active element can take input. So, exit immediately if it + # cannot. canTakeInput = DomUtils.isSelectable(element) and DomUtils.isDOMDescendant findModeAnchorNode, element canTakeInput ||= element.isContentEditable canTakeInput ||= findModeAnchorNode.parentElement?.isContentEditable diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index b80a78ee..41d82add 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -17,34 +17,7 @@ isEmbed =(element) -> isFocusable =(element) -> isEditable(element) or isEmbed element -# This mode is installed when insert mode is active. -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: (event = null) -> - super() - 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. - @insertModeLock.blur() - - # Static method. Check whether insert mode is currently active. - @isActive: (extra) -> extra?.insertModeIsActive - -# Trigger insert mode: +# Automatically trigger insert mode: # - On a keydown event in a contentEditable element. # - When a focusable element receives the focus. # @@ -53,8 +26,8 @@ class InsertModeTrigger extends Mode constructor: -> super name: "insert-trigger" - keydown: (event, extra) => - return @continueBubbling if InsertModeTrigger.isDisabled extra + keydown: (event) => + return @continueBubbling if InsertModeTrigger.isSuppressed() # 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. @@ -63,31 +36,56 @@ class InsertModeTrigger extends Mode @stopBubblingAndTrue @push - focus: (event, extra) => + focus: (event) => @alwaysContinueBubbling => - return @continueBubbling if InsertModeTrigger.isDisabled extra + return @continueBubbling if InsertModeTrigger.isSuppressed() return if not isFocusable 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 - # Allow other modes to disable this trigger. Static. - @disable: (extra) -> extra.disableInsertModeTrigger = true - @isDisabled: (extra) -> extra?.disableInsertModeTrigger + # Allow other modes (notably InsertModeBlocker, below) to suppress this trigger. All static. + @suppressors: 0 + @isSuppressed: -> 0 < @suppressors + @suppress: -> @suppressors += 1 + @unsuppress: -> @suppressors -= 1 -# Disables InsertModeTrigger. This is used by find mode and by findFocus to prevent unintentionally dropping -# into insert mode on focusable elements. +# Suppresses InsertModeTrigger. This is used by various modes (usually by inheritance) to prevent +# unintentionally dropping into insert mode on focusable elements. class InsertModeBlocker extends Mode constructor: (options = {}) -> + InsertModeTrigger.suppress() options.name ||= "insert-blocker" super options - @push - "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 + exit: -> + super() + InsertModeTrigger.unsuppress() + +# This mode is installed when insert mode is active. +class InsertMode extends Mode + constructor: (@insertModeLock = null) -> + super + name: "insert" + badge: "I" + singleton: InsertMode + keydown: (event) => @stopBubblingAndTrue + keypress: (event) => @stopBubblingAndTrue + keyup: (event) => @stopBubblingAndTrue + exitOnEscape: true + exitOnBlur: @insertModeLock + + exit: (event = null) -> + super() + element = event?.srcElement + if element and isFocusable element + # 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. + element.blur() root = exports ? window root.InsertMode = InsertMode diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 07b4fe4b..e8248c0a 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -380,6 +380,8 @@ 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 keydown: (event) => if event.keyCode == KeyboardUtils.keyCodes.tab -- cgit v1.2.3 From 637d90be6847051d20a4cf3b704d599c877a97d3 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 8 Jan 2015 11:09:06 +0000 Subject: Modes; more changes... - Simplify InsertMode Trigger/Blocker (yet again). - Reduce badge flicker for singletons. --- content_scripts/mode.coffee | 24 ++++++++---- content_scripts/mode_insert.coffee | 80 ++++++++++++++++++-------------------- 2 files changed, 53 insertions(+), 51 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 160debc4..632d3e99 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -143,15 +143,19 @@ class Mode # without having to be concerned with the result of the handler itself. alwaysContinueBubbling: (func) -> handlerStack.alwaysContinueBubbling func + # User for sometimes suppressing badge updates. + @badgeSuppressor: new Utils.Suppressor() + # 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 (hence this frame) # has the focus. @updateBadge: -> - if document.hasFocus() - handlerStack.bubbleEvent "updateBadge", badge = {badge: ""} - chrome.runtime.sendMessage - handler: "setBadge" - badge: badge.badge + @badgeSuppressor.unlessSuppressed -> + if document.hasFocus() + handlerStack.bubbleEvent "updateBadge", badge = {badge: ""} + chrome.runtime.sendMessage + handler: "setBadge" + badge: badge.badge # Temporarily install a mode to protect a function call, then exit the mode. For example, temporarily # install an InsertModeBlocker. @@ -163,14 +167,18 @@ class Mode registerSingleton: do -> singletons = {} # Static. (key) -> - singletons[key].exit() if singletons[key] + # We're currently installing a new mode. So we'll be updating the badge shortly. Therefore, we can + # 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] singletons[key] = @ @onExit => delete singletons[key] if singletons[key] == @ # 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 other modes. +# badge choice of the other active modes. # Note. We also create the the one-and-only instance, here. new class BadgeMode extends Mode constructor: (options) -> @@ -179,7 +187,7 @@ new class BadgeMode extends Mode trackState: true @push - "focus": => @alwaysContinueBubbling => Mode.updateBadge() + "focus": => @alwaysContinueBubbling -> Mode.updateBadge() chooseBadge: (badge) -> # If we're not enabled, then post an empty badge. diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 41d82add..5280aada 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -17,75 +17,69 @@ isEmbed =(element) -> isFocusable =(element) -> isEditable(element) or isEmbed element +# This mode is installed when insert mode is active. +class InsertMode extends Mode + constructor: (@insertModeLock = null) -> + super + name: "insert" + badge: "I" + singleton: InsertMode + keydown: (event) => @stopBubblingAndTrue + keypress: (event) => @stopBubblingAndTrue + keyup: (event) => @stopBubblingAndTrue + exitOnEscape: true + exitOnBlur: @insertModeLock + + exit: (event = null) -> + super() + element = event?.srcElement + if element and isFocusable element + # 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. + element.blur() + # Automatically trigger insert 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. class InsertModeTrigger extends Mode constructor: -> super name: "insert-trigger" keydown: (event) => - return @continueBubbling if InsertModeTrigger.isSuppressed() - # 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. - return @continueBubbling unless document.activeElement?.isContentEditable - new InsertMode document.activeElement - @stopBubblingAndTrue + 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. + return @continueBubbling unless document.activeElement?.isContentEditable + new InsertMode document.activeElement + @stopBubblingAndTrue @push focus: (event) => - @alwaysContinueBubbling => - return @continueBubbling if InsertModeTrigger.isSuppressed() + triggerSuppressor.unlessSuppressed => return if not isFocusable 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 - # Allow other modes (notably InsertModeBlocker, below) to suppress this trigger. All static. - @suppressors: 0 - @isSuppressed: -> 0 < @suppressors - @suppress: -> @suppressors += 1 - @unsuppress: -> @suppressors -= 1 +# Used by InsertModeBlocker to suppress InsertModeTrigger; see below. +triggerSuppressor = new Utils.Suppressor true # Suppresses InsertModeTrigger. This is used by various modes (usually by inheritance) to prevent # unintentionally dropping into insert mode on focusable elements. class InsertModeBlocker extends Mode constructor: (options = {}) -> - InsertModeTrigger.suppress() + triggerSuppressor.suppress() options.name ||= "insert-blocker" super options - - exit: -> - super() - InsertModeTrigger.unsuppress() - -# This mode is installed when insert mode is active. -class InsertMode extends Mode - constructor: (@insertModeLock = null) -> - super - name: "insert" - badge: "I" - singleton: InsertMode - keydown: (event) => @stopBubblingAndTrue - keypress: (event) => @stopBubblingAndTrue - keyup: (event) => @stopBubblingAndTrue - exitOnEscape: true - exitOnBlur: @insertModeLock - - exit: (event = null) -> - super() - element = event?.srcElement - if element and isFocusable element - # 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. - element.blur() + @onExit -> triggerSuppressor.unsuppress() root = exports ? window root.InsertMode = InsertMode -- cgit v1.2.3 From 2fe40dd69bb93b620da60464b9cb57c36adaeca1 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 8 Jan 2015 12:02:03 +0000 Subject: Modes; incorporate small changes from #1413. Slightly more significant: Move several utilities to dome_utils. --- content_scripts/mode.coffee | 70 ++++++++++++++++++---------------- content_scripts/mode_insert.coffee | 25 ++---------- content_scripts/vimium_frontend.coffee | 5 +-- 3 files changed, 43 insertions(+), 57 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 632d3e99..8e37ee36 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -8,8 +8,10 @@ # # badge: # 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. +# Optional. Define a badge if the badge is constant; for example, in insert mode the badge is always "I". +# Otherwise, do not define a badge, but instead override the chooseBadge method; for example, in passkeys +# mode, the badge may be "P" or "", depending on the configuration state. Or, if the mode *never* shows a +# badge, then do neither. # # keydown: # keypress: @@ -40,6 +42,7 @@ count = 0 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. @@ -48,32 +51,31 @@ class Mode stopBubblingAndTrue: handlerStack.stopBubblingAndTrue stopBubblingAndFalse: handlerStack.stopBubblingAndFalse - constructor: (options={}) -> - @options = options + constructor: (@options={}) -> @handlers = [] @exitHandlers = [] @modeIsActive = true - @badge = options.badge || "" - @name = options.name || "anonymous" + @badge = @options.badge || "" + @name = @options.name || "anonymous" @count = ++count console.log @count, "create:", @name if Mode.debug @push - keydown: options.keydown || null - keypress: options.keypress || null - keyup: options.keyup || null + 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 + # 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 + @registerSingleton @options.singleton if @options.singleton - # If options.exitOnEscape is truthy, then the mode will exit when the escape key is pressed. The + # 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 + if @options.exitOnEscape # Note. This handler ends up above the mode's own key handlers on the handler stack, so it takes # priority. @push @@ -83,36 +85,36 @@ class Mode DomUtils.suppressKeyupAfterEscape handlerStack @suppressEvent - # If options.exitOnBlur is truthy, then it should be an element. The mode will exit when that element + # If @options.exitOnBlur is truthy, then it should be an element. The mode will exit when that element # loses the focus. - if options.exitOnBlur + if @options.exitOnBlur @push - "blur": (event) => @alwaysContinueBubbling => @exit() if event.srcElement == options.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, + # 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 + if @options.trackState @enabled = false @passKeys = "" @push - "registerStateChange": ({enabled: enabled, passKeys: passKeys}) => + "registerStateChange": ({ enabled: enabled, passKeys: passKeys }) => @alwaysContinueBubbling => if enabled != @enabled or passKeys != @passKeys @enabled = enabled @passKeys = passKeys @registerStateChange?() - # If options.trapAllKeyboardEvents is truthy, then it should be an element. All keyboard events on that + # 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 + 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 + 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 Mode.updateBadge() if @badge # End of Mode.constructor(). @@ -139,9 +141,11 @@ class Mode chooseBadge: (badge) -> badge.badge ||= @badge - # Shorthand for an otherwise long name. This allow us to write handlers which always yield the same value, - # without having to be concerned with the result of the handler itself. - alwaysContinueBubbling: (func) -> handlerStack.alwaysContinueBubbling func + # Shorthand for an otherwise long name. This wraps a handler with an arbitrary return value, and always + # yields @continueBubbling instead. This simplifies handlers if they always continue bubbling (a common + # case), because they do not need to be concerned with their return value (which helps keep code concise and + # clear). + alwaysContinueBubbling: handlerStack.alwaysContinueBubbling # User for sometimes suppressing badge updates. @badgeSuppressor: new Utils.Suppressor() @@ -152,13 +156,13 @@ class Mode @updateBadge: -> @badgeSuppressor.unlessSuppressed -> if document.hasFocus() - handlerStack.bubbleEvent "updateBadge", badge = {badge: ""} + handlerStack.bubbleEvent "updateBadge", badge = { badge: "" } chrome.runtime.sendMessage handler: "setBadge" badge: badge.badge # Temporarily install a mode to protect a function call, then exit the mode. For example, temporarily - # install an InsertModeBlocker. + # install an InsertModeBlocker, so that focus events don't unintentionally drop us into insert mode. @runIn: (mode, func) -> mode = new mode() func() @@ -181,7 +185,7 @@ class Mode # badge choice of the other active modes. # Note. We also create the the one-and-only instance, here. new class BadgeMode extends Mode - constructor: (options) -> + constructor: () -> super name: "badge" trackState: true diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 5280aada..7f1d5ddc 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -1,22 +1,4 @@ -# Input or text elements are considered focusable and able to receieve their own keyboard events, and will -# enter insert mode if focused. Also note that the "contentEditable" attribute can be set on any element -# which makes it a rich text editor, like the notes on jjot.com. -isEditable =(element) -> - return true if element.isContentEditable - nodeName = element.nodeName?.toLowerCase() - # Use a blacklist instead of a whitelist because new form controls are still being implemented for html5. - if nodeName == "input" and element.type not in ["radio", "checkbox"] - return true - nodeName in ["textarea", "select"] - -# 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 - # This mode is installed when insert mode is active. class InsertMode extends Mode constructor: (@insertModeLock = null) -> @@ -33,7 +15,7 @@ class InsertMode extends Mode exit: (event = null) -> super() element = event?.srcElement - if element and isFocusable element + if element and DomUtils.isFocusable element # 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 @@ -63,11 +45,12 @@ class InsertModeTrigger extends Mode @push focus: (event) => triggerSuppressor.unlessSuppressed => - return if not isFocusable event.target + return unless DomUtils.isFocusable 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 + if document.activeElement and DomUtils.isEditable document.activeElement + new InsertMode document.activeElement # Used by InsertModeBlocker to suppress InsertModeTrigger; see below. triggerSuppressor = new Utils.Suppressor true diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index e8248c0a..b1fc3c6f 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -123,15 +123,14 @@ initializePreDomReady = -> settings.addEventListener("load", LinkHints.init.bind(LinkHints)) settings.load() - # Install normal mode. This is at the bottom of both the mode stack and the handler stack, and is never - # deactivated. + # Install normal mode. This is near the bottom of the handler stack, and is never deactivated. new NormalMode() # Initialize the scroller. The scroller installs a key handler, and this is next on the handler stack, # immediately above normal mode. Scroller.init settings - # Install passKeys and insert modes. These too are permanently on the stack (although not always active). + # Install passKeys mode and the insert-mode trigger. These too are permanently on the stack. passKeysMode = new PassKeysMode() new InsertModeTrigger() Mode.updateBadge() -- cgit v1.2.3 From 7f2cb179432d6c93c81a26a7efb1dc42a291f11a Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 8 Jan 2015 13:16:29 +0000 Subject: Modes; fix for tests. --- content_scripts/vimium_frontend.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'content_scripts') diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index b1fc3c6f..d67f57d0 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -394,7 +394,8 @@ extend window, @exit() @continueBubbling - @exit() if visibleInputs.length == 1 + visibleInputs[selectedInputIndex].element.focus() + return @exit() if visibleInputs.length == 1 hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' exit: -> -- cgit v1.2.3 From 359fbbbcd286f16de5b23db5f4bb8dbbb2b5b6ac Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 9 Jan 2015 09:09:19 +0000 Subject: Modes; fix click handling for all "overlay" modes. From #1413... Go here: http://jsfiddle.net/smblott/9u7geasd/ In the result window: Type /Fish (do not press enter). Click in one of the text areas. Press Esc. Type aaa - you're in insert mode. Type jk - hmm, where did they go? Type o - oops, you're also in normal mode. --- content_scripts/mode_find.coffee | 8 -------- content_scripts/mode_insert.coffee | 13 +++++++++++++ content_scripts/vimium_frontend.coffee | 22 +++++++++++++--------- 3 files changed, 26 insertions(+), 17 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 2ef74a89..40245d14 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -47,13 +47,5 @@ class PostFindMode extends InsertModeBlocker 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. So - # we have to handle this case separately. - click: (event) => - @alwaysContinueBubbling => - new InsertMode element 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 7f1d5ddc..bfc79aa9 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -64,6 +64,19 @@ class InsertModeBlocker extends Mode super options @onExit -> triggerSuppressor.unsuppress() + @push + "click": (event) => + @alwaysContinueBubbling => + # The user knows best; so, if the user clicks on something, we get 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. + if document.activeElement and + event.target == document.activeElement and DomUtils.isEditable document.activeElement + new InsertMode document.activeElement + root = exports ? window root.InsertMode = InsertMode root.InsertModeTrigger = InsertModeTrigger diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index d67f57d0..97d4cd73 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -391,18 +391,21 @@ extend window, visibleInputs[selectedInputIndex].element.focus() @suppressEvent else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey - @exit() + @exit event @continueBubbling visibleInputs[selectedInputIndex].element.focus() - return @exit() if visibleInputs.length == 1 - hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' + if visibleInputs.length == 1 + @exit() + else + hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' exit: -> - DomUtils.removeElement hintContainingDiv - visibleInputs[selectedInputIndex].element.focus() - new InsertMode visibleInputs[selectedInputIndex].element super() + DomUtils.removeElement hintContainingDiv + if document.activeElement == visibleInputs[selectedInputIndex].element + # InsertModeBlocker handles the "click" case. + new InsertMode document.activeElement unless event?.type == "click" # 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 @@ -733,7 +736,7 @@ handleEnterForFindMode = -> document.body.classList.add("vimiumFindMode") settings.set("findModeRawQuery", findModeQuery.rawQuery) -class FindMode extends Mode +class FindMode extends InsertModeBlocker constructor: -> super name: "find" @@ -762,8 +765,9 @@ class FindMode extends Mode exit: (event) -> super() - handleEscapeForFindMode() if event and KeyboardUtils.isEscape event - new PostFindMode findModeAnchorNode + handleEscapeForFindMode() if event?.type == "keydown" and KeyboardUtils.isEscape event + handleEscapeForFindMode() if event?.type == "click" + new PostFindMode findModeAnchorNode unless event?.type == "click" performFindInPlace = -> cachedScrollX = window.scrollX -- cgit v1.2.3 From 5d653d8fbab350ae7737d6f91a93df10477b172d Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 9 Jan 2015 09:16:09 +0000 Subject: Modes; allow click on focusInput overlays. This is @mrmr1993's trick from #1258. See: https://github.com/philc/vimium/pull/1258/files --- content_scripts/vimium.css | 1 + 1 file changed, 1 insertion(+) (limited to 'content_scripts') diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css index ec1a09e6..e79f72d3 100644 --- a/content_scripts/vimium.css +++ b/content_scripts/vimium.css @@ -97,6 +97,7 @@ div.internalVimiumInputHint { display: block; background-color: rgba(255, 247, 133, 0.3); border: solid 1px #C38A22; + pointer-events: none; } div.internalVimiumSelectedInputHint { -- cgit v1.2.3 From 2e6eb69e99f29acc432b750501168d2a15116e6f Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 9 Jan 2015 10:51:19 +0000 Subject: Modes; various changes... - Refactor insert-mode constructor. - Gneralise focusInput. --- content_scripts/mode_find.coffee | 3 ++- content_scripts/mode_insert.coffee | 42 +++++++++++++++++++++------------- content_scripts/vimium_frontend.coffee | 19 ++++++++++----- 3 files changed, 41 insertions(+), 23 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 40245d14..3b9f951e 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -35,7 +35,8 @@ class PostFindMode extends InsertModeBlocker keydown: (event) -> if element == document.activeElement and KeyboardUtils.isEscape event self.exit() - new InsertMode element + new InsertMode + targetElement: element DomUtils.suppressKeyupAfterEscape handlerStack return false @remove() diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index bfc79aa9..c0a61d31 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -1,8 +1,8 @@ # This mode is installed when insert mode is active. class InsertMode extends Mode - constructor: (@insertModeLock = null) -> - super + constructor: (options = {}) -> + defaults = name: "insert" badge: "I" singleton: InsertMode @@ -10,18 +10,23 @@ class InsertMode extends Mode keypress: (event) => @stopBubblingAndTrue keyup: (event) => @stopBubblingAndTrue exitOnEscape: true - exitOnBlur: @insertModeLock + blurOnExit: true + + options = extend defaults, options + options.exitOnBlur = options.targetElement || null + super options exit: (event = null) -> super() - element = event?.srcElement - if element and DomUtils.isFocusable element - # 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. - element.blur() + if @options.blurOnExit + element = event?.srcElement + if element and DomUtils.isFocusable element + # 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. + element.blur() # Automatically trigger insert mode: # - On a keydown event in a contentEditable element. @@ -39,18 +44,21 @@ class InsertModeTrigger extends Mode # and unfortunately, the focus event happens *before* the change is made. Therefore, we need to # check again whether the active element is contentEditable. return @continueBubbling unless document.activeElement?.isContentEditable - new InsertMode document.activeElement + new InsertMode + targetElement: document.activeElement @stopBubblingAndTrue @push focus: (event) => triggerSuppressor.unlessSuppressed => return unless DomUtils.isFocusable event.target - new InsertMode event.target + new InsertMode + targetElement: event.target # We may already have focussed an input, so check. if document.activeElement and DomUtils.isEditable document.activeElement - new InsertMode document.activeElement + new InsertMode + targetElement: document.activeElement # Used by InsertModeBlocker to suppress InsertModeTrigger; see below. triggerSuppressor = new Utils.Suppressor true @@ -61,6 +69,7 @@ class InsertModeBlocker extends Mode constructor: (options = {}) -> triggerSuppressor.suppress() options.name ||= "insert-blocker" + options.onClickMode ||= InsertMode super options @onExit -> triggerSuppressor.unsuppress() @@ -72,10 +81,11 @@ class InsertModeBlocker extends Mode # 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. + # specially. @options.onClickMode is the mode to use. if document.activeElement and event.target == document.activeElement and DomUtils.isEditable document.activeElement - new InsertMode document.activeElement + new @options.onClickMode + targetElement: document.activeElement root = exports ? window root.InsertMode = InsertMode diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 97d4cd73..99fe4990 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -6,7 +6,7 @@ # passKeysMode = null -insertModeLock = null +targetElement = null findMode = false findModeQuery = { rawQuery: "", matchCount: 0 } findModeQueryHasResults = false @@ -342,10 +342,11 @@ extend window, enterVisualMode: => new VisualMode() - focusInput: (count) -> + focusInput: (count, targetMode = InsertMode) -> # 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 @@ -382,6 +383,8 @@ extend window, # 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 + onClickMode: targetMode # For InsertModeBlocker super-class. keydown: (event) => if event.keyCode == KeyboardUtils.keyCodes.tab hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint' @@ -404,8 +407,12 @@ extend window, super() DomUtils.removeElement hintContainingDiv if document.activeElement == visibleInputs[selectedInputIndex].element - # InsertModeBlocker handles the "click" case. - new InsertMode document.activeElement unless event?.type == "click" + # The InsertModeBlocker super-class handles the "click" case. + unless event?.type == "click" + # In the legacy (and probably common) case, we're entering insert mode here. However, it could be + # some other mode. + new @options.targetMode + targetElement: document.activeElement # 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 @@ -634,8 +641,8 @@ isEditable = (target) -> # # We cannot count on 'focus' and 'blur' events to happen sequentially. For example, if blurring element A # causes element B to come into focus, we may get "B focus" before "A blur". Thus we only leave insert mode -# when the last editable element that came into focus -- which insertModeLock points to -- has been blurred. -# If insert mode is entered manually (via pressing 'i'), then we set insertModeLock to 'undefined', and only +# when the last editable element that came into focus -- which targetElement points to -- has been blurred. +# If insert mode is entered manually (via pressing 'i'), then we set targetElement to 'undefined', and only # leave insert mode when the user presses . # Note. This returns the truthiness of target, which is required by isInsertMode. # -- cgit v1.2.3 From 6008bd71fc091918acc73c5b6f2576ad40b7764a Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 9 Jan 2015 11:35:48 +0000 Subject: Modes; always choose insert mode for click. --- content_scripts/vimium_frontend.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'content_scripts') diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 99fe4990..d91bb181 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -384,7 +384,9 @@ extend window, # shouldn't happen anyway. However, it does no harm to enforce it. singleton: FocusSelector targetMode: targetMode - onClickMode: targetMode # For InsertModeBlocker super-class. + # For the InsertModeBlocker super-class (we'll always choose InsertMode on click). See comment in + # InsertModeBlocker for an explanation of why this is needed. + onClickMode: targetMode keydown: (event) => if event.keyCode == KeyboardUtils.keyCodes.tab hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint' -- cgit v1.2.3 From dc423eff18b2b2654c175633cd11e28ea9279fd7 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 9 Jan 2015 16:53:35 +0000 Subject: Modes; rework blocker for contentEditable. --- content_scripts/mode.coffee | 4 +++- content_scripts/mode_insert.coffee | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 8e37ee36..b6cb5fae 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -175,7 +175,9 @@ 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] + console.log singletons[key].count, "singleton:", @name, "(deactivating)" + singletons[key].exit() singletons[key] = @ @onExit => delete singletons[key] if singletons[key] == @ diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index c0a61d31..5375bcdc 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -44,6 +44,7 @@ class InsertModeTrigger extends Mode # and unfortunately, the focus event happens *before* the change is made. Therefore, we need to # check again whether the active element is contentEditable. return @continueBubbling unless document.activeElement?.isContentEditable + console.log @count, @name, "fired (by keydown)" new InsertMode targetElement: document.activeElement @stopBubblingAndTrue @@ -52,6 +53,7 @@ class InsertModeTrigger extends Mode focus: (event) => triggerSuppressor.unlessSuppressed => return unless DomUtils.isFocusable event.target + console.log @count, @name, "fired (by focus)" new InsertMode targetElement: event.target -- cgit v1.2.3 From 8e65f74eea14850794ded12a0039a80e825ffa8d Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 9 Jan 2015 17:38:57 +0000 Subject: Modes; handle normal mode return values. Up until this point in the development of modes, we've just let the normal mode handlers return whatever they previously would have returned. This allowed keyboard events to continue bubbling down the stack, but didn't matter, because normal mode is the last keyboard handler on the stack. This changes that. Now, normal-mode key handlers return the right value to have the handler stack stop or continue bubbling, as appropriate. --- content_scripts/vimium_frontend.coffee | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index d91bb181..97fbc56f 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -465,17 +465,20 @@ 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 }) @@ -520,37 +523,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 +583,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() -- cgit v1.2.3 From e2380ff9417834900649173750edf17a26a8b703 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 9 Jan 2015 18:12:58 +0000 Subject: Modes; block unmapped keys with contentEditable. --- content_scripts/mode_insert.coffee | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 5375bcdc..cbeea48f 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -15,8 +15,10 @@ class InsertMode extends Mode options = extend defaults, options options.exitOnBlur = options.targetElement || null super options + triggerSuppressor.suppress() exit: (event = null) -> + triggerSuppressor.unsuppress() super() if @options.blurOnExit element = event?.srcElement @@ -44,7 +46,6 @@ class InsertModeTrigger extends Mode # and unfortunately, the focus event happens *before* the change is made. Therefore, we need to # check again whether the active element is contentEditable. return @continueBubbling unless document.activeElement?.isContentEditable - console.log @count, @name, "fired (by keydown)" new InsertMode targetElement: document.activeElement @stopBubblingAndTrue @@ -53,7 +54,6 @@ class InsertModeTrigger extends Mode focus: (event) => triggerSuppressor.unlessSuppressed => return unless DomUtils.isFocusable event.target - console.log @count, @name, "fired (by focus)" new InsertMode targetElement: event.target @@ -89,6 +89,36 @@ class InsertModeBlocker extends Mode 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: +# - they haven't been handled by any other mode (so not by normal mode, passkeys mode, insert mode, and so +# on), and +# - 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 +# handling keyboard events. +new class ContentEditableTrap extends Mode + constructor: -> + super + name: "content-editable-trap" + keydown: (event) => @handle => DomUtils.suppressPropagation event + 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. + isContentEditableFocused: -> + element = document.getSelection()?.anchorNode?.parentElement + 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 -- cgit v1.2.3 From d97e7786cb04dbbe5cae8e4b86e25437f66eb799 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 9 Jan 2015 22:08:37 +0000 Subject: Modes; fix focus return value for InsertModeTrigger. --- content_scripts/mode_insert.coffee | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index cbeea48f..9a2d5ce1 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -53,9 +53,10 @@ class InsertModeTrigger extends Mode @push focus: (event) => triggerSuppressor.unlessSuppressed => - return unless DomUtils.isFocusable event.target - new InsertMode - targetElement: event.target + @alwaysContinueBubbling => + if DomUtils.isFocusable event.target + new InsertMode + targetElement: event.target # We may already have focussed an input, so check. if document.activeElement and DomUtils.isEditable document.activeElement @@ -63,7 +64,7 @@ class InsertModeTrigger extends Mode targetElement: document.activeElement # Used by InsertModeBlocker to suppress InsertModeTrigger; see below. -triggerSuppressor = new Utils.Suppressor true +triggerSuppressor = new Utils.Suppressor true # Note: true == @continueBubbling # Suppresses InsertModeTrigger. This is used by various modes (usually by inheritance) to prevent # unintentionally dropping into insert mode on focusable elements. -- cgit v1.2.3 From ac90db47aa2671cd663cc6a9cdf783dc30a582e9 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 10 Jan 2015 00:00:11 +0000 Subject: Modes; more changes... - Better comments. - Strip unnecessary handlers for leaving post-find mode. - Simplify passKeys. - focusInput now re-bubbles its triggering keydown event. --- content_scripts/mode.coffee | 3 +- content_scripts/mode_find.coffee | 17 ++++------- content_scripts/mode_insert.coffee | 53 +++++++++++++++++----------------- content_scripts/mode_passkeys.coffee | 13 ++++----- content_scripts/vimium_frontend.coffee | 25 ++++++++-------- 5 files changed, 54 insertions(+), 57 deletions(-) (limited to 'content_scripts') 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 = -> -- cgit v1.2.3 From fdcdd0113049042c94b2b56a6b716e2da58b860e Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 10 Jan 2015 08:25:02 +0000 Subject: Modes; instrument for debugging... - Set Mode.debug to true to see mode activation/deactivation on the console. - Use Mode.log() to see a list of currently-active modes. - Use handlerStack.debugOn() to enable debugging of the handler stack. --- content_scripts/mode.coffee | 55 +++++++++++++++++++++++++--------- content_scripts/mode_find.coffee | 1 + content_scripts/mode_insert.coffee | 20 +++++++------ content_scripts/scroller.coffee | 15 ++++++---- content_scripts/vimium_frontend.coffee | 14 ++++++--- 5 files changed, 73 insertions(+), 32 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 37f3a8c2..a33197b0 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -38,12 +38,14 @@ # 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; short, readable names for handlerStack event-handler return values. continueBubbling: true @@ -60,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 @@ -80,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 @@ -90,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, @@ -98,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 @@ -110,30 +116,38 @@ class Mode # 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 + _name: "mode-#{@id}/trapAllKeyboardEvents" + keydown: (event) => + if event.srcElement == @options.trapAllKeyboardEvents then @suppressEvent else @continueBubbling + keypress: (event) => + if event.srcElement == @options.trapAllKeyboardEvents then @suppressEvent else @continueBubbling + keyup: (event) => + if event.srcElement == @options.trapAllKeyboardEvents then @suppressEvent else @continueBubbling 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 @@ -177,12 +191,24 @@ class Mode # flickering in some cases. Mode.badgeSuppressor.runSuppresed => if singletons[key] - console.log singletons[key].count, "singleton:", @name, "(deactivating)" + @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. @@ -194,6 +220,7 @@ new class BadgeMode extends Mode trackState: true @push + _name: "mode-#{@id}/focus" "focus": => @alwaysContinueBubbling -> Mode.updateBadge() chooseBadge: (badge) -> diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index d63b3319..0ce03af6 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -33,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() diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 144b0be6..7668d794 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -51,6 +51,7 @@ class InsertModeTrigger extends Mode @stopBubblingAndTrue @push + _name: "mode-#{@id}/activate-on-focus" focus: (event) => triggerSuppressor.unlessSuppressed => @alwaysContinueBubbling => @@ -78,6 +79,7 @@ class InsertModeBlocker extends Mode @onExit -> triggerSuppressor.unsuppress() @push + _name: "mode-#{@id}/bail-on-click" "click": (event) => @alwaysContinueBubbling => # The user knows best; so, if the user clicks on something, the insert-mode blocker gets out of the @@ -92,18 +94,18 @@ class InsertModeBlocker extends Mode 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 -# 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), +# There's some unfortunate feature interaction with chrome's contentEditable handling. If the selection is +# contentEditable and a descendant of the active element, then chrome focuses it on any unsuppressed keyboard +# 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, insert, ...), # - 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, we shouldn't usually be blocking keyboard events for other extensions or # the page itself. -# handling keyboard events. +# 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. new class ContentEditableTrap extends Mode constructor: -> super @@ -112,10 +114,10 @@ new class ContentEditableTrap extends Mode keypress: (event) => @handle => @suppressEvent keyup: (event) => @handle => @suppressEvent - handle: (func) -> if @isContentEditableFocused() then func() else @continueBubbling + handle: (func) -> if @wouldTriggerInsert() then func() else @continueBubbling # True if the selection is content editable and a descendant of the active element. - isContentEditableFocused: -> + wouldTriggerInsert: -> element = document.getSelection()?.anchorNode?.parentElement return element?.isContentEditable and document.activeElement and diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index 889dc042..f70d3aed 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -124,12 +124,15 @@ CoreScroller = @keyIsDown = false handlerStack.push + _name: 'scroller/track-key-down/up' 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 a9bf30a3..1406b1e7 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -396,10 +396,16 @@ extend window, visibleInputs[selectedInputIndex].element.focus() @suppressEvent else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey - @exit event - # 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 + 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() return @exit() if visibleInputs.length == 1 -- cgit v1.2.3 From 2199ad1bf9a7b063cc68a8e75f7a4a76ba125588 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 10 Jan 2015 11:31:57 +0000 Subject: Modes; revert to master's handling of #1415. The behaviour in the situations described in #1415 require more thought and discussion. --- content_scripts/mode_find.coffee | 9 ++------- content_scripts/mode_insert.coffee | 29 ----------------------------- 2 files changed, 2 insertions(+), 36 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 0ce03af6..740358d5 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -1,14 +1,10 @@ # NOTE(smblott). Ultimately, all of the FindMode-related code should be moved to this file. # 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: +# special considerations apply. We implement two special cases: # 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 -# alternative. -# 3. If the very-next keystroke is Escape, then drop immediately into insert mode. +# 2. If the very-next keystroke is Escape, then drop immediately into insert mode. # class PostFindMode extends InsertModeBlocker constructor: (findModeAnchorNode) -> @@ -20,7 +16,6 @@ class PostFindMode extends InsertModeBlocker # instance is automatically deactivated when a new instance is activated. singleton: PostFindMode exitOnBlur: element - trapAllKeyboardEvents: element return @exit() unless element and findModeAnchorNode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 7668d794..1887adbc 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -94,35 +94,6 @@ class InsertModeBlocker extends Mode new @options.onClickMode targetElement: document.activeElement -# There's some unfortunate feature interaction with chrome's contentEditable handling. If the selection is -# contentEditable and a descendant of the active element, then chrome focuses it on any unsuppressed keyboard -# 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, insert, ...), -# - 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, we shouldn't usually be blocking keyboard events for other extensions or -# the page itself. -# 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. -new class ContentEditableTrap extends Mode - constructor: -> - super - name: "content-editable-trap" - keydown: (event) => @handle => DomUtils.suppressPropagation event - keypress: (event) => @handle => @suppressEvent - keyup: (event) => @handle => @suppressEvent - - handle: (func) -> if @wouldTriggerInsert() then func() else @continueBubbling - - # True if the selection is content editable and a descendant of the active element. - wouldTriggerInsert: -> - element = document.getSelection()?.anchorNode?.parentElement - return element?.isContentEditable and - document.activeElement and - DomUtils.isDOMDescendant document.activeElement, element - root = exports ? window root.InsertMode = InsertMode root.InsertModeTrigger = InsertModeTrigger -- cgit v1.2.3 From 35cb54fec7242fac5c68503a32ef9dd4fea5d9b6 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 10 Jan 2015 12:17:22 +0000 Subject: Modes; minor changes. --- content_scripts/mode_insert.coffee | 4 +++- content_scripts/scroller.coffee | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 1887adbc..5720c901 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -1,5 +1,6 @@ -# This mode is installed only 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 = @@ -13,6 +14,7 @@ class InsertMode extends Mode blurOnExit: true targetElement: null + # If options.targetElement blurs, we exit. options.exitOnBlur ||= options.targetElement super extend defaults, options triggerSuppressor.suppress() diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index f70d3aed..6e2e1ffc 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -124,7 +124,7 @@ CoreScroller = @keyIsDown = false handlerStack.push - _name: 'scroller/track-key-down/up' + _name: 'scroller/track-key-status' keydown: (event) => handlerStack.alwaysContinueBubbling => @keyIsDown = true -- cgit v1.2.3 From c554d1fd5b6d81506864516b6f86a14f8672bec5 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 10 Jan 2015 15:05:58 +0000 Subject: Modes; reinstate key blockers: - when the selection is contentEditable - in PostFindMode Restricted to printable characters. --- content_scripts/mode.coffee | 20 +++++++++----------- content_scripts/mode_find.coffee | 9 +++++++-- content_scripts/mode_insert.coffee | 28 ++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 13 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index a33197b0..5c6201e0 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -111,23 +111,21 @@ 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 + # If @options.suppressPrintableEvents is truthy, then it should be an element. All printable keyboard + # events on that element are suppressed, if necessary (that is, *after* bubbling down the handler stack). + # We only suppress keypress events. This is used by PostFindMode to protect active, editable elements. + # Note: We use unshift here, not push, so the handler is installed at the bottom of the stack. + if @options.suppressPrintableEvents @unshift - _name: "mode-#{@id}/trapAllKeyboardEvents" - keydown: (event) => - if event.srcElement == @options.trapAllKeyboardEvents then @suppressEvent else @continueBubbling + _name: "mode-#{@id}/suppressPrintableEvents" keypress: (event) => - if event.srcElement == @options.trapAllKeyboardEvents then @suppressEvent else @continueBubbling - keyup: (event) => - if event.srcElement == @options.trapAllKeyboardEvents then @suppressEvent else @continueBubbling + if DomUtils.isPrintable(event) and + event.srcElement == @options.suppressPrintableEvents then @suppressEvent else @continueBubbling Mode.updateBadge() if @badge Mode.modes.push @ @log() if @debug - handlerStack.debugOn() + # handlerStack.debugOn() # End of Mode constructor. push: (handlers) -> diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 740358d5..91ae4507 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -1,10 +1,14 @@ # NOTE(smblott). Ultimately, all of the FindMode-related code should be moved to this file. # When we use find mode, the selection/focus can end up in a focusable/editable element. In this situation, -# special considerations apply. We implement two special cases: +# special considerations apply. We implement three special cases: # 1. Prevent keyboard events from dropping us unintentionally into insert mode. This is achieved by # inheriting from InsertModeBlocker. -# 2. If the very-next keystroke is Escape, then drop immediately into insert mode. +# 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. # class PostFindMode extends InsertModeBlocker constructor: (findModeAnchorNode) -> @@ -16,6 +20,7 @@ class PostFindMode extends InsertModeBlocker # instance is automatically deactivated when a new instance is activated. singleton: PostFindMode exitOnBlur: element + suppressPrintableEvents: element return @exit() unless element and findModeAnchorNode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 5720c901..b907f22e 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -96,6 +96,34 @@ class InsertModeBlocker extends Mode new @options.onClickMode targetElement: document.activeElement +# There's an unfortunate feature interaction between chrome's contentEditable handling and our insert mode. +# If the selection is contentEditable and a descendant of the active element, then chrome focuses it on any +# unsuppressed printable keypress. This drops us unintentally into insert mode. See #1415. A single +# instance of this mode sits near the bottom of the handler stack and suppresses each keypress event if: +# - it hasn't been handled by any other mode (so not by normal mode, passkeys, insert, ...), +# - it represents a printable character, +# - 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, we shouldn't usually be blocking keyboard events for other extensions or +# the page itself. +# There's some controversy as to whether this is the right thing to do. See discussion in #1415. This +# implements Option 2 from there. +new class ContentEditableTrap extends Mode + constructor: -> + super + name: "content-editable-trap" + keypress: (event) => + if @wouldTriggerInsert event then @suppressEvent else @continueBubbling + + # True if the selection is content editable and a descendant of the active element. + wouldTriggerInsert: (event) -> + element = document.getSelection()?.anchorNode?.parentElement + return element?.isContentEditable and + document.activeElement and + DomUtils. isPrintable event and + DomUtils.isDOMDescendant document.activeElement, element + root = exports ? window root.InsertMode = InsertMode root.InsertModeTrigger = InsertModeTrigger -- cgit v1.2.3 From 704ae28629154a732e20e16d56b23af265d51b85 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 10 Jan 2015 16:01:40 +0000 Subject: Modes; better printable detection, move to keyboard_utils. --- content_scripts/mode.coffee | 2 +- content_scripts/mode_insert.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 5c6201e0..19354d94 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -119,7 +119,7 @@ class Mode @unshift _name: "mode-#{@id}/suppressPrintableEvents" keypress: (event) => - if DomUtils.isPrintable(event) and + if KeyboardUtils.isPrintable(event) and event.srcElement == @options.suppressPrintableEvents then @suppressEvent else @continueBubbling Mode.updateBadge() if @badge diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index b907f22e..31bae8ec 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -121,7 +121,7 @@ new class ContentEditableTrap extends Mode element = document.getSelection()?.anchorNode?.parentElement return element?.isContentEditable and document.activeElement and - DomUtils. isPrintable event and + KeyboardUtils.isPrintable(event) and DomUtils.isDOMDescendant document.activeElement, element root = exports ? window -- cgit v1.2.3 From 80ad0bc3087a3bf00d61bdd6c9cf48e971e22480 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 10 Jan 2015 18:52:24 +0000 Subject: Modes; re-architect key suppression and passkeys. --- content_scripts/mode.coffee | 33 ++++++++++++++++++++++++++------- content_scripts/mode_insert.coffee | 28 ---------------------------- content_scripts/mode_passkeys.coffee | 4 ++-- content_scripts/vimium_frontend.coffee | 2 ++ 4 files changed, 30 insertions(+), 37 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 19354d94..0fcab675 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -111,16 +111,18 @@ 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 (that is, *after* bubbling down the handler stack). - # We only suppress keypress events. This is used by PostFindMode to protect active, editable elements. - # Note: We use unshift here, not push, so the handler is installed at the bottom of the stack. + # 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 - @unshift + @push _name: "mode-#{@id}/suppressPrintableEvents" keypress: (event) => - if KeyboardUtils.isPrintable(event) and - event.srcElement == @options.suppressPrintableEvents then @suppressEvent else @continueBubbling + @alwaysContinueBubbling => + if event.srcElement == @options.suppressPrintableEvents + if KeyboardUtils.isPrintable(event) + event.vimium_suppress_event = true Mode.updateBadge() if @badge Mode.modes.push @ @@ -217,6 +219,9 @@ 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() @@ -228,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_insert.coffee b/content_scripts/mode_insert.coffee index 31bae8ec..5720c901 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -96,34 +96,6 @@ class InsertModeBlocker extends Mode new @options.onClickMode targetElement: document.activeElement -# There's an unfortunate feature interaction between chrome's contentEditable handling and our insert mode. -# If the selection is contentEditable and a descendant of the active element, then chrome focuses it on any -# unsuppressed printable keypress. This drops us unintentally into insert mode. See #1415. A single -# instance of this mode sits near the bottom of the handler stack and suppresses each keypress event if: -# - it hasn't been handled by any other mode (so not by normal mode, passkeys, insert, ...), -# - it represents a printable character, -# - 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, we shouldn't usually be blocking keyboard events for other extensions or -# the page itself. -# There's some controversy as to whether this is the right thing to do. See discussion in #1415. This -# implements Option 2 from there. -new class ContentEditableTrap extends Mode - constructor: -> - super - name: "content-editable-trap" - keypress: (event) => - if @wouldTriggerInsert event then @suppressEvent else @continueBubbling - - # True if the selection is content editable and a descendant of the active element. - wouldTriggerInsert: (event) -> - element = document.getSelection()?.anchorNode?.parentElement - return element?.isContentEditable and - document.activeElement and - KeyboardUtils.isPrintable(event) and - DomUtils.isDOMDescendant document.activeElement, element - root = exports ? window root.InsertMode = InsertMode root.InsertModeTrigger = InsertModeTrigger diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee index 112e14ed..c4df06dc 100644 --- a/content_scripts/mode_passkeys.coffee +++ b/content_scripts/mode_passkeys.coffee @@ -12,8 +12,8 @@ class PassKeysMode extends Mode # 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. handleKeyChar: (keyChar) -> - return @stopBubblingAndTrue if keyChar and not @keyQueue and 0 <= @passKeys.indexOf keyChar - @continueBubbling + @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? diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 1406b1e7..0da59f03 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -462,6 +462,7 @@ KeydownEvents = # onKeypress = (event) -> + return true if event.vimium_suppress_normal_mode keyChar = "" # Ignore modifier keys by themselves. @@ -491,6 +492,7 @@ onKeypress = (event) -> return true 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 -- cgit v1.2.3 From 06a2ea5ccdef3703df23fe4233921bd2a6af3abf Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 11 Jan 2015 05:22:03 +0000 Subject: Modes; various tweeks. --- content_scripts/mode.coffee | 4 ++-- content_scripts/mode_passkeys.coffee | 6 ++++-- content_scripts/vimium_frontend.coffee | 2 -- 3 files changed, 6 insertions(+), 6 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 0fcab675..a56a3215 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -212,7 +212,7 @@ class Mode # 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. -# Note. We also create the the one-and-only instance, here. +# Note. We create the the one-and-only instance, here. new class BadgeMode extends Mode constructor: () -> super @@ -236,7 +236,7 @@ new class BadgeMode extends Mode # 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. +# Note. We create the the one-and-only instance, here. new class KeySuppressor extends Mode constructor: -> super diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee index c4df06dc..dde91c13 100644 --- a/content_scripts/mode_passkeys.coffee +++ b/content_scripts/mode_passkeys.coffee @@ -12,8 +12,10 @@ class PassKeysMode extends Mode # 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. handleKeyChar: (keyChar) -> - @alwaysContinueBubbling => - event.vimium_suppress_normal_mode = true if keyChar and not @keyQueue and 0 <= @passKeys.indexOf keyChar + if keyChar and not @keyQueue and 0 <= @passKeys.indexOf keyChar + @stopBubblingAndTrue + else + @continueBubbling configure: (request) -> @keyQueue = request.keyQueue if request.keyQueue? diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 0da59f03..1406b1e7 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -462,7 +462,6 @@ KeydownEvents = # onKeypress = (event) -> - return true if event.vimium_suppress_normal_mode keyChar = "" # Ignore modifier keys by themselves. @@ -492,7 +491,6 @@ onKeypress = (event) -> return true 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 -- cgit v1.2.3 From d65075a3b66fae93a10b849162fa907d0eb99846 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 11 Jan 2015 07:15:06 +0000 Subject: Modes; add DOM tests. --- content_scripts/mode.coffee | 2 +- content_scripts/vimium_frontend.coffee | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index a56a3215..46f5c3b7 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -44,7 +44,7 @@ count = 0 class Mode # 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 + debug: false @modes: [] # Constants; short, readable names for handlerStack event-handler return values. diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 1406b1e7..ed5844dc 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -612,7 +612,8 @@ checkIfEnabledForUrl = -> enabled: response.isEnabledForUrl passKeys: response.passKeys -refreshCompletionKeys = (response) -> +# Exported to window, but only for DOM tests. +window.refreshCompletionKeys = (response) -> if (response) currentCompletionKeys = response.completionKeys -- cgit v1.2.3 From e8f10007f1528808f72be6fac829cc55309527f2 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 11 Jan 2015 07:15:20 +0000 Subject: Modes; add DOM tests. --- content_scripts/mode.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 46f5c3b7..6e40089e 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -44,7 +44,7 @@ count = 0 class Mode # 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: false + debug: true @modes: [] # Constants; short, readable names for handlerStack event-handler return values. @@ -121,7 +121,7 @@ class Mode keypress: (event) => @alwaysContinueBubbling => if event.srcElement == @options.suppressPrintableEvents - if KeyboardUtils.isPrintable(event) + if KeyboardUtils.isPrintable event event.vimium_suppress_event = true Mode.updateBadge() if @badge -- cgit v1.2.3 From 355bb5fb2a06b4465a354350e2fa78ab5d53cb0b Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 11 Jan 2015 10:20:54 +0000 Subject: Modes; rework debugging support. --- content_scripts/mode.coffee | 26 ++++++++++++-------------- content_scripts/mode_find.coffee | 5 ++--- 2 files changed, 14 insertions(+), 17 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 6e40089e..61e51b1c 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -63,7 +63,7 @@ class Mode @count = ++count @id = "#{@name}-#{@count}" - @logger "activate:", @id if @debug + @log "activate:", @id if @debug @push keydown: @options.keydown || null @@ -126,7 +126,7 @@ class Mode Mode.updateBadge() if @badge Mode.modes.push @ - @log() if @debug + @logStack() if @debug # handlerStack.debugOn() # End of Mode constructor. @@ -144,7 +144,7 @@ class Mode exit: -> if @modeIsActive - @logger "deactivate:", @id if @debug + @log "deactivate:", @id if @debug handler() for handler in @exitHandlers handlerStack.remove handlerId for handlerId in @handlers Mode.modes = Mode.modes.filter (mode) => mode != @ @@ -191,23 +191,20 @@ class Mode # flickering in some cases. Mode.badgeSuppressor.runSuppresed => if singletons[key] - @logger "singleton:", "deactivating #{singletons[key].id}" if @debug + @log "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 + logStack: -> + @log "active modes (top to bottom):" + for mode in Mode.modes[..].reverse() + @log " ", mode.id - logger: (args...) -> - handlerStack.log args... + log: (args...) -> + console.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 @@ -245,7 +242,8 @@ new class KeySuppressor extends Mode keypress: (event) => @handle event keyup: (event) => @handle event - handle: (event) -> if event.vimium_suppress_event then @suppressEvent else @continueBubbling + 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 91ae4507..30f136e9 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -4,10 +4,9 @@ # special considerations apply. We implement three special cases: # 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 +# 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, although Option 3 would be a reasonable -# alternative. +# See discussion in #1415. This implements Option 2 from there. # 3. If the very-next keystroke is Escape, then drop immediately into insert mode. # class PostFindMode extends InsertModeBlocker -- cgit v1.2.3 From a8096d235eae39d309c0ffd74e0d2493ff12dd22 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 11 Jan 2015 11:42:02 +0000 Subject: Modes; tweek tests. --- content_scripts/mode.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 61e51b1c..84e3e75c 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -44,7 +44,7 @@ count = 0 class Mode # 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 + debug: false @modes: [] # Constants; short, readable names for handlerStack event-handler return values. -- cgit v1.2.3 From f76c15c6ae6565c0c08569a127974dfd3383ed88 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 11 Jan 2015 12:19:19 +0000 Subject: Modes; tweaks, including more tests. --- content_scripts/mode.coffee | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 84e3e75c..dd797c46 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -189,10 +189,9 @@ class Mode # We're currently installing a new mode. So we'll be updating the badge shortly. Therefore, we can # suppress badge updates while exiting any existing active singleton. This prevents the badge from # flickering in some cases. - Mode.badgeSuppressor.runSuppresed => - if singletons[key] - @log "singleton:", "deactivating #{singletons[key].id}" if @debug - singletons[key].exit() + if singletons[key] + @log "singleton:", "deactivating #{singletons[key].id}" if @debug + Mode.badgeSuppressor.runSuppresed -> singletons[key].exit() singletons[key] = @ @onExit => delete singletons[key] if singletons[key] == @ @@ -216,7 +215,7 @@ 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 + # FIXME(smblott) BadgeMode is currently triggering an 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 -- cgit v1.2.3 From 8066a3838ef44b010f6dfb46cea8b47c6bdfc087 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 11 Jan 2015 15:01:12 +0000 Subject: Modes; yet more tweaks, yet more tests. --- content_scripts/mode.coffee | 14 ++++++++---- content_scripts/vimium_frontend.coffee | 42 ++++++++++++++++++---------------- 2 files changed, 32 insertions(+), 24 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index dd797c46..1c6cddb1 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -44,7 +44,7 @@ count = 0 class Mode # 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: false + debug: true @modes: [] # Constants; short, readable names for handlerStack event-handler return values. @@ -205,10 +205,14 @@ class Mode log: (args...) -> console.log args... + # Return the name of the must-recently activated mode. + @top: -> + @modes[@modes.length-1]?.name + # 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. -# Note. We create the the one-and-only instance, here. +# Note. We create the the one-and-only instance here. new class BadgeMode extends Mode constructor: () -> super @@ -223,16 +227,18 @@ new class BadgeMode extends Mode "focus": => @alwaysContinueBubbling -> Mode.updateBadge() chooseBadge: (badge) -> - # If we're not enabled, then post an empty badge. + # If we're not enabled, then post an empty badge. BadgeMode is last, so this takes priority. badge.badge = "" unless @enabled + # When the registerStateChange event bubbles to the bottom of the stack, all modes have been notified. So + # it's now time to update the badge. 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 create the the one-and-only instance, here. +# Note. We create the the one-and-only instance here. new class KeySuppressor extends Mode constructor: -> super diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index ed5844dc..09e19486 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -108,9 +108,9 @@ class NormalMode extends Mode super name: "normal" badge: "N" - keydown: onKeydown - keypress: onKeypress - keyup: onKeyup + keydown: (event) => onKeydown.call @, event + keypress: (event) => onKeypress.call @, event + keyup: (event) => onKeyup.call @, event chooseBadge: (badge) -> super badge @@ -460,7 +460,7 @@ KeydownEvents = # # Note that some keys will only register keydown events and not keystroke events, e.g. ESC. # - +# @/this, here, is the the normal-mode Mode object. onKeypress = (event) -> keyChar = "" @@ -471,25 +471,26 @@ onKeypress = (event) -> # Enter insert mode when the user enables the native find interface. if (keyChar == "f" && KeyboardUtils.isPrimaryModifierKey(event)) enterInsertModeWithoutShowingIndicator() - return handlerStack.stopBubblingAndTrue + return @stopBubblingAndTrue if (keyChar) if (findMode) handleKeyCharForFindMode(keyChar) DomUtils.suppressEvent(event) - return handlerStack.stopBubblingAndTrue + return @stopBubblingAndTrue else if (!isInsertMode() && !findMode) if (isPassKey keyChar) - return handlerStack.stopBubblingAndTrue + return @stopBubblingAndTrue if currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar) DomUtils.suppressEvent(event) keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) - return handlerStack.stopBubblingAndTrue + return @stopBubblingAndTrue keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) - return true + return @continueBubbling +# @/this, here, is the the normal-mode Mode object. onKeydown = (event) -> keyChar = "" @@ -529,37 +530,37 @@ onKeydown = (event) -> exitInsertMode() DomUtils.suppressEvent event KeydownEvents.push event - return handlerStack.stopBubblingAndTrue + return @stopBubblingAndTrue else if (findMode) if (KeyboardUtils.isEscape(event)) handleEscapeForFindMode() DomUtils.suppressEvent event KeydownEvents.push event - return handlerStack.stopBubblingAndTrue + return @stopBubblingAndTrue else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) handleDeleteForFindMode() DomUtils.suppressEvent event KeydownEvents.push event - return handlerStack.stopBubblingAndTrue + return @stopBubblingAndTrue else if (event.keyCode == keyCodes.enter) handleEnterForFindMode() DomUtils.suppressEvent event KeydownEvents.push event - return handlerStack.stopBubblingAndTrue + return @stopBubblingAndTrue else if (!modifiers) DomUtils.suppressPropagation(event) KeydownEvents.push event - return handlerStack.stopBubblingAndTrue + return @stopBubblingAndTrue else if (isShowingHelpDialog && KeyboardUtils.isEscape(event)) hideHelpDialog() DomUtils.suppressEvent event KeydownEvents.push event - return handlerStack.stopBubblingAndTrue + return @stopBubblingAndTrue else if (!isInsertMode() && !findMode) if (keyChar) @@ -567,7 +568,7 @@ onKeydown = (event) -> DomUtils.suppressEvent event KeydownEvents.push event keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) - return handlerStack.stopBubblingAndTrue + return @stopBubblingAndTrue keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) @@ -589,14 +590,15 @@ onKeydown = (event) -> isValidFirstKey(KeyboardUtils.getKeyChar(event)))) DomUtils.suppressPropagation(event) KeydownEvents.push event - return handlerStack.stopBubblingAndTrue + return @stopBubblingAndTrue - return true + return @continueBubbling +# @/this, here, is the the normal-mode Mode object. onKeyup = (event) -> - return true unless KeydownEvents.pop event + return @continueBubbling unless KeydownEvents.pop event DomUtils.suppressPropagation(event) - handlerStack.stopBubblingAndTrue + @stopBubblingAndTrue checkIfEnabledForUrl = -> url = window.location.toString() -- cgit v1.2.3 From c5f3abacb27afd2c014a18bede8e6281e272a14a Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 11 Jan 2015 17:37:50 +0000 Subject: Modes; incorporate link hints. --- content_scripts/link_hints.coffee | 51 ++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 20 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 9f21d109..6738c499 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -8,13 +8,16 @@ # 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. # -OPEN_IN_CURRENT_TAB = {} -OPEN_IN_NEW_BG_TAB = {} -OPEN_IN_NEW_FG_TAB = {} -OPEN_WITH_QUEUE = {} -COPY_LINK_URL = {} -OPEN_INCOGNITO = {} -DOWNLOAD_LINK_URL = {} +# The "name" property here 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: "C" } +OPEN_IN_NEW_BG_TAB = { name: "bg-tab", key: "B" } +OPEN_IN_NEW_FG_TAB = { name: "fg-tab", key: "F" } +OPEN_WITH_QUEUE = { name: "queue", key: "Q" } +COPY_LINK_URL = { name: "link", key: "C" } +OPEN_INCOGNITO = { name: "incognito", key: "I" } +DOWNLOAD_LINK_URL = { name: "download", key: "D" } LinkHints = hintMarkerContainingDiv: null @@ -62,13 +65,21 @@ LinkHints = @hintMarkerContainingDiv = DomUtils.addElementList(hintMarkers, { id: "vimiumHintMarkerContainer", className: "vimiumReset" }) - # handlerStack is declared by vimiumFrontend.js - @handlerId = handlerStack.push({ - keydown: @onKeyDownInMode.bind(this, hintMarkers), - # trap all key events - keypress: -> false - keyup: -> false - }) + @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 key events + keypress: => @stopBubblingAndFalse + keyup: => @stopBubblingAndFalse + + exit: (delay, callback) => + super() + LinkHints.deactivateMode delay, callback setOpenLinkMode: (@mode) -> if @mode is OPEN_IN_NEW_BG_TAB or @mode is OPEN_IN_NEW_FG_TAB or @mode is OPEN_WITH_QUEUE @@ -267,13 +278,14 @@ LinkHints = # TODO(philc): Ignore keys that have modifiers. if (KeyboardUtils.isEscape(event)) - @deactivateMode() + # TODO(smblott). Now unreachable. Clean up. Left like this for now to keep the diff clean. + @handlerMode.exit() else keyResult = @getMarkerMatcher().matchHintsByKey(hintMarkers, event) linksMatched = keyResult.linksMatched delay = keyResult.delay ? 0 if (linksMatched.length == 0) - @deactivateMode() + @handlerMode.exit() else if (linksMatched.length == 1) @activateLink(linksMatched[0], delay) else @@ -291,7 +303,7 @@ LinkHints = clickEl = matchedLink.clickableItem if (DomUtils.isSelectable(clickEl)) DomUtils.simulateSelect(clickEl) - @deactivateMode(delay, -> LinkHints.delayMode = false) + @handlerMode.exit delay, -> LinkHints.delayMode = false else # TODO figure out which other input elements should not receive focus if (clickEl.nodeName.toLowerCase() == "input" && clickEl.type != "button") @@ -299,11 +311,11 @@ LinkHints = DomUtils.flashRect(matchedLink.rect) @linkActivator(clickEl) if @mode is OPEN_WITH_QUEUE - @deactivateMode delay, -> + @handlerMode.exit delay, -> LinkHints.delayMode = false LinkHints.activateModeWithQueue() else - @deactivateMode(delay, -> LinkHints.delayMode = false) + @handlerMode.exit delay, -> LinkHints.delayMode = false # # Shows the marker, highlighting matchingCharCount characters. @@ -330,7 +342,6 @@ LinkHints = if (LinkHints.hintMarkerContainingDiv) DomUtils.removeElement LinkHints.hintMarkerContainingDiv LinkHints.hintMarkerContainingDiv = null - handlerStack.remove @handlerId HUD.hide() @isActive = false -- cgit v1.2.3 From 0dbc181e577d0699fd25fd5f5f39e2ffb35a4f07 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 12 Jan 2015 07:50:50 +0000 Subject: Modes; re-work key suppression for PostFindMode. --- content_scripts/mode.coffee | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 1c6cddb1..94854b74 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -114,15 +114,17 @@ class Mode # 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. + # 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 - @push + @unshift _name: "mode-#{@id}/suppressPrintableEvents" keypress: (event) => - @alwaysContinueBubbling => - if event.srcElement == @options.suppressPrintableEvents - if KeyboardUtils.isPrintable event - event.vimium_suppress_event = true + if event.srcElement == @options.suppressPrintableEvents and KeyboardUtils.isPrintable event + @suppressEvent + else + @continueBubbling Mode.updateBadge() if @badge Mode.modes.push @ @@ -235,20 +237,5 @@ 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 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 -- cgit v1.2.3 From ee17e7365b84d0c59ce3bcf50c517a7408b372b3 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 12 Jan 2015 09:28:32 +0000 Subject: Modes; (slightly) nicer badge. --- content_scripts/link_hints.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 6738c499..909f50b4 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -11,7 +11,7 @@ # The "name" property here 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: "C" } +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" } OPEN_WITH_QUEUE = { name: "queue", key: "Q" } @@ -70,7 +70,7 @@ LinkHints = constructor: -> super name: "hint/#{mode.name}" - badge: "?#{mode.key}" + badge: "#{mode.key}?" exitOnEscape: true keydown: (event) -> LinkHints.onKeyDownInMode hintMarkers, event # trap all key events -- cgit v1.2.3 From 88e4f1587f30b928ae7cce5d9e01e93b27c87f55 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 12 Jan 2015 15:46:57 +0000 Subject: Modes; hint mode should be an insert-mode blocker... Also: - visual-mode template should block insert. - hint mode should exit on click. --- content_scripts/link_hints.coffee | 3 ++- content_scripts/mode.coffee | 6 ++++++ content_scripts/mode_visual.coffee | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 909f50b4..ad2afaa2 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -66,12 +66,13 @@ LinkHints = { id: "vimiumHintMarkerContainer", className: "vimiumReset" }) @handlerMode = - new class HintMode extends Mode + new class HintMode extends InsertModeBlocker constructor: -> super name: "hint/#{mode.name}" badge: "#{mode.key}?" exitOnEscape: true + exitOnClick: true keydown: (event) -> LinkHints.onKeyDownInMode hintMarkers, event # trap all key events keypress: => @stopBubblingAndFalse diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 94854b74..51bb1c29 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -97,6 +97,12 @@ class Mode _name: "mode-#{@id}/exitOnBlur" "blur": (event) => @alwaysContinueBubbling => @exit() if event.srcElement == @options.exitOnBlur + # If @options.exitOnClick is truthy, then the mode will exit on any click event. + if @options.exitOnClick + @push + _name: "mode-#{@id}/exitOnClick" + "click": (event) => @alwaysContinueBubbling => @exit() + # 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 diff --git a/content_scripts/mode_visual.coffee b/content_scripts/mode_visual.coffee index 2580106d..7b5cc0f6 100644 --- a/content_scripts/mode_visual.coffee +++ b/content_scripts/mode_visual.coffee @@ -1,5 +1,5 @@ -class VisualMode extends Mode +class VisualMode extends InsertModeBlocker constructor: (element=null) -> super name: "visual" -- cgit v1.2.3 From eb8c5587cec5a629dfbddf8a884a2aa6fc06f436 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 13 Jan 2015 05:09:46 +0000 Subject: Modes; block all printable keyboard events. --- content_scripts/mode.coffee | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 51bb1c29..d06f5eae 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -117,21 +117,26 @@ class Mode @passKeys = passKeys @registerStateChange?() - # If @options.suppressPrintableEvents is truthy, then it should be an element. All printable keypress + # 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 - @unshift - _name: "mode-#{@id}/suppressPrintableEvents" - keypress: (event) => + 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 -- cgit v1.2.3 From 7684019cb5d5c1d0ac5d7216653613220b4fd8d9 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 13 Jan 2015 05:50:18 +0000 Subject: Modes; enter insert mode if the selection changes. See discussion in #1415. --- content_scripts/mode_find.coffee | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'content_scripts') diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 30f136e9..519e99ad 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -12,6 +12,7 @@ class PostFindMode extends InsertModeBlocker constructor: (findModeAnchorNode) -> element = document.activeElement + initialSelection = window.getSelection().toString() super name: "post-find" @@ -20,6 +21,15 @@ class PostFindMode extends InsertModeBlocker 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: => + @alwaysContinueBubbling => + if window.getSelection().toString() != initialSelection + @exit() + new InsertMode + targetElement: element return @exit() unless element and findModeAnchorNode -- cgit v1.2.3 From 1191d73c6fea65bcd4ceec807458e81a1a940047 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 13 Jan 2015 10:07:26 +0000 Subject: Modes; temporary commit. --- content_scripts/link_hints.coffee | 5 ++--- content_scripts/mode.coffee | 4 +++- content_scripts/mode_insert.coffee | 31 +++++++++++++++++-------------- 3 files changed, 22 insertions(+), 18 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index ad2afaa2..0668e3ae 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -72,15 +72,14 @@ LinkHints = name: "hint/#{mode.name}" badge: "#{mode.key}?" exitOnEscape: true - exitOnClick: true keydown: (event) -> LinkHints.onKeyDownInMode hintMarkers, event - # trap all key events + # trap all other keyboard events keypress: => @stopBubblingAndFalse keyup: => @stopBubblingAndFalse exit: (delay, callback) => - super() LinkHints.deactivateMode delay, callback + super() setOpenLinkMode: (@mode) -> if @mode is OPEN_IN_NEW_BG_TAB or @mode is OPEN_IN_NEW_FG_TAB or @mode is OPEN_WITH_QUEUE diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index d06f5eae..84b76301 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -101,7 +101,9 @@ class Mode if @options.exitOnClick @push _name: "mode-#{@id}/exitOnClick" - "click": (event) => @alwaysContinueBubbling => @exit() + "click": (event) => @alwaysContinueBubbling => + @clickEvent = event + @exit() # 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. diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 5720c901..dd0c8d16 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -73,12 +73,13 @@ triggerSuppressor = new Utils.Suppressor true # Note: true == @continueBubbling # unintentionally dropping into insert mode on focusable elements. class InsertModeBlocker extends Mode constructor: (options = {}) -> + defaults = + name: "insert-blocker" + # The user knows best; so, if the user clicks on something, the insert-mode blocker gets out of the way. + exitOnClick: true + onClickMode: InsertMode + super extend defaults, 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" @@ -86,15 +87,17 @@ class InsertModeBlocker extends Mode @alwaysContinueBubbling => # 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, 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 - new @options.onClickMode - targetElement: document.activeElement + + exit: -> + super() + # If the element associated with the event 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 @clickEvent?.target? and DomUtils.isFocusable @clickEvent.target + new @options.onClickMode + targetElement: event.target + triggerSuppressor.unsuppress() root = exports ? window root.InsertMode = InsertMode -- cgit v1.2.3 From 7e1c3475ed9241a5e1fbf78b8134e6ed669ea906 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 13 Jan 2015 17:18:50 +0000 Subject: Modes; temporary commit. --- content_scripts/link_hints.coffee | 2 +- content_scripts/mode.coffee | 7 --- content_scripts/mode_find.coffee | 5 +- content_scripts/mode_insert.coffee | 105 ++++++--------------------------- content_scripts/mode_visual.coffee | 2 +- content_scripts/vimium_frontend.coffee | 19 +++--- 6 files changed, 29 insertions(+), 111 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 0668e3ae..b0feea8c 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -66,7 +66,7 @@ LinkHints = { id: "vimiumHintMarkerContainer", className: "vimiumReset" }) @handlerMode = - new class HintMode extends InsertModeBlocker + new class HintMode extends Mode constructor: -> super name: "hint/#{mode.name}" diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 84b76301..98d3df80 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -191,13 +191,6 @@ class Mode handler: "setBadge" badge: badge.badge - # Temporarily install a mode to protect a function call, then exit the mode. For example, temporarily - # install an InsertModeBlocker, so that focus events don't unintentionally drop us into insert mode. - @runIn: (mode, func) -> - mode = new mode() - func() - mode.exit() - registerSingleton: do -> singletons = {} # Static. (key) -> diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 519e99ad..bf6e7f5b 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -2,14 +2,13 @@ # 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 -# inheriting from InsertModeBlocker. +# 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. # 3. If the very-next keystroke is Escape, then drop immediately into insert mode. # -class PostFindMode extends InsertModeBlocker +class PostFindMode extends Mode constructor: (findModeAnchorNode) -> element = document.activeElement initialSelection = window.getSelection().toString() diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index dd0c8d16..678e35cc 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -1,105 +1,34 @@ -# 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 = name: "insert" - badge: "I" - singleton: InsertMode - keydown: (event) => @stopBubblingAndTrue - keypress: (event) => @stopBubblingAndTrue - keyup: (event) => @stopBubblingAndTrue exitOnEscape: true - blurOnExit: true - targetElement: null + keydown: (event) => @handler event + keypress: (event) => @handler event + keyup: (event) => @handler event - # 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 - if element and DomUtils.isFocusable element - # 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. - element.blur() - -# Automatically trigger insert 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 (just above normal mode and passkeys mode) on the handler stack. -class InsertModeTrigger extends Mode - constructor: -> - super - name: "insert-trigger" - keydown: (event) => - 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 (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 => - if DomUtils.isFocusable event.target - new InsertMode - targetElement: event.target + "blur": => @exit() - # We may have already focussed an input element, so check. - if document.activeElement and DomUtils.isEditable document.activeElement - new InsertMode - targetElement: document.activeElement + active: -> + document.activeElement and DomUtils.isFocusable document.activeElement -# Used by InsertModeBlocker to suppress InsertModeTrigger; see below. -triggerSuppressor = new Utils.Suppressor true # Note: true == @continueBubbling + handler: (event) -> + if @active() then @stopBubblingAndTrue else @continueBubbling -# 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 = {}) -> - defaults = - name: "insert-blocker" - # The user knows best; so, if the user clicks on something, the insert-mode blocker gets out of the way. - exitOnClick: true - onClickMode: InsertMode - super extend defaults, options - triggerSuppressor.suppress() - - @push - _name: "mode-#{@id}/bail-on-click" - "click": (event) => - @alwaysContinueBubbling => - # The user knows best; so, if the user clicks on something, the insert-mode blocker gets out of the - # way. + exit: () -> + document.activeElement.blur() if @active() + if @options.permanentInsertMode + # We don't really exit if we're permanently installed. + Mode.updateBadge() + else + super() - exit: -> - super() - # If the element associated with the event 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 @clickEvent?.target? and DomUtils.isFocusable @clickEvent.target - new @options.onClickMode - targetElement: event.target - triggerSuppressor.unsuppress() + chooseBadge: (badge) -> + badge.badge ||= "I" if @active() root = exports ? window root.InsertMode = InsertMode -root.InsertModeTrigger = InsertModeTrigger -root.InsertModeBlocker = InsertModeBlocker diff --git a/content_scripts/mode_visual.coffee b/content_scripts/mode_visual.coffee index 7b5cc0f6..2580106d 100644 --- a/content_scripts/mode_visual.coffee +++ b/content_scripts/mode_visual.coffee @@ -1,5 +1,5 @@ -class VisualMode extends InsertModeBlocker +class VisualMode extends Mode constructor: (element=null) -> super name: "visual" diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 09e19486..f2e0cb2a 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -123,17 +123,12 @@ initializePreDomReady = -> settings.addEventListener("load", LinkHints.init.bind(LinkHints)) settings.load() - # Install normal mode. This is near the bottom of the handler stack, and is never deactivated. + # Install permanent modes and handlers. new NormalMode() - - # Initialize the scroller. The scroller installs a key handler, and this is next on the handler stack, - # immediately above normal mode. Scroller.init settings - - # Install passKeys mode and the insert-mode trigger. These too are permanently on the stack. passKeysMode = new PassKeysMode() - new InsertModeTrigger() - Mode.updateBadge() + new InsertMode + permanentInsertMode: true checkIfEnabledForUrl() @@ -375,7 +370,7 @@ extend window, id: "vimiumInputMarkerContainer" className: "vimiumReset" - new class FocusSelector extends InsertModeBlocker + new class FocusSelector extends Mode constructor: -> super name: "focus-selector" @@ -767,7 +762,7 @@ handleEnterForFindMode = -> document.body.classList.add("vimiumFindMode") settings.set("findModeRawQuery", findModeQuery.rawQuery) -class FindMode extends InsertModeBlocker +class FindMode extends Mode constructor: -> super name: "find" @@ -828,8 +823,10 @@ executeFind = (query, options) -> HUD.hide(true) # ignore the selectionchange event generated by find() document.removeEventListener("selectionchange",restoreDefaultSelectionHighlight, true) - Mode.runIn InsertModeBlocker, -> + handlerId = handlerStack.push + focus: -> handlerStack.stopBubblingAndTrue result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false) + handlerStack.remove setTimeout( -> document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true) 0) -- cgit v1.2.3 From 9b0a48955c61c262cc4428b2360938d4b54d2d41 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Wed, 14 Jan 2015 07:57:09 +0000 Subject: Modes; substantial reworking of insert mode (and friends). --- content_scripts/link_hints.coffee | 37 ++++++++---------- content_scripts/mode.coffee | 21 ----------- content_scripts/mode_find.coffee | 58 +++++++++++++++++----------- content_scripts/mode_insert.coffee | 69 +++++++++++++++++++++++++--------- content_scripts/vimium_frontend.coffee | 40 ++++---------------- 5 files changed, 112 insertions(+), 113 deletions(-) (limited to 'content_scripts') 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) -- cgit v1.2.3 From 3e0378d0bc5d85ffec0ef49f7c421edbe9c073ec Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Wed, 14 Jan 2015 12:43:41 +0000 Subject: Modes; rework PostFindMode (again). --- content_scripts/mode.coffee | 4 +--- content_scripts/mode_find.coffee | 36 ++++++++++++++++++++-------------- content_scripts/mode_insert.coffee | 13 ++++++------ content_scripts/vimium_frontend.coffee | 18 +++++++---------- 4 files changed, 36 insertions(+), 35 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index ebb3e8bc..2b35f0de 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -101,9 +101,7 @@ class Mode if @options.exitOnClick @push _name: "mode-#{@id}/exitOnClick" - "click": (event) => @alwaysContinueBubbling => - @clickEvent = event - @exit() + "click": (event) => @alwaysContinueBubbling => @exit event # 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. diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 08bc9e5d..4906d363 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -9,18 +9,28 @@ # class PostFindMode extends Mode constructor: (findModeAnchorNode) -> - element = document.activeElement + # Locate the element we need to protect and focus it. Usually, we can just rely on insert mode to have + # picked it up (when it received the focus). + element = InsertMode.permanentInstance.insertModeLock + unless element? + # If insert mode hasn't picked up the element, then it could be content editable. As a heuristic, we + # start at findModeAnchorNode and walk up the DOM, stopping at the last node encountered which is + # contentEditable. If that node is a descendent of the active element, then we use it. + element = findModeAnchorNode + element = element.parentElement while element?.parentElement?.isContentEditable + return unless element?.isContentEditable + return unless document.activeElement and DomUtils.isDOMDescendant document.activeElement, element + element.focus() 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. + # Be a singleton. That way, we don't have to keep track of any currently-active instance. singleton: PostFindMode exitOnBlur: element exitOnClick: true - keydown: (event) -> InsertMode.suppressEvent event - keypress: (event) -> InsertMode.suppressEvent event + keydown: (event) -> InsertMode.suppressEvent event # Truthy. + keypress: (event) -> InsertMode.suppressEvent event # Truthy. keyup: (event) => @alwaysContinueBubbling => if document.getSelection().type != "Range" @@ -30,15 +40,6 @@ class PostFindMode extends Mode else InsertMode.suppressEvent event - return @exit() unless element and findModeAnchorNode - - # Special considerations only arise if the active element can take input. So, exit immediately if it - # cannot. - canTakeInput = DomUtils.isSelectable(element) and DomUtils.isDOMDescendant findModeAnchorNode, element - canTakeInput ||= element.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 @@ -60,7 +61,7 @@ class PostFindMode extends Mode else @continueBubbling - # Note. We use unshift here, instead of push; therefore we see events *after* normal mode, and so only + # Note. We use unshift here, instead of push. We see events *after* normal mode, so we only see # unmapped keys. @unshift _name: "mode-#{@id}/suppressPrintableEvents" @@ -68,5 +69,10 @@ class PostFindMode extends Mode keypress: handler keyup: handler +# NOTE. There's a problem with this approach when a find/search lands in a contentEditable element. Chrome +# generates a focus event triggering insert mode (good), then immediately generates a "blur" event, disabling +# insert mode again. Nevertheless, unmapped keys *do* result in the element being focused again. +# So, asking insert mode whether it's active is giving us the wrong answer. + root = exports ? window root.PostFindMode = PostFindMode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 4be9c589..89077c6a 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -1,6 +1,11 @@ class InsertMode extends Mode + # There is one permanently-installed instance of InsertMode. This allows PostFindMode to query its state. + @permanentInstance: null + constructor: (options = {}) -> + InsertMode.permanentInstance ||= @ + defaults = name: "insert" keydown: (event) => @handleKeydownEvent event @@ -51,12 +56,8 @@ class InsertMode extends Mode 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 + # If this is the permanently-installed instance, then we don't actually exit; instead, we just reset. + if @ == InsertMode.permanentInstance then @insertModeLock = null else super() chooseBadge: (badge) -> badge.badge ||= "I" if @isActive() diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 00d90e81..e14813f7 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -389,7 +389,6 @@ extend window, visibleInputs[selectedInputIndex].element.focus() return @exit() if visibleInputs.length == 1 - hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' exit: -> @@ -747,6 +746,7 @@ class FindMode extends Mode name: "find" badge: "/" exitOnEscape: true + exitOnClick: true keydown: (event) => if event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey @@ -772,8 +772,8 @@ class FindMode extends Mode 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" + if findModeQueryHasResults and event?.type != "click" + new PostFindMode findModeAnchorNode performFindInPlace = -> cachedScrollX = window.scrollX @@ -863,15 +863,11 @@ findAndFocus = (backwards) -> findModeQueryHasResults = executeFind(query, { backwards: backwards, caseSensitive: !findModeQuery.ignoreCase }) - if (!findModeQueryHasResults) + if findModeQueryHasResults + focusFoundLink() + new PostFindMode findModeAnchorNode if findModeQueryHasResults + else HUD.showForDuration("No matches for '" + findModeQuery.rawQuery + "'", 1000) - return - - # if we have found an input element via 'n', pressing immediately afterwards sends us into insert - # mode - new PostFindMode findModeAnchorNode - - focusFoundLink() window.performFind = -> findAndFocus() -- cgit v1.2.3 From ab56d8bcd6686991483694d7153c4d0c9b5e513a Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Wed, 14 Jan 2015 13:33:29 +0000 Subject: Modes; fix tests. --- content_scripts/mode.coffee | 6 +++--- content_scripts/mode_insert.coffee | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 2b35f0de..ab482e8f 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -44,7 +44,7 @@ count = 0 class Mode # 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 + debug: false @modes: [] # Constants; short, readable names for handlerStack event-handler return values. @@ -95,7 +95,7 @@ class Mode if @options.exitOnBlur @push _name: "mode-#{@id}/exitOnBlur" - "blur": (event) => @alwaysContinueBubbling => @exit() if event.srcElement == @options.exitOnBlur + "blur": (event) => @alwaysContinueBubbling => @exit() if event.target == @options.exitOnBlur # If @options.exitOnClick is truthy, then the mode will exit on any click event. if @options.exitOnClick @@ -192,7 +192,7 @@ class Mode # Return the name of the must-recently activated mode. @top: -> - @modes[@modes.length-1]?.name + @modes[@modes.length-1] # 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 diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 89077c6a..135df0d0 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -16,7 +16,7 @@ class InsertMode extends Mode @insertModeLock = if options.targetElement? then options.targetElement else null @push - "blur": => @alwaysContinueBubbling => + "blur": (event) => @alwaysContinueBubbling => if DomUtils.isFocusable event.target @exit event.target Mode.updateBadge() -- cgit v1.2.3 From b594caa3eb792dfeb9d423c81a5136102a013b0a Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Wed, 14 Jan 2015 15:15:40 +0000 Subject: Modes; more reworking. --- content_scripts/mode.coffee | 6 +++--- content_scripts/mode_insert.coffee | 33 ++++++++++++++++++--------------- content_scripts/vimium_frontend.coffee | 8 +++++--- 3 files changed, 26 insertions(+), 21 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index ab482e8f..79559e35 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -44,7 +44,7 @@ count = 0 class Mode # 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: false + debug: true @modes: [] # Constants; short, readable names for handlerStack event-handler return values. @@ -86,7 +86,7 @@ class Mode _name: "mode-#{@id}/exitOnEscape" "keydown": (event) => return @continueBubbling unless KeyboardUtils.isEscape event - @exit event + @exit event, event.srcElement DomUtils.suppressKeyupAfterEscape handlerStack @suppressEvent @@ -117,8 +117,8 @@ class Mode @passKeys = passKeys @registerStateChange?() - Mode.updateBadge() if @badge Mode.modes.push @ + Mode.updateBadge() @logStack() if @debug # End of Mode constructor. diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 135df0d0..d26f2568 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -1,10 +1,12 @@ class InsertMode extends Mode - # There is one permanently-installed instance of InsertMode. This allows PostFindMode to query its state. + # There is one permanently-installed instance of InsertMode. This allows PostFindMode to query the active + # element. @permanentInstance: null constructor: (options = {}) -> InsertMode.permanentInstance ||= @ + @global = options.global defaults = name: "insert" @@ -18,7 +20,7 @@ class InsertMode extends Mode @push "blur": (event) => @alwaysContinueBubbling => if DomUtils.isFocusable event.target - @exit event.target + @exit event, event.target Mode.updateBadge() "focus": (event) => @alwaysContinueBubbling => @insertModeLock = event.target if DomUtils.isFocusable event.target @@ -28,7 +30,7 @@ class InsertMode extends Mode @insertModeLock = event.target if document.activeElement and DomUtils.isFocusable document.activeElement isActive: -> - return true if @insertModeLock != null + return true if @insertModeLock != null or @global # 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. @@ -39,14 +41,7 @@ class InsertMode extends Mode 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() + @exit event, event.srcElement Mode.updateBadge() @suppressEvent @@ -54,10 +49,18 @@ class InsertMode extends Mode handleKeyEvent: (event) -> if @isActive() and event != InsertMode.suppressedEvent then @stopBubblingAndTrue else @continueBubbling - exit: (target) -> - if target == undefined or target == @insertModeLock - # If this is the permanently-installed instance, then we don't actually exit; instead, we just reset. - if @ == InsertMode.permanentInstance then @insertModeLock = null else super() + exit: (_, target) -> + if target and (target == @insertModeLock or @global) and DomUtils.isFocusable target + # 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. + target.blur() + if target == undefined or target == @insertModeLock or @global + @insertModeLock = null + # Now really exit, unless this is the permanently-installed instance. + super() unless @ == InsertMode.permanentInstance chooseBadge: (badge) -> badge.badge ||= "I" if @isActive() diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index e14813f7..3dc8b93d 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -6,7 +6,6 @@ # passKeysMode = null -insertMode = null targetElement = null findMode = false findModeQuery = { rawQuery: "", matchCount: 0 } @@ -128,7 +127,7 @@ initializePreDomReady = -> new NormalMode() Scroller.init settings passKeysMode = new PassKeysMode() - insertMode = new InsertMode() + new InsertMode() checkIfEnabledForUrl() @@ -332,7 +331,8 @@ extend window, HUD.showForDuration("Yanked URL", 1000) enterInsertMode: -> - new InsertMode() + new InsertMode + global: true enterVisualMode: => new VisualMode() @@ -394,6 +394,8 @@ extend window, exit: -> DomUtils.removeElement hintContainingDiv super() + new InsertMode + targetElement: visibleInputs[selectedInputIndex].element # 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 -- cgit v1.2.3 From 0afb3d08d58e45d8392ed153f7043726125d7a45 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 15 Jan 2015 06:41:59 +0000 Subject: Modes; tweaks and fiddles. --- content_scripts/link_hints.coffee | 1 + content_scripts/mode.coffee | 9 +++++++++ content_scripts/mode_find.coffee | 22 +++++++++++----------- content_scripts/mode_insert.coffee | 5 ++--- content_scripts/vimium_frontend.coffee | 15 ++++++--------- 5 files changed, 29 insertions(+), 23 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 5e95ef99..3c8240c0 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -271,6 +271,7 @@ LinkHints = # TODO(philc): Ignore keys that have modifiers. if (KeyboardUtils.isEscape(event)) + DomUtils.suppressKeyupAfterEscape handlerStack @deactivateMode() else keyResult = @getMarkerMatcher().matchHintsByKey(hintMarkers, event) diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 79559e35..d14778a8 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -194,6 +194,14 @@ class Mode @top: -> @modes[@modes.length-1] +# UIMode is a mode for Vimium UI components. They share a common singleton, so new UI components displace +# previously-active UI components. For example, the FocusSelector mode displaces PostFindMode. +class UIMode extends Mode + constructor: (options) -> + defaults = + singleton: UIMode + super extend defaults, options + # 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. @@ -222,3 +230,4 @@ new class BadgeMode extends Mode root = exports ? window root.Mode = Mode +root.UIMode = UIMode diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 4906d363..e79bc0dd 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -4,29 +4,29 @@ # special considerations apply. We implement three special cases: # 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. +# #1415. # 3. If the very-next keystroke is Escape, then drop immediately into insert mode. # -class PostFindMode extends Mode +class PostFindMode extends UIMode constructor: (findModeAnchorNode) -> - # Locate the element we need to protect and focus it. Usually, we can just rely on insert mode to have - # picked it up (when it received the focus). + # Locate the element we need to protect and focus it, if necessary. Usually, we can just rely on insert + # mode to have picked it up (when it received the focus). element = InsertMode.permanentInstance.insertModeLock unless element? - # If insert mode hasn't picked up the element, then it could be content editable. As a heuristic, we - # start at findModeAnchorNode and walk up the DOM, stopping at the last node encountered which is - # contentEditable. If that node is a descendent of the active element, then we use it. + # For contentEditable elements, chrome does not leave them focused, so insert mode does not pick them + # up. We start at findModeAnchorNode and walk up the DOM, stopping at the last node encountered which is + # contentEditable. element = findModeAnchorNode element = element.parentElement while element?.parentElement?.isContentEditable return unless element?.isContentEditable + # The element might be disabled (and therefore unable to receive focus), we use the approximate + # heuristic of checking that element is an ancestor of the active element. return unless document.activeElement and DomUtils.isDOMDescendant document.activeElement, element - element.focus() + element.focus() 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. - singleton: PostFindMode exitOnBlur: element exitOnClick: true keydown: (event) -> InsertMode.suppressEvent event # Truthy. @@ -53,7 +53,7 @@ class PostFindMode extends Mode @remove() true # Continue bubbling. - # Prevent printable keyboard events from propagating to to the page; see Option 2 from #1415. + # Prevent printable keyboard events from propagating to to the page; see #1415. do => handler = (event) => if event.srcElement == element and KeyboardUtils.isPrintable event diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index d26f2568..f815090a 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -25,9 +25,8 @@ class InsertMode extends Mode "focus": (event) => @alwaysContinueBubbling => @insertModeLock = event.target if DomUtils.isFocusable event.target - if @insertModeLock == null - # We may already have focused an input element, so check. - @insertModeLock = event.target if document.activeElement and DomUtils.isFocusable document.activeElement + # We may already have focused an input element, so check. + @insertModeLock = document.activeElement if document.activeElement and DomUtils.isEditable document.activeElement isActive: -> return true if @insertModeLock != null or @global diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 3dc8b93d..e536ebbc 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -369,7 +369,7 @@ extend window, id: "vimiumInputMarkerContainer" className: "vimiumReset" - new class FocusSelector extends Mode + new class FocusSelector extends UIMode constructor: -> super name: "focus-selector" @@ -387,15 +387,12 @@ extend window, @exit() @continueBubbling + @onExit -> DomUtils.removeElement hintContainingDiv visibleInputs[selectedInputIndex].element.focus() - return @exit() if visibleInputs.length == 1 - hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' - - exit: -> - DomUtils.removeElement hintContainingDiv - super() - new InsertMode - targetElement: visibleInputs[selectedInputIndex].element + if visibleInputs.length == 1 + @exit() + else + hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' # 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 -- cgit v1.2.3 From 455ee7fcdea7baf1aeaed67603ec87004c1c8cce Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 15 Jan 2015 11:35:09 +0000 Subject: Modes; yet more teaks and fiddles. --- content_scripts/mode.coffee | 84 +++++++++++++--------------------- content_scripts/mode_find.coffee | 54 +++++++++++----------- content_scripts/mode_insert.coffee | 1 + content_scripts/mode_passkeys.coffee | 7 +-- content_scripts/vimium_frontend.coffee | 35 ++++++-------- 5 files changed, 80 insertions(+), 101 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index d14778a8..9f820469 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -1,14 +1,14 @@ # -# A mode implements a number of keyboard event handlers which are pushed onto the handler stack when the mode -# is activated, and popped off when it is deactivated. The Mode class constructor takes a single argument, -# options, which can define (amongst other things): +# A mode implements a number of keyboard (and possibly other) event handlers which are pushed onto the handler +# stack when the mode 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). -# Optional. Define a badge if the badge is constant; for example, in insert mode the badge is always "I". +# Optional. Define a badge if the badge is constant; for example, in find mode the badge is always "/". # Otherwise, do not define a badge, but instead override the chooseBadge method; for example, in passkeys # mode, the badge may be "P" or "", depending on the configuration state. Or, if the mode *never* shows a # badge, then do neither. @@ -26,35 +26,23 @@ # "focus": (event) => .... # Any such handlers are removed when the mode is deactivated. # -# To activate a mode, use: -# myMode = new MyMode() -# -# 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. +# Debug only. count = 0 class Mode - # 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. + # If Mode.debug is true, then we generate a trace of modes being activated and deactivated on the console. debug: true @modes: [] - # Constants; short, readable names for handlerStack event-handler return values. + # Constants; short, readable names for the return values expected by handlerStack.bubbleEvent. continueBubbling: true suppressEvent: false stopBubblingAndTrue: handlerStack.stopBubblingAndTrue stopBubblingAndFalse: handlerStack.stopBubblingAndFalse restartBubbling: handlerStack.restartBubbling - constructor: (@options={}) -> + constructor: (@options = {}) -> @handlers = [] @exitHandlers = [] @modeIsActive = true @@ -71,14 +59,12 @@ class Mode 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. + # Some modes are singletons: there may be at most one instance active at any 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. New instances deactivate existing instances. @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 is truthy, then the mode will exit when the escape key is pressed. if @options.exitOnEscape # Note. This handler ends up above the mode's own key handlers on the handler stack, so it takes # priority. @@ -110,12 +96,11 @@ class Mode @passKeys = "" @push _name: "mode-#{@id}/registerStateChange" - "registerStateChange": ({ enabled: enabled, passKeys: passKeys }) => - @alwaysContinueBubbling => - if enabled != @enabled or passKeys != @passKeys - @enabled = enabled - @passKeys = passKeys - @registerStateChange?() + "registerStateChange": ({ enabled: enabled, passKeys: passKeys }) => @alwaysContinueBubbling => + if enabled != @enabled or passKeys != @passKeys + @enabled = enabled + @passKeys = passKeys + @registerStateChange?() Mode.modes.push @ Mode.updateBadge() @@ -150,11 +135,10 @@ class Mode # Shorthand for an otherwise long name. This wraps a handler with an arbitrary return value, and always # yields @continueBubbling instead. This simplifies handlers if they always continue bubbling (a common - # case), because they do not need to be concerned with their return value (which helps keep code concise and - # clear). + # case), because they do not need to be concerned with the value they yield. alwaysContinueBubbling: handlerStack.alwaysContinueBubbling - # User for sometimes suppressing badge updates. + # Used for sometimes suppressing badge updates. @badgeSuppressor: new Utils.Suppressor() # Static method. Used externally and internally to initiate bubbling of an updateBadge event and to send @@ -171,11 +155,11 @@ class Mode registerSingleton: do -> singletons = {} # Static. (key) -> - # We're currently installing a new mode. So we'll be updating the badge shortly. Therefore, we can - # suppress badge updates while exiting any existing active singleton. This prevents the badge from - # flickering in some cases. if singletons[key] @log "singleton:", "deactivating #{singletons[key].id}" if @debug + # We're currently installing a new mode. So we'll be updating the badge shortly. Therefore, we can + # suppress badge updates while deactivating the existing singleton. This prevents the badge from + # flickering in some cases. Mode.badgeSuppressor.runSuppresed -> singletons[key].exit() singletons[key] = @ @@ -184,28 +168,26 @@ class Mode # Debugging routines. logStack: -> @log "active modes (top to bottom):" - for mode in Mode.modes[..].reverse() - @log " ", mode.id + @log " ", mode.id for mode in Mode.modes[..].reverse() log: (args...) -> console.log args... - # Return the name of the must-recently activated mode. + # Return the must-recently activated mode (only used in tests). @top: -> @modes[@modes.length-1] -# UIMode is a mode for Vimium UI components. They share a common singleton, so new UI components displace -# previously-active UI components. For example, the FocusSelector mode displaces PostFindMode. -class UIMode extends Mode +# InputController is a super-class for modes which control insert mode: PostFindMode and FocusSelector. It's +# a singleton, so no two instances may be active at the same time. +class InputController extends Mode constructor: (options) -> defaults = - singleton: UIMode + singleton: InputController super extend defaults, options # 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. -# Note. We create the the one-and-only instance here. +# badge choice of the other modes. We create the the one-and-only instance here. new class BadgeMode extends Mode constructor: () -> super @@ -213,14 +195,14 @@ new class BadgeMode extends Mode trackState: true # FIXME(smblott) BadgeMode is currently triggering an 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. + # lot, considerably more than 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) -> - # If we're not enabled, then post an empty badge. BadgeMode is last, so this takes priority. + # If we're not enabled, then post an empty badge. badge.badge = "" unless @enabled # When the registerStateChange event bubbles to the bottom of the stack, all modes have been notified. So @@ -230,4 +212,4 @@ new class BadgeMode extends Mode root = exports ? window root.Mode = Mode -root.UIMode = UIMode +root.InputController = InputController diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index e79bc0dd..f151b8cd 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -1,32 +1,33 @@ # NOTE(smblott). Ultimately, all of the FindMode-related code should be moved to this file. -# When we use find mode, the selection/focus can end up in a focusable/editable element. In this situation, +# When we use find mode, the selection/focus can land 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. -# 2. Prevent all printable keypress events on the active element from propagating beyond normal mode. See -# #1415. +# 1. Disable keyboard events in insert mode, because the user hasn't asked to enter insert mode. +# 2. Prevent printable keyboard events from propagating to the page; see #1415. # 3. If the very-next keystroke is Escape, then drop immediately into insert mode. # -class PostFindMode extends UIMode +class PostFindMode extends InputController constructor: (findModeAnchorNode) -> - # Locate the element we need to protect and focus it, if necessary. Usually, we can just rely on insert - # mode to have picked it up (when it received the focus). - element = InsertMode.permanentInstance.insertModeLock - unless element? - # For contentEditable elements, chrome does not leave them focused, so insert mode does not pick them - # up. We start at findModeAnchorNode and walk up the DOM, stopping at the last node encountered which is - # contentEditable. - element = findModeAnchorNode - element = element.parentElement while element?.parentElement?.isContentEditable - return unless element?.isContentEditable - # The element might be disabled (and therefore unable to receive focus), we use the approximate - # heuristic of checking that element is an ancestor of the active element. - return unless document.activeElement and DomUtils.isDOMDescendant document.activeElement, element - element.focus() + # Locate the element we need to protect. In most cases, it's just the active element. + element = + if document.activeElement and DomUtils.isEditable document.activeElement + document.activeElement + else + # For contentEditable elements, chrome does not focus them, although they are activated by keystrokes. + # We need to find the element ourselves. + element = findModeAnchorNode + element = element.parentElement while element.parentElement?.isContentEditable + if element.isContentEditable + if DomUtils.isDOMDescendant element, findModeAnchorNode + # TODO(smblott). We shouldn't really need to focus the element, here. Need to look into why this + # is necessary. + element.focus() + element + + return unless element super name: "post-find" - badge: "N" # Pretend to be normal mode (because we don't want the insert-mode badge). exitOnBlur: element exitOnClick: true keydown: (event) -> InsertMode.suppressEvent event # Truthy. @@ -35,7 +36,7 @@ class PostFindMode extends UIMode @alwaysContinueBubbling => 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. + # of the way. See Option 5c from #1415. @exit() else InsertMode.suppressEvent event @@ -45,7 +46,7 @@ class PostFindMode extends UIMode @push _name: "mode-#{@id}/handle-escape" keydown: (event) -> - if document.activeElement == element and KeyboardUtils.isEscape event + if KeyboardUtils.isEscape event DomUtils.suppressKeyupAfterEscape handlerStack self.exit() false # Suppress event. @@ -53,7 +54,7 @@ class PostFindMode extends UIMode @remove() true # Continue bubbling. - # Prevent printable keyboard events from propagating to to the page; see #1415. + # Prevent printable keyboard events from propagating to the page; see #1415. do => handler = (event) => if event.srcElement == element and KeyboardUtils.isPrintable event @@ -69,10 +70,9 @@ class PostFindMode extends UIMode keypress: handler keyup: handler -# NOTE. There's a problem with this approach when a find/search lands in a contentEditable element. Chrome -# generates a focus event triggering insert mode (good), then immediately generates a "blur" event, disabling -# insert mode again. Nevertheless, unmapped keys *do* result in the element being focused again. -# So, asking insert mode whether it's active is giving us the wrong answer. + chooseBadge: (badge) -> + # If PostFindMode is active, then we don't want the "I" badge from insert mode. + InsertMode.suppressEvent badge root = exports ? window root.PostFindMode = PostFindMode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index f815090a..9be520c7 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -62,6 +62,7 @@ class InsertMode extends Mode super() unless @ == InsertMode.permanentInstance chooseBadge: (badge) -> + return if badge == InsertMode.suppressedEvent badge.badge ||= "I" if @isActive() # Static stuff to allow PostFindMode to suppress insert mode. diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee index dde91c13..a6cd7d2d 100644 --- a/content_scripts/mode_passkeys.coffee +++ b/content_scripts/mode_passkeys.coffee @@ -8,6 +8,10 @@ class PassKeysMode extends Mode keypress: (event) => @handleKeyChar String.fromCharCode event.charCode keyup: (event) => @handleKeyChar String.fromCharCode event.charCode + @keyQueue = "" + @push + registerKeyQueue: ({ keyQueue: keyQueue }) => @alwaysContinueBubbling => @keyQueue = 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. @@ -17,9 +21,6 @@ class PassKeysMode extends Mode else @continueBubbling - configure: (request) -> - @keyQueue = request.keyQueue if request.keyQueue? - chooseBadge: (badge) -> badge.badge ||= "P" if @passKeys and not @keyQueue diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index e536ebbc..7d24e714 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -5,7 +5,6 @@ # "domReady". # -passKeysMode = null targetElement = null findMode = false findModeQuery = { rawQuery: "", matchCount: 0 } @@ -103,19 +102,6 @@ frameId = Math.floor(Math.random()*999999999) hasModifiersRegex = /^<([amc]-)+.>/ -class NormalMode extends Mode - constructor: -> - super - name: "normal" - badge: "N" - keydown: (event) => onKeydown.call @, event - keypress: (event) => onKeypress.call @, event - keyup: (event) => onKeyup.call @, event - - chooseBadge: (badge) -> - super badge - badge.badge = "" unless isEnabledForUrl - # # Complete initialization work that sould be done prior to DOMReady. # @@ -123,11 +109,20 @@ initializePreDomReady = -> settings.addEventListener("load", LinkHints.init.bind(LinkHints)) settings.load() - # Install permanent modes and handlers. - new NormalMode() + class NormalMode extends Mode + constructor: -> + super + name: "normal" + keydown: (event) => onKeydown.call @, event + keypress: (event) => onKeypress.call @, event + keyup: (event) => onKeyup.call @, event + + # Install the permanent modes and handlers. The permanent insert mode operates only when focusable/editable + # elements have the focus. + new NormalMode Scroller.init settings - passKeysMode = new PassKeysMode() - new InsertMode() + new PassKeysMode + new InsertMode checkIfEnabledForUrl() @@ -157,7 +152,7 @@ initializePreDomReady = -> setState: setState currentKeyQueue: (request) -> keyQueue = request.keyQueue - passKeysMode.configure request + handlerStack.bubbleEvent "registerKeyQueue", { keyQueue: keyQueue } chrome.runtime.onMessage.addListener (request, sender, sendResponse) -> # In the options page, we will receive requests from both content and background scripts. ignore those @@ -369,7 +364,7 @@ extend window, id: "vimiumInputMarkerContainer" className: "vimiumReset" - new class FocusSelector extends UIMode + new class FocusSelector extends InputController constructor: -> super name: "focus-selector" -- cgit v1.2.3 From 091cd99b6fcbb17f30e552b0c0f6461c4c1529cb Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 15 Jan 2015 18:00:18 +0000 Subject: Modes; refactoring. - refactor PostFindMode (to keep separate functionality separated). --- content_scripts/mode.coffee | 9 ---- content_scripts/mode_find.coffee | 78 +++++++++++++--------------------- content_scripts/vimium_frontend.coffee | 15 ++++--- 3 files changed, 39 insertions(+), 63 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 9f820469..cc1250b4 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -177,14 +177,6 @@ class Mode @top: -> @modes[@modes.length-1] -# InputController is a super-class for modes which control insert mode: PostFindMode and FocusSelector. It's -# a singleton, so no two instances may be active at the same time. -class InputController extends Mode - constructor: (options) -> - defaults = - singleton: InputController - super extend defaults, options - # 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 modes. We create the the one-and-only instance here. @@ -212,4 +204,3 @@ new class BadgeMode extends Mode root = exports ? window root.Mode = Mode -root.InputController = InputController diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index f151b8cd..cfcd18b5 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -1,45 +1,42 @@ # NOTE(smblott). Ultimately, all of the FindMode-related code should be moved to this file. +# This is Used as a sub-class to PostFindMode. It prevents printable characters from being passed through to +# underlying input element; see #1415. Note, also, that the "Range" condition in the keyup handler +# implmements option 5c from #1415. +class SuppressPrintable extends Mode + constructor: (options) -> + super options + handler = (event) => if KeyboardUtils.isPrintable event then @suppressEvent else @continueBubbling + + # Note: we use unshift here. We see events *after* normal mode, so we only see unmapped keys. + @unshift + _name: "mode-#{@id}/suppressPrintableEvents" + keydown: handler + keypress: handler + keyup: (event) => + if document.getSelection().type.toLowerCase() != "range" then @exit() else handler event + # When we use find mode, the selection/focus can land in a focusable/editable element. In this situation, # special considerations apply. We implement three special cases: -# 1. Disable keyboard events in insert mode, because the user hasn't asked to enter insert mode. -# 2. Prevent printable keyboard events from propagating to the page; see #1415. +# 1. Disable insert mode, because the user hasn't asked to enter insert mode. We do this by using +# InsertMode.suppressEvent. +# 2. Prevent printable keyboard events from propagating to the page; see #1415. We do this by inheriting +# from SuppressPrintable. # 3. If the very-next keystroke is Escape, then drop immediately into insert mode. # -class PostFindMode extends InputController +class PostFindMode extends SuppressPrintable constructor: (findModeAnchorNode) -> - # Locate the element we need to protect. In most cases, it's just the active element. - element = - if document.activeElement and DomUtils.isEditable document.activeElement - document.activeElement - else - # For contentEditable elements, chrome does not focus them, although they are activated by keystrokes. - # We need to find the element ourselves. - element = findModeAnchorNode - element = element.parentElement while element.parentElement?.isContentEditable - if element.isContentEditable - if DomUtils.isDOMDescendant element, findModeAnchorNode - # TODO(smblott). We shouldn't really need to focus the element, here. Need to look into why this - # is necessary. - element.focus() - element - - return unless element + element = document.activeElement + return unless element and DomUtils.isEditable element super name: "post-find" + singleton: PostFindMode exitOnBlur: element exitOnClick: true - keydown: (event) -> InsertMode.suppressEvent event # Truthy. - keypress: (event) -> InsertMode.suppressEvent event # Truthy. - keyup: (event) => - @alwaysContinueBubbling => - 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. See Option 5c from #1415. - @exit() - else - InsertMode.suppressEvent event + keydown: (event) -> InsertMode.suppressEvent event # Always truthy, so always continues bubbling. + keypress: (event) -> InsertMode.suppressEvent event + keyup: (event) -> InsertMode.suppressEvent event # If the very-next keydown is Esc, drop immediately into insert mode. self = @ @@ -54,25 +51,8 @@ class PostFindMode extends InputController @remove() true # Continue bubbling. - # Prevent printable keyboard events from propagating to the page; see #1415. - do => - handler = (event) => - if event.srcElement == element and KeyboardUtils.isPrintable event - @suppressEvent - else - @continueBubbling - - # Note. We use unshift here, instead of push. We see events *after* normal mode, so we only see - # unmapped keys. - @unshift - _name: "mode-#{@id}/suppressPrintableEvents" - keydown: handler - keypress: handler - keyup: handler - - chooseBadge: (badge) -> - # If PostFindMode is active, then we don't want the "I" badge from insert mode. - InsertMode.suppressEvent badge + # If PostFindMode is active, then we suppress the "I" badge from insert mode. + chooseBadge: (badge) -> InsertMode.suppressEvent badge root = exports ? window root.PostFindMode = PostFindMode diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 7d24e714..3049784e 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -364,11 +364,14 @@ extend window, id: "vimiumInputMarkerContainer" className: "vimiumReset" - new class FocusSelector extends InputController + new class FocusSelector extends Mode constructor: -> super name: "focus-selector" badge: "?" + # We share a singleton with PostFindMode. That way, a new FocusSelector displaces any existing + # PostFindMode. + singleton: PostFindMode exitOnClick: true keydown: (event) => if event.keyCode == KeyboardUtils.keyCodes.tab @@ -805,10 +808,12 @@ executeFind = (query, options) -> # preventDefault() findModeAnchorNode = document.getSelection().anchorNode - # If the anchor node is outside of the active element, then blur the active element. We don't want to leave - # behind an inappropriate active element. This fixes #1412. - if document.activeElement and not DomUtils.isDOMDescendant findModeAnchorNode, document.activeElement - document.activeElement.blur() + # TODO(smblott). Disabled. This is the wrong test. Should be reinstated when we have the right test, which + # looks like it should be "isSelected" from #1431. + # # If the anchor node not a descendent of the active element, then blur the active element. We don't want to + # # leave behind an inappropriate active element. This fixes #1412. + # if document.activeElement and not DomUtils.isDOMDescendant document.activeElement, findModeAnchorNode + # document.activeElement.blur() result -- cgit v1.2.3 From bbc7257842293fbd58dd2f84a58c86691ceae3e1 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 16 Jan 2015 07:31:24 +0000 Subject: Modes; tweaks. --- content_scripts/mode_find.coffee | 18 +++++++----- content_scripts/mode_insert.coffee | 53 +++++++++++++++++++--------------- content_scripts/vimium_frontend.coffee | 4 +-- 3 files changed, 43 insertions(+), 32 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index cfcd18b5..21638a34 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -1,20 +1,24 @@ # NOTE(smblott). Ultimately, all of the FindMode-related code should be moved to this file. -# This is Used as a sub-class to PostFindMode. It prevents printable characters from being passed through to -# underlying input element; see #1415. Note, also, that the "Range" condition in the keyup handler -# implmements option 5c from #1415. +# This prevents printable characters from being passed through to underlying page; see #1415. class SuppressPrintable extends Mode constructor: (options) -> super options handler = (event) => if KeyboardUtils.isPrintable event then @suppressEvent else @continueBubbling - # Note: we use unshift here. We see events *after* normal mode, so we only see unmapped keys. + # We use unshift here, so we see events after normal mode, so we only see unmapped keys. @unshift _name: "mode-#{@id}/suppressPrintableEvents" keydown: handler keypress: handler keyup: (event) => - if document.getSelection().type.toLowerCase() != "range" then @exit() else handler event + # If the selection is no longer a range, then the user is interacting with the input element, so we + # get out of the way. See discussion of option 5c from #1415. + if document.getSelection().type != "Range" + console.log "aaa", @options.targetElement + @exit() + else + handler event # When we use find mode, the selection/focus can land in a focusable/editable element. In this situation, # special considerations apply. We implement three special cases: @@ -25,9 +29,9 @@ class SuppressPrintable extends Mode # 3. If the very-next keystroke is Escape, then drop immediately into insert mode. # class PostFindMode extends SuppressPrintable - constructor: (findModeAnchorNode) -> + constructor: -> + return unless document.activeElement and DomUtils.isEditable document.activeElement element = document.activeElement - return unless element and DomUtils.isEditable element super name: "post-find" diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 9be520c7..204c629d 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -1,7 +1,6 @@ class InsertMode extends Mode - # There is one permanently-installed instance of InsertMode. This allows PostFindMode to query the active - # element. + # There is one permanently-installed instance of InsertMode. @permanentInstance: null constructor: (options = {}) -> @@ -15,25 +14,34 @@ class InsertMode extends Mode keyup: (event) => @handleKeyEvent event super extend defaults, options - @insertModeLock = if options.targetElement? then options.targetElement else null + + @insertModeLock = + if document.activeElement and DomUtils.isEditable document.activeElement + # We have already focused an input element, so use it. + document.activeElement + else + null @push "blur": (event) => @alwaysContinueBubbling => - if DomUtils.isFocusable event.target - @exit event, event.target - Mode.updateBadge() + target = event.target + # We can't rely on focus and blur events arriving in the expected order. When the active element + # changes, we might get "blur" before "focus". The approach we take is to track the active element in + # @insertModeLock, and exit only when the that element blurs. + @exit event, target if target == @insertModeLock and DomUtils.isFocusable target "focus": (event) => @alwaysContinueBubbling => - @insertModeLock = event.target if DomUtils.isFocusable event.target - - # We may already have focused an input element, so check. - @insertModeLock = document.activeElement if document.activeElement and DomUtils.isEditable document.activeElement + if @insertModeLock != event.target and DomUtils.isFocusable event.target + @insertModeLock = event.target + Mode.updateBadge() isActive: -> - return true if @insertModeLock != null or @global + return true if @insertModeLock or @global # 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 + if document.activeElement?.isContentEditable and @insertModeLock != document.activeElement + @insertModeLock = document.activeElement + Mode.updateBadge() @insertModeLock != null handleKeydownEvent: (event) -> @@ -41,7 +49,6 @@ class InsertMode extends Mode return @stopBubblingAndTrue unless KeyboardUtils.isEscape event DomUtils.suppressKeyupAfterEscape handlerStack @exit event, event.srcElement - Mode.updateBadge() @suppressEvent # Handles keypress and keyup events. @@ -49,17 +56,17 @@ class InsertMode extends Mode if @isActive() and event != InsertMode.suppressedEvent then @stopBubblingAndTrue else @continueBubbling exit: (_, target) -> - if target and (target == @insertModeLock or @global) and DomUtils.isFocusable target - # 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. - target.blur() - if target == undefined or target == @insertModeLock or @global + if (target and target == @insertModeLock) or @global or target == undefined @insertModeLock = null - # Now really exit, unless this is the permanently-installed instance. - super() unless @ == InsertMode.permanentInstance + if target and DomUtils.isFocusable target + # 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() 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. + target.blur() + # Really exit, but only if this isn't the permanently-installed instance. + if @ == InsertMode.permanentInstance then Mode.updateBadge() else super() chooseBadge: (badge) -> return if badge == InsertMode.suppressedEvent diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 3049784e..b2c591fd 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -770,7 +770,7 @@ class FindMode extends Mode handleEscapeForFindMode() if event?.type == "keydown" and KeyboardUtils.isEscape event handleEscapeForFindMode() if event?.type == "click" if findModeQueryHasResults and event?.type != "click" - new PostFindMode findModeAnchorNode + new PostFindMode performFindInPlace = -> cachedScrollX = window.scrollX @@ -864,7 +864,7 @@ findAndFocus = (backwards) -> if findModeQueryHasResults focusFoundLink() - new PostFindMode findModeAnchorNode if findModeQueryHasResults + new PostFindMode() if findModeQueryHasResults else HUD.showForDuration("No matches for '" + findModeQuery.rawQuery + "'", 1000) -- cgit v1.2.3 From cd49c88eaad6550ba768159347be6c88f1c26d15 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 16 Jan 2015 12:08:42 +0000 Subject: Modes; clean up. --- content_scripts/mode.coffee | 79 ++++++++++++++++------------------ content_scripts/mode_find.coffee | 13 +++--- content_scripts/mode_insert.coffee | 33 +++++++------- content_scripts/mode_passkeys.coffee | 13 ++---- content_scripts/vimium_frontend.coffee | 12 +++--- 5 files changed, 71 insertions(+), 79 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index cc1250b4..6bd09af2 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -9,7 +9,7 @@ # badge: # A badge (to appear on the browser popup). # Optional. Define a badge if the badge is constant; for example, in find mode the badge is always "/". -# Otherwise, do not define a badge, but instead override the chooseBadge method; for example, in passkeys +# Otherwise, do not define a badge, but instead override the updateBadge method; for example, in passkeys # mode, the badge may be "P" or "", depending on the configuration state. Or, if the mode *never* shows a # badge, then do neither. # @@ -51,18 +51,13 @@ class Mode @count = ++count @id = "#{@name}-#{@count}" - @log "activate:", @id if @debug + @log "activate:", @id @push 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 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. New instances deactivate existing instances. - @registerSingleton @options.singleton if @options.singleton + updateBadge: (badge) => @alwaysContinueBubbling => @updateBadge badge # If @options.exitOnEscape is truthy, then the mode will exit when the escape key is pressed. if @options.exitOnEscape @@ -72,8 +67,8 @@ class Mode _name: "mode-#{@id}/exitOnEscape" "keydown": (event) => return @continueBubbling unless KeyboardUtils.isEscape event - @exit event, event.srcElement DomUtils.suppressKeyupAfterEscape handlerStack + @exit event, event.srcElement @suppressEvent # If @options.exitOnBlur is truthy, then it should be an element. The mode will exit when that element @@ -89,22 +84,40 @@ class Mode _name: "mode-#{@id}/exitOnClick" "click": (event) => @alwaysContinueBubbling => @exit event + # Some modes are singletons: there may be at most one instance active at any time. A mode is a singleton + # if @options.singleton is truthy. The value of @options.singleton should be the which is intended to + # be unique. New instances deactivate existing instances. + if @options.singleton + do => + singletons = Mode.singletons ||= {} + key = @options.singleton + @onExit => delete singletons[key] if singletons[key] == @ + if singletons[key] + @log "singleton:", "deactivating #{singletons[key].id}" + # We're currently installing a new mode, so we'll be updating the badge shortly. Therefore, we can + # suppress badge updates while deactivating the existing singleton. This can prevent badge flicker. + singletons[key].exit() + singletons[key] = @ + # 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. + # and calls @registerStateChange() (if defined) whenever the state changes. The mode also tracks the + # keyQueue in @keyQueue. if @options.trackState @enabled = false @passKeys = "" + @keyQueue = "" @push _name: "mode-#{@id}/registerStateChange" - "registerStateChange": ({ enabled: enabled, passKeys: passKeys }) => @alwaysContinueBubbling => + registerStateChange: ({ enabled: enabled, passKeys: passKeys }) => @alwaysContinueBubbling => if enabled != @enabled or passKeys != @passKeys @enabled = enabled @passKeys = passKeys @registerStateChange?() + registerKeyQueue: ({ keyQueue: keyQueue }) => @alwaysContinueBubbling => @keyQueue = keyQueue Mode.modes.push @ Mode.updateBadge() - @logStack() if @debug + @logStack() # End of Mode constructor. push: (handlers) -> @@ -121,7 +134,7 @@ class Mode exit: -> if @modeIsActive - @log "deactivate:", @id if @debug + @log "deactivate:", @id handler() for handler in @exitHandlers handlerStack.remove handlerId for handlerId in @handlers Mode.modes = Mode.modes.filter (mode) => mode != @ @@ -129,8 +142,8 @@ class Mode @modeIsActive = false # 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. It is overridden in sub-classes. - chooseBadge: (badge) -> + # opportunity to choose a badge. This is overridden in sub-classes. + updateBadge: (badge) -> badge.badge ||= @badge # Shorthand for an otherwise long name. This wraps a handler with an arbitrary return value, and always @@ -138,40 +151,24 @@ class Mode # case), because they do not need to be concerned with the value they yield. alwaysContinueBubbling: handlerStack.alwaysContinueBubbling - # Used for sometimes suppressing badge updates. - @badgeSuppressor: new Utils.Suppressor() - # 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 (hence this frame) # has the focus. @updateBadge: -> - @badgeSuppressor.unlessSuppressed -> - if document.hasFocus() - handlerStack.bubbleEvent "updateBadge", badge = { badge: "" } - chrome.runtime.sendMessage - handler: "setBadge" - badge: badge.badge - - registerSingleton: do -> - singletons = {} # Static. - (key) -> - if singletons[key] - @log "singleton:", "deactivating #{singletons[key].id}" if @debug - # We're currently installing a new mode. So we'll be updating the badge shortly. Therefore, we can - # suppress badge updates while deactivating the existing singleton. This prevents the badge from - # flickering in some cases. - Mode.badgeSuppressor.runSuppresed -> singletons[key].exit() - singletons[key] = @ - - @onExit => delete singletons[key] if singletons[key] == @ + if document.hasFocus() + handlerStack.bubbleEvent "updateBadge", badge = badge: "" + chrome.runtime.sendMessage + handler: "setBadge" + badge: badge.badge # Debugging routines. logStack: -> - @log "active modes (top to bottom):" - @log " ", mode.id for mode in Mode.modes[..].reverse() + if @debug + @log "active modes (top to bottom):" + @log " ", mode.id for mode in Mode.modes[..].reverse() log: (args...) -> - console.log args... + console.log args... if @debug # Return the must-recently activated mode (only used in tests). @top: -> @@ -193,7 +190,7 @@ new class BadgeMode extends Mode _name: "mode-#{@id}/focus" "focus": => @alwaysContinueBubbling -> Mode.updateBadge() - chooseBadge: (badge) -> + updateBadge: (badge) -> # If we're not enabled, then post an empty badge. badge.badge = "" unless @enabled diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 21638a34..6b4f6bb1 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -5,6 +5,7 @@ class SuppressPrintable extends Mode constructor: (options) -> super options handler = (event) => if KeyboardUtils.isPrintable event then @suppressEvent else @continueBubbling + type = document.getSelection().type # We use unshift here, so we see events after normal mode, so we only see unmapped keys. @unshift @@ -12,13 +13,9 @@ class SuppressPrintable extends Mode keydown: handler keypress: handler keyup: (event) => - # If the selection is no longer a range, then the user is interacting with the input element, so we - # get out of the way. See discussion of option 5c from #1415. - if document.getSelection().type != "Range" - console.log "aaa", @options.targetElement - @exit() - else - handler event + # If the selection types has changed (usually, no longer "Range"), then the user is interacting with + # the input element, so we get out of the way. See discussion of option 5c from #1415. + if document.getSelection().type != type then @exit() else handler event # When we use find mode, the selection/focus can land in a focusable/editable element. In this situation, # special considerations apply. We implement three special cases: @@ -56,7 +53,7 @@ class PostFindMode extends SuppressPrintable true # Continue bubbling. # If PostFindMode is active, then we suppress the "I" badge from insert mode. - chooseBadge: (badge) -> InsertMode.suppressEvent badge + updateBadge: (badge) -> InsertMode.suppressEvent badge root = exports ? window root.PostFindMode = PostFindMode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 204c629d..ef7223ad 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -1,10 +1,14 @@ class InsertMode extends Mode - # There is one permanently-installed instance of InsertMode. + # There is one permanently-installed instance of InsertMode. It watches for focus changes and + # activates/deactivates itself accordingly. @permanentInstance: null constructor: (options = {}) -> InsertMode.permanentInstance ||= @ + @permanent = (@ == InsertMode.permanentInstance) + + # If truthy, then options.global indicates that we were activated by the user (with "i"). @global = options.global defaults = @@ -17,7 +21,7 @@ class InsertMode extends Mode @insertModeLock = if document.activeElement and DomUtils.isEditable document.activeElement - # We have already focused an input element, so use it. + # An input element is already active, so use it. document.activeElement else null @@ -26,15 +30,16 @@ class InsertMode extends Mode "blur": (event) => @alwaysContinueBubbling => target = event.target # We can't rely on focus and blur events arriving in the expected order. When the active element - # changes, we might get "blur" before "focus". The approach we take is to track the active element in - # @insertModeLock, and exit only when the that element blurs. + # changes, we might get "blur" before "focus". We track the active element in @insertModeLock, and + # exit only when that element blurs. @exit event, target if target == @insertModeLock and DomUtils.isFocusable target "focus": (event) => @alwaysContinueBubbling => if @insertModeLock != event.target and DomUtils.isFocusable event.target @insertModeLock = event.target Mode.updateBadge() - isActive: -> + isActive: (event) -> + return false if event == InsertMode.suppressedEvent return true if @insertModeLock or @global # 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 @@ -45,7 +50,7 @@ class InsertMode extends Mode @insertModeLock != null handleKeydownEvent: (event) -> - return @continueBubbling if event == InsertMode.suppressedEvent or not @isActive() + return @continueBubbling unless @isActive event return @stopBubblingAndTrue unless KeyboardUtils.isEscape event DomUtils.suppressKeyupAfterEscape handlerStack @exit event, event.srcElement @@ -53,26 +58,24 @@ class InsertMode extends Mode # Handles keypress and keyup events. handleKeyEvent: (event) -> - if @isActive() and event != InsertMode.suppressedEvent then @stopBubblingAndTrue else @continueBubbling + if @isActive event then @stopBubblingAndTrue else @continueBubbling exit: (_, target) -> if (target and target == @insertModeLock) or @global or target == undefined @insertModeLock = null if target and DomUtils.isFocusable target - # Remove the focus, so the user can't just get himself back into insert mode by typing in the same input - # box. + # Remove the focus, so the user can't just get 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. target.blur() - # Really exit, but only if this isn't the permanently-installed instance. - if @ == InsertMode.permanentInstance then Mode.updateBadge() else super() + # Exit, but only if this isn't the permanently-installed instance. + if @permanent then Mode.updateBadge() else super() - chooseBadge: (badge) -> - return if badge == InsertMode.suppressedEvent - badge.badge ||= "I" if @isActive() + updateBadge: (badge) -> + badge.badge ||= "I" if @isActive badge - # Static stuff to allow PostFindMode to suppress insert mode. + # Static stuff. This allows PostFindMode to suppress insert mode. @suppressedEvent: null @suppressEvent: (event) -> @suppressedEvent = event diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee index a6cd7d2d..a40fe7a6 100644 --- a/content_scripts/mode_passkeys.coffee +++ b/content_scripts/mode_passkeys.coffee @@ -3,25 +3,20 @@ class PassKeysMode extends Mode constructor: -> super name: "passkeys" - trackState: true + trackState: true # Maintain @enabled, @passKeys and @keyQueue. keydown: (event) => @handleKeyChar KeyboardUtils.getKeyChar event keypress: (event) => @handleKeyChar String.fromCharCode event.charCode keyup: (event) => @handleKeyChar String.fromCharCode event.charCode - @keyQueue = "" - @push - registerKeyQueue: ({ keyQueue: keyQueue }) => @alwaysContinueBubbling => @keyQueue = 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. + # 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. handleKeyChar: (keyChar) -> if keyChar and not @keyQueue and 0 <= @passKeys.indexOf keyChar @stopBubblingAndTrue else @continueBubbling - chooseBadge: (badge) -> + updateBadge: (badge) -> badge.badge ||= "P" if @passKeys and not @keyQueue root = exports ? window diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index b2c591fd..0a034e28 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -118,7 +118,7 @@ initializePreDomReady = -> keyup: (event) => onKeyup.call @, event # Install the permanent modes and handlers. The permanent insert mode operates only when focusable/editable - # elements have the focus. + # elements are active. new NormalMode Scroller.init settings new PassKeysMode @@ -360,10 +360,6 @@ extend window, hint - hintContainingDiv = DomUtils.addElementList hints, - id: "vimiumInputMarkerContainer" - className: "vimiumReset" - new class FocusSelector extends Mode constructor: -> super @@ -386,6 +382,10 @@ extend window, @continueBubbling @onExit -> DomUtils.removeElement hintContainingDiv + hintContainingDiv = DomUtils.addElementList hints, + id: "vimiumInputMarkerContainer" + className: "vimiumReset" + visibleInputs[selectedInputIndex].element.focus() if visibleInputs.length == 1 @exit() @@ -396,7 +396,7 @@ extend window, # 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 ) -> - return false # Diabled. + return false # Disabled. return !keyQueue and passKeys and 0 <= passKeys.indexOf(keyChar) # Track which keydown events we have handled, so that we can subsequently suppress the corresponding keyup -- cgit v1.2.3 From 9cb0f2853a155e39270282e6ed224966afffc61e Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 17 Jan 2015 05:51:52 +0000 Subject: Modes; yet more tweaks... - Mainly comments. - Rename chooseBadge to updateBadge (for consistency). - No badge for passkeys; also fix tests. --- content_scripts/mode.coffee | 6 ++---- content_scripts/mode_find.coffee | 22 ++++++++++++---------- content_scripts/mode_insert.coffee | 16 ++++++++-------- content_scripts/mode_passkeys.coffee | 5 +++-- 4 files changed, 25 insertions(+), 24 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 6bd09af2..2ff71ca8 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -85,8 +85,8 @@ class Mode "click": (event) => @alwaysContinueBubbling => @exit event # Some modes are singletons: there may be at most one instance active at any time. A mode is a singleton - # if @options.singleton is truthy. The value of @options.singleton should be the which is intended to - # be unique. New instances deactivate existing instances. + # if @options.singleton is truthy. The value of @options.singleton should be the key the which is + # intended to be unique. New instances deactivate existing instances with the same key. if @options.singleton do => singletons = Mode.singletons ||= {} @@ -94,8 +94,6 @@ class Mode @onExit => delete singletons[key] if singletons[key] == @ if singletons[key] @log "singleton:", "deactivating #{singletons[key].id}" - # We're currently installing a new mode, so we'll be updating the badge shortly. Therefore, we can - # suppress badge updates while deactivating the existing singleton. This can prevent badge flicker. singletons[key].exit() singletons[key] = @ diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 6b4f6bb1..35352277 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -1,6 +1,7 @@ -# NOTE(smblott). Ultimately, all of the FindMode-related code should be moved to this file. +# NOTE(smblott). Ultimately, all of the FindMode-related code should be moved here. -# This prevents printable characters from being passed through to underlying page; see #1415. +# This prevents unmapped printable characters from being passed through to underlying page; see #1415. Only +# used by PostFindMode, below. class SuppressPrintable extends Mode constructor: (options) -> super options @@ -9,20 +10,20 @@ class SuppressPrintable extends Mode # We use unshift here, so we see events after normal mode, so we only see unmapped keys. @unshift - _name: "mode-#{@id}/suppressPrintableEvents" + _name: "mode-#{@id}/suppress-printable" keydown: handler keypress: handler keyup: (event) => - # If the selection types has changed (usually, no longer "Range"), then the user is interacting with + # If the selection type has changed (usually, no longer "Range"), then the user is interacting with # the input element, so we get out of the way. See discussion of option 5c from #1415. if document.getSelection().type != type then @exit() else handler event -# When we use find mode, the selection/focus can land in a focusable/editable element. In this situation, -# special considerations apply. We implement three special cases: +# When we use find, the selection/focus can land in a focusable/editable element. In this situation, special +# considerations apply. We implement three special cases: # 1. Disable insert mode, because the user hasn't asked to enter insert mode. We do this by using # InsertMode.suppressEvent. -# 2. Prevent printable keyboard events from propagating to the page; see #1415. We do this by inheriting -# from SuppressPrintable. +# 2. Prevent unmapped printable keyboard events from propagating to the page; see #1415. We do this by +# inheriting from SuppressPrintable. # 3. If the very-next keystroke is Escape, then drop immediately into insert mode. # class PostFindMode extends SuppressPrintable @@ -39,7 +40,8 @@ class PostFindMode extends SuppressPrintable keypress: (event) -> InsertMode.suppressEvent event keyup: (event) -> InsertMode.suppressEvent event - # If the very-next keydown is Esc, drop immediately into insert mode. + # If the very-next keydown is Escape, then exit immediately, thereby passing subsequent keys to the + # underlying insert-mode instance. self = @ @push _name: "mode-#{@id}/handle-escape" @@ -53,7 +55,7 @@ class PostFindMode extends SuppressPrintable true # Continue bubbling. # If PostFindMode is active, then we suppress the "I" badge from insert mode. - updateBadge: (badge) -> InsertMode.suppressEvent badge + updateBadge: (badge) -> InsertMode.suppressEvent badge # Always truthy. root = exports ? window root.PostFindMode = PostFindMode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index ef7223ad..123c72be 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -1,14 +1,14 @@ class InsertMode extends Mode - # There is one permanently-installed instance of InsertMode. It watches for focus changes and - # activates/deactivates itself accordingly. + # There is one permanently-installed instance of InsertMode. It tracks focus changes and + # activates/deactivates itself (by setting @insertModeLock) accordingly. @permanentInstance: null constructor: (options = {}) -> InsertMode.permanentInstance ||= @ @permanent = (@ == InsertMode.permanentInstance) - # If truthy, then options.global indicates that we were activated by the user (with "i"). + # If truthy, then we were activated by the user (with "i"). @global = options.global defaults = @@ -21,7 +21,7 @@ class InsertMode extends Mode @insertModeLock = if document.activeElement and DomUtils.isEditable document.activeElement - # An input element is already active, so use it. + # An input element is already active, so use it. document.activeElement else null @@ -30,9 +30,9 @@ class InsertMode extends Mode "blur": (event) => @alwaysContinueBubbling => target = event.target # We can't rely on focus and blur events arriving in the expected order. When the active element - # changes, we might get "blur" before "focus". We track the active element in @insertModeLock, and + # changes, we might get "focus" before "blur". We track the active element in @insertModeLock, and # exit only when that element blurs. - @exit event, target if target == @insertModeLock and DomUtils.isFocusable target + @exit event, target if target == @insertModeLock "focus": (event) => @alwaysContinueBubbling => if @insertModeLock != event.target and DomUtils.isFocusable event.target @insertModeLock = event.target @@ -44,7 +44,7 @@ class InsertMode extends Mode # 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. - if document.activeElement?.isContentEditable and @insertModeLock != document.activeElement + if @insertModeLock != document.activeElement and document.activeElement?.isContentEditable @insertModeLock = document.activeElement Mode.updateBadge() @insertModeLock != null @@ -75,7 +75,7 @@ class InsertMode extends Mode updateBadge: (badge) -> badge.badge ||= "I" if @isActive badge - # Static stuff. This allows PostFindMode to suppress insert mode. + # Static stuff. This allows PostFindMode to suppress the permanently-installed InsertMode instance. @suppressedEvent: null @suppressEvent: (event) -> @suppressedEvent = event diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee index a40fe7a6..94a7c7ec 100644 --- a/content_scripts/mode_passkeys.coffee +++ b/content_scripts/mode_passkeys.coffee @@ -16,8 +16,9 @@ class PassKeysMode extends Mode else @continueBubbling - updateBadge: (badge) -> - badge.badge ||= "P" if @passKeys and not @keyQueue + # Disabled, pending experimentation with how/whether to use badges (smblott, 2015/01/17). + # updateBadge: (badge) -> + # badge.badge ||= "P" if @passKeys and not @keyQueue root = exports ? window root.PassKeysMode = PassKeysMode -- cgit v1.2.3 From 0e59b99e95e6a4fd3f64fd284e7417ba5f7e22e1 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 18 Jan 2015 06:27:38 +0000 Subject: Modes; pre-merge clean up. --- content_scripts/link_hints.coffee | 1 - content_scripts/mode.coffee | 15 +++++++------- content_scripts/mode_find.coffee | 21 ++++++++++++------- content_scripts/mode_insert.coffee | 38 +++++++++++++++++----------------- content_scripts/vimium_frontend.coffee | 27 +++++++++++------------- 5 files changed, 52 insertions(+), 50 deletions(-) (limited to 'content_scripts') diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 3c8240c0..5e41cbbb 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -10,7 +10,6 @@ # # 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" } diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 2ff71ca8..a74acfed 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -24,8 +24,10 @@ # responds to "focus" events, then push an additional handler: # @push # "focus": (event) => .... -# Any such handlers are removed when the mode is deactivated. +# Such handlers are removed when the mode is deactivated. # +# The following events can be handled: +# keydown, keypress, keyup, click, focus and blur # Debug only. count = 0 @@ -85,8 +87,8 @@ class Mode "click": (event) => @alwaysContinueBubbling => @exit event # Some modes are singletons: there may be at most one instance active at any time. A mode is a singleton - # if @options.singleton is truthy. The value of @options.singleton should be the key the which is - # intended to be unique. New instances deactivate existing instances with the same key. + # if @options.singleton is truthy. The value of @options.singleton should be the key which is intended to + # be unique. New instances deactivate existing instances with the same key. if @options.singleton do => singletons = Mode.singletons ||= {} @@ -99,7 +101,7 @@ class Mode # 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. The mode also tracks the - # keyQueue in @keyQueue. + # current keyQueue in @keyQueue. if @options.trackState @enabled = false @passKeys = "" @@ -115,7 +117,7 @@ class Mode Mode.modes.push @ Mode.updateBadge() - @logStack() + @logModes() # End of Mode constructor. push: (handlers) -> @@ -124,7 +126,6 @@ class Mode unshift: (handlers) -> handlers._name ||= "mode-#{@id}" - handlers._name += "/unshifted" @handlers.push handlerStack.unshift handlers onExit: (handler) -> @@ -160,7 +161,7 @@ class Mode badge: badge.badge # Debugging routines. - logStack: -> + logModes: -> if @debug @log "active modes (top to bottom):" @log " ", mode.id for mode in Mode.modes[..].reverse() diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 35352277..dff63949 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -33,6 +33,8 @@ class PostFindMode extends SuppressPrintable super name: "post-find" + # We show a "?" badge, but only while an Escape activates insert mode. + badge: "?" singleton: PostFindMode exitOnBlur: element exitOnClick: true @@ -42,20 +44,23 @@ class PostFindMode extends SuppressPrintable # If the very-next keydown is Escape, then exit immediately, thereby passing subsequent keys to the # underlying insert-mode instance. - self = @ @push _name: "mode-#{@id}/handle-escape" - keydown: (event) -> + keydown: (event) => if KeyboardUtils.isEscape event DomUtils.suppressKeyupAfterEscape handlerStack - self.exit() - false # Suppress event. + @exit() + @suppressEvent else - @remove() - true # Continue bubbling. + handlerStack.remove() + @badge = "" + Mode.updateBadge() + @continueBubbling - # If PostFindMode is active, then we suppress the "I" badge from insert mode. - updateBadge: (badge) -> InsertMode.suppressEvent badge # Always truthy. + updateBadge: (badge) -> + badge.badge ||= @badge + # Suppress the "I" badge from insert mode. + InsertMode.suppressEvent badge # Always truthy. root = exports ? window root.PostFindMode = PostFindMode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 123c72be..196f910b 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -11,11 +11,18 @@ class InsertMode extends Mode # If truthy, then we were activated by the user (with "i"). @global = options.global + handleKeyEvent = (event) => + return @continueBubbling unless @isActive event + return @stopBubblingAndTrue unless event.type == 'keydown' and KeyboardUtils.isEscape event + DomUtils.suppressKeyupAfterEscape handlerStack + @exit event, event.srcElement + @suppressEvent + defaults = name: "insert" - keydown: (event) => @handleKeydownEvent event - keypress: (event) => @handleKeyEvent event - keyup: (event) => @handleKeyEvent event + keypress: handleKeyEvent + keyup: handleKeyEvent + keydown: handleKeyEvent super extend defaults, options @@ -32,11 +39,10 @@ class InsertMode extends Mode # We can't rely on focus and blur events arriving in the expected order. When the active element # changes, we might get "focus" before "blur". We track the active element in @insertModeLock, and # exit only when that element blurs. - @exit event, target if target == @insertModeLock + @exit event, target if @insertModeLock and target == @insertModeLock "focus": (event) => @alwaysContinueBubbling => if @insertModeLock != event.target and DomUtils.isFocusable event.target - @insertModeLock = event.target - Mode.updateBadge() + @activateOnElement event.target isActive: (event) -> return false if event == InsertMode.suppressedEvent @@ -44,24 +50,18 @@ class InsertMode extends Mode # 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. - if @insertModeLock != document.activeElement and document.activeElement?.isContentEditable - @insertModeLock = document.activeElement - Mode.updateBadge() + @activateOnElement document.activeElement if document.activeElement?.isContentEditable @insertModeLock != null - handleKeydownEvent: (event) -> - return @continueBubbling unless @isActive event - return @stopBubblingAndTrue unless KeyboardUtils.isEscape event - DomUtils.suppressKeyupAfterEscape handlerStack - @exit event, event.srcElement - @suppressEvent - - # Handles keypress and keyup events. - handleKeyEvent: (event) -> - if @isActive event then @stopBubblingAndTrue else @continueBubbling + activateOnElement: (element) -> + @log "#{@id}: activating (permanent)" if @debug and @permanent + @insertModeLock = element + Mode.updateBadge() exit: (_, target) -> + # Note: target == undefined, here, is required only for tests. if (target and target == @insertModeLock) or @global or target == undefined + @log "#{@id}: deactivating (permanent)" if @debug and @permanent and @insertModeLock @insertModeLock = null if target and DomUtils.isFocusable target # Remove the focus, so the user can't just get back into insert mode by typing in the same input box. diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 0a034e28..6a26a133 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -117,10 +117,11 @@ initializePreDomReady = -> keypress: (event) => onKeypress.call @, event keyup: (event) => onKeyup.call @, event - # Install the permanent modes and handlers. The permanent insert mode operates only when focusable/editable - # elements are active. + Scroller.init settings + + # Install the permanent modes. The permanently-installed insert mode tracks focus/blur events, and + # activates/deactivates itself accordingly. new NormalMode - Scroller.init settings new PassKeysMode new InsertMode @@ -242,8 +243,6 @@ enterInsertModeIfElementIsFocused = -> enterInsertModeWithoutShowingIndicator(document.activeElement) onDOMActivate = (event) -> handlerStack.bubbleEvent 'DOMActivate', event -onFocus = (event) -> handlerStack.bubbleEvent 'focus', event -onBlur = (event) -> handlerStack.bubbleEvent 'blur', event executePageCommand = (request) -> return unless frameId == request.frameId @@ -326,8 +325,7 @@ extend window, HUD.showForDuration("Yanked URL", 1000) enterInsertMode: -> - new InsertMode - global: true + new InsertMode global: true enterVisualMode: => new VisualMode() @@ -804,17 +802,16 @@ executeFind = (query, options) -> -> document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true) 0) + # We are either in normal mode ("n"), or find mode ("/"). We are not in insert mode. Nevertheless, if a + # previous find landed in an editable element, then that element may still be activated. In this case, we + # don't want to leave it behind (see #1412). + if document.activeElement and DomUtils.isEditable document.activeElement + if not DomUtils.isSelected document.activeElement + document.activeElement.blur() + # we need to save the anchor node here because seems to nullify it, regardless of whether we do # preventDefault() findModeAnchorNode = document.getSelection().anchorNode - - # TODO(smblott). Disabled. This is the wrong test. Should be reinstated when we have the right test, which - # looks like it should be "isSelected" from #1431. - # # If the anchor node not a descendent of the active element, then blur the active element. We don't want to - # # leave behind an inappropriate active element. This fixes #1412. - # if document.activeElement and not DomUtils.isDOMDescendant document.activeElement, findModeAnchorNode - # document.activeElement.blur() - result restoreDefaultSelectionHighlight = -> document.body.classList.remove("vimiumFindMode") -- cgit v1.2.3 From 5d087c89917e21872711b7b908fcdd3c7e9e7f17 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 18 Jan 2015 10:28:17 +0000 Subject: Modes; disable debugging output by default. --- content_scripts/mode.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'content_scripts') diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index a74acfed..acc3978e 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -34,7 +34,7 @@ count = 0 class Mode # If Mode.debug is true, then we generate a trace of modes being activated and deactivated on the console. - debug: true + debug: false @modes: [] # Constants; short, readable names for the return values expected by handlerStack.bubbleEvent. -- cgit v1.2.3