aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--background_scripts/main.coffee23
-rw-r--r--content_scripts/link_hints.coffee20
-rw-r--r--content_scripts/ui_component.coffee47
-rw-r--r--content_scripts/vimium_frontend.coffee51
-rw-r--r--content_scripts/vomnibar.coffee26
-rw-r--r--lib/dom_utils.coffee6
-rw-r--r--pages/vomnibar.coffee12
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?()