summaryrefslogtreecommitdiffstats
path: root/toolkit/jetpack/sdk/panel.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/jetpack/sdk/panel.js')
-rw-r--r--toolkit/jetpack/sdk/panel.js427
1 files changed, 427 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/panel.js b/toolkit/jetpack/sdk/panel.js
new file mode 100644
index 000000000..4b625799d
--- /dev/null
+++ b/toolkit/jetpack/sdk/panel.js
@@ -0,0 +1,427 @@
+/* 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": {
+ "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);
+});