|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 module.metadata = { |
|
8 "stability": "unstable" |
|
9 }; |
|
10 |
|
11 const { Cc, Ci } = require("chrome"); |
|
12 const { setTimeout } = require("../timers"); |
|
13 const { platform } = require("../system"); |
|
14 const { getMostRecentBrowserWindow, getOwnerBrowserWindow, |
|
15 getHiddenWindow, getScreenPixelsPerCSSPixel } = require("../window/utils"); |
|
16 |
|
17 const { create: createFrame, swapFrameLoaders } = require("../frame/utils"); |
|
18 const { window: addonWindow } = require("../addon/window"); |
|
19 const { isNil } = require("../lang/type"); |
|
20 const events = require("../system/events"); |
|
21 |
|
22 |
|
23 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; |
|
24 |
|
25 function calculateRegion({ position, width, height, defaultWidth, defaultHeight }, rect) { |
|
26 position = position || {}; |
|
27 |
|
28 let x, y; |
|
29 |
|
30 let hasTop = !isNil(position.top); |
|
31 let hasRight = !isNil(position.right); |
|
32 let hasBottom = !isNil(position.bottom); |
|
33 let hasLeft = !isNil(position.left); |
|
34 let hasWidth = !isNil(width); |
|
35 let hasHeight = !isNil(height); |
|
36 |
|
37 // if width is not specified by constructor or show's options, then get |
|
38 // the default width |
|
39 if (!hasWidth) |
|
40 width = defaultWidth; |
|
41 |
|
42 // if height is not specified by constructor or show's options, then get |
|
43 // the default height |
|
44 if (!hasHeight) |
|
45 height = defaultHeight; |
|
46 |
|
47 // default position is centered |
|
48 x = (rect.right - width) / 2; |
|
49 y = (rect.top + rect.bottom - height) / 2; |
|
50 |
|
51 if (hasTop) { |
|
52 y = rect.top + position.top; |
|
53 |
|
54 if (hasBottom && !hasHeight) |
|
55 height = rect.bottom - position.bottom - y; |
|
56 } |
|
57 else if (hasBottom) { |
|
58 y = rect.bottom - position.bottom - height; |
|
59 } |
|
60 |
|
61 if (hasLeft) { |
|
62 x = position.left; |
|
63 |
|
64 if (hasRight && !hasWidth) |
|
65 width = rect.right - position.right - x; |
|
66 } |
|
67 else if (hasRight) { |
|
68 x = rect.right - width - position.right; |
|
69 } |
|
70 |
|
71 return {x: x, y: y, width: width, height: height}; |
|
72 } |
|
73 |
|
74 function open(panel, options, anchor) { |
|
75 // Wait for the XBL binding to be constructed |
|
76 if (!panel.openPopup) setTimeout(open, 50, panel, options, anchor); |
|
77 else display(panel, options, anchor); |
|
78 } |
|
79 exports.open = open; |
|
80 |
|
81 function isOpen(panel) { |
|
82 return panel.state === "open" |
|
83 } |
|
84 exports.isOpen = isOpen; |
|
85 |
|
86 function isOpening(panel) { |
|
87 return panel.state === "showing" |
|
88 } |
|
89 exports.isOpening = isOpening |
|
90 |
|
91 function close(panel) { |
|
92 // Sometimes "TypeError: panel.hidePopup is not a function" is thrown |
|
93 // when quitting the host application while a panel is visible. To suppress |
|
94 // these errors, check for "hidePopup" in panel before calling it. |
|
95 // It's not clear if there's an issue or it's expected behavior. |
|
96 |
|
97 return panel.hidePopup && panel.hidePopup(); |
|
98 } |
|
99 exports.close = close |
|
100 |
|
101 |
|
102 function resize(panel, width, height) { |
|
103 // Resize the iframe instead of using panel.sizeTo |
|
104 // because sizeTo doesn't work with arrow panels |
|
105 panel.firstChild.style.width = width + "px"; |
|
106 panel.firstChild.style.height = height + "px"; |
|
107 } |
|
108 exports.resize = resize |
|
109 |
|
110 function display(panel, options, anchor) { |
|
111 let document = panel.ownerDocument; |
|
112 |
|
113 let x, y; |
|
114 let { width, height, defaultWidth, defaultHeight } = options; |
|
115 |
|
116 let popupPosition = null; |
|
117 |
|
118 // Panel XBL has some SDK incompatible styling decisions. We shim panel |
|
119 // instances until proper fix for Bug 859504 is shipped. |
|
120 shimDefaultStyle(panel); |
|
121 |
|
122 if (!anchor) { |
|
123 // The XUL Panel doesn't have an arrow, so the margin needs to be reset |
|
124 // in order to, be positioned properly |
|
125 panel.style.margin = "0"; |
|
126 |
|
127 let viewportRect = document.defaultView.gBrowser.getBoundingClientRect(); |
|
128 |
|
129 ({x, y, width, height}) = calculateRegion(options, viewportRect); |
|
130 } |
|
131 else { |
|
132 // The XUL Panel has an arrow, so the margin needs to be reset |
|
133 // to the default value. |
|
134 panel.style.margin = ""; |
|
135 let { CustomizableUI, window } = anchor.ownerDocument.defaultView; |
|
136 |
|
137 // In Australis, widgets may be positioned in an overflow panel or the |
|
138 // menu panel. |
|
139 // In such cases clicking this widget will hide the overflow/menu panel, |
|
140 // and the widget's panel will show instead. |
|
141 // If `CustomizableUI` is not available, it means the anchor is not in a |
|
142 // chrome browser window, and therefore there is no need for this check. |
|
143 if (CustomizableUI) { |
|
144 let node = anchor; |
|
145 ({anchor}) = CustomizableUI.getWidget(anchor.id).forWindow(window); |
|
146 |
|
147 // if `node` is not the `anchor` itself, it means the widget is |
|
148 // positioned in a panel, therefore we have to hide it before show |
|
149 // the widget's panel in the same anchor |
|
150 if (node !== anchor) |
|
151 CustomizableUI.hidePanelForNode(anchor); |
|
152 } |
|
153 |
|
154 width = width || defaultWidth; |
|
155 height = height || defaultHeight; |
|
156 |
|
157 // Open the popup by the anchor. |
|
158 let rect = anchor.getBoundingClientRect(); |
|
159 |
|
160 let zoom = getScreenPixelsPerCSSPixel(window); |
|
161 let screenX = rect.left + window.mozInnerScreenX * zoom; |
|
162 let screenY = rect.top + window.mozInnerScreenY * zoom; |
|
163 |
|
164 // Set up the vertical position of the popup relative to the anchor |
|
165 // (always display the arrow on anchor center) |
|
166 let horizontal, vertical; |
|
167 if (screenY > window.screen.availHeight / 2 + height) |
|
168 vertical = "top"; |
|
169 else |
|
170 vertical = "bottom"; |
|
171 |
|
172 if (screenY > window.screen.availWidth / 2 + width) |
|
173 horizontal = "left"; |
|
174 else |
|
175 horizontal = "right"; |
|
176 |
|
177 let verticalInverse = vertical == "top" ? "bottom" : "top"; |
|
178 popupPosition = vertical + "center " + verticalInverse + horizontal; |
|
179 |
|
180 // Allow panel to flip itself if the panel can't be displayed at the |
|
181 // specified position (useful if we compute a bad position or if the |
|
182 // user moves the window and panel remains visible) |
|
183 panel.setAttribute("flip", "both"); |
|
184 } |
|
185 |
|
186 // Resize the iframe instead of using panel.sizeTo |
|
187 // because sizeTo doesn't work with arrow panels |
|
188 panel.firstChild.style.width = width + "px"; |
|
189 panel.firstChild.style.height = height + "px"; |
|
190 |
|
191 panel.openPopup(anchor, popupPosition, x, y); |
|
192 } |
|
193 exports.display = display; |
|
194 |
|
195 // This utility function is just a workaround until Bug 859504 has shipped. |
|
196 function shimDefaultStyle(panel) { |
|
197 let document = panel.ownerDocument; |
|
198 // Please note that `panel` needs to be part of document in order to reach |
|
199 // it's anonymous nodes. One of the anonymous node has a big padding which |
|
200 // doesn't work well since panel frame needs to fill all of the panel. |
|
201 // XBL binding is a not the best option as it's applied asynchronously, and |
|
202 // makes injected frames behave in strange way. Also this feels a lot |
|
203 // cheaper to do. |
|
204 ["panel-inner-arrowcontent", "panel-arrowcontent"].forEach(function(value) { |
|
205 let node = document.getAnonymousElementByAttribute(panel, "class", value); |
|
206 if (node) node.style.padding = 0; |
|
207 }); |
|
208 } |
|
209 |
|
210 function show(panel, options, anchor) { |
|
211 // Prevent the panel from getting focus when showing up |
|
212 // if focus is set to false |
|
213 panel.setAttribute("noautofocus", !options.focus); |
|
214 |
|
215 let window = anchor && getOwnerBrowserWindow(anchor); |
|
216 let { document } = window ? window : getMostRecentBrowserWindow(); |
|
217 attach(panel, document); |
|
218 |
|
219 open(panel, options, anchor); |
|
220 } |
|
221 exports.show = show |
|
222 |
|
223 function setupPanelFrame(frame) { |
|
224 frame.setAttribute("flex", 1); |
|
225 frame.setAttribute("transparent", "transparent"); |
|
226 frame.setAttribute("autocompleteenabled", true); |
|
227 if (platform === "darwin") { |
|
228 frame.style.borderRadius = "6px"; |
|
229 frame.style.padding = "1px"; |
|
230 } |
|
231 } |
|
232 |
|
233 function make(document) { |
|
234 document = document || getMostRecentBrowserWindow().document; |
|
235 let panel = document.createElementNS(XUL_NS, "panel"); |
|
236 panel.setAttribute("type", "arrow"); |
|
237 |
|
238 // Note that panel is a parent of `viewFrame` who's `docShell` will be |
|
239 // configured at creation time. If `panel` and there for `viewFrame` won't |
|
240 // have an owner document attempt to access `docShell` will throw. There |
|
241 // for we attach panel to a document. |
|
242 attach(panel, document); |
|
243 |
|
244 let frameOptions = { |
|
245 allowJavascript: true, |
|
246 allowPlugins: true, |
|
247 allowAuth: true, |
|
248 allowWindowControl: false, |
|
249 // Need to override `nodeName` to use `iframe` as `browsers` save session |
|
250 // history and in consequence do not dispatch "inner-window-destroyed" |
|
251 // notifications. |
|
252 browser: false, |
|
253 // Note that use of this URL let's use swap frame loaders earlier |
|
254 // than if we used default "about:blank". |
|
255 uri: "data:text/plain;charset=utf-8," |
|
256 }; |
|
257 |
|
258 let backgroundFrame = createFrame(addonWindow, frameOptions); |
|
259 setupPanelFrame(backgroundFrame); |
|
260 |
|
261 let viewFrame = createFrame(panel, frameOptions); |
|
262 setupPanelFrame(viewFrame); |
|
263 |
|
264 function onDisplayChange({type, target}) { |
|
265 // Events from child element like <select /> may propagate (dropdowns are |
|
266 // popups too), in which case frame loader shouldn't be swapped. |
|
267 // See Bug 886329 |
|
268 if (target !== this) return; |
|
269 |
|
270 try { swapFrameLoaders(backgroundFrame, viewFrame); } |
|
271 catch(error) { console.exception(error); } |
|
272 events.emit(type, { subject: panel }); |
|
273 } |
|
274 |
|
275 function onContentReady({target, type}) { |
|
276 if (target === getContentDocument(panel)) { |
|
277 style(panel); |
|
278 events.emit(type, { subject: panel }); |
|
279 } |
|
280 } |
|
281 |
|
282 function onContentLoad({target, type}) { |
|
283 if (target === getContentDocument(panel)) |
|
284 events.emit(type, { subject: panel }); |
|
285 } |
|
286 |
|
287 function onContentChange({subject, type}) { |
|
288 let document = subject; |
|
289 if (document === getContentDocument(panel) && document.defaultView) |
|
290 events.emit(type, { subject: panel }); |
|
291 } |
|
292 |
|
293 function onPanelStateChange({type}) { |
|
294 events.emit(type, { subject: panel }) |
|
295 } |
|
296 |
|
297 panel.addEventListener("popupshowing", onDisplayChange, false); |
|
298 panel.addEventListener("popuphiding", onDisplayChange, false); |
|
299 panel.addEventListener("popupshown", onPanelStateChange, false); |
|
300 panel.addEventListener("popuphidden", onPanelStateChange, false); |
|
301 |
|
302 // Panel content document can be either in panel `viewFrame` or in |
|
303 // a `backgroundFrame` depending on panel state. Listeners are set |
|
304 // on both to avoid setting and removing listeners on panel state changes. |
|
305 |
|
306 panel.addEventListener("DOMContentLoaded", onContentReady, true); |
|
307 backgroundFrame.addEventListener("DOMContentLoaded", onContentReady, true); |
|
308 |
|
309 panel.addEventListener("load", onContentLoad, true); |
|
310 backgroundFrame.addEventListener("load", onContentLoad, true); |
|
311 |
|
312 events.on("document-element-inserted", onContentChange); |
|
313 |
|
314 |
|
315 panel.backgroundFrame = backgroundFrame; |
|
316 |
|
317 // Store event listener on the panel instance so that it won't be GC-ed |
|
318 // while panel is alive. |
|
319 panel.onContentChange = onContentChange; |
|
320 |
|
321 return panel; |
|
322 } |
|
323 exports.make = make; |
|
324 |
|
325 function attach(panel, document) { |
|
326 document = document || getMostRecentBrowserWindow().document; |
|
327 let container = document.getElementById("mainPopupSet"); |
|
328 if (container !== panel.parentNode) { |
|
329 detach(panel); |
|
330 document.getElementById("mainPopupSet").appendChild(panel); |
|
331 } |
|
332 } |
|
333 exports.attach = attach; |
|
334 |
|
335 function detach(panel) { |
|
336 if (panel.parentNode) panel.parentNode.removeChild(panel); |
|
337 } |
|
338 exports.detach = detach; |
|
339 |
|
340 function dispose(panel) { |
|
341 panel.backgroundFrame.parentNode.removeChild(panel.backgroundFrame); |
|
342 panel.backgroundFrame = null; |
|
343 events.off("document-element-inserted", panel.onContentChange); |
|
344 panel.onContentChange = null; |
|
345 detach(panel); |
|
346 } |
|
347 exports.dispose = dispose; |
|
348 |
|
349 function style(panel) { |
|
350 /** |
|
351 Injects default OS specific panel styles into content document that is loaded |
|
352 into given panel. Optionally `document` of the browser window can be |
|
353 given to inherit styles from it, by default it will use either panel owner |
|
354 document or an active browser's document. It should not matter though unless |
|
355 Firefox decides to style windows differently base on profile or mode like |
|
356 chrome for example. |
|
357 **/ |
|
358 |
|
359 try { |
|
360 let document = panel.ownerDocument; |
|
361 let contentDocument = getContentDocument(panel); |
|
362 let window = document.defaultView; |
|
363 let node = document.getAnonymousElementByAttribute(panel, "class", |
|
364 "panel-arrowcontent") || |
|
365 // Before bug 764755, anonymous content was different: |
|
366 // TODO: Remove this when targeting FF16+ |
|
367 document.getAnonymousElementByAttribute(panel, "class", |
|
368 "panel-inner-arrowcontent"); |
|
369 |
|
370 let { color, fontFamily, fontSize, fontWeight } = window.getComputedStyle(node); |
|
371 |
|
372 let style = contentDocument.createElement("style"); |
|
373 style.id = "sdk-panel-style"; |
|
374 style.textContent = "body { " + |
|
375 "color: " + color + ";" + |
|
376 "font-family: " + fontFamily + ";" + |
|
377 "font-weight: " + fontWeight + ";" + |
|
378 "font-size: " + fontSize + ";" + |
|
379 "}"; |
|
380 |
|
381 let container = contentDocument.head ? contentDocument.head : |
|
382 contentDocument.documentElement; |
|
383 |
|
384 if (container.firstChild) |
|
385 container.insertBefore(style, container.firstChild); |
|
386 else |
|
387 container.appendChild(style); |
|
388 } |
|
389 catch (error) { |
|
390 console.error("Unable to apply panel style"); |
|
391 console.exception(error); |
|
392 } |
|
393 } |
|
394 exports.style = style; |
|
395 |
|
396 let getContentFrame = panel => |
|
397 (isOpen(panel) || isOpening(panel)) ? |
|
398 panel.firstChild : |
|
399 panel.backgroundFrame |
|
400 exports.getContentFrame = getContentFrame; |
|
401 |
|
402 function getContentDocument(panel) getContentFrame(panel).contentDocument |
|
403 exports.getContentDocument = getContentDocument; |
|
404 |
|
405 function setURL(panel, url) getContentFrame(panel).setAttribute("src", url) |
|
406 exports.setURL = setURL; |