/* 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";
// The panel module currently supports only Firefox.
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps
module.metadata = {
"stability": "stable",
"engines": {
"Firefox": "*"
}
};
const { Ci } = require("chrome");
const { validateOptions: valid } = require('./deprecated/api-utils');
const { Symbiont } = require('./content/content');
const { EventEmitter } = require('./deprecated/events');
const { setTimeout } = require('./timers');
const runtime = require('./system/runtime');
const { getDocShell } = require("./frame/utils");
const { getWindow } = require('./panel/window');
const { isPrivateBrowsingSupported } = require('./self');
const { isWindowPBSupported } = require('./private-browsing/utils');
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
ON_SHOW = 'popupshown',
ON_HIDE = 'popuphidden',
validNumber = { is: ['number', 'undefined', 'null'] };
if (isPrivateBrowsingSupported && isWindowPBSupported) {
throw Error('The panel module cannot be used with per-window private browsing at the moment, see Bug 816257');
}
/**
* Emits show and hide events.
*/
const Panel = Symbiont.resolve({
constructor: '_init',
_onInit: '_onSymbiontInit',
destroy: '_symbiontDestructor',
_documentUnload: '_workerDocumentUnload'
}).compose({
_frame: Symbiont.required,
_init: Symbiont.required,
_onSymbiontInit: Symbiont.required,
_symbiontDestructor: Symbiont.required,
_emit: Symbiont.required,
on: Symbiont.required,
removeListener: Symbiont.required,
_inited: false,
/**
* If set to `true` frame loaders between xul panel frame and
* hidden frame are swapped. If set to `false` frame loaders are
* set back to normal. Setting the value that was already set will
* have no effect.
*/
set _frameLoadersSwapped(value) {
if (this.__frameLoadersSwapped == value) return;
this._frame.QueryInterface(Ci.nsIFrameLoaderOwner)
.swapFrameLoaders(this._viewFrame);
this.__frameLoadersSwapped = value;
},
__frameLoadersSwapped: false,
constructor: function Panel(options) {
this._onShow = this._onShow.bind(this);
this._onHide = this._onHide.bind(this);
this.on('inited', this._onSymbiontInit.bind(this));
this.on('propertyChange', this._onChange.bind(this));
options = options || {};
if ('onShow' in options)
this.on('show', options.onShow);
if ('onHide' in options)
this.on('hide', options.onHide);
if ('width' in options)
this.width = options.width;
if ('height' in options)
this.height = options.height;
if ('contentURL' in options)
this.contentURL = options.contentURL;
this._init(options);
},
_destructor: function _destructor() {
this.hide();
this._removeAllListeners('show');
this._removeAllListeners('hide');
this._removeAllListeners('propertyChange');
this._removeAllListeners('inited');
// defer cleanup to be performed after panel gets hidden
this._xulPanel = null;
this._symbiontDestructor(this);
this._removeAllListeners();
},
destroy: function destroy() {
this._destructor();
},
/* Public API: Panel.width */
get width() this._width,
set width(value)
this._width = valid({ $: value }, { $: validNumber }).$ || this._width,
_width: 320,
/* Public API: Panel.height */
get height() this._height,
set height(value)
this._height = valid({ $: value }, { $: validNumber }).$ || this._height,
_height: 240,
/* Public API: Panel.isShowing */
get isShowing() !!this._xulPanel && this._xulPanel.state == "open",
/* Public API: Panel.show */
show: function show(anchor) {
anchor = anchor || null;
let anchorWindow = getWindow(anchor);
// If there is no open window, or the anchor is in a private window
// then we will not be able to display the panel
if (!anchorWindow) {
return;
}
let document = anchorWindow.document;
let xulPanel = this._xulPanel;
if (!xulPanel) {
xulPanel = this._xulPanel = document.createElementNS(XUL_NS, 'panel');
xulPanel.setAttribute("type", "arrow");
// One anonymous node has a big padding that doesn't work well with
// Jetpack, as we would like to display an iframe that completely fills
// the panel.
// -> Use a XBL wrapper with inner stylesheet to remove this padding.
let css = ".panel-inner-arrowcontent, .panel-arrowcontent {padding: 0;}";
let originalXBL = "chrome://global/content/bindings/popup.xml#arrowpanel";
let binding =
'' +
'' +
'' +
'' +
'' +
'' +
'';
xulPanel.style.MozBinding = 'url("data:text/xml;charset=utf-8,' +
document.defaultView.encodeURIComponent(binding) + '")';
let frame = document.createElementNS(XUL_NS, 'iframe');
frame.setAttribute('type', 'content');
frame.setAttribute('flex', '1');
frame.setAttribute('transparent', 'transparent');
if (runtime.OS === "Darwin") {
frame.style.borderRadius = "6px";
frame.style.padding = "1px";
}
// Load an empty document in order to have an immediatly loaded iframe,
// so swapFrameLoaders is going to work without having to wait for load.
frame.setAttribute("src","data:;charset=utf-8,");
xulPanel.appendChild(frame);
document.getElementById("mainPopupSet").appendChild(xulPanel);
}
let { width, height } = this, x, y, position;
if (!anchor) {
// Open the popup in the middle of the window.
x = document.documentElement.clientWidth / 2 - width / 2;
y = document.documentElement.clientHeight / 2 - height / 2;
position = null;
}
else {
// Open the popup by the anchor.
let rect = anchor.getBoundingClientRect();
let window = anchor.ownerDocument.defaultView;
let zoom = window.mozScreenPixelsPerCSSPixel;
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";
position = 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)
xulPanel.setAttribute("flip","both");
}
// Resize the iframe instead of using panel.sizeTo
// because sizeTo doesn't work with arrow panels
xulPanel.firstChild.style.width = width + "px";
xulPanel.firstChild.style.height = height + "px";
// Wait for the XBL binding to be constructed
function waitForBinding() {
if (!xulPanel.openPopup) {
setTimeout(waitForBinding, 50);
return;
}
xulPanel.openPopup(anchor, position, x, y);
}
waitForBinding();
return this._public;
},
/* Public API: Panel.hide */
hide: function hide() {
// The popuphiding handler takes care of swapping back the frame loaders
// and removing the XUL panel from the application window, we just have to
// trigger it by hiding the popup.
// XXX Sometimes I get "TypeError: xulPanel.hidePopup is not a function"
// when quitting the host application while a panel is visible. To suppress
// them, this now checks for "hidePopup" in xulPanel before calling it.
// It's not clear if there's an actual issue or the error is just normal.
let xulPanel = this._xulPanel;
if (xulPanel && "hidePopup" in xulPanel)
xulPanel.hidePopup();
return this._public;
},
/* Public API: Panel.resize */
resize: function resize(width, height) {
this.width = width;
this.height = height;
// Resize the iframe instead of using panel.sizeTo
// because sizeTo doesn't work with arrow panels
let xulPanel = this._xulPanel;
if (xulPanel) {
xulPanel.firstChild.style.width = width + "px";
xulPanel.firstChild.style.height = height + "px";
}
},
// While the panel is visible, this is the XUL we use to display it.
// Otherwise, it's null.
get _xulPanel() this.__xulPanel,
set _xulPanel(value) {
let xulPanel = this.__xulPanel;
if (value === xulPanel) return;
if (xulPanel) {
xulPanel.removeEventListener(ON_HIDE, this._onHide, false);
xulPanel.removeEventListener(ON_SHOW, this._onShow, false);
xulPanel.parentNode.removeChild(xulPanel);
}
if (value) {
value.addEventListener(ON_HIDE, this._onHide, false);
value.addEventListener(ON_SHOW, this._onShow, false);
}
this.__xulPanel = value;
},
__xulPanel: null,
get _viewFrame() this.__xulPanel.children[0],
/**
* When the XUL panel becomes hidden, we swap frame loaders back to move
* the content of the panel to the hidden frame & remove panel element.
*/
_onHide: function _onHide() {
try {
this._frameLoadersSwapped = false;
this._xulPanel = null;
this._emit('hide');
} catch(e) {
this._emit('error', e);
}
},
/**
* Retrieve computed text color style in order to apply to the iframe
* document. As MacOS background is dark gray, we need to use skin's
* text color.
*/
_applyStyleToDocument: function _applyStyleToDocument() {
try {
let win = this._xulPanel.ownerDocument.defaultView;
let node = win.document.getAnonymousElementByAttribute(
this._xulPanel, "class", "panel-arrowcontent");
if (!node) {
// Before bug 764755, anonymous content was different:
// TODO: Remove this when targeting FF16+
node = win.document.getAnonymousElementByAttribute(
this._xulPanel, "class", "panel-inner-arrowcontent");
}
let textColor = win.getComputedStyle(node).getPropertyValue("color");
let doc = this._xulPanel.firstChild.contentDocument;
let style = doc.createElement("style");
style.textContent = "body { color: " + textColor + "; }";
let container = doc.head ? doc.head : doc.documentElement;
if (container.firstChild)
container.insertBefore(style, container.firstChild);
else
container.appendChild(style);
}
catch(e) {
console.error("Unable to apply panel style");
console.exception(e);
}
},
/**
* When the XUL panel becomes shown, we swap frame loaders between panel
* frame and hidden frame to preserve state of the content dom.
*/
_onShow: function _onShow() {
try {
if (!this._inited) { // defer if not initialized yet
this.on('inited', this._onShow.bind(this));
} else {
this._frameLoadersSwapped = true;
this._applyStyleToDocument();
this._emit('show');
}
} catch(e) {
this._emit('error', e);
}
},
/**
* Notification that panel was fully initialized.
*/
_onInit: function _onInit() {
this._inited = true;
// Avoid panel document from resizing the browser window
// New platform capability added through bug 635673
let docShell = getDocShell(this._frame);
if (docShell && "allowWindowControl" in docShell)
docShell.allowWindowControl = false;
// perform all deferred tasks like initSymbiont, show, hide ...
// TODO: We're publicly exposing a private event here; this
// 'inited' event should really be made private, somehow.
this._emit('inited');
},
// Catch document unload event in order to rebind load event listener with
// Symbiont._initFrame if Worker._documentUnload destroyed the worker
_documentUnload: function(subject, topic, data) {
if (this._workerDocumentUnload(subject, topic, data)) {
this._initFrame(this._frame);
return true;
}
return false;
},
_onChange: function _onChange(e) {
this._frameLoadersSwapped = false;
if ('contentURL' in e && this._frame) {
// Cleanup the worker before injecting the content script in the new
// document
this._workerCleanup();
this._initFrame(this._frame);
}
}
});
exports.Panel = function(options) Panel(options)
exports.Panel.prototype = Panel.prototype;