403 lines
13 KiB
JavaScript
403 lines
13 KiB
JavaScript
/* 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 { Cc, Ci } = require("chrome");
|
|
|
|
const { validateOptions: valid } = require('./deprecated/api-utils');
|
|
const { Symbiont } = require('./content/content');
|
|
const { EventEmitter } = require('./deprecated/events');
|
|
const timer = require('./timers');
|
|
const runtime = require('./system/runtime');
|
|
const { getMostRecentBrowserWindow } = require('./window/utils');
|
|
const { getDocShell } = require("./frame/utils");
|
|
|
|
const windowMediator = Cc['@mozilla.org/appshell/window-mediator;1'].
|
|
getService(Ci.nsIWindowMediator);
|
|
|
|
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
|
|
ON_SHOW = 'popupshown',
|
|
ON_HIDE = 'popuphidden',
|
|
validNumber = { is: ['number', 'undefined', 'null'] };
|
|
|
|
/**
|
|
* 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 document = getWindow(anchor).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 =
|
|
'<bindings xmlns="http://www.mozilla.org/xbl">' +
|
|
'<binding id="id" extends="' + originalXBL + '">' +
|
|
'<resources>' +
|
|
'<stylesheet src="data:text/css;charset=utf-8,' +
|
|
document.defaultView.encodeURIComponent(css) + '"/>' +
|
|
'</resources>' +
|
|
'</binding>' +
|
|
'</bindings>';
|
|
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) {
|
|
timer.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 <panel> 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;
|
|
|
|
function getWindow(anchor) {
|
|
let window;
|
|
|
|
if (anchor) {
|
|
let anchorWindow = anchor.ownerDocument.defaultView.top;
|
|
let anchorDocument = anchorWindow.document;
|
|
|
|
let enumerator = windowMediator.getEnumerator("navigator:browser");
|
|
while (enumerator.hasMoreElements()) {
|
|
let enumWindow = enumerator.getNext();
|
|
|
|
// Check if the anchor is in this browser window.
|
|
if (enumWindow == anchorWindow) {
|
|
window = anchorWindow;
|
|
break;
|
|
}
|
|
|
|
// Check if the anchor is in a browser tab in this browser window.
|
|
let browser = enumWindow.gBrowser.getBrowserForDocument(anchorDocument);
|
|
if (browser) {
|
|
window = enumWindow;
|
|
break;
|
|
}
|
|
|
|
// Look in other subdocuments (sidebar, etc.)?
|
|
}
|
|
}
|
|
|
|
// If we didn't find the anchor's window (or we have no anchor),
|
|
// return the most recent browser window.
|
|
if (!window)
|
|
window = getMostRecentBrowserWindow();
|
|
|
|
return window;
|
|
}
|
|
|