/* 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 and SeaMonkey. // See: https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps module.metadata = { "stability": "stable", "engines": { "Palemoon": "*", "Firefox": "*", "SeaMonkey": "*" } }; const { Cu, Ci } = require("chrome"); const { setTimeout } = require('./timers'); const { Class } = require("./core/heritage"); const { merge } = require("./util/object"); const { WorkerHost } = require("./content/utils"); const { Worker } = require("./deprecated/sync-worker"); const { Disposable } = require("./core/disposable"); const { WeakReference } = require('./core/reference'); 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 { getDocShell } = require('./frame/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, isNumber } = require("./lang/type"); const { getAttachEventType } = require("./content/utils"); const { number, boolean, object } = require('./deprecated/api-utils'); const { Style } = require("./stylesheet/style"); const { attach, detach } = require("./content/mod"); var isRect = ({top, right, bottom, left}) => [top, right, bottom, left]. some(value => isNumber(value) && !isNaN(value)); var isSDKObj = obj => obj instanceof Class; var rectContract = contract({ top: number, right: number, bottom: number, left: number }); var position = { is: object, map: v => (isNil(v) || isSDKObj(v) || !isObject(v)) ? v : rectContract(v), ok: v => isNil(v) || isSDKObj(v) || (isObject(v) && isRect(v)), msg: 'The option "position" must be a SDK object registered as anchor; ' + 'or an object with one or more of the following keys set to numeric ' + 'values: top, right, bottom, left.' } var displayContract = contract({ width: number, height: number, focus: boolean, position: position }); var panelContract = contract(merge({ // contentStyle* / contentScript* are sharing the same validation constraints, // so they can be mostly reused, except for the messages. contentStyle: merge(Object.create(loaderContract.rules.contentScript), { msg: 'The `contentStyle` option must be a string or an array of strings.' }), contentStyleFile: merge(Object.create(loaderContract.rules.contentScriptFile), { msg: 'The `contentStyleFile` option must be a local URL or an array of URLs' }), contextMenu: boolean, allow: { is: ['object', 'undefined', 'null'], map: function (allow) { return { script: !allow || allow.script !== false }} }, }, displayContract.rules, loaderContract.rules)); function Allow(panel) { return { get script() { return getDocShell(viewFor(panel).backgroundFrame).allowJavascript; }, set script(value) { return setScriptState(panel, value); }, }; } function setScriptState(panel, value) { let view = viewFor(panel); getDocShell(view.backgroundFrame).allowJavascript = value; getDocShell(view.viewFrame).allowJavascript = value; view.setAttribute("sdkscriptenabled", "" + value); } function isDisposed(panel) { return !views.has(panel); } var panels = new WeakMap(); var models = new WeakMap(); var views = new WeakMap(); var workers = new WeakMap(); var styles = new WeakMap(); const viewFor = (panel) => views.get(panel); const modelFor = (panel) => models.get(panel); const panelFor = (view) => panels.get(view); const workerFor = (panel) => workers.get(panel); const styleFor = (panel) => styles.get(panel); function getPanelFromWeakRef(weakRef) { if (!weakRef) { return null; } let panel = weakRef.get(); if (!panel) { return null; } if (isDisposed(panel)) { return null; } return panel; } var SinglePanelManager = { visiblePanel: null, enqueuedPanel: null, enqueuedPanelCallback: null, // Calls |callback| with no arguments when the panel may be shown. requestOpen: function(panelToOpen, callback) { let currentPanel = getPanelFromWeakRef(SinglePanelManager.visiblePanel); if (currentPanel || SinglePanelManager.enqueuedPanel) { SinglePanelManager.enqueuedPanel = Cu.getWeakReference(panelToOpen); SinglePanelManager.enqueuedPanelCallback = callback; if (currentPanel && currentPanel.isShowing) { currentPanel.hide(); } } else { SinglePanelManager.notifyPanelCanOpen(panelToOpen, callback); } }, notifyPanelCanOpen: function(panel, callback) { let view = viewFor(panel); // Can't pass an arrow function as the event handler because we need to be // able to call |removeEventListener| later. view.addEventListener("popuphidden", SinglePanelManager.onVisiblePanelHidden, true); view.addEventListener("popupshown", SinglePanelManager.onVisiblePanelShown, false); SinglePanelManager.enqueuedPanel = null; SinglePanelManager.enqueuedPanelCallback = null; SinglePanelManager.visiblePanel = Cu.getWeakReference(panel); callback(); }, onVisiblePanelShown: function(event) { let panel = panelFor(event.target); if (SinglePanelManager.enqueuedPanel) { // Another panel started waiting for |panel| to close before |panel| was // even done opening. panel.hide(); } }, onVisiblePanelHidden: function(event) { let view = event.target; let panel = panelFor(view); let currentPanel = getPanelFromWeakRef(SinglePanelManager.visiblePanel); if (currentPanel && currentPanel != panel) { return; } SinglePanelManager.visiblePanel = null; view.removeEventListener("popuphidden", SinglePanelManager.onVisiblePanelHidden, true); view.removeEventListener("popupshown", SinglePanelManager.onVisiblePanelShown, false); let nextPanel = getPanelFromWeakRef(SinglePanelManager.enqueuedPanel); let nextPanelCallback = SinglePanelManager.enqueuedPanelCallback; if (nextPanel) { SinglePanelManager.notifyPanelCanOpen(nextPanel, nextPanelCallback); } } }; 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, WeakReference ], extends: WorkerHost(workerFor), setup: function setup(options) { let model = merge({ defaultWidth: 320, defaultHeight: 240, focus: true, position: Object.freeze({}), contextMenu: false }, panelContract(options)); model.ready = false; models.set(this, model); if (model.contentStyle || model.contentStyleFile) { styles.set(this, Style({ uri: model.contentStyleFile, source: model.contentStyle })); } // Setup view let viewOptions = {allowJavascript: !model.allow || (model.allow.script !== false)}; let view = domPanel.make(null, viewOptions); panels.set(view, this); views.set(this, view); // Load panel content. domPanel.setURL(view, model.contentURL); // Allow context menu domPanel.allowContextMenu(view, model.contextMenu); // 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); workerFor(this).destroy(); detach(styleFor(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() { return modelFor(this).width; }, set width(value) { this.resize(value, this.height); }, /* Public API: Panel.height */ get height() { return modelFor(this).height; }, set height(value) { this.resize(this.width, value); }, /* Public API: Panel.focus */ get focus() { return modelFor(this).focus; }, /* Public API: Panel.position */ get position() { return modelFor(this).position; }, /* Public API: Panel.contextMenu */ get contextMenu() { return modelFor(this).contextMenu; }, set contextMenu(allow) { let model = modelFor(this); model.contextMenu = panelContract({ contextMenu: allow }).contextMenu; domPanel.allowContextMenu(viewFor(this), model.contextMenu); }, get contentURL() { return 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. workerFor(this).detach(); }, get allow() { return Allow(this); }, set allow(value) { let allowJavascript = panelContract({ allow: value }).allow.script; return setScriptState(this, value); }, /* Public API: Panel.isShowing */ get isShowing() { return !isDisposed(this) && domPanel.isOpen(viewFor(this)); }, /* Public API: Panel.show */ show: function show(options={}, anchor) { SinglePanelManager.requestOpen(this, () => { 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.position || model.position); options = merge({ position: model.position, width: model.width, height: model.height, defaultWidth: model.defaultWidth, defaultHeight: model.defaultHeight, focus: model.focus, contextMenu: model.contextMenu }, 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. var panelEvents = filter(events, ({target}) => panelFor(target)); // Panel events emitted after panel has being shown. var shows = filter(panelEvents, ({type}) => type === "popupshown"); // Panel events emitted after panel became hidden. var hides = filter(panelEvents, ({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. var ready = filter(panelEvents, ({type, target}) => getAttachEventType(modelFor(panelFor(target))) === type); // Panel event emitted when the contents of the panel has been loaded. var readyToShow = filter(panelEvents, ({type}) => type === "DOMContentLoaded"); // Styles should be always added as soon as possible, and doesn't makes them // depends on `contentScriptWhen` var start = filter(panelEvents, ({type}) => type === "document-element-inserted"); // Forward panel show / hide events to panel's own event listeners. on(shows, "data", ({target}) => { let panel = panelFor(target); if (modelFor(panel).ready) emit(panel, "show"); }); on(hides, "data", ({target}) => { let panel = panelFor(target); if (modelFor(panel).ready) emit(panel, "hide"); }); on(ready, "data", ({target}) => { let panel = panelFor(target); let window = domPanel.getContentDocument(target).defaultView; workerFor(panel).attach(window); }); on(readyToShow, "data", ({target}) => { let panel = panelFor(target); if (!modelFor(panel).ready) { modelFor(panel).ready = true; if (viewFor(panel).state == "open") emit(panel, "show"); } }); on(start, "data", ({target}) => { let panel = panelFor(target); let window = domPanel.getContentDocument(target).defaultView; attach(styleFor(panel), window); });