gecko-dev/addon-sdk/source/lib/sdk/panel.js

265 строки
8.3 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 { Ci } = require("chrome");
const { validateOptions: valid } = require('./deprecated/api-utils');
const { setTimeout } = require('./timers');
const { isPrivateBrowsingSupported } = require('./self');
const { isWindowPBSupported } = require('./private-browsing/utils');
const { Class } = require("./core/heritage");
const { merge } = require("./util/object");
const { WorkerHost, Worker, detach, attach, destroy,
requiresAddonGlobal } = require("./worker/utils");
const { Disposable } = require("./core/disposable");
const { contract: loaderContract } = require("./content/loader");
const { contract } = require("./util/contract");
const { on, off, emit, setListeners } = require("./event/core");
const { EventTarget } = require("./event/target");
const domPanel = require("./panel/utils");
const { events } = require("./panel/events");
const systemEvents = require("./system/events");
const { filter, pipe } = require("./event/utils");
const { getNodeView, getActiveView } = require("./view/core");
const { isNil, isObject } = require("./lang/type");
const { getAttachEventType } = require("./content/utils");
let number = { is: ['number', 'undefined', 'null'] };
let boolean = { is: ['boolean', 'undefined', 'null'] };
let rectContract = contract({
top: number,
right: number,
bottom: number,
left: number
});
let rect = {
is: ['object', 'undefined', 'null'],
map: function(v) isNil(v) || !isObject(v) ? v : rectContract(v)
}
let displayContract = contract({
width: number,
height: number,
focus: boolean,
position: rect
});
let panelContract = contract(merge({}, displayContract.rules, loaderContract.rules));
function isDisposed(panel) !views.has(panel);
let panels = new WeakMap();
let models = new WeakMap();
let views = new WeakMap();
let workers = new WeakMap();
function viewFor(panel) views.get(panel)
function modelFor(panel) models.get(panel)
function panelFor(view) panels.get(view)
function workerFor(panel) workers.get(panel)
// Utility function takes `panel` instance and makes sure it will be
// automatically hidden as soon as other panel is shown.
let setupAutoHide = new function() {
let refs = new WeakMap();
return function setupAutoHide(panel) {
// Create system event listener that reacts to any panel showing and
// hides given `panel` if it's not the one being shown.
function listener({subject}) {
// It could be that listener is not GC-ed in the same cycle as
// panel in such case we remove listener manually.
let view = viewFor(panel);
if (!view) systemEvents.off("popupshowing", listener);
else if (subject !== view) panel.hide();
}
// system event listener is intentionally weak this way we'll allow GC
// to claim panel if it's no longer referenced by an add-on code. This also
// helps minimizing cleanup required on unload.
systemEvents.on("popupshowing", listener);
// To make sure listener is not claimed by GC earlier than necessary we
// associate it with `panel` it's associated with. This way it won't be
// GC-ed earlier than `panel` itself.
refs.set(panel, listener);
}
}
const Panel = Class({
implements: [
// Generate accessors for the validated properties that update model on
// set and return values from model on get.
panelContract.properties(modelFor),
EventTarget,
Disposable
],
extends: WorkerHost(workerFor),
setup: function setup(options) {
let model = merge({
defaultWidth: 320,
defaultHeight: 240,
focus: true,
position: Object.freeze({}),
}, panelContract(options));
models.set(this, model);
// Setup listeners.
setListeners(this, options);
// Setup view
let view = domPanel.make();
panels.set(view, this);
views.set(this, view);
// Load panel content.
domPanel.setURL(view, model.contentURL);
setupAutoHide(this);
let worker = new Worker(options);
workers.set(this, worker);
// pipe events from worker to a panel.
pipe(worker, this);
},
dispose: function dispose() {
this.hide();
off(this);
destroy(workerFor(this));
domPanel.dispose(viewFor(this));
// Release circular reference between view and panel instance. This
// way view will be GC-ed. And panel as well once all the other refs
// will be removed from it.
views.delete(this);
},
/* Public API: Panel.width */
get width() modelFor(this).width,
set width(value) this.resize(value, this.height),
/* Public API: Panel.height */
get height() modelFor(this).height,
set height(value) this.resize(this.width, value),
/* Public API: Panel.focus */
get focus() modelFor(this).focus,
/* Public API: Panel.position */
get position() modelFor(this).position,
get contentURL() modelFor(this).contentURL,
set contentURL(value) {
let model = modelFor(this);
model.contentURL = panelContract({ contentURL: value }).contentURL;
domPanel.setURL(viewFor(this), model.contentURL);
// Detach worker so that messages send will be queued until it's
// reatached once panel content is ready.
detach(workerFor(this));
},
/* Public API: Panel.isShowing */
get isShowing() !isDisposed(this) && domPanel.isOpen(viewFor(this)),
/* Public API: Panel.show */
show: function show(options, anchor) {
if (options instanceof Ci.nsIDOMElement) {
[anchor, options] = [options, null];
}
if (anchor instanceof Ci.nsIDOMElement) {
console.warn(
"Passing a DOM node to Panel.show() method is an unsupported " +
"feature that will be soon replaced. " +
"See: https://bugzilla.mozilla.org/show_bug.cgi?id=878877"
);
}
let model = modelFor(this);
let view = viewFor(this);
let anchorView = getNodeView(anchor);
options = merge({
position: model.position,
width: model.width,
height: model.height,
defaultWidth: model.defaultWidth,
defaultHeight: model.defaultHeight,
focus: model.focus
}, displayContract(options));
if (!isDisposed(this))
domPanel.show(view, options, anchorView);
return this;
},
/* Public API: Panel.hide */
hide: function hide() {
// Quit immediately if panel is disposed or there is no state change.
domPanel.close(viewFor(this));
return this;
},
/* Public API: Panel.resize */
resize: function resize(width, height) {
let model = modelFor(this);
let view = viewFor(this);
let change = panelContract({
width: width || model.width || model.defaultWidth,
height: height || model.height || model.defaultHeight
});
model.width = change.width
model.height = change.height
domPanel.resize(view, model.width, model.height);
return this;
}
});
exports.Panel = Panel;
// Note must be defined only after value to `Panel` is assigned.
getActiveView.define(Panel, viewFor);
// Filter panel events to only panels that are create by this module.
let panelEvents = filter(events, function({target}) panelFor(target));
// Panel events emitted after panel has being shown.
let shows = filter(panelEvents, function({type}) type === "popupshown");
// Panel events emitted after panel became hidden.
let hides = filter(panelEvents, function({type}) type === "popuphidden");
// Panel events emitted after content inside panel is ready. For different
// panels ready may mean different state based on `contentScriptWhen` attribute.
// Weather given event represents readyness is detected by `getAttachEventType`
// helper function.
let ready = filter(panelEvents, function({type, target})
getAttachEventType(modelFor(panelFor(target))) === type);
// Forward panel show / hide events to panel's own event listeners.
on(shows, "data", function({target}) emit(panelFor(target), "show"));
on(hides, "data", function({target}) emit(panelFor(target), "hide"));
on(ready, "data", function({target}) {
let worker = workerFor(panelFor(target));
attach(worker, domPanel.getContentDocument(target).defaultView);
});