utils.js 12 KB

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