123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264 |
- /* 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, detach, attach, destroy } = require("./content/utils");
- const { Worker } = require("./content/worker");
- 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, stripListeners } = 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 view
- let view = domPanel.make();
- panels.set(view, this);
- views.set(this, view);
- // Load panel content.
- domPanel.setURL(view, model.contentURL);
- setupAutoHide(this);
- // Setup listeners.
- setListeners(this, options);
- let worker = new Worker(stripListeners(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);
- });
|