diff options
Diffstat (limited to 'content_scripts')
| -rw-r--r-- | content_scripts/hud.coffee | 44 | ||||
| -rw-r--r-- | content_scripts/link_hints.coffee | 199 | ||||
| -rw-r--r-- | content_scripts/marks.coffee | 79 | ||||
| -rw-r--r-- | content_scripts/mode.coffee | 38 | ||||
| -rw-r--r-- | content_scripts/mode_find.coffee | 79 | ||||
| -rw-r--r-- | content_scripts/mode_insert.coffee | 94 | ||||
| -rw-r--r-- | content_scripts/mode_key_handler.coffee | 56 | ||||
| -rw-r--r-- | content_scripts/mode_normal.coffee | 369 | ||||
| -rw-r--r-- | content_scripts/mode_visual.coffee | 23 | ||||
| -rw-r--r-- | content_scripts/scroller.coffee | 28 | ||||
| -rw-r--r-- | content_scripts/ui_component.coffee | 3 | ||||
| -rw-r--r-- | content_scripts/vimium.css | 2 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 442 | ||||
| -rw-r--r-- | content_scripts/vomnibar.coffee | 11 | 
14 files changed, 759 insertions, 708 deletions
| diff --git a/content_scripts/hud.coffee b/content_scripts/hud.coffee index b2780491..42a960da 100644 --- a/content_scripts/hud.coffee +++ b/content_scripts/hud.coffee @@ -9,6 +9,8 @@ HUD =    findMode: null    abandon: -> @hudUI?.hide false +  pasteListener: null # Set by @pasteFromClipboard to handle the value returned by pasteResponse +    # This HUD is styled to precisely mimick the chrome HUD on Mac. Use the "has_popup_and_link_hud.html"    # test harness to tweak these styles to match Chrome's. One limitation of our HUD display is that    # it doesn't sit on top of horizontal scrollbars like Chrome's HUD does. @@ -35,7 +37,9 @@ HUD =        @tween.fade 1.0, 150    search: (data) -> -    @findMode.findInPlace data.query +    # NOTE(mrmr1993): On Firefox, window.find moves the window focus away from the HUD. We use postFindFocus +    # to put it back, so the user can continue typing. +    @findMode.findInPlace data.query, {"postFindFocus": @hudUI.iframeElement.contentWindow}      # Show the number of matches in the HUD UI.      matchCount = if FindMode.query.parsedQuery.length > 0 then FindMode.query.matchCount else 0 @@ -66,20 +70,47 @@ HUD =      focusNode = DomUtils.getSelectionFocusElement()      document.activeElement?.blur() -    focusNode?.focus() +    focusNode?.focus?()      if exitEventIsEnter -      handleEnterForFindMode() +      FindMode.handleEnter()        if FindMode.query.hasResults          postExit = -> new PostFindMode      else if exitEventIsEscape -      # We don't want FindMode to handle the click events that handleEscapeForFindMode can generate, so we +      # We don't want FindMode to handle the click events that FindMode.handleEscape can generate, so we        # wait until the mode is closed before running it. -      postExit = handleEscapeForFindMode +      postExit = FindMode.handleEscape      @findMode.exit()      postExit?() +  # These commands manage copying and pasting from the clipboard in the HUD frame. +  # NOTE(mrmr1993): We need this to copy and paste on Firefox: +  # * an element can't be focused in the background page, so copying/pasting doesn't work +  # * we don't want to disrupt the focus in the page, in case the page is listening for focus/blur events. +  # * the HUD shouldn't be active for this frame while any of the copy/paste commands are running. +  copyToClipboard: (text) -> +    DomUtils.documentComplete => +      @init() +      @hudUI?.postMessage {name: "copyToClipboard", data: text} + +  pasteFromClipboard: (@pasteListener) -> +    DomUtils.documentComplete => +      @init() +      # Show the HUD frame, so Firefox will actually perform the paste. +      @hudUI.toggleIframeElementClasses "vimiumUIComponentHidden", "vimiumUIComponentVisible" +      @tween.fade 0, 0 +      @hudUI.postMessage {name: "pasteFromClipboard"} + +  pasteResponse: ({data}) -> +    # Hide the HUD frame again. +    @hudUI.toggleIframeElementClasses "vimiumUIComponentVisible", "vimiumUIComponentHidden" +    @unfocusIfFocused() +    @pasteListener data + +  unfocusIfFocused: -> +    document.activeElement.blur() if document.activeElement == @hudUI?.iframeElement +  class Tween    opacity: 0    intervalId: -1 @@ -125,5 +156,6 @@ class Tween        }      """ -root = exports ? window +root = exports ? (window.root ?= {})  root.HUD = HUD +extend window, root unless exports? diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 0014e20a..0592c96d 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -31,7 +31,7 @@ COPY_LINK_URL =    indicator: "Copy link URL to Clipboard"    linkActivator: (link) ->      if link.href? -      chrome.runtime.sendMessage handler: "copyToClipboard", data: link.href +      HUD.copyToClipboard link.href        url = link.href        url = url[0..25] + "...." if 28 < url.length        HUD.showForDuration "Yanked #{url}", 2000 @@ -128,7 +128,7 @@ LinkHints =          if isSuccess            # Wait for the next tick to allow the previous mode to exit.  It might yet generate a click event,            # which would cause our new mode to exit immediately. -          Utils.nextTick -> LinkHints.activateMode count-1, mode +          Utils.nextTick -> LinkHints.activateMode count-1, {mode}    activateModeToOpenInNewTab: (count) -> @activateMode count, mode: OPEN_IN_NEW_BG_TAB    activateModeToOpenInNewForegroundTab: (count) -> @activateMode count, mode: OPEN_IN_NEW_FG_TAB @@ -166,17 +166,15 @@ class LinkHintsMode        name: "hint/#{@mode.name}"        indicator: false        singleton: "link-hints-mode" -      passInitialKeyupEvents: true        suppressAllKeyboardEvents: true        suppressTrailingKeyEvents: true        exitOnEscape: true        exitOnClick: true        keydown: @onKeyDownInMode.bind this -      keypress: @onKeyPressInMode.bind this      @hintMode.onExit (event) =>        if event?.type == "click" or (event?.type == "keydown" and -        (KeyboardUtils.isEscape(event) or event.keyCode in [keyCodes.backspace, keyCodes.deleteKey])) +        (KeyboardUtils.isEscape(event) or KeyboardUtils.isBackspace event))            HintCoordinator.sendMessage "exit", isSuccess: false      # Note(philc): Append these markers as top level children instead of as child nodes to the link itself, @@ -230,86 +228,72 @@ class LinkHintsMode        linkText: desc.linkText        stableSortCount: ++@stableSortCount -  # Handles <Shift> and <Ctrl>. +  # Handles all keyboard events.    onKeyDownInMode: (event) ->      return if event.repeat -    @keydownKeyChar = KeyboardUtils.getKeyChar(event).toLowerCase() -    previousTabCount = @tabCount -    @tabCount = 0 - -    # NOTE(smblott) As of 1.54, the Ctrl modifier doesn't work for filtered link hints; therefore we only -    # offer the control modifier for alphabet hints.  It is not clear whether we should fix this.  As of -    # 16-03-28, nobody has complained. -    modifiers = [keyCodes.shiftKey] -    modifiers.push keyCodes.ctrlKey unless Settings.get "filterLinkHints" - -    if event.keyCode in modifiers and +    # NOTE(smblott) The modifier behaviour here applies only to alphabet hints. +    if event.key in ["Control", "Shift"] and not Settings.get("filterLinkHints") and        @mode in [ OPEN_IN_CURRENT_TAB, OPEN_WITH_QUEUE, OPEN_IN_NEW_BG_TAB, OPEN_IN_NEW_FG_TAB ] -        @tabCount = previousTabCount          # Toggle whether to open the link in a new or current tab.          previousMode = @mode -        keyCode = event.keyCode +        key = event.key -        switch keyCode -          when keyCodes.shiftKey +        switch key +          when "Shift"              @setOpenLinkMode(if @mode is OPEN_IN_CURRENT_TAB then OPEN_IN_NEW_BG_TAB else OPEN_IN_CURRENT_TAB) -          when keyCodes.ctrlKey +          when "Control"              @setOpenLinkMode(if @mode is OPEN_IN_NEW_FG_TAB then OPEN_IN_NEW_BG_TAB else OPEN_IN_NEW_FG_TAB) -        handlerId = handlerStack.push +        handlerId = @hintMode.push            keyup: (event) => -            if event.keyCode == keyCode +            if event.key == key                handlerStack.remove()                @setOpenLinkMode previousMode              true # Continue bubbling the event. -        # For some (unknown) reason, we don't always receive the keyup event needed to remove this handler. -        # Therefore, we ensure that it's always removed when hint mode exits.  See #1911 and #1926. -        @hintMode.onExit -> handlerStack.remove handlerId - -    else if event.keyCode in [ keyCodes.backspace, keyCodes.deleteKey ] +    else if KeyboardUtils.isBackspace event        if @markerMatcher.popKeyChar() +        @tabCount = 0          @updateVisibleMarkers()        else          # Exit via @hintMode.exit(), so that the LinkHints.activate() "onExit" callback sees the key event and          # knows not to restart hints mode.          @hintMode.exit event -    else if event.keyCode == keyCodes.enter +    else if event.key == "Enter"        # Activate the active hint, if there is one.  Only FilterHints uses an active hint.        HintCoordinator.sendMessage "activateActiveHintMarker" if @markerMatcher.activeHintMarker -    else if event.keyCode == keyCodes.tab -      @tabCount = previousTabCount + (if event.shiftKey then -1 else 1) -      @updateVisibleMarkers @tabCount +    else if event.key == "Tab" +      if event.shiftKey then @tabCount-- else @tabCount++ +      @updateVisibleMarkers() -    else if event.keyCode == keyCodes.space and @markerMatcher.shouldRotateHints event -      @tabCount = previousTabCount +    else if event.key == " " and @markerMatcher.shouldRotateHints event        HintCoordinator.sendMessage "rotateHints"      else -      @tabCount = previousTabCount if event.ctrlKey or event.metaKey or event.altKey -      return - -    # We've handled the event, so suppress it and update the mode indicator. -    DomUtils.suppressEvent event - -  # Handles normal input. -  onKeyPressInMode: (event) -> -    return if event.repeat - -    keyChar = String.fromCharCode(event.charCode).toLowerCase() -    if keyChar -      @markerMatcher.pushKeyChar keyChar, @keydownKeyChar -      @updateVisibleMarkers() +      unless event.repeat +        keyChar = +          if Settings.get "filterLinkHints" +            KeyboardUtils.getKeyChar(event) +          else +            KeyboardUtils.getKeyChar(event).toLowerCase() +        if keyChar +          keyChar = " " if keyChar == "space" +          if keyChar.length == 1 +            @tabCount = 0 +            @markerMatcher.pushKeyChar keyChar +            @updateVisibleMarkers() +          else +            return handlerStack.suppressPropagation -    # We've handled the event, so suppress it. -    DomUtils.suppressEvent event +    handlerStack.suppressEvent -  updateVisibleMarkers: (tabCount = 0) -> +  updateVisibleMarkers: ->      {hintKeystrokeQueue, linkTextKeystrokeQueue} = @markerMatcher -    HintCoordinator.sendMessage "updateKeyState", {hintKeystrokeQueue, linkTextKeystrokeQueue, tabCount} +    HintCoordinator.sendMessage "updateKeyState", +      {hintKeystrokeQueue, linkTextKeystrokeQueue, tabCount: @tabCount}    updateKeyState: ({hintKeystrokeQueue, linkTextKeystrokeQueue, tabCount}) ->      extend @markerMatcher, {hintKeystrokeQueue, linkTextKeystrokeQueue} @@ -318,7 +302,7 @@ class LinkHintsMode      if linksMatched.length == 0        @deactivateMode()      else if linksMatched.length == 1 -      @activateLink linksMatched[0], userMightOverType ? false +      @activateLink linksMatched[0], userMightOverType      else        @hideMarker marker for marker in @hintMarkers        @showMarker matched, @markerMatcher.hintKeystrokeQueue.length for matched in linksMatched @@ -329,7 +313,7 @@ class LinkHintsMode    rotateHints: do ->      markerOverlapsStack = (marker, stack) ->        for otherMarker in stack -        return true if Rect.rectsOverlap marker.markerRect, otherMarker.markerRect +        return true if Rect.intersects marker.markerRect, otherMarker.markerRect        false      -> @@ -372,7 +356,7 @@ class LinkHintsMode    # When only one hint remains, activate it in the appropriate way.  The current frame may or may not contain    # the matched link, and may or may not have the focus.  The resulting four cases are accounted for here by    # selectively pushing the appropriate HintCoordinator.onExit handlers. -  activateLink: (linkMatched, userMightOverType=false) -> +  activateLink: (linkMatched, userMightOverType = false) ->      @removeHintMarkers()      if linkMatched.isLocalMarker @@ -398,25 +382,26 @@ class LinkHintsMode                clickEl.focus()              linkActivator clickEl -    installKeyboardBlocker = (startKeyboardBlocker) -> -      if linkMatched.isLocalMarker -        {top: viewportTop, left: viewportLeft} = DomUtils.getViewportTopLeft() -        for rect in (Rect.copy rect for rect in clickEl.getClientRects()) -          extend rect, top: rect.top + viewportTop, left: rect.left + viewportLeft -          flashEl = DomUtils.addFlashRect rect -          do (flashEl) -> HintCoordinator.onExit.push -> DomUtils.removeElement flashEl - -      if windowIsFocused() -        startKeyboardBlocker (isSuccess) -> HintCoordinator.sendMessage "exit", {isSuccess} +    # If flash elements are created, then this function can be used later to remove them. +    removeFlashElements = -> +    if linkMatched.isLocalMarker +      {top: viewportTop, left: viewportLeft} = DomUtils.getViewportTopLeft() +      flashElements = for rect in clickEl.getClientRects() +        DomUtils.addFlashRect Rect.translate rect, viewportLeft, viewportTop +      removeFlashElements = -> DomUtils.removeElement flashEl for flashEl in flashElements      # If we're using a keyboard blocker, then the frame with the focus sends the "exit" message, otherwise the      # frame containing the matched link does. -    if userMightOverType and Settings.get "waitForEnterForFilteredHints" -      installKeyboardBlocker (callback) -> new WaitForEnter callback -    else if userMightOverType -      installKeyboardBlocker (callback) -> new TypingProtector 200, callback +    if userMightOverType +      HintCoordinator.onExit.push removeFlashElements +      if windowIsFocused() +        callback = (isSuccess) -> HintCoordinator.sendMessage "exit", {isSuccess} +        if Settings.get "waitForEnterForFilteredHints" +          new WaitForEnter callback +        else +          new TypingProtector 200, callback      else if linkMatched.isLocalMarker -      DomUtils.flashRect linkMatched.rect +      Utils.setTimeout 400, removeFlashElements        HintCoordinator.sendMessage "exit", isSuccess: true    # @@ -444,12 +429,7 @@ class LinkHintsMode  # Use characters for hints, and do not filter links by their text.  class AlphabetHints    constructor: -> -    @linkHintCharacters = Settings.get "linkHintCharacters" -    # We use the keyChar from keydown if the link-hint characters are all "a-z0-9".  This is the default -    # settings value, and preserves the legacy behavior (which always used keydown) for users which are -    # familiar with that behavior.  Otherwise, we use keyChar from keypress, which admits non-Latin -    # characters. See #1722. -    @useKeydown = /^[a-z0-9]*$/.test @linkHintCharacters +    @linkHintCharacters = Settings.get("linkHintCharacters").toLowerCase()      @hintKeystrokeQueue = []    fillInMarkers: (hintMarkers) -> @@ -478,17 +458,17 @@ class AlphabetHints      matchString = @hintKeystrokeQueue.join ""      linksMatched: hintMarkers.filter (linkMarker) -> linkMarker.hintString.startsWith matchString -  pushKeyChar: (keyChar, keydownKeyChar) -> -    @hintKeystrokeQueue.push (if @useKeydown then keydownKeyChar else keyChar) +  pushKeyChar: (keyChar) -> +    @hintKeystrokeQueue.push keyChar    popKeyChar: -> @hintKeystrokeQueue.pop()    # For alphabet hints, <Space> always rotates the hints, regardless of modifiers.    shouldRotateHints: -> true -# Use numbers (usually) for hints, and also filter links by their text. +# Use characters for hints, and also filter links by their text.  class FilterHints    constructor: -> -    @linkHintNumbers = Settings.get "linkHintNumbers" +    @linkHintNumbers = Settings.get("linkHintNumbers").toUpperCase()      @hintKeystrokeQueue = []      @linkTextKeystrokeQueue = []      @activeHintMarker = null @@ -535,17 +515,18 @@ class FilterHints      linksMatched: linksMatched      userMightOverType: @hintKeystrokeQueue.length == 0 and 0 < @linkTextKeystrokeQueue.length -  pushKeyChar: (keyChar, keydownKeyChar) -> -    # For filtered hints, we *always* use the keyChar value from keypress, because there is no obvious and -    # easy-to-understand meaning for choosing one of keyChar or keydownKeyChar (as there is for alphabet -    # hints). +  pushKeyChar: (keyChar) ->      if 0 <= @linkHintNumbers.indexOf keyChar        @hintKeystrokeQueue.push keyChar +    else if keyChar.toLowerCase() != keyChar and @linkHintNumbers.toLowerCase() != @linkHintNumbers.toUpperCase() +      # The the keyChar is upper case and the link hint "numbers" contain characters (e.g. [a-zA-Z]).  We don't want +      # some upper-case letters matching hints (above) and some matching text (below), so we ignore such keys. +      return      # We only accept <Space> and characters which are not used for splitting (e.g. "a", "b", etc., but not "-").      else if keyChar == " " or not @splitRegexp.test keyChar        # Since we might renumber the hints, we should reset the current hintKeyStrokeQueue.        @hintKeystrokeQueue = [] -      @linkTextKeystrokeQueue.push keyChar +      @linkTextKeystrokeQueue.push keyChar.toLowerCase()    popKeyChar: ->      @hintKeystrokeQueue.pop() or @linkTextKeystrokeQueue.pop() @@ -626,7 +607,9 @@ LocalHints =    # image), therefore we always return a array of element/rect pairs (which may also be a singleton or empty).    #    getVisibleClickable: (element) -> -    tagName = element.tagName.toLowerCase() +    # Get the tag name.  However, `element.tagName` can be an element (not a string, see #2305), so we guard +    # against that. +    tagName = element.tagName.toLowerCase?() ? ""      isClickable = false      onlyHasTabIndex = false      possibleFalsePositive = false @@ -668,9 +651,12 @@ LocalHints =      isClickable ||= @checkForAngularJs element      # Check for attributes that make an element clickable regardless of its tagName. -    if (element.hasAttribute("onclick") or -        element.getAttribute("role")?.toLowerCase() in ["button", "link"] or -        element.getAttribute("contentEditable")?.toLowerCase() in ["", "contentEditable", "true"]) +    if element.hasAttribute("onclick") or +        (role = element.getAttribute "role") and role.toLowerCase() in [ +          "button" , "tab" , "link", "checkbox", "menuitem", "menuitemcheckbox", "menuitemradio" +        ] or +        (contentEditable = element.getAttribute "contentEditable") and +          contentEditable.toLowerCase() in ["", "contenteditable", "true"]        isClickable = true      # Check for jsaction event listeners on the element. @@ -821,25 +807,10 @@ LocalHints =        hint.rect.left += left      if Settings.get "filterLinkHints" -      @withLabelMap (labelMap) => -        extend hint, @generateLinkText labelMap, hint for hint in localHints +      extend hint, @generateLinkText hint for hint in localHints      localHints -  # Generate a map of input element => label text, call a callback with it. -  withLabelMap: (callback) -> -    labelMap = {} -    labels = document.querySelectorAll "label" -    for label in labels -      forElement = label.getAttribute "for" -      if forElement -        labelText = label.textContent.trim() -        # Remove trailing ":" commonly found in labels. -        if labelText[labelText.length-1] == ":" -          labelText = labelText.substr 0, labelText.length-1 -        labelMap[forElement] = labelText -    callback labelMap - -  generateLinkText: (labelMap, hint) -> +  generateLinkText: (hint) ->      element = hint.element      linkText = ""      showLinkText = false @@ -847,9 +818,14 @@ LocalHints =      nodeName = element.nodeName.toLowerCase()      if nodeName == "input" -      if labelMap[element.id] -        linkText = labelMap[element.id] +      if element.labels? and element.labels.length > 0 +        linkText = element.labels[0].textContent.trim() +        # Remove trailing ":" commonly found in labels. +        if linkText[linkText.length-1] == ":" +          linkText = linkText[...linkText.length-1]          showLinkText = true +      else if element.getAttribute("type")?.toLowerCase() == "file" +        linkText = "Choose File"        else if element.type != "password"          linkText = element.value          if not linkText and 'placeholder' of element @@ -899,15 +875,16 @@ class WaitForEnter extends Mode      @push        keydown: (event) => -        if event.keyCode == keyCodes.enter +        if event.key == "Enter"            @exit()            callback true # true -> isSuccess.          else if KeyboardUtils.isEscape event            @exit()            callback false # false -> isSuccess. -root = exports ? window +root = exports ? (window.root ?= {})  root.LinkHints = LinkHints  root.HintCoordinator = HintCoordinator  # For tests:  extend root, {LinkHintsMode, LocalHints, AlphabetHints, WaitForEnter} +extend window, root unless exports? diff --git a/content_scripts/marks.coffee b/content_scripts/marks.coffee index 73191b1b..fb1d1b1d 100644 --- a/content_scripts/marks.coffee +++ b/content_scripts/marks.coffee @@ -40,22 +40,24 @@ Marks =        indicator: "Create mark..."        exitOnEscape: true        suppressAllKeyboardEvents: true -      keypress: (event) => -        keyChar = String.fromCharCode event.charCode -        @exit => -          if @isGlobalMark event, keyChar -            # We record the current scroll position, but only if this is the top frame within the tab. -            # Otherwise, we'll fetch the scroll position of the top frame from the background page later. -            [ scrollX, scrollY ] = [ window.scrollX, window.scrollY ] if DomUtils.isTopFrame() -            chrome.runtime.sendMessage -              handler: 'createMark' -              markName: keyChar -              scrollX: scrollX -              scrollY: scrollY -            , => @showMessage "Created global mark", keyChar -          else -            localStorage[@getLocationKey keyChar] = @getMarkString() -            @showMessage "Created local mark", keyChar +      keydown: (event) => +        if KeyboardUtils.isPrintable event +          keyChar = KeyboardUtils.getKeyChar event +          @exit => +            if @isGlobalMark event, keyChar +              # We record the current scroll position, but only if this is the top frame within the tab. +              # Otherwise, we'll fetch the scroll position of the top frame from the background page later. +              [ scrollX, scrollY ] = [ window.scrollX, window.scrollY ] if DomUtils.isTopFrame() +              chrome.runtime.sendMessage +                handler: 'createMark' +                markName: keyChar +                scrollX: scrollX +                scrollY: scrollY +              , => @showMessage "Created global mark", keyChar +            else +              localStorage[@getLocationKey keyChar] = @getMarkString() +              @showMessage "Created local mark", keyChar +          handlerStack.suppressEvent    activateGotoMode: (count, {registryEntry}) ->      @currentRegistryEntry = registryEntry @@ -64,27 +66,30 @@ Marks =        indicator: "Go to mark..."        exitOnEscape: true        suppressAllKeyboardEvents: true -      keypress: (event) => -        @exit => -          markName = String.fromCharCode event.charCode -          if @isGlobalMark event, markName -            # This key must match @getLocationKey() in the back end. -            key = "vimiumGlobalMark|#{markName}" -            chrome.storage.sync.get key, (items) -> -              if key of items -                chrome.runtime.sendMessage handler: 'gotoMark', markName: markName -                HUD.showForDuration "Jumped to global mark '#{markName}'", 1000 -              else -                HUD.showForDuration "Global mark not set '#{markName}'", 1000 -          else -            markString = @localRegisters[markName] ? localStorage[@getLocationKey markName] -            if markString? -              @setPreviousPosition() -              position = JSON.parse markString -              window.scrollTo position.scrollX, position.scrollY -              @showMessage "Jumped to local mark", markName +      keydown: (event) => +        if KeyboardUtils.isPrintable event +          @exit => +            keyChar = KeyboardUtils.getKeyChar event +            if @isGlobalMark event, keyChar +              # This key must match @getLocationKey() in the back end. +              key = "vimiumGlobalMark|#{keyChar}" +              Settings.storage.get key, (items) -> +                if key of items +                  chrome.runtime.sendMessage handler: 'gotoMark', markName: keyChar +                  HUD.showForDuration "Jumped to global mark '#{keyChar}'", 1000 +                else +                  HUD.showForDuration "Global mark not set '#{keyChar}'", 1000              else -              @showMessage "Local mark not set", markName +              markString = @localRegisters[keyChar] ? localStorage[@getLocationKey keyChar] +              if markString? +                @setPreviousPosition() +                position = JSON.parse markString +                window.scrollTo position.scrollX, position.scrollY +                @showMessage "Jumped to local mark", keyChar +              else +                @showMessage "Local mark not set", keyChar +          handlerStack.suppressEvent -root = exports ? window +root = exports ? (window.root ?= {})  root.Marks =  Marks +extend window, root unless exports? diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 6508627e..a4a91c1f 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -55,7 +55,7 @@ class Mode      # the need for modes which suppress all keyboard events 1) to provide handlers for all of those events,      # or 2) to worry about event suppression and event-handler return values.      if @options.suppressAllKeyboardEvents -      for type in [ "keydown", "keypress", "keyup" ] +      for type in [ "keydown", "keypress" ]          do (handler = @options[type]) =>            @options[type] = (event) => @alwaysSuppressPropagation => handler? event @@ -81,8 +81,7 @@ class Mode          _name: "mode-#{@id}/exitOnEscape"          "keydown": (event) =>            return @continueBubbling unless KeyboardUtils.isEscape event -          DomUtils.suppressKeyupAfterEscape handlerStack -          @exit event, event.srcElement +          @exit event, event.target            @suppressEvent      # If @options.exitOnBlur is truthy, then it should be an element.  The mode will exit when that element @@ -121,16 +120,6 @@ class Mode        singletons[key]?.exit()        singletons[key] = this -    # If @options.passInitialKeyupEvents is set, then we pass initial non-printable keyup events to the page -    # or to other extensions (because the corresponding keydown events were passed).  This is used when -    # activating link hints, see #1522. -    if @options.passInitialKeyupEvents -      @push -        _name: "mode-#{@id}/passInitialKeyupEvents" -        keydown: => @alwaysContinueBubbling -> handlerStack.remove() -        keyup: (event) => -          if KeyboardUtils.isPrintable event then @suppressPropagation else @passEventToPage -      # if @options.suppressTrailingKeyEvents is set, then  -- on exit -- we suppress all key events until a      # subsquent (non-repeat) keydown or keypress.  In particular, the intention is to catch keyup events for      # keys which we have handled, but which otherwise might trigger page actions (if the page is listening for @@ -148,7 +137,6 @@ class Mode            name: "suppress-trailing-key-events"            keydown: handler            keypress: handler -          keyup: -> handlerStack.suppressPropagation      Mode.modes.push this      @setIndicator() @@ -174,15 +162,16 @@ class Mode      @exitHandlers.push handler    exit: (args...) -> -    if @modeIsActive -      @log "deactivate:", @id -      unless @modeIsExiting -        @modeIsExiting = true -        handler args... for handler in @exitHandlers -        handlerStack.remove handlerId for handlerId in @handlers -      Mode.modes = Mode.modes.filter (mode) => mode != this -      @modeIsActive = false -      @setIndicator() +    return if @modeIsExiting or not @modeIsActive +    @log "deactivate:", @id +    @modeIsExiting = true + +    handler args... for handler in @exitHandlers +    handlerStack.remove handlerId for handlerId in @handlers +    Mode.modes = Mode.modes.filter (mode) => mode != this + +    @modeIsActive = false +    @setIndicator()    # Debugging routines.    logModes: -> @@ -209,5 +198,6 @@ class SuppressAllKeyboardEvents extends Mode        suppressAllKeyboardEvents: true      super extend defaults, options -root = exports ? window +root = exports ? (window.root ?= {})  extend root, {Mode, SuppressAllKeyboardEvents} +extend window, root unless exports? diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 8621edf8..f19b5db4 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -6,7 +6,7 @@ class SuppressPrintable extends Mode    constructor: (options) ->      super options      handler = (event) => if KeyboardUtils.isPrintable event then @suppressEvent else @continueBubbling -    type = document.getSelection().type +    type = DomUtils.getSelectionType()      # We use unshift here, so we see events after normal mode, so we only see unmapped keys.      @unshift @@ -16,7 +16,7 @@ class SuppressPrintable extends Mode        keyup: (event) =>          # 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 +        @exit() if DomUtils.getSelectionType() != type  # 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: @@ -47,7 +47,6 @@ class PostFindMode extends SuppressPrintable        _name: "mode-#{@id}/handle-escape"        keydown: (event) =>          if KeyboardUtils.isEscape event -          DomUtils.suppressKeyupAfterEscape handlerStack            @exit()            @suppressEvent          else @@ -80,15 +79,16 @@ class FindMode extends Mode    exit: (event) ->      super() -    handleEscapeForFindMode() if event +    FindMode.handleEscape() if event    restoreSelection: -> +    return unless @initialRange      range = @initialRange      selection = getSelection()      selection.removeAllRanges()      selection.addRange range -  findInPlace: (query) -> +  findInPlace: (query, options) ->      # If requested, restore the scroll position (so that failed searches leave the scroll position unchanged).      @checkReturnToViewPort()      FindMode.updateQuery query @@ -96,7 +96,7 @@ class FindMode extends Mode      # match as the user adds matching characters, or removes previously-matched characters. See #1434.      @restoreSelection()      query = if FindMode.query.isRegex then FindMode.getNextQueryFromRegexMatches(0) else FindMode.query.parsedQuery -    FindMode.query.hasResults = FindMode.execute query +    FindMode.query.hasResults = FindMode.execute query, options    @updateQuery: (query) ->      @query.rawQuery = query @@ -179,7 +179,13 @@ class FindMode extends Mode        # ignore the selectionchange event generated by find()        document.removeEventListener("selectionchange", @restoreDefaultSelectionHighlight, true) -    result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false) +    try +      result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false) +    catch # Failed searches throw on Firefox. + +    # window.find focuses the |window| that it is called on. This gives us an opportunity to (re-)focus +    # another element/window, if that isn't the behaviour we want. +    options.postFindFocus?.focus()      if options.colorSelection        setTimeout( @@ -194,22 +200,73 @@ class FindMode extends Mode      result -  @restoreDefaultSelectionHighlight: -> document.body.classList.remove("vimiumFindMode") +  @restoreDefaultSelectionHighlight: forTrusted -> document.body.classList.remove("vimiumFindMode") + +  # The user has found what they're looking for and is finished searching. We enter insert mode, if possible. +  @handleEscape: -> +    document.body.classList.remove("vimiumFindMode") +    # Removing the class does not re-color existing selections. we recreate the current selection so it reverts +    # back to the default color. +    selection = window.getSelection() +    unless selection.isCollapsed +      range = window.getSelection().getRangeAt(0) +      window.getSelection().removeAllRanges() +      window.getSelection().addRange(range) +    focusFoundLink() || selectFoundInputElement() + +  # Save the query so the user can do further searches with it. +  @handleEnter: -> +    focusFoundLink() +    document.body.classList.add("vimiumFindMode") +    FindMode.saveQuery() + +  @findNext: (backwards) -> +    Marks.setPreviousPosition() +    FindMode.query.hasResults = FindMode.execute null, {backwards} + +    if FindMode.query.hasResults +      focusFoundLink() +      new PostFindMode() +    else +      HUD.showForDuration("No matches for '#{FindMode.query.rawQuery}'", 1000)    checkReturnToViewPort: ->      window.scrollTo @scrollX, @scrollY if @options.returnToViewport  getCurrentRange = ->    selection = getSelection() -  if selection.type == "None" +  if DomUtils.getSelectionType(selection) == "None"      range = document.createRange()      range.setStart document.body, 0      range.setEnd document.body, 0      range    else -    selection.collapseToStart() if selection.type == "Range" +    selection.collapseToStart() if DomUtils.getSelectionType(selection) == "Range"      selection.getRangeAt 0 -root = exports ? window +getLinkFromSelection = -> +  node = window.getSelection().anchorNode +  while (node && node != document.body) +    return node if (node.nodeName.toLowerCase() == "a") +    node = node.parentNode +  null + +focusFoundLink = -> +  if (FindMode.query.hasResults) +    link = getLinkFromSelection() +    link.focus() if link + +selectFoundInputElement = -> +  # Since the last focused element might not be the one currently pointed to by find (e.g.  the current one +  # might be disabled and therefore unable to receive focus), we use the approximate heuristic of checking +  # that the last anchor node is an ancestor of our element. +  findModeAnchorNode = document.getSelection().anchorNode +  if (FindMode.query.hasResults && document.activeElement && +      DomUtils.isSelectable(document.activeElement) && +      DomUtils.isDOMDescendant(findModeAnchorNode, document.activeElement)) +    DomUtils.simulateSelect(document.activeElement) + +root = exports ? (window.root ?= {})  root.PostFindMode = PostFindMode  root.FindMode = FindMode +extend window, root unless exports? diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 4cb0a39e..511602e7 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -11,93 +11,44 @@ class InsertMode extends Mode      handleKeyEvent = (event) =>        return @continueBubbling unless @isActive event +      # See comment here: https://github.com/philc/vimium/commit/48c169bd5a61685bb4e67b1e76c939dbf360a658. +      activeElement = @getActiveElement() +      return @passEventToPage if activeElement == document.body and activeElement.isContentEditable +        # Check for a pass-next-key key.        if KeyboardUtils.getKeyCharString(event) in Settings.get "passNextKeyKeys"          new PassNextKeyMode -        return @suppressEvent - -      return @passEventToPage unless event.type == 'keydown' and KeyboardUtils.isEscape event -      DomUtils.suppressKeyupAfterEscape handlerStack -      target = event.srcElement -      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. -        target.blur() -      else if target?.shadowRoot and @insertModeLock -        # An editable element in a shadow DOM is focused; blur it. -        @insertModeLock.blur() -      @exit event, event.srcElement -      @suppressEvent + +      else if event.type == 'keydown' and KeyboardUtils.isEscape(event) +        activeElement.blur() if DomUtils.isFocusable activeElement +        @exit() unless @permanent + +      else +        return @passEventToPage + +      return @suppressEvent      defaults =        name: "insert"        indicator: if not @permanent and not Settings.get "hideHud"  then "Insert mode"        keypress: handleKeyEvent -      keyup: handleKeyEvent        keydown: handleKeyEvent      super extend defaults, options -    @insertModeLock = -      if options.targetElement and DomUtils.isEditable options.targetElement -        # The caller has told us which element to activate on. -        options.targetElement -      else if document.activeElement and DomUtils.isEditable document.activeElement -        # An input element is already active, so use it. -        document.activeElement -      else -        null - -    @push -      _name: "mode-#{@id}-focus" -      "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 "focus" before "blur".  We track the active element in @insertModeLock, and -        # exit only when that element blurs. -        @exit event, target if @insertModeLock and target == @insertModeLock -      "focus": (event) => @alwaysContinueBubbling => -        if @insertModeLock != event.target and DomUtils.isFocusable event.target -          @activateOnElement event.target -        else if event.target.shadowRoot -          # A focusable element inside the shadow DOM might have been selected. If so, we can catch the focus -          # event inside the shadow DOM. This fixes #853. -          shadowRoot = event.target.shadowRoot -          eventListeners = {} -          for type in [ "focus", "blur" ] -            eventListeners[type] = do (type) -> -              (event) -> handlerStack.bubbleEvent type, event -            shadowRoot.addEventListener type, eventListeners[type], true - -          handlerStack.push -            _name: "shadow-DOM-input-mode" -            blur: (event) -> -              if event.target.shadowRoot == shadowRoot -                handlerStack.remove() -                for own type, listener of eventListeners -                  shadowRoot.removeEventListener type, listener, true -      # Only for tests.  This gives us a hook to test the status of the permanently-installed instance.      InsertMode.permanentInstance = this if @permanent    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 -    # active element is contentEditable. -    @activateOnElement document.activeElement if document.activeElement?.isContentEditable -    @insertModeLock != null - -  activateOnElement: (element) -> -    @log "#{@id}: activating (permanent)" if @debug and @permanent -    @insertModeLock = element - -  exit: (_, target)  -> -    if (target and target == @insertModeLock) or @global or target == undefined -      @log "#{@id}: deactivating (permanent)" if @debug and @permanent and @insertModeLock -      @insertModeLock = null -      # Exit, but only if this isn't the permanently-installed instance. -      super() unless @permanent +    return true if @global +    DomUtils.isFocusable @getActiveElement() + +  getActiveElement: -> +    activeElement = document.activeElement +    while activeElement?.shadowRoot?.activeElement +      activeElement = activeElement.shadowRoot.activeElement +    activeElement    # Static stuff. This allows PostFindMode to suppress the permanently-installed InsertMode instance.    @suppressedEvent: null @@ -129,6 +80,7 @@ class PassNextKeyMode extends Mode                @exit()          @passEventToPage -root = exports ? window +root = exports ? (window.root ?= {})  root.InsertMode = InsertMode  root.PassNextKeyMode = PassNextKeyMode +extend window, root unless exports? diff --git a/content_scripts/mode_key_handler.coffee b/content_scripts/mode_key_handler.coffee index 480a79af..cca6b77a 100644 --- a/content_scripts/mode_key_handler.coffee +++ b/content_scripts/mode_key_handler.coffee @@ -12,7 +12,6 @@  # consists of a (non-empty) list of such mappings.  class KeyHandlerMode extends Mode -  keydownEvents: {}    setKeyMapping: (@keyMapping) -> @reset()    setPassKeys: (@passKeys) -> @reset()    # Only for tests. @@ -28,59 +27,41 @@ class KeyHandlerMode extends Mode      super extend options,        keydown: @onKeydown.bind this -      keypress: @onKeypress.bind this -      keyup: @onKeyup.bind this -      # We cannot track keyup events if we lose the focus. -      blur: (event) => @alwaysContinueBubbling => @keydownEvents = {} if event.target == window -    @mapKeyRegistry = {} -    Utils.monitorChromeStorage "mapKeyRegistry", (value) => @mapKeyRegistry = value +    if options.exitOnEscape +      # If we're part way through a command's key sequence, then a first Escape should reset the key state, +      # and only a second Escape should actually exit this mode. +      @push +        _name: "key-handler-escape-listener" +        keydown: (event) => +          if KeyboardUtils.isEscape(event) and not @isInResetState() +            @reset() +            @suppressEvent +          else +            @continueBubbling    onKeydown: (event) ->      keyChar = KeyboardUtils.getKeyCharString event -    keyChar = @mapKeyRegistry[keyChar] ? keyChar      isEscape = KeyboardUtils.isEscape event      if isEscape and (@countPrefix != 0 or @keyState.length != 1) -      @keydownEvents[event.keyCode] = true -      @reset() -      @suppressEvent +      DomUtils.consumeKeyup event, => @reset()      # If the help dialog loses the focus, then Escape should hide it; see point 2 in #2045.      else if isEscape and HelpDialog?.isShowing() -      @keydownEvents[event.keyCode] = true        HelpDialog.toggle()        @suppressEvent      else if isEscape        @continueBubbling      else if @isMappedKey keyChar -      @keydownEvents[event.keyCode] = true -      @handleKeyChar keyChar -    else if not keyChar and (keyChar = KeyboardUtils.getKeyChar event) and -        (@isMappedKey(keyChar) or @isCountKey keyChar) -      # We will possibly be handling a subsequent keypress event, so suppress propagation of this event to -      # prevent triggering page event listeners (e.g. Google instant Search). -      @keydownEvents[event.keyCode] = true -      @suppressPropagation -    else -      @continueBubbling - -  onKeypress: (event) -> -    keyChar = KeyboardUtils.getKeyCharString event -    keyChar = @mapKeyRegistry[keyChar] ? keyChar -    if @isMappedKey keyChar        @handleKeyChar keyChar +      @suppressEvent      else if @isCountKey keyChar        digit = parseInt keyChar        @reset if @keyState.length == 1 then @countPrefix * 10 + digit else digit        @suppressEvent      else -      @reset() +      @reset() if keyChar        @continueBubbling -  onKeyup: (event) -> -    return @continueBubbling unless event.keyCode of @keydownEvents -    delete @keydownEvents[event.keyCode] -    @suppressPropagation -    # This tests whether there is a mapping of keyChar in the current key state (and accounts for pass keys).    isMappedKey: (keyChar) ->      (mapping for mapping in @keyState when keyChar of mapping)[0]? and not @isPassKey keyChar @@ -92,7 +73,10 @@ class KeyHandlerMode extends Mode    # Keystrokes are *never* considered pass keys if the user has begun entering a command.  So, for example, if    # 't' is a passKey, then the "t"-s of 'gt' and '99t' are neverthless handled as regular keys.    isPassKey: (keyChar) -> -    @countPrefix == 0 and @keyState.length == 1 and keyChar in (@passKeys ? "") +    @isInResetState() and keyChar in (@passKeys ? "") + +  isInResetState: -> +    @countPrefix == 0 and @keyState.length == 1    handleKeyChar: (keyChar) ->      bgLog "handle key #{keyChar} (#{@name})" @@ -106,7 +90,9 @@ class KeyHandlerMode extends Mode        bgLog "  invoke #{command.command} count=#{count} "        @reset()        @commandHandler {command, count} +      @exit() if @options.count? and --@options.count <= 0      @suppressEvent -root = exports ? window +root = exports ? (window.root ?= {})  root.KeyHandlerMode = KeyHandlerMode +extend window, root unless exports? diff --git a/content_scripts/mode_normal.coffee b/content_scripts/mode_normal.coffee new file mode 100644 index 00000000..1fe0618e --- /dev/null +++ b/content_scripts/mode_normal.coffee @@ -0,0 +1,369 @@ +class NormalMode extends KeyHandlerMode +  constructor: (options = {}) -> +    defaults = +      name: "normal" +      indicator: false # There is normally no mode indicator in normal mode. +      commandHandler: @commandHandler.bind this + +    super extend defaults, options + +    chrome.storage.local.get "normalModeKeyStateMapping", (items) => +      @setKeyMapping items.normalModeKeyStateMapping + +    chrome.storage.onChanged.addListener (changes, area) => +      if area == "local" and changes.normalModeKeyStateMapping?.newValue +        @setKeyMapping changes.normalModeKeyStateMapping.newValue + +  commandHandler: ({command: registryEntry, count}) -> +    count *= registryEntry.options.count ? 1 +    count = 1 if registryEntry.noRepeat + +    if registryEntry.repeatLimit? and registryEntry.repeatLimit < count +      return unless confirm """ +        You have asked Vimium to perform #{count} repetitions of the command: #{registryEntry.description}.\n +        Are you sure you want to continue?""" + +    if registryEntry.topFrame +      # We never return to a UI-component frame (e.g. the help dialog), it might have lost the focus. +      sourceFrameId = if window.isVimiumUIComponent then 0 else frameId +      chrome.runtime.sendMessage +        handler: "sendMessageToFrames", message: {name: "runInTopFrame", sourceFrameId, registryEntry} +    else if registryEntry.background +      chrome.runtime.sendMessage {handler: "runBackgroundCommand", registryEntry, count} +    else +      NormalModeCommands[registryEntry.command] count, {registryEntry} + +enterNormalMode = (count) -> +  new NormalMode +    indicator: "Normal mode (pass keys disabled)" +    exitOnEscape: true +    singleton: "enterNormalMode" +    count: count + +NormalModeCommands = +  # Scrolling. +  scrollToBottom: -> +    Marks.setPreviousPosition() +    Scroller.scrollTo "y", "max" +  scrollToTop: (count) -> +    Marks.setPreviousPosition() +    Scroller.scrollTo "y", (count - 1) * Settings.get("scrollStepSize") +  scrollToLeft: -> Scroller.scrollTo "x", 0 +  scrollToRight: -> Scroller.scrollTo "x", "max" +  scrollUp: (count) -> Scroller.scrollBy "y", -1 * Settings.get("scrollStepSize") * count +  scrollDown: (count) -> Scroller.scrollBy "y", Settings.get("scrollStepSize") * count +  scrollPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1/2 * count +  scrollPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1/2 * count +  scrollFullPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1 * count +  scrollFullPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1 * count +  scrollLeft: (count) -> Scroller.scrollBy "x", -1 * Settings.get("scrollStepSize") * count +  scrollRight: (count) -> Scroller.scrollBy "x", Settings.get("scrollStepSize") * count + +  # Page state. +  reload: (count, options) -> +    hard = options.registryEntry.options.hard ? false +    window.location.reload(hard) +  goBack: (count) -> history.go(-count) +  goForward: (count) -> history.go(count) + +  # Url manipulation. +  goUp: (count) -> +    url = window.location.href +    if (url[url.length - 1] == "/") +      url = url.substring(0, url.length - 1) + +    urlsplit = url.split("/") +    # make sure we haven't hit the base domain yet +    if (urlsplit.length > 3) +      urlsplit = urlsplit.slice(0, Math.max(3, urlsplit.length - count)) +      window.location.href = urlsplit.join('/') + +  goToRoot: -> +    window.location.href = window.location.origin + +  toggleViewSource: -> +    chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) -> +      if (url.substr(0, 12) == "view-source:") +        url = url.substr(12, url.length - 12) +      else +        url = "view-source:" + url +      chrome.runtime.sendMessage {handler: "openUrlInNewTab", url} + +  copyCurrentUrl: -> +    chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) -> +      HUD.copyToClipboard url +      url = url[0..25] + "...." if 28 < url.length +      HUD.showForDuration("Yanked #{url}", 2000) + +  openCopiedUrlInNewTab: (count) -> +    HUD.pasteFromClipboard (url) -> +      chrome.runtime.sendMessage { handler: "openUrlInNewTab", url, count } + +  openCopiedUrlInCurrentTab: -> +    HUD.pasteFromClipboard (url) -> +      chrome.runtime.sendMessage { handler: "openUrlInCurrentTab", url } + +  # Mode changes. +  enterInsertMode: -> +    # If a focusable element receives the focus, then we exit and leave the permanently-installed insert-mode +    # instance to take over. +    new InsertMode global: true, exitOnFocus: true + +  enterVisualMode: -> +    new VisualMode userLaunchedMode: true + +  enterVisualLineMode: -> +    new VisualLineMode userLaunchedMode: true + +  enterFindMode: -> +    Marks.setPreviousPosition() +    new FindMode() + +  # Find. +  performFind: (count) -> FindMode.findNext false for [0...count] by 1 +  performBackwardsFind: (count) -> FindMode.findNext true for [0...count] by 1 + +  # Misc. +  mainFrame: -> focusThisFrame highlight: true, forceFocusThisFrame: true +  showHelp: (sourceFrameId) -> HelpDialog.toggle {sourceFrameId, showAllCommandDetails: false} + +  passNextKey: (count, options) -> +    if options.registryEntry.options.normal +      enterNormalMode count +    else +      new PassNextKeyMode count + +  goPrevious: -> +    previousPatterns = Settings.get("previousPatterns") || "" +    previousStrings = previousPatterns.split(",").filter( (s) -> s.trim().length ) +    findAndFollowRel("prev") || findAndFollowLink(previousStrings) + +  goNext: -> +    nextPatterns = Settings.get("nextPatterns") || "" +    nextStrings = nextPatterns.split(",").filter( (s) -> s.trim().length ) +    findAndFollowRel("next") || findAndFollowLink(nextStrings) + +  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. +    resultSet = DomUtils.evaluateXPath textInputXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE +    visibleInputs = +      for i in [0...resultSet.snapshotLength] by 1 +        element = resultSet.snapshotItem i +        continue unless DomUtils.getVisibleClientRect element, true +        { element, index: i, rect: Rect.copy element.getBoundingClientRect() } + +    visibleInputs.sort ({element: element1, index: i1}, {element: element2, index: i2}) -> +      # Put elements with a lower positive tabIndex first, keeping elements in DOM order. +      if element1.tabIndex > 0 +        if element2.tabIndex > 0 +          tabDifference = element1.tabIndex - element2.tabIndex +          if tabDifference != 0 +            tabDifference +          else +            i1 - i2 +        else +          -1 +      else if element2.tabIndex > 0 +        1 +      else +        i1 - i2 + +    if visibleInputs.length == 0 +      HUD.showForDuration("There are no inputs to focus.", 1000) +      return + +    # This is a hack to improve usability on the Vimium options page.  We prime the recently-focused input +    # to be the key-mappings input.  Arguably, this is the input that the user is most likely to use. +    recentlyFocusedElement = lastFocusedInput() + +    selectedInputIndex = +      if count == 1 +        # As the starting index, we pick that of the most recently focused input element (or 0). +        elements = visibleInputs.map (visibleInput) -> visibleInput.element +        Math.max 0, elements.indexOf recentlyFocusedElement +      else +        Math.min(count, visibleInputs.length) - 1 + +    hints = for tuple in visibleInputs +      hint = DomUtils.createElement "div" +      hint.className = "vimiumReset internalVimiumInputHint vimiumInputHint" + +      # minus 1 for the border +      hint.style.left = (tuple.rect.left - 1) + window.scrollX + "px" +      hint.style.top = (tuple.rect.top - 1) + window.scrollY  + "px" +      hint.style.width = tuple.rect.width + "px" +      hint.style.height = tuple.rect.height + "px" + +      hint + +    new FocusSelector hints, visibleInputs, selectedInputIndex + +if LinkHints? +  extend NormalModeCommands, +    "LinkHints.activateMode": LinkHints.activateMode.bind LinkHints +    "LinkHints.activateModeToOpenInNewTab": LinkHints.activateModeToOpenInNewTab.bind LinkHints +    "LinkHints.activateModeToOpenInNewForegroundTab": LinkHints.activateModeToOpenInNewForegroundTab.bind LinkHints +    "LinkHints.activateModeWithQueue": LinkHints.activateModeWithQueue.bind LinkHints +    "LinkHints.activateModeToOpenIncognito": LinkHints.activateModeToOpenIncognito.bind LinkHints +    "LinkHints.activateModeToDownloadLink": LinkHints.activateModeToDownloadLink.bind LinkHints +    "LinkHints.activateModeToCopyLinkUrl": LinkHints.activateModeToCopyLinkUrl.bind LinkHints + +if Vomnibar? +  extend NormalModeCommands, +    "Vomnibar.activate": Vomnibar.activate.bind Vomnibar +    "Vomnibar.activateInNewTab": Vomnibar.activateInNewTab.bind Vomnibar +    "Vomnibar.activateTabSelection": Vomnibar.activateTabSelection.bind Vomnibar +    "Vomnibar.activateBookmarks": Vomnibar.activateBookmarks.bind Vomnibar +    "Vomnibar.activateBookmarksInNewTab": Vomnibar.activateBookmarksInNewTab.bind Vomnibar +    "Vomnibar.activateEditUrl": Vomnibar.activateEditUrl.bind Vomnibar +    "Vomnibar.activateEditUrlInNewTab": Vomnibar.activateEditUrlInNewTab.bind Vomnibar + +if Marks? +  extend NormalModeCommands, +    "Marks.activateCreateMode": Marks.activateCreateMode.bind Marks +    "Marks.activateGotoMode": Marks.activateGotoMode.bind Marks + +# 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 +# fetch it each time. +# Should we include the HTML5 date pickers here? + +# The corresponding XPath for such elements. +textInputXPath = (-> +  textInputTypes = [ "text", "search", "email", "url", "number", "password", "date", "tel" ] +  inputElements = ["input[" + +    "(" + textInputTypes.map((type) -> '@type="' + type + '"').join(" or ") + "or not(@type))" + +    " and not(@disabled or @readonly)]", +    "textarea", "*[@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"] +  DomUtils?.makeXPath(inputElements) +)() + +# used by the findAndFollow* functions. +followLink = (linkElement) -> +  if (linkElement.nodeName.toLowerCase() == "link") +    window.location.href = linkElement.href +  else +    # if we can click on it, don't simply set location.href: some next/prev links are meant to trigger AJAX +    # calls, like the 'more' button on GitHub's newsfeed. +    linkElement.scrollIntoView() +    DomUtils.simulateClick(linkElement) + +# +# Find and follow a link which matches any one of a list of strings. If there are multiple such links, they +# are prioritized for shortness, by their position in :linkStrings, how far down the page they are located, +# and finally by whether the match is exact. Practically speaking, this means we favor 'next page' over 'the +# next big thing', and 'more' over 'nextcompany', even if 'next' occurs before 'more' in :linkStrings. +# +findAndFollowLink = (linkStrings) -> +  linksXPath = DomUtils.makeXPath(["a", "*[@onclick or @role='link' or contains(@class, 'button')]"]) +  links = DomUtils.evaluateXPath(linksXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) +  candidateLinks = [] + +  # at the end of this loop, candidateLinks will contain all visible links that match our patterns +  # links lower in the page are more likely to be the ones we want, so we loop through the snapshot backwards +  for i in [(links.snapshotLength - 1)..0] by -1 +    link = links.snapshotItem(i) + +    # ensure link is visible (we don't mind if it is scrolled offscreen) +    boundingClientRect = link.getBoundingClientRect() +    if (boundingClientRect.width == 0 || boundingClientRect.height == 0) +      continue +    computedStyle = window.getComputedStyle(link, null) +    if (computedStyle.getPropertyValue("visibility") != "visible" || +        computedStyle.getPropertyValue("display") == "none") +      continue + +    linkMatches = false +    for linkString in linkStrings +      if link.innerText.toLowerCase().indexOf(linkString) != -1 || +          0 <= link.value?.indexOf? linkString +        linkMatches = true +        break +    continue unless linkMatches + +    candidateLinks.push(link) + +  return if (candidateLinks.length == 0) + +  for link in candidateLinks +    link.wordCount = link.innerText.trim().split(/\s+/).length + +  # We can use this trick to ensure that Array.sort is stable. We need this property to retain the reverse +  # in-page order of the links. + +  candidateLinks.forEach((a,i) -> a.originalIndex = i) + +  # favor shorter links, and ignore those that are more than one word longer than the shortest link +  candidateLinks = +    candidateLinks +      .sort((a, b) -> +        if (a.wordCount == b.wordCount) then a.originalIndex - b.originalIndex else a.wordCount - b.wordCount +      ) +      .filter((a) -> a.wordCount <= candidateLinks[0].wordCount + 1) + +  for linkString in linkStrings +    exactWordRegex = +      if /\b/.test(linkString[0]) or /\b/.test(linkString[linkString.length - 1]) +        new RegExp "\\b" + linkString + "\\b", "i" +      else +        new RegExp linkString, "i" +    for candidateLink in candidateLinks +      if exactWordRegex.test(candidateLink.innerText) || +          (candidateLink.value && exactWordRegex.test(candidateLink.value)) +        followLink(candidateLink) +        return true +  false + +findAndFollowRel = (value) -> +  relTags = ["link", "a", "area"] +  for tag in relTags +    elements = document.getElementsByTagName(tag) +    for element in elements +      if (element.hasAttribute("rel") && element.rel.toLowerCase() == value) +        followLink(element) +        return true + +class FocusSelector extends Mode +  constructor: (hints, visibleInputs, selectedInputIndex) -> +    super +      name: "focus-selector" +      exitOnClick: true +      keydown: (event) => +        if event.key == "Tab" +          hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint' +          selectedInputIndex += hints.length + (if event.shiftKey then -1 else 1) +          selectedInputIndex %= hints.length +          hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' +          DomUtils.simulateSelect visibleInputs[selectedInputIndex].element +          @suppressEvent +        else unless event.key == "Shift" +          @exit() +          # Give the new mode the opportunity to handle the event. +          @restartBubbling + +    @hintContainingDiv = DomUtils.addElementList hints, +      id: "vimiumInputMarkerContainer" +      className: "vimiumReset" + +    DomUtils.simulateSelect visibleInputs[selectedInputIndex].element +    if visibleInputs.length == 1 +      @exit() +      return +    else +      hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' + +  exit: -> +    super() +    DomUtils.removeElement @hintContainingDiv +    if document.activeElement and DomUtils.isEditable document.activeElement +      new InsertMode +        singleton: "post-find-mode/focus-input" +        targetElement: document.activeElement +        indicator: false + +root = exports ? (window.root ?= {}) +root.NormalMode = NormalMode +root.NormalModeCommands = NormalModeCommands +extend window, root unless exports? diff --git a/content_scripts/mode_visual.coffee b/content_scripts/mode_visual.coffee index 1c05cd85..4c6578cd 100644 --- a/content_scripts/mode_visual.coffee +++ b/content_scripts/mode_visual.coffee @@ -70,7 +70,7 @@ class Movement      else        @selection.modify @alterMethod, direction, granularity -  # Return a simple camparable value which depends on various aspects of the selection.  This is used to +  # Return a simple comparable value which depends on various aspects of the selection.  This is used to    # detect, after a movement, whether the selection has changed.    hashSelection: ->      range = @selection.getRangeAt(0) @@ -159,7 +159,7 @@ class Movement    # Scroll the focus into view.    scrollIntoView: -> -    unless @selection.type == "None" +    unless DomUtils.getSelectionType(@selection) == "None"        elementWithFocus = DomUtils.getElementWithFocus @selection, @getDirection() == backward        Scroller.scrollIntoView elementWithFocus if elementWithFocus @@ -240,7 +240,7 @@ class VisualMode extends KeyHandlerMode        commandHandler: @commandHandler.bind this      # If there was a range selection when the user lanuched visual mode, then we retain the selection on exit. -    @shouldRetainSelectionOnExit = @options.userLaunchedMode and @selection.type == "Range" +    @shouldRetainSelectionOnExit = @options.userLaunchedMode and DomUtils.getSelectionType(@selection) == "Range"      @onExit (event = null) =>        if @shouldRetainSelectionOnExit @@ -258,7 +258,7 @@ class VisualMode extends KeyHandlerMode        _name: "#{@id}/enter/click"        # Yank on <Enter>.        keypress: (event) => -        if event.keyCode == keyCodes.enter +        if event.key == "Enter"            unless event.metaKey or event.ctrlKey or event.altKey or event.shiftKey              @yank()              return @suppressEvent @@ -269,7 +269,7 @@ class VisualMode extends KeyHandlerMode      # Establish or use the initial selection.  If that's not possible, then enter caret mode.      unless @name == "caret" -      if @selection.type in [ "Caret", "Range" ] +      if DomUtils.getSelectionType(@selection) in [ "Caret", "Range" ]          selectionRect = @selection.getRangeAt(0).getBoundingClientRect()          if window.vimiumDomTestsAreRunning            # We're running the DOM tests, where getBoundingClientRect() isn't available. @@ -277,7 +277,7 @@ class VisualMode extends KeyHandlerMode          selectionRect = Rect.intersect selectionRect, Rect.create 0, 0, window.innerWidth, window.innerHeight          if selectionRect.height >= 0 and selectionRect.width >= 0            # The selection is visible in the current viewport. -          if @selection.type == "Caret" +          if DomUtils.getSelectionType(@selection) == "Caret"              # The caret is in the viewport. Make make it visible.              @movement.extendByOneCharacter(forward) or @movement.extendByOneCharacter backward          else @@ -285,7 +285,7 @@ class VisualMode extends KeyHandlerMode            # more likely to be interested in visible content.            @selection.removeAllRanges() -      if @selection.type != "Range" and @name != "caret" +      if DomUtils.getSelectionType(@selection) != "Range" and @name != "caret"          new CaretMode          HUD.showForDuration "No usable selection, entering caret mode...", 2500 @@ -312,7 +312,7 @@ class VisualMode extends KeyHandlerMode    yank: (args = {}) ->      @yankedText = @selection.toString()      @exit() -    chrome.runtime.sendMessage handler: "copyToClipboard", data: @yankedText +    HUD.copyToClipboard @yankedText      message = @yankedText.replace /\s+/g, " "      message = message[...12] + "..." if 15 < @yankedText.length @@ -341,10 +341,10 @@ class CaretMode extends VisualMode      super extend options, name: "caret", indicator: "Caret mode", alterMethod: "move"      # Establish the initial caret. -    switch @selection.type +    switch DomUtils.getSelectionType(@selection)        when "None"          @establishInitialSelectionAnchor() -        if @selection.type == "None" +        if DomUtils.getSelectionType(@selection) == "None"            @exit()            HUD.showForDuration "Create a selection before entering visual mode.", 2500            return @@ -380,6 +380,7 @@ class CaretMode extends VisualMode            return true      false -root = exports ? window +root = exports ? (window.root ?= {})  root.VisualMode = VisualMode  root.VisualLineMode = VisualLineMode +extend window, root unless exports? diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index 3a1b3772..f65062e4 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -95,7 +95,14 @@ findScrollableElement = (element, direction, amount, factor) ->  # On some pages, the scrolling element is not actually scrollable.  Here, we search the document for the  # largest visible element which does scroll vertically. This is used to initialize activatedElement. See  # #1358. -firstScrollableElement = (element=getScrollingElement()) -> +firstScrollableElement = (element = null) -> +  unless element +    scrollingElement = getScrollingElement() +    if doesScroll(scrollingElement, "y", 1, 1) or doesScroll(scrollingElement, "y", -1, 1) +      return scrollingElement +    else +      element = document.body ? getScrollingElement() +    if doesScroll(element, "y", 1, 1) or doesScroll(element, "y", -1, 1)      element    else @@ -128,9 +135,11 @@ checkVisibility = (element) ->  CoreScroller =    init: ->      @time = 0 -    @lastEvent = null -    @keyIsDown = false +    @lastEvent = @keyIsDown = null +    @installCanceEventListener() +  # This installs listeners for events which should cancel smooth scrolling. +  installCanceEventListener: ->      # NOTE(smblott) With extreme keyboard configurations, Chrome sometimes does not get a keyup event for      # every keydown, in which case tapping "j" scrolls indefinitely.  This appears to be a Chrome/OS/XOrg bug      # of some kind.  See #1549. @@ -141,11 +150,11 @@ CoreScroller =            @keyIsDown = true            @time += 1 unless event.repeat            @lastEvent = event -      keyup: => +      keyup: (event) =>          handlerStack.alwaysContinueBubbling =>            @keyIsDown = false            @time += 1 -      blur: => +      blur: (event) =>          handlerStack.alwaysContinueBubbling =>            @time += 1 if event.target == window @@ -175,7 +184,7 @@ CoreScroller =      return if @lastEvent?.repeat      activationTime = ++@time -    myKeyIsStillDown = => @time == activationTime and @keyIsDown +    myKeyIsStillDown = => @time == activationTime and @keyIsDown ? true      # Store amount's sign and make amount positive; the arithmetic is clearer when amount is positive.      sign = getSign amount @@ -188,6 +197,7 @@ CoreScroller =      totalElapsed = 0.0      calibration = 1.0      previousTimestamp = null +    cancelEventListener = @installCanceEventListener()      animate = (timestamp) =>        previousTimestamp ?= timestamp @@ -215,13 +225,14 @@ CoreScroller =          requestAnimationFrame animate        else          # We're done. +        handlerStack.remove cancelEventListener          checkVisibility element      # If we've been asked not to be continuous, then we advance time, so the myKeyIsStillDown test always      # fails.      ++@time unless continuous -    # Launch animator. +    # Start scrolling.      requestAnimationFrame animate  # Scroller contains the two main scroll functions which are used by clients. @@ -297,5 +308,6 @@ Scroller =          element = findScrollableElement element, "x", amount, 1          CoreScroller.scroll element, "x", amount, false -root = exports ? window +root = exports ? (window.root ?= {})  root.Scroller = Scroller +extend window, root unless exports? diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee index 203f0c8c..c71bfb35 100644 --- a/content_scripts/ui_component.coffee +++ b/content_scripts/ui_component.coffee @@ -96,5 +96,6 @@ class UIComponent          @options = null          @postMessage "hidden" # Inform the UI component that it is hidden. -root = exports ? window +root = exports ? (window.root ?= {})  root.UIComponent = UIComponent +extend window, root unless exports? diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css index 3e8f65d6..54256199 100644 --- a/content_scripts/vimium.css +++ b/content_scripts/vimium.css @@ -80,7 +80,7 @@ div.internalVimiumHintMarker {    overflow: hidden;    font-size: 11px;    padding: 1px 3px 0px 3px; -  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785), color-stop(100%,#FFC542)); +  background: linear-gradient(to bottom, #FFF785 0%,#FFC542 100%);    border: solid 1px #C38A22;    border-radius: 3px;    box-shadow: 0px 3px 7px 0px rgba(0, 0, 0, 0.3); diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 21826944..432fa7a2 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -2,6 +2,12 @@  # This content script must be run prior to domReady so that we perform some operations very early.  # +root = exports ? (window.root ?= {}) +# On Firefox, sometimes the variables assigned to window are lost (bug 1408996), so we reinstall them. +# NOTE(mrmr1993): This bug leads to catastrophic failure (ie. nothing works and errors abound). +DomUtils.documentReady -> +  root.extend window, root unless extend? +  isEnabledForUrl = true  isIncognitoMode = chrome.extension.inIncognitoContext  normalMode = null @@ -10,25 +16,12 @@ normalMode = null  windowIsFocused = do ->    windowHasFocus = null    DomUtils.documentReady -> windowHasFocus = document.hasFocus() -  window.addEventListener "focus", (event) -> windowHasFocus = true if event.target == window; true -  window.addEventListener "blur", (event) -> windowHasFocus = false if event.target == window; true +  window.addEventListener "focus", forTrusted (event) -> +    windowHasFocus = true if event.target == window; true +  window.addEventListener "blur", forTrusted (event) -> +    windowHasFocus = false if event.target == window; true    -> windowHasFocus -# 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 -# fetch it each time. -# Should we include the HTML5 date pickers here? - -# The corresponding XPath for such elements. -textInputXPath = (-> -  textInputTypes = [ "text", "search", "email", "url", "number", "password", "date", "tel" ] -  inputElements = ["input[" + -    "(" + textInputTypes.map((type) -> '@type="' + type + '"').join(" or ") + "or not(@type))" + -    " and not(@disabled or @readonly)]", -    "textarea", "*[@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"] -  DomUtils.makeXPath(inputElements) -)() -  # This is set by Frame.registerFrameId(). A frameId of 0 indicates that this is the top frame in the tab.  frameId = null @@ -109,47 +102,13 @@ handlerStack.push          target = target.parentElement      true -class NormalMode extends KeyHandlerMode -  constructor: (options = {}) -> -    super extend options, -      name: "normal" -      indicator: false # There is no mode indicator in normal mode. -      commandHandler: @commandHandler.bind this - -    chrome.storage.local.get "normalModeKeyStateMapping", (items) => -      @setKeyMapping items.normalModeKeyStateMapping - -    chrome.storage.onChanged.addListener (changes, area) => -      if area == "local" and changes.normalModeKeyStateMapping?.newValue -        @setKeyMapping changes.normalModeKeyStateMapping.newValue - -    # Initialize components which normal mode depends upon. -    Scroller.init() -    FindModeHistory.init() - -  commandHandler: ({command: registryEntry, count}) -> -    count *= registryEntry.options.count ? 1 -    count = 1 if registryEntry.noRepeat - -    if registryEntry.repeatLimit? and registryEntry.repeatLimit < count -      return unless confirm """ -        You have asked Vimium to perform #{count} repetitions of the command: #{registryEntry.description}.\n -        Are you sure you want to continue?""" - -    if registryEntry.topFrame -      # We never return to a UI-component frame (e.g. the help dialog), it might have lost the focus. -      sourceFrameId = if window.isVimiumUIComponent then 0 else frameId -      chrome.runtime.sendMessage -        handler: "sendMessageToFrames", message: {name: "runInTopFrame", sourceFrameId, registryEntry} -    else if registryEntry.background -      chrome.runtime.sendMessage {handler: "runBackgroundCommand", registryEntry, count} -    else -      Utils.invokeCommandString registryEntry.command, count, {registryEntry} -  installModes = ->    # Install the permanent modes. The permanently-installed insert mode tracks focus/blur events, and    # activates/deactivates itself accordingly.    normalMode = new NormalMode +  # Initialize components upon which normal mode depends. +  Scroller.init() +  FindModeHistory.init()    new InsertMode permanent: true    new GrabBackFocus if isEnabledForUrl    normalMode # Return the normalMode object (for the tests). @@ -157,9 +116,14 @@ installModes = ->  initializeOnEnabledStateKnown = (isEnabledForUrl) ->    installModes() unless normalMode    if isEnabledForUrl -    # We only initialize (and activate) the Vomnibar in the top frame.  Also, we do not initialize the -    # Vomnibar until we know that Vimium is enabled.  Thereafter, there's no more initialization to do. -    DomUtils.documentComplete Vomnibar.init.bind Vomnibar if DomUtils.isTopFrame() +    unless Utils.isFirefox() and document.documentElement.namespaceURI != "http://www.w3.org/1999/xhtml" +      # We only initialize (and activate) the Vomnibar in the top frame.  Also, we do not initialize the +      # Vomnibar until we know that Vimium is enabled.  Thereafter, there's no more initialization to do. +      # +      # NOTE(mrmr1993): In XML documents on Firefox, injecting HTML into the DOM breaks the rendering, so we +      # lazy load the Vomnibar. This comes with the expected issues, but is better than breaking all XML +      # documents. +      DomUtils.documentComplete Vomnibar.init.bind Vomnibar if DomUtils.isTopFrame()      initializeOnEnabledStateKnown = ->  # @@ -178,7 +142,7 @@ initializePreDomReady = ->      frameFocused: -> # A frame has received the focus; we don't care here (UI components handle this).      checkEnabledAfterURLChange: checkEnabledAfterURLChange      runInTopFrame: ({sourceFrameId, registryEntry}) -> -      Utils.invokeCommandString registryEntry.command, sourceFrameId, registryEntry if DomUtils.isTopFrame() +      NormalModeCommands[registryEntry.command] sourceFrameId, registryEntry if DomUtils.isTopFrame()      linkHintsMessage: (request) -> HintCoordinator[request.messageType] request    chrome.runtime.onMessage.addListener (request, sender, sendResponse) -> @@ -192,9 +156,10 @@ initializePreDomReady = ->  # Wrapper to install event listeners.  Syntactic sugar.  installListener = (element, event, callback) -> -  element.addEventListener(event, -> +  element.addEventListener(event, forTrusted(-> +    root.extend window, root unless extend? # See #2800.      if isEnabledForUrl then callback.apply(this, arguments) else true -  , true) +  ), true)  #  # Installing or uninstalling listeners is error prone. Instead we elect to check isEnabledForUrl each time so @@ -215,7 +180,7 @@ installListeners = Utils.makeIdempotent ->  # - Tell the background page this frame's URL.  # - Check if we should be enabled.  # -onFocus = (event) -> +onFocus = forTrusted (event) ->    if event.target == window      chrome.runtime.sendMessage handler: "frameFocused"      checkIfEnabledForUrl true @@ -223,7 +188,7 @@ onFocus = (event) ->  # We install these listeners directly (that is, we don't use installListener) because we still need to receive  # events when Vimium is not enabled.  window.addEventListener "focus", onFocus -window.addEventListener "hashchange", onFocus +window.addEventListener "hashchange", checkEnabledAfterURLChange  initializeOnDomReady = ->    # Tell the background page we're in the domReady state. @@ -237,7 +202,7 @@ Frame =    postMessage: (handler, request = {}) -> @port.postMessage extend request, {handler}    linkHintsMessage: (request) -> HintCoordinator[request.messageType] request    registerFrameId: ({chromeFrameId}) -> -    frameId = window.frameId = chromeFrameId +    frameId = root.frameId = window.frameId = chromeFrameId      # We register a frame immediately only if it is focused or its window isn't tiny.  We register tiny      # frames later, when necessary.  This affects focusFrame() and link hints.      if windowIsFocused() or not DomUtils.windowIsTooSmall() @@ -247,20 +212,21 @@ Frame =          window.removeEventListener "focus", focusHandler          window.removeEventListener "resize", resizeHandler          Frame.postMessage "registerFrame" -      window.addEventListener "focus", focusHandler = -> +      window.addEventListener "focus", focusHandler = forTrusted (event) ->          postRegisterFrame() if event.target == window -      window.addEventListener "resize", resizeHandler = -> +      window.addEventListener "resize", resizeHandler = forTrusted (event) ->          postRegisterFrame() unless DomUtils.windowIsTooSmall()    init: ->      @port = chrome.runtime.connect name: "frames"      @port.onMessage.addListener (request) => +      root.extend window, root unless extend? # See #2800 and #2831.        (@listeners[request.handler] ? this[request.handler]) request      # We disable the content scripts when we lose contact with the background page, or on unload.      @port.onDisconnect.addListener disconnect = Utils.makeIdempotent => @disconnect() -    window.addEventListener "unload", disconnect +    window.addEventListener "unload", forTrusted disconnect    disconnect: ->      try @postMessage "unregisterFrame" @@ -272,7 +238,7 @@ Frame =      handlerStack.reset()      isEnabledForUrl = false      window.removeEventListener "focus", onFocus -    window.removeEventListener "hashchange", onFocus +    window.removeEventListener "hashchange", checkEnabledAfterURLChange  setScrollPosition = ({ scrollX, scrollY }) ->    DomUtils.documentReady -> @@ -314,170 +280,30 @@ focusThisFrame = (request) ->        chrome.runtime.sendMessage handler: "nextFrame"        return    window.focus() +  # On Firefox, window.focus doesn't always draw focus back from a child frame (bug 554039). +  # We blur the active element if it is an iframe, which gives the window back focus as intended. +  document.activeElement.blur() if document.activeElement.tagName.toLowerCase() == "iframe"    flashFrame() if request.highlight -extend window, -  scrollToBottom: -> -    Marks.setPreviousPosition() -    Scroller.scrollTo "y", "max" -  scrollToTop: (count) -> -    Marks.setPreviousPosition() -    Scroller.scrollTo "y", (count - 1) * Settings.get("scrollStepSize") -  scrollToLeft: -> Scroller.scrollTo "x", 0 -  scrollToRight: -> Scroller.scrollTo "x", "max" -  scrollUp: (count) -> Scroller.scrollBy "y", -1 * Settings.get("scrollStepSize") * count -  scrollDown: (count) -> Scroller.scrollBy "y", Settings.get("scrollStepSize") * count -  scrollPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1/2 * count -  scrollPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1/2 * count -  scrollFullPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1 * count -  scrollFullPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1 * count -  scrollLeft: (count) -> Scroller.scrollBy "x", -1 * Settings.get("scrollStepSize") * count -  scrollRight: (count) -> Scroller.scrollBy "x", Settings.get("scrollStepSize") * count - -extend window, -  reload: -> window.location.reload() -  goBack: (count) -> history.go(-count) -  goForward: (count) -> history.go(count) - -  goUp: (count) -> -    url = window.location.href -    if (url[url.length - 1] == "/") -      url = url.substring(0, url.length - 1) - -    urlsplit = url.split("/") -    # make sure we haven't hit the base domain yet -    if (urlsplit.length > 3) -      urlsplit = urlsplit.slice(0, Math.max(3, urlsplit.length - count)) -      window.location.href = urlsplit.join('/') - -  goToRoot: -> -    window.location.href = window.location.origin - -  mainFrame: -> focusThisFrame highlight: true, forceFocusThisFrame: true - -  toggleViewSource: -> -    chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) -> -      if (url.substr(0, 12) == "view-source:") -        url = url.substr(12, url.length - 12) -      else -        url = "view-source:" + url -      chrome.runtime.sendMessage {handler: "openUrlInNewTab", url} - -  copyCurrentUrl: -> -    # TODO(ilya): When the following bug is fixed, revisit this approach of sending back to the background -    # page to copy. -    # http://code.google.com/p/chromium/issues/detail?id=55188 -    chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) -> -      chrome.runtime.sendMessage { handler: "copyToClipboard", data: url } -      url = url[0..25] + "...." if 28 < url.length -      HUD.showForDuration("Yanked #{url}", 2000) - -  enterInsertMode: -> -    # If a focusable element receives the focus, then we exit and leave the permanently-installed insert-mode -    # instance to take over. -    new InsertMode global: true, exitOnFocus: true - -  enterVisualMode: -> -    new VisualMode userLaunchedMode: true - -  enterVisualLineMode: -> -    new VisualLineMode userLaunchedMode: true - -  passNextKey: (count) -> -    new PassNextKeyMode count - -  focusInput: do -> -    # Track the most recently focused input element. -    recentlyFocusedElement = null -    window.addEventListener "focus", -      (event) -> recentlyFocusedElement = event.target if DomUtils.isEditable event.target -    , true - -    (count) -> -      mode = 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. -      # The mode argument is the mode to enter once an input is selected. -      resultSet = DomUtils.evaluateXPath textInputXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE -      visibleInputs = -        for i in [0...resultSet.snapshotLength] by 1 -          element = resultSet.snapshotItem i -          continue unless DomUtils.getVisibleClientRect element, true -          { element, rect: Rect.copy element.getBoundingClientRect() } - -      if visibleInputs.length == 0 -        HUD.showForDuration("There are no inputs to focus.", 1000) -        return - -      # This is a hack to improve usability on the Vimium options page.  We prime the recently-focused input -      # to be the key-mappings input.  Arguably, this is the input that the user is most likely to use. -      recentlyFocusedElement ?= document.getElementById "keyMappings" if window.isVimiumOptionsPage - -      selectedInputIndex = -        if count == 1 -          # As the starting index, we pick that of the most recently focused input element (or 0). -          elements = visibleInputs.map (visibleInput) -> visibleInput.element -          Math.max 0, elements.indexOf recentlyFocusedElement -        else -          Math.min(count, visibleInputs.length) - 1 - -      hints = for tuple in visibleInputs -        hint = DomUtils.createElement "div" -        hint.className = "vimiumReset internalVimiumInputHint vimiumInputHint" - -        # minus 1 for the border -        hint.style.left = (tuple.rect.left - 1) + window.scrollX + "px" -        hint.style.top = (tuple.rect.top - 1) + window.scrollY  + "px" -        hint.style.width = tuple.rect.width + "px" -        hint.style.height = tuple.rect.height + "px" - -        hint - -      new class FocusSelector extends Mode -        constructor: -> -          super -            name: "focus-selector" -            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' -                DomUtils.simulateSelect visibleInputs[selectedInputIndex].element -                @suppressEvent -              else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey -                @exit() -                # Give the new mode the opportunity to handle the event. -                @restartBubbling - -          @hintContainingDiv = DomUtils.addElementList hints, -            id: "vimiumInputMarkerContainer" -            className: "vimiumReset" - -          DomUtils.simulateSelect visibleInputs[selectedInputIndex].element -          if visibleInputs.length == 1 -            @exit() -            return -          else -            hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' - -        exit: -> -          super() -          DomUtils.removeElement @hintContainingDiv -          if mode and document.activeElement and DomUtils.isEditable document.activeElement -            new mode -              singleton: "post-find-mode/focus-input" -              targetElement: document.activeElement -              indicator: false +# Used by focusInput command. +root.lastFocusedInput = do -> +  # Track the most recently focused input element. +  recentlyFocusedElement = null +  window.addEventListener "focus", +    forTrusted (event) -> +      DomUtils = window.DomUtils ? root.DomUtils # Workaround FF bug 1408996. +      if DomUtils.isEditable event.target +        recentlyFocusedElement = event.target +  , true +  -> recentlyFocusedElement  # Checks if Vimium should be enabled or not in this frame.  As a side effect, it also informs the background  # page whether this frame has the focus, allowing the background page to track the active frame's URL and set  # the page icon.  checkIfEnabledForUrl = do ->    Frame.addEventListener "isEnabledForUrl", (response) -> -    {isEnabledForUrl, passKeys, frameIsFocused} = response +    {isEnabledForUrl, passKeys, frameIsFocused, isFirefox} = response +    Utils.isFirefox = -> isFirefox      initializeOnEnabledStateKnown isEnabledForUrl      normalMode.setPassKeys passKeys      # Hide the HUD if we're not enabled. @@ -488,168 +314,11 @@ checkIfEnabledForUrl = do ->  # When we're informed by the background page that a URL in this tab has changed, we check if we have the  # correct enabled state (but only if this frame has the focus). -checkEnabledAfterURLChange = -> +checkEnabledAfterURLChange = forTrusted ->    checkIfEnabledForUrl() if windowIsFocused() -handleEscapeForFindMode = -> -  document.body.classList.remove("vimiumFindMode") -  # removing the class does not re-color existing selections. we recreate the current selection so it reverts -  # back to the default color. -  selection = window.getSelection() -  unless selection.isCollapsed -    range = window.getSelection().getRangeAt(0) -    window.getSelection().removeAllRanges() -    window.getSelection().addRange(range) -  focusFoundLink() || selectFoundInputElement() - -# <esc> sends us into insert mode if possible, but <cr> does not. -# <esc> corresponds approximately to 'nevermind, I have found it already' while <cr> means 'I want to save -# this query and do more searches with it' -handleEnterForFindMode = -> -  focusFoundLink() -  document.body.classList.add("vimiumFindMode") -  FindMode.saveQuery() - -focusFoundLink = -> -  if (FindMode.query.hasResults) -    link = getLinkFromSelection() -    link.focus() if link - -selectFoundInputElement = -> -  # Since the last focused element might not be the one currently pointed to by find (e.g.  the current one -  # might be disabled and therefore unable to receive focus), we use the approximate heuristic of checking -  # that the last anchor node is an ancestor of our element. -  findModeAnchorNode = document.getSelection().anchorNode -  if (FindMode.query.hasResults && document.activeElement && -      DomUtils.isSelectable(document.activeElement) && -      DomUtils.isDOMDescendant(findModeAnchorNode, document.activeElement)) -    DomUtils.simulateSelect(document.activeElement) - -findAndFocus = (backwards) -> -  Marks.setPreviousPosition() -  FindMode.query.hasResults = FindMode.execute null, {backwards} - -  if FindMode.query.hasResults -    focusFoundLink() -    new PostFindMode() -  else -    HUD.showForDuration("No matches for '#{FindMode.query.rawQuery}'", 1000) - -performFind = (count) -> findAndFocus false for [0...count] by 1 -performBackwardsFind = (count) -> findAndFocus true for [0...count] by 1 - -getLinkFromSelection = -> -  node = window.getSelection().anchorNode -  while (node && node != document.body) -    return node if (node.nodeName.toLowerCase() == "a") -    node = node.parentNode -  null - -# used by the findAndFollow* functions. -followLink = (linkElement) -> -  if (linkElement.nodeName.toLowerCase() == "link") -    window.location.href = linkElement.href -  else -    # if we can click on it, don't simply set location.href: some next/prev links are meant to trigger AJAX -    # calls, like the 'more' button on GitHub's newsfeed. -    linkElement.scrollIntoView() -    DomUtils.simulateClick(linkElement) - -# -# Find and follow a link which matches any one of a list of strings. If there are multiple such links, they -# are prioritized for shortness, by their position in :linkStrings, how far down the page they are located, -# and finally by whether the match is exact. Practically speaking, this means we favor 'next page' over 'the -# next big thing', and 'more' over 'nextcompany', even if 'next' occurs before 'more' in :linkStrings. -# -findAndFollowLink = (linkStrings) -> -  linksXPath = DomUtils.makeXPath(["a", "*[@onclick or @role='link' or contains(@class, 'button')]"]) -  links = DomUtils.evaluateXPath(linksXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) -  candidateLinks = [] - -  # at the end of this loop, candidateLinks will contain all visible links that match our patterns -  # links lower in the page are more likely to be the ones we want, so we loop through the snapshot backwards -  for i in [(links.snapshotLength - 1)..0] by -1 -    link = links.snapshotItem(i) - -    # ensure link is visible (we don't mind if it is scrolled offscreen) -    boundingClientRect = link.getBoundingClientRect() -    if (boundingClientRect.width == 0 || boundingClientRect.height == 0) -      continue -    computedStyle = window.getComputedStyle(link, null) -    if (computedStyle.getPropertyValue("visibility") != "visible" || -        computedStyle.getPropertyValue("display") == "none") -      continue - -    linkMatches = false -    for linkString in linkStrings -      if link.innerText.toLowerCase().indexOf(linkString) != -1 || -          0 <= link.value?.indexOf? linkString -        linkMatches = true -        break -    continue unless linkMatches - -    candidateLinks.push(link) - -  return if (candidateLinks.length == 0) - -  for link in candidateLinks -    link.wordCount = link.innerText.trim().split(/\s+/).length - -  # We can use this trick to ensure that Array.sort is stable. We need this property to retain the reverse -  # in-page order of the links. - -  candidateLinks.forEach((a,i) -> a.originalIndex = i) - -  # favor shorter links, and ignore those that are more than one word longer than the shortest link -  candidateLinks = -    candidateLinks -      .sort((a, b) -> -        if (a.wordCount == b.wordCount) then a.originalIndex - b.originalIndex else a.wordCount - b.wordCount -      ) -      .filter((a) -> a.wordCount <= candidateLinks[0].wordCount + 1) - -  for linkString in linkStrings -    exactWordRegex = -      if /\b/.test(linkString[0]) or /\b/.test(linkString[linkString.length - 1]) -        new RegExp "\\b" + linkString + "\\b", "i" -      else -        new RegExp linkString, "i" -    for candidateLink in candidateLinks -      if exactWordRegex.test(candidateLink.innerText) || -          (candidateLink.value && exactWordRegex.test(candidateLink.value)) -        followLink(candidateLink) -        return true -  false - -findAndFollowRel = (value) -> -  relTags = ["link", "a", "area"] -  for tag in relTags -    elements = document.getElementsByTagName(tag) -    for element in elements -      if (element.hasAttribute("rel") && element.rel.toLowerCase() == value) -        followLink(element) -        return true - -window.goPrevious = -> -  previousPatterns = Settings.get("previousPatterns") || "" -  previousStrings = previousPatterns.split(",").filter( (s) -> s.trim().length ) -  findAndFollowRel("prev") || findAndFollowLink(previousStrings) - -window.goNext = -> -  nextPatterns = Settings.get("nextPatterns") || "" -  nextStrings = nextPatterns.split(",").filter( (s) -> s.trim().length ) -  findAndFollowRel("next") || findAndFollowLink(nextStrings) - -# Enters find mode.  Returns the new find-mode instance. -enterFindMode = -> -  Marks.setPreviousPosition() -  new FindMode() - -window.showHelp = (sourceFrameId) -> -  HelpDialog.toggle {sourceFrameId, showAllCommandDetails: false} -  # If we are in the help dialog iframe, then HelpDialog is already defined with the necessary functions. -window.HelpDialog ?= +root.HelpDialog ?=    helpUI: null    isShowing: -> @helpUI?.showing    abort: -> @helpUI.hide false if @isShowing() @@ -666,14 +335,13 @@ window.HelpDialog ?=  initializePreDomReady()  DomUtils.documentReady initializeOnDomReady -root = exports ? window  root.handlerStack = handlerStack  root.frameId = frameId  root.Frame = Frame  root.windowIsFocused = windowIsFocused  root.bgLog = bgLog -# These are exported for find mode and link-hints mode. -extend root, {handleEscapeForFindMode, handleEnterForFindMode, performFind, performBackwardsFind, -  enterFindMode, focusThisFrame} +# These are exported for normal mode and link-hints mode. +extend root, {focusThisFrame}  # These are exported only for the tests. -extend root, {installModes, installListeners} +extend root, {installModes} +extend window, root unless exports? diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee index 04499523..ad98aa48 100644 --- a/content_scripts/vomnibar.coffee +++ b/content_scripts/vomnibar.coffee @@ -53,10 +53,11 @@ Vomnibar =    #   selectFirst - Optional, boolean. Whether to select the first entry.    #   newTab      - Optional, boolean. Whether to open the result in a new tab.    open: (sourceFrameId, options) -> -    if @vomnibarUI? -      # The Vomnibar cannot coexist with the help dialog (it causes focus issues). -      HelpDialog.abort() -      @vomnibarUI.activate extend options, { name: "activate", sourceFrameId, focus: true } +    @init() +    # The Vomnibar cannot coexist with the help dialog (it causes focus issues). +    HelpDialog.abort() +    @vomnibarUI.activate extend options, { name: "activate", sourceFrameId, focus: true } -root = exports ? window +root = exports ? (window.root ?= {})  root.Vomnibar = Vomnibar +extend window, root unless exports? | 
