/* 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 { 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 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"); } // 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); if (platform === "darwin") { frame.style.borderRadius = "6px"; 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); // Note that panel is a parent of `viewFrame` who's `docShell` will be // configured at creation time. If `panel` and there for `viewFrame` won't // have an owner document attempt to access `docShell` will throw. There // for we attach panel to a document. 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, // Note that use of this URL let's use swap frame loaders earlier // than if we used default "about:blank". uri: "data:text/plain;charset=utf-8," }; let backgroundFrame = createFrame(addonWindow, frameOptions); setupPanelFrame(backgroundFrame); let viewFrame = createFrame(panel, frameOptions); setupPanelFrame(viewFrame); function onDisplayChange({type, target}) { // Events from child element like