addon-sdk/source/lib/sdk/panel.js

changeset 0
6474c204b198
     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 +});

mercurial