1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/addon-sdk/source/lib/sdk/panel.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,305 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 +"use strict"; 1.8 + 1.9 +// The panel module currently supports only Firefox. 1.10 +// See: https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps 1.11 +module.metadata = { 1.12 + "stability": "stable", 1.13 + "engines": { 1.14 + "Firefox": "*" 1.15 + } 1.16 +}; 1.17 + 1.18 +const { Ci } = require("chrome"); 1.19 +const { setTimeout } = require('./timers'); 1.20 +const { isPrivateBrowsingSupported } = require('./self'); 1.21 +const { isWindowPBSupported } = require('./private-browsing/utils'); 1.22 +const { Class } = require("./core/heritage"); 1.23 +const { merge } = require("./util/object"); 1.24 +const { WorkerHost } = require("./content/utils"); 1.25 +const { Worker } = require("./content/worker"); 1.26 +const { Disposable } = require("./core/disposable"); 1.27 +const { WeakReference } = require('./core/reference'); 1.28 +const { contract: loaderContract } = require("./content/loader"); 1.29 +const { contract } = require("./util/contract"); 1.30 +const { on, off, emit, setListeners } = require("./event/core"); 1.31 +const { EventTarget } = require("./event/target"); 1.32 +const domPanel = require("./panel/utils"); 1.33 +const { events } = require("./panel/events"); 1.34 +const systemEvents = require("./system/events"); 1.35 +const { filter, pipe, stripListeners } = require("./event/utils"); 1.36 +const { getNodeView, getActiveView } = require("./view/core"); 1.37 +const { isNil, isObject, isNumber } = require("./lang/type"); 1.38 +const { getAttachEventType } = require("./content/utils"); 1.39 +const { number, boolean, object } = require('./deprecated/api-utils'); 1.40 +const { Style } = require("./stylesheet/style"); 1.41 +const { attach, detach } = require("./content/mod"); 1.42 + 1.43 +let isRect = ({top, right, bottom, left}) => [top, right, bottom, left]. 1.44 + some(value => isNumber(value) && !isNaN(value)); 1.45 + 1.46 +let isSDKObj = obj => obj instanceof Class; 1.47 + 1.48 +let rectContract = contract({ 1.49 + top: number, 1.50 + right: number, 1.51 + bottom: number, 1.52 + left: number 1.53 +}); 1.54 + 1.55 +let position = { 1.56 + is: object, 1.57 + map: v => (isNil(v) || isSDKObj(v) || !isObject(v)) ? v : rectContract(v), 1.58 + ok: v => isNil(v) || isSDKObj(v) || (isObject(v) && isRect(v)), 1.59 + msg: 'The option "position" must be a SDK object registered as anchor; ' + 1.60 + 'or an object with one or more of the following keys set to numeric ' + 1.61 + 'values: top, right, bottom, left.' 1.62 +} 1.63 + 1.64 +let displayContract = contract({ 1.65 + width: number, 1.66 + height: number, 1.67 + focus: boolean, 1.68 + position: position 1.69 +}); 1.70 + 1.71 +let panelContract = contract(merge({ 1.72 + // contentStyle* / contentScript* are sharing the same validation constraints, 1.73 + // so they can be mostly reused, except for the messages. 1.74 + contentStyle: merge(Object.create(loaderContract.rules.contentScript), { 1.75 + msg: 'The `contentStyle` option must be a string or an array of strings.' 1.76 + }), 1.77 + contentStyleFile: merge(Object.create(loaderContract.rules.contentScriptFile), { 1.78 + msg: 'The `contentStyleFile` option must be a local URL or an array of URLs' 1.79 + }) 1.80 +}, displayContract.rules, loaderContract.rules)); 1.81 + 1.82 + 1.83 +function isDisposed(panel) !views.has(panel); 1.84 + 1.85 +let panels = new WeakMap(); 1.86 +let models = new WeakMap(); 1.87 +let views = new WeakMap(); 1.88 +let workers = new WeakMap(); 1.89 +let styles = new WeakMap(); 1.90 + 1.91 +const viewFor = (panel) => views.get(panel); 1.92 +const modelFor = (panel) => models.get(panel); 1.93 +const panelFor = (view) => panels.get(view); 1.94 +const workerFor = (panel) => workers.get(panel); 1.95 +const styleFor = (panel) => styles.get(panel); 1.96 + 1.97 +// Utility function takes `panel` instance and makes sure it will be 1.98 +// automatically hidden as soon as other panel is shown. 1.99 +let setupAutoHide = new function() { 1.100 + let refs = new WeakMap(); 1.101 + 1.102 + return function setupAutoHide(panel) { 1.103 + // Create system event listener that reacts to any panel showing and 1.104 + // hides given `panel` if it's not the one being shown. 1.105 + function listener({subject}) { 1.106 + // It could be that listener is not GC-ed in the same cycle as 1.107 + // panel in such case we remove listener manually. 1.108 + let view = viewFor(panel); 1.109 + if (!view) systemEvents.off("popupshowing", listener); 1.110 + else if (subject !== view) panel.hide(); 1.111 + } 1.112 + 1.113 + // system event listener is intentionally weak this way we'll allow GC 1.114 + // to claim panel if it's no longer referenced by an add-on code. This also 1.115 + // helps minimizing cleanup required on unload. 1.116 + systemEvents.on("popupshowing", listener); 1.117 + // To make sure listener is not claimed by GC earlier than necessary we 1.118 + // associate it with `panel` it's associated with. This way it won't be 1.119 + // GC-ed earlier than `panel` itself. 1.120 + refs.set(panel, listener); 1.121 + } 1.122 +} 1.123 + 1.124 +const Panel = Class({ 1.125 + implements: [ 1.126 + // Generate accessors for the validated properties that update model on 1.127 + // set and return values from model on get. 1.128 + panelContract.properties(modelFor), 1.129 + EventTarget, 1.130 + Disposable, 1.131 + WeakReference 1.132 + ], 1.133 + extends: WorkerHost(workerFor), 1.134 + setup: function setup(options) { 1.135 + let model = merge({ 1.136 + defaultWidth: 320, 1.137 + defaultHeight: 240, 1.138 + focus: true, 1.139 + position: Object.freeze({}), 1.140 + }, panelContract(options)); 1.141 + models.set(this, model); 1.142 + 1.143 + if (model.contentStyle || model.contentStyleFile) { 1.144 + styles.set(this, Style({ 1.145 + uri: model.contentStyleFile, 1.146 + source: model.contentStyle 1.147 + })); 1.148 + } 1.149 + 1.150 + // Setup view 1.151 + let view = domPanel.make(); 1.152 + panels.set(view, this); 1.153 + views.set(this, view); 1.154 + 1.155 + // Load panel content. 1.156 + domPanel.setURL(view, model.contentURL); 1.157 + 1.158 + setupAutoHide(this); 1.159 + 1.160 + // Setup listeners. 1.161 + setListeners(this, options); 1.162 + let worker = new Worker(stripListeners(options)); 1.163 + workers.set(this, worker); 1.164 + 1.165 + // pipe events from worker to a panel. 1.166 + pipe(worker, this); 1.167 + }, 1.168 + dispose: function dispose() { 1.169 + this.hide(); 1.170 + off(this); 1.171 + 1.172 + workerFor(this).destroy(); 1.173 + detach(styleFor(this)); 1.174 + 1.175 + domPanel.dispose(viewFor(this)); 1.176 + 1.177 + // Release circular reference between view and panel instance. This 1.178 + // way view will be GC-ed. And panel as well once all the other refs 1.179 + // will be removed from it. 1.180 + views.delete(this); 1.181 + }, 1.182 + /* Public API: Panel.width */ 1.183 + get width() modelFor(this).width, 1.184 + set width(value) this.resize(value, this.height), 1.185 + /* Public API: Panel.height */ 1.186 + get height() modelFor(this).height, 1.187 + set height(value) this.resize(this.width, value), 1.188 + 1.189 + /* Public API: Panel.focus */ 1.190 + get focus() modelFor(this).focus, 1.191 + 1.192 + /* Public API: Panel.position */ 1.193 + get position() modelFor(this).position, 1.194 + 1.195 + get contentURL() modelFor(this).contentURL, 1.196 + set contentURL(value) { 1.197 + let model = modelFor(this); 1.198 + model.contentURL = panelContract({ contentURL: value }).contentURL; 1.199 + domPanel.setURL(viewFor(this), model.contentURL); 1.200 + // Detach worker so that messages send will be queued until it's 1.201 + // reatached once panel content is ready. 1.202 + workerFor(this).detach(); 1.203 + }, 1.204 + 1.205 + /* Public API: Panel.isShowing */ 1.206 + get isShowing() !isDisposed(this) && domPanel.isOpen(viewFor(this)), 1.207 + 1.208 + /* Public API: Panel.show */ 1.209 + show: function show(options={}, anchor) { 1.210 + if (options instanceof Ci.nsIDOMElement) { 1.211 + [anchor, options] = [options, null]; 1.212 + } 1.213 + 1.214 + if (anchor instanceof Ci.nsIDOMElement) { 1.215 + console.warn( 1.216 + "Passing a DOM node to Panel.show() method is an unsupported " + 1.217 + "feature that will be soon replaced. " + 1.218 + "See: https://bugzilla.mozilla.org/show_bug.cgi?id=878877" 1.219 + ); 1.220 + } 1.221 + 1.222 + let model = modelFor(this); 1.223 + let view = viewFor(this); 1.224 + let anchorView = getNodeView(anchor || options.position || model.position); 1.225 + 1.226 + options = merge({ 1.227 + position: model.position, 1.228 + width: model.width, 1.229 + height: model.height, 1.230 + defaultWidth: model.defaultWidth, 1.231 + defaultHeight: model.defaultHeight, 1.232 + focus: model.focus 1.233 + }, displayContract(options)); 1.234 + 1.235 + if (!isDisposed(this)) 1.236 + domPanel.show(view, options, anchorView); 1.237 + 1.238 + return this; 1.239 + }, 1.240 + 1.241 + /* Public API: Panel.hide */ 1.242 + hide: function hide() { 1.243 + // Quit immediately if panel is disposed or there is no state change. 1.244 + domPanel.close(viewFor(this)); 1.245 + 1.246 + return this; 1.247 + }, 1.248 + 1.249 + /* Public API: Panel.resize */ 1.250 + resize: function resize(width, height) { 1.251 + let model = modelFor(this); 1.252 + let view = viewFor(this); 1.253 + let change = panelContract({ 1.254 + width: width || model.width || model.defaultWidth, 1.255 + height: height || model.height || model.defaultHeight 1.256 + }); 1.257 + 1.258 + model.width = change.width 1.259 + model.height = change.height 1.260 + 1.261 + domPanel.resize(view, model.width, model.height); 1.262 + 1.263 + return this; 1.264 + } 1.265 +}); 1.266 +exports.Panel = Panel; 1.267 + 1.268 +// Note must be defined only after value to `Panel` is assigned. 1.269 +getActiveView.define(Panel, viewFor); 1.270 + 1.271 +// Filter panel events to only panels that are create by this module. 1.272 +let panelEvents = filter(events, ({target}) => panelFor(target)); 1.273 + 1.274 +// Panel events emitted after panel has being shown. 1.275 +let shows = filter(panelEvents, ({type}) => type === "popupshown"); 1.276 + 1.277 +// Panel events emitted after panel became hidden. 1.278 +let hides = filter(panelEvents, ({type}) => type === "popuphidden"); 1.279 + 1.280 +// Panel events emitted after content inside panel is ready. For different 1.281 +// panels ready may mean different state based on `contentScriptWhen` attribute. 1.282 +// Weather given event represents readyness is detected by `getAttachEventType` 1.283 +// helper function. 1.284 +let ready = filter(panelEvents, ({type, target}) => 1.285 + getAttachEventType(modelFor(panelFor(target))) === type); 1.286 + 1.287 +// Styles should be always added as soon as possible, and doesn't makes them 1.288 +// depends on `contentScriptWhen` 1.289 +let start = filter(panelEvents, ({type}) => type === "document-element-inserted"); 1.290 + 1.291 +// Forward panel show / hide events to panel's own event listeners. 1.292 +on(shows, "data", ({target}) => emit(panelFor(target), "show")); 1.293 + 1.294 +on(hides, "data", ({target}) => emit(panelFor(target), "hide")); 1.295 + 1.296 +on(ready, "data", ({target}) => { 1.297 + let panel = panelFor(target); 1.298 + let window = domPanel.getContentDocument(target).defaultView; 1.299 + 1.300 + workerFor(panel).attach(window); 1.301 +}); 1.302 + 1.303 +on(start, "data", ({target}) => { 1.304 + let panel = panelFor(target); 1.305 + let window = domPanel.getContentDocument(target).defaultView; 1.306 + 1.307 + attach(styleFor(panel), window); 1.308 +});