addon-sdk/source/lib/sdk/panel/utils.js

branch
TOR_BUG_3246
changeset 7
129ffea94266
equal deleted inserted replaced
-1:000000000000 0:530e3f85a583
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;

mercurial