diff options
| -rw-r--r-- | background_scripts/main.coffee | 23 | ||||
| -rw-r--r-- | content_scripts/link_hints.coffee | 20 | ||||
| -rw-r--r-- | content_scripts/ui_component.coffee | 47 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 51 | ||||
| -rw-r--r-- | content_scripts/vomnibar.coffee | 26 | ||||
| -rw-r--r-- | lib/dom_utils.coffee | 6 | ||||
| -rw-r--r-- | pages/vomnibar.coffee | 12 | 
7 files changed, 146 insertions, 39 deletions
| diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index a7a1533c..642913a5 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -300,9 +300,8 @@ BackgroundCommands =        chrome.tabs.sendMessage(tab.id, { name: "focusFrame", frameId: frames[0], highlight: true }))    mainFrame: ->      chrome.tabs.getSelected null, (tab) -> -      # Messages sent with a frameId of zero in the options argument (as below) are delivered only to the -      # tab's main frame. -      chrome.tabs.sendMessage tab.id, { name: "focusFrame", frameId: 0, highlight: true }, frameId: 0 +      # The front end interprets a frameId of 0 to mean the main/top from. +      chrome.tabs.sendMessage tab.id, name: "focusFrame", frameId: 0, highlight: true    closeTabsOnLeft: -> removeTabsRelative "before"    closeTabsOnRight: -> removeTabsRelative "after" @@ -602,9 +601,23 @@ unregisterFrame = (request, sender) ->  handleFrameFocused = (request, sender) ->    tabId = sender.tab.id    urlForTab[tabId] = request.url -  if frameIdsForTab[tabId]? +  # Cycle frameIdsForTab to the focused frame.  However, also ensure that we don't inadvertently register a +  # frame which wasn't previously registered (such as a frameset). +  if frameIdsForTab[tabId]? and request.frameId in frameIdsForTab[tabId]      frameIdsForTab[tabId] =        [request.frameId, (frameIdsForTab[tabId].filter (id) -> id != request.frameId)...] +  # Inform all frames that a frame has received the focus. +  chrome.tabs.sendMessage sender.tab.id, +    name: "frameFocused" +    focusFrameId: request.frameId + +# Send a message to all frames in the current tab. +sendMessageToFrames = (request, sender) -> +  chrome.tabs.sendMessage sender.tab.id, request.message + +# For debugging only. This allows content scripts to log messages to the background page's console. +bgLog = (request, sender) -> +  console.log "#{sender.tab.id}/#{request.frameId}", request.message  # Port handler mapping  portHandlers = @@ -632,6 +645,8 @@ sendRequestHandlers =    createMark: Marks.create.bind(Marks)    gotoMark: Marks.goto.bind(Marks)    setIcon: setIcon +  sendMessageToFrames: sendMessageToFrames +  log: bgLog  # We always remove chrome.storage.local/findModeRawQueryListIncognito on startup.  chrome.storage.local.remove "findModeRawQueryListIncognito" diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 73af6a06..6bc37aaf 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -32,6 +32,8 @@ LinkHints =      if settings.get("filterLinkHints") then filterHints else alphabetHints    # lock to ensure only one instance runs at a time    isActive: false +  # Call this function on exit (if defined). +  onExit: null    #    # To be called after linkHints has been generated from linkHintsBase. @@ -54,7 +56,11 @@ LinkHints =        return      @isActive = true -    hintMarkers = (@createMarkerFor(el) for el in @getVisibleClickableElements()) +    elements = @getVisibleClickableElements() +    # For these modes, we filter out those elements which don't have an HREF (since there's nothing we can do +    # with them). +    elements = (el for el in elements when el.element.href?) if mode in [ COPY_LINK_URL, OPEN_INCOGNITO ] +    hintMarkers = (@createMarkerFor(el) for el in elements)      @getMarkerMatcher().fillInMarkers(hintMarkers)      @hintMode = new Mode @@ -92,8 +98,14 @@ LinkHints =            altKey: false      else if @mode is COPY_LINK_URL        @hintMode.setIndicator "Copy link URL to Clipboard" -      @linkActivator = (link) -> -        chrome.runtime.sendMessage handler: "copyToClipboard", data: link.href +      @linkActivator = (link) => +        if link.href? +          chrome.runtime.sendMessage handler: "copyToClipboard", data: link.href +          url = link.href +          url = url[0..25] + "...." if 28 < url.length +          @onExit = -> HUD.showForDuration "Yanked #{url}", 2000 +        else +          @onExit = -> HUD.showForDuration "No link to yank.", 2000      else if @mode is OPEN_INCOGNITO        @hintMode.setIndicator "Open link in incognito window"        @linkActivator = (link) -> @@ -341,6 +353,8 @@ LinkHints =          DomUtils.removeElement LinkHints.hintMarkerContainingDiv        LinkHints.hintMarkerContainingDiv = null        @hintMode.exit() +      @onExit?() +      @onExit = null        @isActive = false      # we invoke the deactivate() function directly instead of using setTimeout(callback, 0) so that diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee index dadc84b5..ea0bf776 100644 --- a/content_scripts/ui_component.coffee +++ b/content_scripts/ui_component.coffee @@ -2,6 +2,7 @@ class UIComponent    iframeElement: null    iframePort: null    showing: null +  options: null    constructor: (iframeUrl, className, @handleMessage) ->      @iframeElement = document.createElement "iframe" @@ -14,6 +15,13 @@ class UIComponent      # Hide the iframe, but don't interfere with the focus.      @hide false +    # If any other frame in the current tab receives the focus, then we hide the UI component. +    # NOTE(smblott) This is correct for the vomnibar, but might be incorrect (and need to be revisited) for +    # other UI components. +    chrome.runtime.onMessage.addListener (request) => +      @postMessage "hide" if @showing and request.name == "frameFocused" and request.focusFrameId != frameId +      false # Free up the sendResponse handler. +    # Open a port and pass it to the iframe via window.postMessage.    openPort: ->      messageChannel = new MessageChannel() @@ -27,8 +35,8 @@ class UIComponent    postMessage: (message) ->      @iframePort.postMessage message -  activate: (message) -> -    @postMessage message if message? +  activate: (@options) -> +    @postMessage @options if @options?      @show() unless @showing      @iframeElement.focus() @@ -36,6 +44,9 @@ class UIComponent      @postMessage message if message?      @iframeElement.classList.remove "vimiumUIComponentHidden"      @iframeElement.classList.add "vimiumUIComponentShowing" +    # The window may not have the focus.  We focus it now, to prevent the "focus" listener below from firing +    # immediately. +    window.focus()      window.addEventListener "focus", @onFocus = (event) =>        if event.target == window          window.removeEventListener "focus", @onFocus @@ -44,12 +55,38 @@ class UIComponent      @showing = true    hide: (focusWindow = true)-> -    @iframeElement.classList.remove "vimiumUIComponentShowing" -    @iframeElement.classList.add "vimiumUIComponentHidden" +    @refocusSourceFrame @options?.sourceFrameId if focusWindow      window.removeEventListener "focus", @onFocus if @onFocus      @onFocus = null -    window.focus() if focusWindow +    @iframeElement.classList.remove "vimiumUIComponentShowing" +    @iframeElement.classList.add "vimiumUIComponentHidden" +    @options = null      @showing = false +  # Refocus the frame from which the UI component was opened.  This may be different from the current frame. +  # After hiding the UI component, Chrome refocuses the containing frame. To avoid a race condition, we need +  # to wait until that frame first receives the focus, before then focusing the frame which should now have +  # the focus. +  refocusSourceFrame: (sourceFrameId) -> +    if @showing and sourceFrameId? and sourceFrameId != frameId +      refocusSourceFrame = -> +        chrome.runtime.sendMessage +          handler: "sendMessageToFrames" +          message: +            name: "focusFrame" +            frameId: sourceFrameId +            highlight: false +            highlightOnlyIfNotTop: true + +      if windowIsFocused() and false +        # We already have the focus. +        refocusSourceFrame() +      else +        # We don't yet have the focus (but we'll be getting it soon). +        window.addEventListener "focus", handler = (event) -> +          if event.target == window +            window.removeEventListener "focus", handler +            refocusSourceFrame() +  root = exports ? window  root.UIComponent = UIComponent diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index fde3d167..ec39ccde 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -19,6 +19,13 @@ keyQueue = null  currentCompletionKeys = ""  validFirstKeys = "" +# We track whther the current window has the focus or not. +windowIsFocused = do -> +  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 +  -> 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. @@ -96,6 +103,11 @@ settings =  #  frameId = 1 + Math.floor(Math.random()*999999999) +# For debugging only. This logs to the console on the background page. +bgLog = (args...) -> +  args = (arg.toString() for arg in args) +  chrome.runtime.sendMessage handler: "log", frameId: frameId, message: args.join " " +  # If an input grabs the focus before the user has interacted with the page, then grab it back (if the  # grabBackFocus option is set).  class GrabBackFocus extends Mode @@ -167,7 +179,7 @@ initializePreDomReady = ->    requestHandlers =      showHUDforDuration: (request) -> HUD.showForDuration request.text, request.duration      toggleHelpDialog: (request) -> toggleHelpDialog(request.dialogHtml, request.frameId) -    focusFrame: (request) -> if (frameId == request.frameId) then focusThisFrame(request.highlight) +    focusFrame: (request) -> if (frameId == request.frameId) then focusThisFrame request      refreshCompletionKeys: refreshCompletionKeys      getScrollPosition: -> scrollX: window.scrollX, scrollY: window.scrollY      setScrollPosition: (request) -> setScrollPosition request.scrollX, request.scrollY @@ -175,6 +187,8 @@ initializePreDomReady = ->      currentKeyQueue: (request) ->        keyQueue = request.keyQueue        handlerStack.bubbleEvent "registerKeyQueue", { keyQueue: keyQueue } +    # A frame has received the focus.  We don't care, here (the Vomnibar/UI-component handles this). +    frameFocused: ->    chrome.runtime.onMessage.addListener (request, sender, sendResponse) ->      # In the options page, we will receive requests from both content and background scripts. ignore those @@ -182,10 +196,11 @@ initializePreDomReady = ->      return if sender.tab and not sender.tab.url.startsWith 'chrome-extension://'      # These requests are delivered to the options page, but there are no handlers there.      return if request.handler in [ "registerFrame", "frameFocused", "unregisterFrame" ] -    shouldHandleRequest = isEnabledForUrl -    # Requests with a frameId of zero are sent to and only received by the tab's main frame.  We *always* -    # handle the listed requests in that frame (even if Vimium is otherwise disabled). -    if request.frameId == 0 and request.name in [ "focusFrame" ] +    # We handle the message if we're enabled, or if it's one of these listed message types. +    shouldHandleRequest = isEnabledForUrl or request.name in [ "executePageCommand" ] +    # Requests with a frameId of zero should only be handled in the main/top frame (regardless of whether +    # Vimium is enabled there). +    if request.frameId == 0 and DomUtils.isTopFrame()        request.frameId = frameId        shouldHandleRequest = true      sendResponse requestHandlers[request.name](request, sender) if shouldHandleRequest @@ -238,7 +253,8 @@ initializeOnDomReady = ->    # Tell the background page we're in the dom ready state.    chrome.runtime.connect({ name: "domReady" })    CursorHider.init() -  Vomnibar.init() +  # We only initialize the vomnibar in the tab's main frame, because it's only ever opened there. +  Vomnibar.init() if DomUtils.isTopFrame()  registerFrame = ->    # Don't register frameset containers; focusing them is no use. @@ -252,10 +268,21 @@ unregisterFrame = ->    chrome.runtime.sendMessage      handler: "unregisterFrame"      frameId: frameId -    tab_is_closing: window.top == window.self +    tab_is_closing: DomUtils.isTopFrame()  executePageCommand = (request) -> -  return unless frameId == request.frameId +  # Vomnibar commands are handled in the tab's main/top frame.  They are handled even if Vimium is otherwise +  # disabled in the frame. +  if request.command.split(".")[0] == "Vomnibar" +    if DomUtils.isTopFrame() +      # We pass the frameId from request.  That's the frame which originated the request, so that's the frame +      # which should receive the focus when the vomnibar closes. +      Utils.invokeCommandString request.command, [ request.frameId ] +      refreshCompletionKeys request +    return + +  # All other commands are handled in their frame (but only if Vimium is enabled). +  return unless frameId == request.frameId and isEnabledForUrl    if (request.passCountToFunction)      Utils.invokeCommandString(request.command, [request.count]) @@ -271,7 +298,7 @@ setScrollPosition = (scrollX, scrollY) ->  #  # Called from the backend in order to change frame focus.  # -window.focusThisFrame = (shouldHighlight) -> +window.focusThisFrame = (request) ->    if window.innerWidth < 3 or window.innerHeight < 3      # This frame is too small to focus. Cancel and tell the background frame to focus the next one instead.      # This affects sites like Google Inbox, which have many tiny iframes. See #1317. @@ -279,7 +306,9 @@ window.focusThisFrame = (shouldHighlight) ->      chrome.runtime.sendMessage({ handler: "nextFrame", frameId: frameId })      return    window.focus() -  if (document.body && shouldHighlight) +  shouldHighlight = request.highlight +  shouldHighlight ||= request.highlightOnlyIfNotTop and not DomUtils.isTopFrame() +  if document.body and shouldHighlight      borderWas = document.body.style.border      document.body.style.border = '5px solid yellow'      setTimeout((-> document.body.style.border = borderWas), 200) @@ -1197,3 +1226,5 @@ root.settings = settings  root.HUD = HUD  root.handlerStack = handlerStack  root.frameId = frameId +root.windowIsFocused = windowIsFocused +root.bgLog = bgLog diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee index c4cfc8b9..2529c077 100644 --- a/content_scripts/vomnibar.coffee +++ b/content_scripts/vomnibar.coffee @@ -4,31 +4,33 @@  Vomnibar =    vomnibarUI: null -  activate: -> @open {completer:"omni"} -  activateInNewTab: -> @open { +  # sourceFrameId here (and below) is the ID of the frame from which this request originates, which may be different +  # from the current frame. +  activate: (sourceFrameId) -> @open sourceFrameId, {completer:"omni"} +  activateInNewTab: (sourceFrameId) -> @open sourceFrameId, {      completer: "omni"      selectFirst: false      newTab: true    } -  activateTabSelection: -> @open { +  activateTabSelection: (sourceFrameId) -> @open sourceFrameId, {      completer: "tabs"      selectFirst: true    } -  activateBookmarks: -> @open { +  activateBookmarks: (sourceFrameId) -> @open sourceFrameId, {      completer: "bookmarks"      selectFirst: true    } -  activateBookmarksInNewTab: -> @open { +  activateBookmarksInNewTab: (sourceFrameId) -> @open sourceFrameId, {      completer: "bookmarks"      selectFirst: true      newTab: true    } -  activateEditUrl: -> @open { +  activateEditUrl: (sourceFrameId) -> @open sourceFrameId, {      completer: "omni"      selectFirst: false      query: window.location.href    } -  activateEditUrlInNewTab: -> @open { +  activateEditUrlInNewTab: (sourceFrameId) -> @open sourceFrameId, {      completer: "omni"      selectFirst: false      query: window.location.href @@ -38,9 +40,11 @@ Vomnibar =    init: ->      unless @vomnibarUI?        @vomnibarUI = new UIComponent "pages/vomnibar.html", "vomnibarFrame", (event) => -        if event.data == "hide" -          @vomnibarUI.hide() -          @vomnibarUI.postMessage "hidden" +        @vomnibarUI.hide() if event.data == "hide" +      # Whenever the window receives the focus, we tell the Vomnibar UI that it has been hidden (regardless of +      # whether it was previously visible). +      window.addEventListener "focus", (event) => +        @vomnibarUI.postMessage "hidden" if event.target == window; true    # This function opens the vomnibar. It accepts options, a map with the values: @@ -48,7 +52,7 @@ Vomnibar =    #   query       - Optional. Text to prefill the Vomnibar with.    #   selectFirst - Optional, boolean. Whether to select the first entry.    #   newTab      - Optional, boolean. Whether to open the result in a new tab. -  open: (options) -> @vomnibarUI.activate options +  open: (sourceFrameId, options) -> @vomnibarUI.activate extend options, { sourceFrameId }  root = exports ? window  root.Vomnibar = Vomnibar diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index 0231f994..efb125f6 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -27,6 +27,12 @@ DomUtils =    removeElement: (el) -> el.parentNode.removeChild el    # +  # Test whether the current frame is the top/main frame. +  # +  isTopFrame: -> +    window.top == window.self + +  #    # Takes an array of XPath selectors, adds the necessary namespaces (currently only XHTML), and applies them    # to the document root. The namespaceResolver in evaluateXPath should be kept in sync with the namespaces    # here. diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index 06ec9ee9..b133b126 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -63,14 +63,14 @@ class VomnibarUI    # The sequence of events when the vomnibar is hidden is as follows:    # 1. Post a "hide" message to the host page. -  # 2. The host page hides the vomnibar and posts back a "hidden" message. -  # 3. Only once "hidden" message is received here is any required action (callback) invoked (in onHidden). -  # This ensures that the vomnibar is actually hidden, and avoids flicker after opening a link in a new tab -  # (see #1485). -  hide: (callback = null) -> +  # 2. The host page hides the vomnibar. +  # 3. When that page receives the focus, and it posts back a "hidden" message. +  # 3. Only once the "hidden" message is received here is any required action  invoked (in onHidden). +  # This ensures that the vomnibar is actually hidden before any new tab is created, and avoids flicker after +  # opening a link in a new tab then returning to the original tab (see #1485). +  hide: (@postHideCallback = null) ->      UIComponentServer.postMessage "hide"      @reset() -    @postHideCallback = callback    onHidden: ->      @postHideCallback?() | 
