diff options
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | background_scripts/commands.coffee | 3 | ||||
| -rw-r--r-- | background_scripts/main.coffee | 22 | ||||
| -rw-r--r-- | content_scripts/ui_component.coffee | 47 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 66 | ||||
| -rw-r--r-- | content_scripts/vomnibar.coffee | 26 | ||||
| -rw-r--r-- | lib/dom_utils.coffee | 6 | ||||
| -rw-r--r-- | pages/vomnibar.coffee | 12 | ||||
| -rw-r--r-- | tests/dom_tests/dom_tests.coffee | 2 |
9 files changed, 147 insertions, 38 deletions
@@ -45,6 +45,7 @@ Navigating the current page: yy copy the current url to the clipboard yf copy a link url to the clipboard gf cycle forward to the next frame + gF focus the main/top frame Navigating to new pages: diff --git a/background_scripts/commands.coffee b/background_scripts/commands.coffee index 9aa90c45..bca1c3a4 100644 --- a/background_scripts/commands.coffee +++ b/background_scripts/commands.coffee @@ -129,6 +129,7 @@ Commands = "goPrevious", "goNext", "nextFrame", + "mainFrame", "Marks.activateCreateMode", "Vomnibar.activateEditUrl", "Vomnibar.activateEditUrlInNewTab", @@ -255,6 +256,7 @@ defaultKeyMappings = "gE": "Vomnibar.activateEditUrlInNewTab" "gf": "nextFrame" + "gF": "mainFrame" "m": "Marks.activateCreateMode" "`": "Marks.activateGotoMode" @@ -350,6 +352,7 @@ commandDescriptions = "Vomnibar.activateEditUrlInNewTab": ["Edit the current URL and open in a new tab", { noRepeat: true }] nextFrame: ["Cycle forward to the next frame on the page", { background: true, passCountToFunction: true }] + mainFrame: ["Select the tab's main/top frame", { background: true, noRepeat: true }] "Marks.activateCreateMode": ["Create a new mark", { noRepeat: true }] "Marks.activateGotoMode": ["Go to a mark", { noRepeat: true }] diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index ab92559f..6fac032c 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -306,6 +306,10 @@ BackgroundCommands = count = (count + Math.max 0, frameIdsForTab[tab.id].indexOf frameId) % frames.length frames = frameIdsForTab[tab.id] = [frames[count..]..., frames[0...count]...] chrome.tabs.sendMessage(tab.id, { name: "focusFrame", frameId: frames[0], highlight: true })) + mainFrame: -> + chrome.tabs.getSelected null, (tab) -> + # 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" @@ -605,9 +609,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 = @@ -635,6 +653,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/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 cc97b515..c41ca62f 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. @@ -92,9 +99,14 @@ settings = @eventListeners[eventName].push(callback) # -# Give this frame a unique id. +# Give this frame a unique (non-zero) id. # -frameId = Math.floor(Math.random()*999999999) +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). @@ -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,16 +187,25 @@ 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: -> updateEnabledForUrlState: updateEnabledForUrlState chrome.runtime.onMessage.addListener (request, sender, sendResponse) -> # In the options page, we will receive requests from both content and background scripts. ignore those # from the former. return if sender.tab and not sender.tab.url.startsWith 'chrome-extension://' - return unless isEnabledForUrl or request.name in ["updateEnabledForUrlState"] # These requests are delivered to the options page, but there are no handlers there. return if request.handler in [ "registerFrame", "frameFocused", "unregisterFrame" ] - sendResponse requestHandlers[request.name](request, sender) + shouldHandleRequest = isEnabledForUrl + # We always handle the message if it's one of these listed message types. + shouldHandleRequest ||= request.name in [ "executePageCommand", "updateEnabledForUrlState" ] + # Requests with a frameId of zero should always and 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 # Ensure the sendResponse callback is freed. false @@ -198,9 +219,11 @@ installListener = (element, event, callback) -> # Installing or uninstalling listeners is error prone. Instead we elect to check isEnabledForUrl each time so # we know whether the listener should run or not. # Run this as early as possible, so the page can't register any event handlers before us. +# Note: We install the listeners even if Vimium is disabled. See comment in commit +# 6446cf04c7b44c3d419dc450a73b60bcaf5cdf02. # installedListeners = false -window.initializeWithState = -> +window.installListeners = -> unless installedListeners # Key event handlers fire on window before they do on document. Prefer window for key events so the page # can't set handlers to grab the keys before us. @@ -234,7 +257,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. @@ -248,10 +272,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]) @@ -267,7 +302,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. @@ -275,7 +310,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) @@ -550,12 +587,11 @@ onKeyup = (event) -> checkIfEnabledForUrl = -> url = window.location.toString() - chrome.runtime.sendMessage { handler: "isEnabledForUrl", url: url }, updateEnabledForUrlState updateEnabledForUrlState = (response) -> - {isEnabledForUrl, passKeys} = response - initializeWithState() + { isEnabledForUrl, passKeys } = response + installListeners() if HUD.isReady() and not isEnabledForUrl # Quickly hide any HUD we might already be showing, e.g. if we entered insert mode on page load. HUD.hide() @@ -1193,3 +1229,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?() diff --git a/tests/dom_tests/dom_tests.coffee b/tests/dom_tests/dom_tests.coffee index 59970d7c..bb09a0a8 100644 --- a/tests/dom_tests/dom_tests.coffee +++ b/tests/dom_tests/dom_tests.coffee @@ -1,6 +1,6 @@ # Install frontend event handlers. -initializeWithState() +installListeners() installListener = (element, event, callback) -> element.addEventListener event, (-> callback.apply(this, arguments)), true |
