diff -r 000000000000 -r 6474c204b198 addon-sdk/source/lib/sdk/panel.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/addon-sdk/source/lib/sdk/panel.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,305 @@ +/* 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 { 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 } = require("./content/utils"); +const { Worker } = require("./content/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 { 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"); + +let isRect = ({top, right, bottom, left}) => [top, right, bottom, left]. + some(value => isNumber(value) && !isNaN(value)); + +let isSDKObj = obj => obj instanceof Class; + +let rectContract = contract({ + top: number, + right: number, + bottom: number, + left: number +}); + +let 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.' +} + +let displayContract = contract({ + width: number, + height: number, + focus: boolean, + position: position +}); + +let 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' + }) +}, 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(); +let 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); + +// 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, + WeakReference + ], + 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); + + if (model.contentStyle || model.contentStyleFile) { + styles.set(this, Style({ + uri: model.contentStyleFile, + source: model.contentStyle + })); + } + + // 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); + + 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() 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. + workerFor(this).detach(); + }, + + /* 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.position || model.position); + + 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, ({target}) => panelFor(target)); + +// Panel events emitted after panel has being shown. +let shows = filter(panelEvents, ({type}) => type === "popupshown"); + +// Panel events emitted after panel became hidden. +let 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. +let ready = filter(panelEvents, ({type, target}) => + getAttachEventType(modelFor(panelFor(target))) === type); + +// Styles should be always added as soon as possible, and doesn't makes them +// depends on `contentScriptWhen` +let start = filter(panelEvents, ({type}) => type === "document-element-inserted"); + +// Forward panel show / hide events to panel's own event listeners. +on(shows, "data", ({target}) => emit(panelFor(target), "show")); + +on(hides, "data", ({target}) => emit(panelFor(target), "hide")); + +on(ready, "data", ({target}) => { + let panel = panelFor(target); + let window = domPanel.getContentDocument(target).defaultView; + + workerFor(panel).attach(window); +}); + +on(start, "data", ({target}) => { + let panel = panelFor(target); + let window = domPanel.getContentDocument(target).defaultView; + + attach(styleFor(panel), window); +});