diff options
| author | Stephen Blott | 2015-01-18 10:39:09 +0000 |
|---|---|---|
| committer | Stephen Blott | 2015-01-18 10:39:09 +0000 |
| commit | a1edae57e2847c2b6ffcae60ea8c9c16216e4692 (patch) | |
| tree | 30ff186038028f9d0c0d5cc08d572ca56dda8819 /content_scripts/vimium_frontend.coffee | |
| parent | 8c9e429074580ea20aba662ee430d87bd73ebc4b (diff) | |
| parent | 5d087c89917e21872711b7b908fcdd3c7e9e7f17 (diff) | |
| download | vimium-a1edae57e2847c2b6ffcae60ea8c9c16216e4692.tar.bz2 | |
Merge pull request #1413 from smblott-github/modes
A modal-browsing framework
Diffstat (limited to 'content_scripts/vimium_frontend.coffee')
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 257 |
1 files changed, 152 insertions, 105 deletions
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index a3ab051b..7121569a 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -4,9 +4,8 @@ # 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 +targetElement = null findMode = false findModeQuery = { rawQuery: "", matchCount: 0 } findModeQueryHasResults = false @@ -21,8 +20,8 @@ isEnabledForUrl = true passKeys = null keyQueue = null # The user's operating system. -currentCompletionKeys = null -validFirstKeys = null +currentCompletionKeys = "" +validFirstKeys = "" # The types in <input type="..."> 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 @@ -110,7 +109,21 @@ initializePreDomReady = -> settings.addEventListener("load", LinkHints.init.bind(LinkHints)) settings.load() - Scroller.init settings + class NormalMode extends Mode + constructor: -> + super + name: "normal" + keydown: (event) => onKeydown.call @, event + keypress: (event) => onKeypress.call @, event + keyup: (event) => onKeyup.call @, event + + Scroller.init settings + + # Install the permanent modes. The permanently-installed insert mode tracks focus/blur events, and + # activates/deactivates itself accordingly. + new NormalMode + new PassKeysMode + new InsertMode checkIfEnabledForUrl() @@ -136,9 +149,11 @@ initializePreDomReady = -> getScrollPosition: -> scrollX: window.scrollX, scrollY: window.scrollY setScrollPosition: (request) -> setScrollPosition request.scrollX, request.scrollY executePageCommand: executePageCommand - getActiveState: -> { enabled: isEnabledForUrl, passKeys: passKeys } + getActiveState: getActiveState setState: setState - currentKeyQueue: (request) -> keyQueue = request.keyQueue + currentKeyQueue: (request) -> + keyQueue = request.keyQueue + 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 @@ -169,11 +184,8 @@ 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 - installListener document, "focus", onFocusCapturePhase - installListener document, "blur", onBlurCapturePhase + for type in ["keydown", "keypress", "keyup", "click", "focus", "blur"] + do (type) -> installListener window, type, (event) -> handlerStack.bubbleEvent type, event installListener document, "DOMActivate", onDOMActivate enterInsertModeIfElementIsFocused() installedListeners = true @@ -182,6 +194,13 @@ setState = (request) -> initializeWhenEnabled(request.passKeys) if request.enabled isEnabledForUrl = request.enabled passKeys = request.passKeys + 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. @@ -305,6 +324,12 @@ extend window, HUD.showForDuration("Yanked URL", 1000) + enterInsertMode: -> + new InsertMode global: true + + 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. @@ -321,10 +346,6 @@ extend window, selectedInputIndex = Math.min(count - 1, visibleInputs.length - 1) - visibleInputs[selectedInputIndex].element.focus() - - return if visibleInputs.length == 1 - hints = for tuple in visibleInputs hint = document.createElement("div") hint.className = "vimiumReset internalVimiumInputHint vimiumInputHint" @@ -337,33 +358,43 @@ extend window, hint - hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' - - hintContainingDiv = DomUtils.addElementList(hints, - { id: "vimiumInputMarkerContainer", className: "vimiumReset" }) + 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 + hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint' + selectedInputIndex += hints.length + (if event.shiftKey then -1 else 1) + selectedInputIndex %= hints.length + hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' + visibleInputs[selectedInputIndex].element.focus() + @suppressEvent + else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey + @exit() + @continueBubbling + + @onExit -> DomUtils.removeElement hintContainingDiv + 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 - - false + 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 # passKey, then 'gt' and '99t' will neverthless be handled by vimium. isPassKey = ( keyChar ) -> + 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 @@ -397,9 +428,8 @@ 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) -> - return unless handlerStack.bubbleEvent('keypress', event) - keyChar = "" # Ignore modifier keys by themselves. @@ -409,23 +439,27 @@ onKeypress = (event) -> # Enter insert mode when the user enables the native find interface. if (keyChar == "f" && KeyboardUtils.isPrimaryModifierKey(event)) enterInsertModeWithoutShowingIndicator() - return + return @stopBubblingAndTrue if (keyChar) if (findMode) handleKeyCharForFindMode(keyChar) DomUtils.suppressEvent(event) + return @stopBubblingAndTrue else if (!isInsertMode() && !findMode) if (isPassKey keyChar) - return undefined - if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)) + return @stopBubblingAndTrue + if currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar) DomUtils.suppressEvent(event) + keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) + return @stopBubblingAndTrue keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) -onKeydown = (event) -> - return unless handlerStack.bubbleEvent('keydown', event) + return @continueBubbling +# @/this, here, is the the normal-mode Mode object. +onKeydown = (event) -> keyChar = "" # handle special keys, and normal input keys with modifiers being pressed. don't handle shiftKey alone (to @@ -464,37 +498,45 @@ onKeydown = (event) -> exitInsertMode() DomUtils.suppressEvent event KeydownEvents.push event + return @stopBubblingAndTrue else if (findMode) if (KeyboardUtils.isEscape(event)) handleEscapeForFindMode() DomUtils.suppressEvent event KeydownEvents.push event + return @stopBubblingAndTrue else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) handleDeleteForFindMode() DomUtils.suppressEvent event KeydownEvents.push event + return @stopBubblingAndTrue else if (event.keyCode == keyCodes.enter) handleEnterForFindMode() DomUtils.suppressEvent event KeydownEvents.push event + return @stopBubblingAndTrue else if (!modifiers) DomUtils.suppressPropagation(event) KeydownEvents.push event + return @stopBubblingAndTrue else if (isShowingHelpDialog && KeyboardUtils.isEscape(event)) hideHelpDialog() DomUtils.suppressEvent event KeydownEvents.push event + return @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 @stopBubblingAndTrue keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) @@ -516,13 +558,15 @@ onKeydown = (event) -> isValidFirstKey(KeyboardUtils.getKeyChar(event)))) DomUtils.suppressPropagation(event) KeydownEvents.push event + return @stopBubblingAndTrue -onKeyup = (event) -> - handledKeydown = KeydownEvents.pop event - return unless handlerStack.bubbleEvent("keyup", event) + return @continueBubbling - # Don't propagate the keyup to the underlying page if Vimium has handled it. See #733. - DomUtils.suppressPropagation(event) if handledKeydown +# @/this, here, is the the normal-mode Mode object. +onKeyup = (event) -> + return @continueBubbling unless KeydownEvents.pop event + DomUtils.suppressPropagation(event) + @stopBubblingAndTrue checkIfEnabledForUrl = -> url = window.location.toString() @@ -534,8 +578,12 @@ 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() + handlerStack.bubbleEvent "registerStateChange", + enabled: response.isEnabledForUrl + passKeys: response.passKeys -refreshCompletionKeys = (response) -> +# Exported to window, but only for DOM tests. +window.refreshCompletionKeys = (response) -> if (response) currentCompletionKeys = response.completionKeys @@ -583,35 +631,21 @@ isEditable = (target) -> 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") - -# # 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 <ESC>. # Note. This returns the truthiness of target, which is required by isInsertMode. # -enterInsertModeWithoutShowingIndicator = (target) -> insertModeLock = target +enterInsertModeWithoutShowingIndicator = (target) -> + return # Disabled. exitInsertMode = (target) -> - if (target == undefined || insertModeLock == target) - insertModeLock = null - HUD.hide() + return # Disabled. isInsertMode = -> - return true if insertModeLock != null - # 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 = -> @@ -701,6 +735,41 @@ handleEnterForFindMode = -> document.body.classList.add("vimiumFindMode") settings.set("findModeRawQuery", findModeQuery.rawQuery) +class FindMode extends Mode + constructor: -> + super + name: "find" + badge: "/" + exitOnEscape: true + exitOnClick: true + + keydown: (event) => + if event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey + handleDeleteForFindMode() + @suppressEvent + else if event.keyCode == keyCodes.enter + handleEnterForFindMode() + @exit() + @suppressEvent + else + DomUtils.suppressPropagation(event) + handlerStack.stopBubblingAndFalse + + keypress: (event) -> + handlerStack.neverContinueBubbling -> + if event.keyCode > 31 + keyChar = String.fromCharCode event.charCode + handleKeyCharForFindMode keyChar if keyChar + + keyup: (event) => @suppressEvent + + exit: (event) -> + super() + handleEscapeForFindMode() if event?.type == "keydown" and KeyboardUtils.isEscape event + handleEscapeForFindMode() if event?.type == "click" + if findModeQueryHasResults and event?.type != "click" + new PostFindMode + performFindInPlace = -> cachedScrollX = window.scrollX cachedScrollY = window.scrollY @@ -719,13 +788,9 @@ 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 - findMode = true - document.body.classList.add("vimiumFindMode") # prevent find from matching its own search query in the HUD @@ -737,7 +802,13 @@ executeFind = (query, options) -> -> document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true) 0) - findMode = oldFindMode + # 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 <esc> seems to nullify it, regardless of whether we do # preventDefault() findModeAnchorNode = document.getSelection().anchorNode @@ -750,13 +821,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. @@ -764,7 +828,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) @@ -795,27 +859,11 @@ findAndFocus = (backwards) -> findModeQueryHasResults = executeFind(query, { backwards: backwards, caseSensitive: !findModeQuery.ignoreCase }) - if (!findModeQueryHasResults) + if findModeQueryHasResults + focusFoundLink() + new PostFindMode() if findModeQueryHasResults + else HUD.showForDuration("No matches for '" + findModeQuery.rawQuery + "'", 1000) - return - - # if we have found an input element via 'n', pressing <esc> 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) - return false # we have "consumed" this event, so do not propagate - return true - }) - - focusFoundLink() window.performFind = -> findAndFocus() @@ -930,11 +978,10 @@ showFindModeHUDForQuery = -> window.enterFindMode = -> findModeQuery = { rawQuery: "" } - findMode = true HUD.show("/") + new FindMode() exitFindMode = -> - findMode = false HUD.hide() window.showHelpDialog = (html, fid) -> |
