aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--background_scripts/commands.coffee3
-rw-r--r--background_scripts/main.coffee22
-rw-r--r--content_scripts/ui_component.coffee47
-rw-r--r--content_scripts/vimium_frontend.coffee66
-rw-r--r--content_scripts/vomnibar.coffee26
-rw-r--r--lib/dom_utils.coffee6
-rw-r--r--pages/vomnibar.coffee12
-rw-r--r--tests/dom_tests/dom_tests.coffee2
9 files changed, 147 insertions, 38 deletions
diff --git a/README.md b/README.md
index 96cde9bb..002cb7d8 100644
--- a/README.md
+++ b/README.md
@@ -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