summaryrefslogtreecommitdiffstats
path: root/toolkit/jetpack/sdk/panel
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/jetpack/sdk/panel')
-rw-r--r--toolkit/jetpack/sdk/panel/events.js27
-rw-r--r--toolkit/jetpack/sdk/panel/utils.js451
2 files changed, 478 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/panel/events.js b/toolkit/jetpack/sdk/panel/events.js
new file mode 100644
index 000000000..f3040a11d
--- /dev/null
+++ b/toolkit/jetpack/sdk/panel/events.js
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// This module basically translates system/events to a SDK standard events
+// so that `map`, `filter` and other utilities could be used with them.
+
+module.metadata = {
+ "stability": "experimental"
+};
+
+const events = require("../system/events");
+const { emit } = require("../event/core");
+
+var channel = {};
+
+function forward({ subject, type, data }) {
+ return emit(channel, "data", { target: subject, type: type, data: data });
+}
+
+["popupshowing", "popuphiding", "popupshown", "popuphidden",
+"document-element-inserted", "DOMContentLoaded", "load"
+].forEach(type => events.on(type, forward));
+
+exports.events = channel;
diff --git a/toolkit/jetpack/sdk/panel/utils.js b/toolkit/jetpack/sdk/panel/utils.js
new file mode 100644
index 000000000..c85b274bc
--- /dev/null
+++ b/toolkit/jetpack/sdk/panel/utils.js
@@ -0,0 +1,451 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.metadata = {
+ "stability": "unstable"
+};
+
+const { Cc, Ci } = require("chrome");
+const { Services } = require("resource://gre/modules/Services.jsm");
+const { setTimeout } = require("../timers");
+const { platform } = require("../system");
+const { getMostRecentBrowserWindow, getOwnerBrowserWindow,
+ getHiddenWindow, getScreenPixelsPerCSSPixel } = require("../window/utils");
+
+const { create: createFrame, swapFrameLoaders, getDocShell } = require("../frame/utils");
+const { window: addonWindow } = require("../addon/window");
+const { isNil } = require("../lang/type");
+const { data } = require('../self');
+
+const events = require("../system/events");
+
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+function calculateRegion({ position, width, height, defaultWidth, defaultHeight }, rect) {
+ position = position || {};
+
+ let x, y;
+
+ let hasTop = !isNil(position.top);
+ let hasRight = !isNil(position.right);
+ let hasBottom = !isNil(position.bottom);
+ let hasLeft = !isNil(position.left);
+ let hasWidth = !isNil(width);
+ let hasHeight = !isNil(height);
+
+ // if width is not specified by constructor or show's options, then get
+ // the default width
+ if (!hasWidth)
+ width = defaultWidth;
+
+ // if height is not specified by constructor or show's options, then get
+ // the default height
+ if (!hasHeight)
+ height = defaultHeight;
+
+ // default position is centered
+ x = (rect.right - width) / 2;
+ y = (rect.top + rect.bottom - height) / 2;
+
+ if (hasTop) {
+ y = rect.top + position.top;
+
+ if (hasBottom && !hasHeight)
+ height = rect.bottom - position.bottom - y;
+ }
+ else if (hasBottom) {
+ y = rect.bottom - position.bottom - height;
+ }
+
+ if (hasLeft) {
+ x = position.left;
+
+ if (hasRight && !hasWidth)
+ width = rect.right - position.right - x;
+ }
+ else if (hasRight) {
+ x = rect.right - width - position.right;
+ }
+
+ return {x: x, y: y, width: width, height: height};
+}
+
+function open(panel, options, anchor) {
+ // Wait for the XBL binding to be constructed
+ if (!panel.openPopup) setTimeout(open, 50, panel, options, anchor);
+ else display(panel, options, anchor);
+}
+exports.open = open;
+
+function isOpen(panel) {
+ return panel.state === "open"
+}
+exports.isOpen = isOpen;
+
+function isOpening(panel) {
+ return panel.state === "showing"
+}
+exports.isOpening = isOpening
+
+function close(panel) {
+ // Sometimes "TypeError: panel.hidePopup is not a function" is thrown
+ // when quitting the host application while a panel is visible. To suppress
+ // these errors, check for "hidePopup" in panel before calling it.
+ // It's not clear if there's an issue or it's expected behavior.
+ // See Bug 1151796.
+
+ return panel.hidePopup && panel.hidePopup();
+}
+exports.close = close
+
+
+function resize(panel, width, height) {
+ // Resize the iframe instead of using panel.sizeTo
+ // because sizeTo doesn't work with arrow panels
+ if (panel.firstChild) {
+ panel.firstChild.style.width = width + "px";
+ panel.firstChild.style.height = height + "px";
+ }
+}
+exports.resize = resize
+
+function display(panel, options, anchor) {
+ let document = panel.ownerDocument;
+
+ let x, y;
+ let { width, height, defaultWidth, defaultHeight } = options;
+
+ let popupPosition = null;
+
+ // Panel XBL has some SDK incompatible styling decisions. We shim panel
+ // instances until proper fix for Bug 859504 is shipped.
+ shimDefaultStyle(panel);
+
+ if (!anchor) {
+ // The XUL Panel doesn't have an arrow, so the margin needs to be reset
+ // in order to, be positioned properly
+ panel.style.margin = "0";
+
+ let viewportRect = document.defaultView.gBrowser.getBoundingClientRect();
+
+ ({x, y, width, height} = calculateRegion(options, viewportRect));
+ }
+ else {
+ // The XUL Panel has an arrow, so the margin needs to be reset
+ // to the default value.
+ panel.style.margin = "";
+ let { CustomizableUI, window } = anchor.ownerDocument.defaultView;
+
+ // In Australis, widgets may be positioned in an overflow panel or the
+ // menu panel.
+ // In such cases clicking this widget will hide the overflow/menu panel,
+ // and the widget's panel will show instead.
+ // If `CustomizableUI` is not available, it means the anchor is not in a
+ // chrome browser window, and therefore there is no need for this check.
+ if (CustomizableUI) {
+ let node = anchor;
+ ({anchor} = CustomizableUI.getWidget(anchor.id).forWindow(window));
+
+ // if `node` is not the `anchor` itself, it means the widget is
+ // positioned in a panel, therefore we have to hide it before show
+ // the widget's panel in the same anchor
+ if (node !== anchor)
+ CustomizableUI.hidePanelForNode(anchor);
+ }
+
+ width = width || defaultWidth;
+ height = height || defaultHeight;
+
+ // Open the popup by the anchor.
+ let rect = anchor.getBoundingClientRect();
+
+ let zoom = getScreenPixelsPerCSSPixel(window);
+ let screenX = rect.left + window.mozInnerScreenX * zoom;
+ let screenY = rect.top + window.mozInnerScreenY * zoom;
+
+ // Set up the vertical position of the popup relative to the anchor
+ // (always display the arrow on anchor center)
+ let horizontal, vertical;
+ if (screenY > window.screen.availHeight / 2 + height)
+ vertical = "top";
+ else
+ vertical = "bottom";
+
+ if (screenY > window.screen.availWidth / 2 + width)
+ horizontal = "left";
+ else
+ horizontal = "right";
+
+ let verticalInverse = vertical == "top" ? "bottom" : "top";
+ popupPosition = vertical + "center " + verticalInverse + horizontal;
+
+ // Allow panel to flip itself if the panel can't be displayed at the
+ // specified position (useful if we compute a bad position or if the
+ // user moves the window and panel remains visible)
+ panel.setAttribute("flip", "both");
+ }
+
+ if (!panel.viewFrame) {
+ panel.viewFrame = document.importNode(panel.backgroundFrame, false);
+ panel.appendChild(panel.viewFrame);
+
+ let {privateBrowsingId} = getDocShell(panel.viewFrame).getOriginAttributes();
+ let principal = Services.scriptSecurityManager.createNullPrincipal({privateBrowsingId});
+ getDocShell(panel.viewFrame).createAboutBlankContentViewer(principal);
+ }
+
+ // Resize the iframe instead of using panel.sizeTo
+ // because sizeTo doesn't work with arrow panels
+ panel.firstChild.style.width = width + "px";
+ panel.firstChild.style.height = height + "px";
+
+ panel.openPopup(anchor, popupPosition, x, y);
+}
+exports.display = display;
+
+// This utility function is just a workaround until Bug 859504 has shipped.
+function shimDefaultStyle(panel) {
+ let document = panel.ownerDocument;
+ // Please note that `panel` needs to be part of document in order to reach
+ // it's anonymous nodes. One of the anonymous node has a big padding which
+ // doesn't work well since panel frame needs to fill all of the panel.
+ // XBL binding is a not the best option as it's applied asynchronously, and
+ // makes injected frames behave in strange way. Also this feels a lot
+ // cheaper to do.
+ ["panel-inner-arrowcontent", "panel-arrowcontent"].forEach(function(value) {
+ let node = document.getAnonymousElementByAttribute(panel, "class", value);
+ if (node) node.style.padding = 0;
+ });
+}
+
+function show(panel, options, anchor) {
+ // Prevent the panel from getting focus when showing up
+ // if focus is set to false
+ panel.setAttribute("noautofocus", !options.focus);
+
+ let window = anchor && getOwnerBrowserWindow(anchor);
+ let { document } = window ? window : getMostRecentBrowserWindow();
+ attach(panel, document);
+
+ open(panel, options, anchor);
+}
+exports.show = show
+
+function onPanelClick(event) {
+ let { target, metaKey, ctrlKey, shiftKey, button } = event;
+ let accel = platform === "darwin" ? metaKey : ctrlKey;
+ let isLeftClick = button === 0;
+ let isMiddleClick = button === 1;
+
+ if ((isLeftClick && (accel || shiftKey)) || isMiddleClick) {
+ let link = target.closest('a');
+
+ if (link && link.href)
+ getMostRecentBrowserWindow().openUILink(link.href, event)
+ }
+}
+
+function setupPanelFrame(frame) {
+ frame.setAttribute("flex", 1);
+ frame.setAttribute("transparent", "transparent");
+ frame.setAttribute("autocompleteenabled", true);
+ frame.setAttribute("tooltip", "aHTMLTooltip");
+ if (platform === "darwin") {
+ frame.style.borderRadius = "var(--arrowpanel-border-radius, 3.5px)";
+ frame.style.padding = "1px";
+ }
+}
+
+function make(document, options) {
+ document = document || getMostRecentBrowserWindow().document;
+ let panel = document.createElementNS(XUL_NS, "panel");
+ panel.setAttribute("type", "arrow");
+ panel.setAttribute("sdkscriptenabled", options.allowJavascript);
+
+ // The panel needs to be attached to a browser window in order for us
+ // to copy browser styles to the content document when it loads.
+ attach(panel, document);
+
+ let frameOptions = {
+ allowJavascript: options.allowJavascript,
+ allowPlugins: true,
+ allowAuth: true,
+ allowWindowControl: false,
+ // Need to override `nodeName` to use `iframe` as `browsers` save session
+ // history and in consequence do not dispatch "inner-window-destroyed"
+ // notifications.
+ browser: false,
+ };
+
+ let backgroundFrame = createFrame(addonWindow, frameOptions);
+ setupPanelFrame(backgroundFrame);
+
+ getDocShell(backgroundFrame).inheritPrivateBrowsingId = false;
+
+ function onPopupShowing({type, target}) {
+ if (target === this) {
+ let attrs = getDocShell(backgroundFrame).getOriginAttributes();
+ getDocShell(panel.viewFrame).setOriginAttributes(attrs);
+
+ swapFrameLoaders(backgroundFrame, panel.viewFrame);
+ }
+ }
+
+ function onPopupHiding({type, target}) {
+ if (target === this) {
+ swapFrameLoaders(backgroundFrame, panel.viewFrame);
+
+ panel.viewFrame.remove();
+ panel.viewFrame = null;
+ }
+ }
+
+ function onContentReady({target, type}) {
+ if (target === getContentDocument(panel)) {
+ style(panel);
+ events.emit(type, { subject: panel });
+ }
+ }
+
+ function onContentLoad({target, type}) {
+ if (target === getContentDocument(panel))
+ events.emit(type, { subject: panel });
+ }
+
+ function onContentChange({subject: document, type}) {
+ if (document === getContentDocument(panel) && document.defaultView)
+ events.emit(type, { subject: panel });
+ }
+
+ function onPanelStateChange({target, type}) {
+ if (target === this)
+ events.emit(type, { subject: panel })
+ }
+
+ panel.addEventListener("popupshowing", onPopupShowing);
+ panel.addEventListener("popuphiding", onPopupHiding);
+ for (let event of ["popupshowing", "popuphiding", "popupshown", "popuphidden"])
+ panel.addEventListener(event, onPanelStateChange);
+
+ panel.addEventListener("click", onPanelClick, false);
+
+ // Panel content document can be either in panel `viewFrame` or in
+ // a `backgroundFrame` depending on panel state. Listeners are set
+ // on both to avoid setting and removing listeners on panel state changes.
+
+ panel.addEventListener("DOMContentLoaded", onContentReady, true);
+ backgroundFrame.addEventListener("DOMContentLoaded", onContentReady, true);
+
+ panel.addEventListener("load", onContentLoad, true);
+ backgroundFrame.addEventListener("load", onContentLoad, true);
+
+ events.on("document-element-inserted", onContentChange);
+
+ panel.backgroundFrame = backgroundFrame;
+ panel.viewFrame = null;
+
+ // Store event listener on the panel instance so that it won't be GC-ed
+ // while panel is alive.
+ panel.onContentChange = onContentChange;
+
+ return panel;
+}
+exports.make = make;
+
+function attach(panel, document) {
+ document = document || getMostRecentBrowserWindow().document;
+ let container = document.getElementById("mainPopupSet");
+ if (container !== panel.parentNode) {
+ detach(panel);
+ document.getElementById("mainPopupSet").appendChild(panel);
+ }
+}
+exports.attach = attach;
+
+function detach(panel) {
+ if (panel.parentNode) panel.parentNode.removeChild(panel);
+}
+exports.detach = detach;
+
+function dispose(panel) {
+ panel.backgroundFrame.remove();
+ panel.backgroundFrame = null;
+ events.off("document-element-inserted", panel.onContentChange);
+ panel.onContentChange = null;
+ detach(panel);
+}
+exports.dispose = dispose;
+
+function style(panel) {
+ /**
+ Injects default OS specific panel styles into content document that is loaded
+ into given panel. Optionally `document` of the browser window can be
+ given to inherit styles from it, by default it will use either panel owner
+ document or an active browser's document. It should not matter though unless
+ Firefox decides to style windows differently base on profile or mode like
+ chrome for example.
+ **/
+
+ try {
+ let document = panel.ownerDocument;
+ let contentDocument = getContentDocument(panel);
+ let window = document.defaultView;
+ let node = document.getAnonymousElementByAttribute(panel, "class",
+ "panel-arrowcontent");
+
+ let { color, fontFamily, fontSize, fontWeight } = window.getComputedStyle(node);
+
+ let style = contentDocument.createElement("style");
+ style.id = "sdk-panel-style";
+ style.textContent = "body { " +
+ "color: " + color + ";" +
+ "font-family: " + fontFamily + ";" +
+ "font-weight: " + fontWeight + ";" +
+ "font-size: " + fontSize + ";" +
+ "}";
+
+ let container = contentDocument.head ? contentDocument.head :
+ contentDocument.documentElement;
+
+ if (container.firstChild)
+ container.insertBefore(style, container.firstChild);
+ else
+ container.appendChild(style);
+ }
+ catch (error) {
+ console.error("Unable to apply panel style");
+ console.exception(error);
+ }
+}
+exports.style = style;
+
+var getContentFrame = panel => panel.viewFrame || panel.backgroundFrame;
+exports.getContentFrame = getContentFrame;
+
+function getContentDocument(panel) {
+ return getContentFrame(panel).contentDocument;
+}
+exports.getContentDocument = getContentDocument;
+
+function setURL(panel, url) {
+ let frame = getContentFrame(panel);
+ let webNav = getDocShell(frame).QueryInterface(Ci.nsIWebNavigation);
+
+ webNav.loadURI(url ? data.url(url) : "about:blank", 0, null, null, null);
+}
+
+exports.setURL = setURL;
+
+function allowContextMenu(panel, allow) {
+ if (allow) {
+ panel.setAttribute("context", "contentAreaContextMenu");
+ }
+ else {
+ panel.removeAttribute("context");
+ }
+}
+exports.allowContextMenu = allowContextMenu;