1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
|
class UIComponent
iframeElement: null
iframePort: null
showing: null
options: null
shadowDOM: null
styleSheetGetter: null
constructor: (iframeUrl, className, @handleMessage) ->
styleSheet = DomUtils.createElement "style"
styleSheet.type = "text/css"
# Default to everything hidden while the stylesheet loads.
styleSheet.innerHTML = "iframe {display: none;}"
# Use an XMLHttpRequest, possibly via the background page, to fetch the stylesheet. This allows us to
# catch and recover from failures that we could not have caught when using CSS @include (eg. #1817).
UIComponent::styleSheetGetter ?= new AsyncDataFetcher @fetchFileContents "content_scripts/vimium.css"
@styleSheetGetter.use (styles) -> styleSheet.innerHTML = styles
@iframeElement = DomUtils.createElement "iframe"
extend @iframeElement,
className: className
seamless: "seamless"
shadowWrapper = DomUtils.createElement "div"
# PhantomJS doesn't support createShadowRoot, so guard against its non-existance.
@shadowDOM = shadowWrapper.createShadowRoot?() ? shadowWrapper
@shadowDOM.appendChild styleSheet
@shadowDOM.appendChild @iframeElement
@showing = true # The iframe is visible now.
# Hide the iframe, but don't interfere with the focus.
@hide false
# Open a port and pass it to the iframe via window.postMessage. We use an AsyncDataFetcher to handle
# requests which arrive before the iframe (and its message handlers) have completed initialization. See
# #1679.
@iframePort = new AsyncDataFetcher (setIframePort) =>
# We set the iframe source and append the new element here (as opposed to above) to avoid a potential
# race condition vis-a-vis the "load" event (because this callback runs on "nextTick").
@iframeElement.src = chrome.runtime.getURL iframeUrl
document.documentElement.appendChild shadowWrapper
@iframeElement.addEventListener "load", =>
# Get vimiumSecret so the iframe can determine that our message isn't the page impersonating us.
chrome.storage.local.get "vimiumSecret", ({ vimiumSecret }) =>
{ port1, port2 } = new MessageChannel
port1.onmessage = (event) => @handleMessage event
@iframeElement.contentWindow.postMessage vimiumSecret, chrome.runtime.getURL(""), [ port2 ]
setIframePort port1
# 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.
# Posts a message (if one is provided), then calls continuation (if provided). The continuation is only
# ever called *after* the message has been posted.
postMessage: (message = null, continuation = null) ->
@iframePort.use (port) =>
port.postMessage message if message?
continuation?()
activate: (@options) ->
@postMessage @options, =>
@show() unless @showing
@iframeElement.focus()
show: (message) ->
@postMessage message, =>
@iframeElement.classList.remove "vimiumUIComponentHidden"
@iframeElement.classList.add "vimiumUIComponentVisible"
# 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
@onFocus = null
@postMessage "hide"
@showing = true
hide: (focusWindow = true)->
@refocusSourceFrame @options?.sourceFrameId if focusWindow
window.removeEventListener "focus", @onFocus if @onFocus
@onFocus = null
@iframeElement.classList.remove "vimiumUIComponentVisible"
@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
# Note(smblott) Disabled prior to 1.50 (or post 1.49) release.
# The UX around flashing the frame isn't quite right yet. We want the frame to flash only if the
# user exits the Vomnibar with Escape.
highlightOnlyIfNotTop: false # true
if windowIsFocused()
# 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()
# Fetch a Vimium file/resource (such as "content_scripts/vimium.css").
# We try making an XMLHttpRequest request. That can fail (see #1817), in which case we fetch the
# file/resource via the background page.
fetchFileContents: (file) -> (callback) ->
request = new XMLHttpRequest()
request.onload = ->
if request.status == 200
callback request.responseText
else
request.onerror()
request.onerror = ->
chrome.runtime.sendMessage
handler: "fetchFileContents"
fileName: file
, callback
request.open "GET", (chrome.runtime.getURL file), true
request.send()
root = exports ? window
root.UIComponent = UIComponent
|