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

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/addon-sdk/source/lib/sdk/widget.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,951 @@
     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 widget module currently supports only Firefox.
    1.10 +// See: https://bugzilla.mozilla.org/show_bug.cgi?id=560716
    1.11 +module.metadata = {
    1.12 +  "stability": "deprecated",
    1.13 +  "engines": {
    1.14 +    "Firefox": "*"
    1.15 +  }
    1.16 +};
    1.17 +
    1.18 +// Widget content types
    1.19 +const CONTENT_TYPE_URI    = 1;
    1.20 +const CONTENT_TYPE_HTML   = 2;
    1.21 +const CONTENT_TYPE_IMAGE  = 3;
    1.22 +
    1.23 +const ERR_CONTENT = "No content or contentURL property found. Widgets must "
    1.24 +                         + "have one or the other.",
    1.25 +      ERR_LABEL = "The widget must have a non-empty label property.",
    1.26 +      ERR_ID = "You have to specify a unique value for the id property of " +
    1.27 +               "your widget in order for the application to remember its " +
    1.28 +               "position.",
    1.29 +      ERR_DESTROYED = "The widget has been destroyed and can no longer be used.";
    1.30 +
    1.31 +const INSERTION_PREF_ROOT = "extensions.sdk-widget-inserted.";
    1.32 +
    1.33 +// Supported events, mapping from DOM event names to our event names
    1.34 +const EVENTS = {
    1.35 +  "click": "click",
    1.36 +  "mouseover": "mouseover",
    1.37 +  "mouseout": "mouseout",
    1.38 +};
    1.39 +
    1.40 +// In the Australis menu panel, normally widgets should be treated like
    1.41 +// normal toolbarbuttons. If they're any wider than this margin, we'll
    1.42 +// treat them as wide widgets instead, which fill up the width of the panel:
    1.43 +const AUSTRALIS_PANEL_WIDE_WIDGET_CUTOFF = 70;
    1.44 +const AUSTRALIS_PANEL_WIDE_CLASSNAME = "panel-wide-item";
    1.45 +
    1.46 +const { validateOptions } = require("./deprecated/api-utils");
    1.47 +const panels = require("./panel");
    1.48 +const { EventEmitter, EventEmitterTrait } = require("./deprecated/events");
    1.49 +const { Trait } = require("./deprecated/traits");
    1.50 +const LightTrait = require('./deprecated/light-traits').Trait;
    1.51 +const { Loader, Symbiont } = require("./content/content");
    1.52 +const { Cortex } = require('./deprecated/cortex');
    1.53 +const windowsAPI = require("./windows");
    1.54 +const { WindowTracker } = require("./deprecated/window-utils");
    1.55 +const { isBrowser } = require("./window/utils");
    1.56 +const { setTimeout } = require("./timers");
    1.57 +const unload = require("./system/unload");
    1.58 +const { getNodeView } = require("./view/core");
    1.59 +const prefs = require('./preferences/service');
    1.60 +
    1.61 +require("./util/deprecate").deprecateUsage(
    1.62 +  "The widget module is deprecated.  " +
    1.63 +  "Please consider using the sdk/ui module instead."
    1.64 +);
    1.65 +
    1.66 +// Data types definition
    1.67 +const valid = {
    1.68 +  number: { is: ["null", "undefined", "number"] },
    1.69 +  string: { is: ["null", "undefined", "string"] },
    1.70 +  id: {
    1.71 +    is: ["string"],
    1.72 +    ok: function (v) v.length > 0,
    1.73 +    msg: ERR_ID,
    1.74 +    readonly: true
    1.75 +  },
    1.76 +  label: {
    1.77 +    is: ["string"],
    1.78 +    ok: function (v) v.length > 0,
    1.79 +    msg: ERR_LABEL
    1.80 +  },
    1.81 +  panel: {
    1.82 +    is: ["null", "undefined", "object"],
    1.83 +    ok: function(v) !v || v instanceof panels.Panel
    1.84 +  },
    1.85 +  width: {
    1.86 +    is: ["null", "undefined", "number"],
    1.87 +    map: function (v) {
    1.88 +      if (null === v || undefined === v) v = 16;
    1.89 +      return v;
    1.90 +    },
    1.91 +    defaultValue: 16
    1.92 +  },
    1.93 +  allow: {
    1.94 +    is: ["null", "undefined", "object"],
    1.95 +    map: function (v) {
    1.96 +      if (!v) v = { script: true };
    1.97 +      return v;
    1.98 +    },
    1.99 +    get defaultValue() ({ script: true })
   1.100 +  },
   1.101 +};
   1.102 +
   1.103 +// Widgets attributes definition
   1.104 +let widgetAttributes = {
   1.105 +  label: valid.label,
   1.106 +  id: valid.id,
   1.107 +  tooltip: valid.string,
   1.108 +  width: valid.width,
   1.109 +  content: valid.string,
   1.110 +  panel: valid.panel,
   1.111 +  allow: valid.allow
   1.112 +};
   1.113 +
   1.114 +// Import data definitions from loader, but don't compose with it as Model
   1.115 +// functions allow us to recreate easily all Loader code.
   1.116 +let loaderAttributes = require("./content/loader").validationAttributes;
   1.117 +for (let i in loaderAttributes)
   1.118 +  widgetAttributes[i] = loaderAttributes[i];
   1.119 +
   1.120 +widgetAttributes.contentURL.optional = true;
   1.121 +
   1.122 +// Widgets public events list, that are automatically binded in options object
   1.123 +const WIDGET_EVENTS = [
   1.124 +  "click",
   1.125 +  "mouseover",
   1.126 +  "mouseout",
   1.127 +  "error",
   1.128 +  "message",
   1.129 +  "attach"
   1.130 +];
   1.131 +
   1.132 +// `Model` utility functions that help creating these various Widgets objects
   1.133 +let model = {
   1.134 +
   1.135 +  // Validate one attribute using api-utils.js:validateOptions function
   1.136 +  _validate: function _validate(name, suspect, validation) {
   1.137 +    let $1 = {};
   1.138 +    $1[name] = suspect;
   1.139 +    let $2 = {};
   1.140 +    $2[name] = validation;
   1.141 +    return validateOptions($1, $2)[name];
   1.142 +  },
   1.143 +
   1.144 +  /**
   1.145 +   * This method has two purposes:
   1.146 +   * 1/ Validate and define, on a given object, a set of attribute
   1.147 +   * 2/ Emit a "change" event on this object when an attribute is changed
   1.148 +   *
   1.149 +   * @params {Object} object
   1.150 +   *    Object on which we can bind attributes on and watch for their changes.
   1.151 +   *    This object must have an EventEmitter interface, or, at least `_emit`
   1.152 +   *    method
   1.153 +   * @params {Object} attrs
   1.154 +   *    Dictionary of attributes definition following api-utils:validateOptions
   1.155 +   *    scheme
   1.156 +   * @params {Object} values
   1.157 +   *    Dictionary of attributes default values
   1.158 +   */
   1.159 +  setAttributes: function setAttributes(object, attrs, values) {
   1.160 +    let properties = {};
   1.161 +    for (let name in attrs) {
   1.162 +      let value = values[name];
   1.163 +      let req = attrs[name];
   1.164 +
   1.165 +      // Retrieve default value from typedef if the value is not defined
   1.166 +      if ((typeof value == "undefined" || value == null) && req.defaultValue)
   1.167 +        value = req.defaultValue;
   1.168 +
   1.169 +      // Check for valid value if value is defined or mandatory
   1.170 +      if (!req.optional || typeof value != "undefined")
   1.171 +        value = model._validate(name, value, req);
   1.172 +
   1.173 +      // In any case, define this property on `object`
   1.174 +      let property = null;
   1.175 +      if (req.readonly) {
   1.176 +        property = {
   1.177 +          value: value,
   1.178 +          writable: false,
   1.179 +          enumerable: true,
   1.180 +          configurable: false
   1.181 +        };
   1.182 +      }
   1.183 +      else {
   1.184 +        property = model._createWritableProperty(name, value);
   1.185 +      }
   1.186 +
   1.187 +      properties[name] = property;
   1.188 +    }
   1.189 +    Object.defineProperties(object, properties);
   1.190 +  },
   1.191 +
   1.192 +  // Generate ES5 property definition for a given attribute
   1.193 +  _createWritableProperty: function _createWritableProperty(name, value) {
   1.194 +    return {
   1.195 +      get: function () {
   1.196 +        return value;
   1.197 +      },
   1.198 +      set: function (newValue) {
   1.199 +        value = newValue;
   1.200 +        // The main goal of all this Model stuff is here:
   1.201 +        // We want to forward all changes to some listeners
   1.202 +        this._emit("change", name, value);
   1.203 +      },
   1.204 +      enumerable: true,
   1.205 +      configurable: false
   1.206 +    };
   1.207 +  },
   1.208 +
   1.209 +  /**
   1.210 +   * Automagically register listeners in options dictionary
   1.211 +   * by detecting listener attributes with name starting with `on`
   1.212 +   *
   1.213 +   * @params {Object} object
   1.214 +   *    Target object that need to follow EventEmitter interface, or, at least,
   1.215 +   *    having `on` method.
   1.216 +   * @params {Array} events
   1.217 +   *    List of events name to automatically bind.
   1.218 +   * @params {Object} listeners
   1.219 +   *    Dictionary of event listener functions to register.
   1.220 +   */
   1.221 +  setEvents: function setEvents(object, events, listeners) {
   1.222 +    for (let i = 0, l = events.length; i < l; i++) {
   1.223 +      let name = events[i];
   1.224 +      let onName = "on" + name[0].toUpperCase() + name.substr(1);
   1.225 +      if (!listeners[onName])
   1.226 +        continue;
   1.227 +      object.on(name, listeners[onName].bind(object));
   1.228 +    }
   1.229 +  }
   1.230 +
   1.231 +};
   1.232 +
   1.233 +function saveInserted(widgetId) {
   1.234 +  prefs.set(INSERTION_PREF_ROOT + widgetId, true);
   1.235 +}
   1.236 +
   1.237 +function haveInserted(widgetId) {
   1.238 +  return prefs.has(INSERTION_PREF_ROOT + widgetId);
   1.239 +}
   1.240 +
   1.241 +const isWide = node => node.classList.contains(AUSTRALIS_PANEL_WIDE_CLASSNAME);
   1.242 +
   1.243 +/**
   1.244 + * Main Widget class: entry point of the widget API
   1.245 + *
   1.246 + * Allow to control all widget across all existing windows with a single object.
   1.247 + * Widget.getView allow to retrieve a WidgetView instance to control a widget
   1.248 + * specific to one window.
   1.249 + */
   1.250 +const WidgetTrait = LightTrait.compose(EventEmitterTrait, LightTrait({
   1.251 +
   1.252 +  _initWidget: function _initWidget(options) {
   1.253 +    model.setAttributes(this, widgetAttributes, options);
   1.254 +
   1.255 +    browserManager.validate(this);
   1.256 +
   1.257 +    // We must have at least content or contentURL defined
   1.258 +    if (!(this.content || this.contentURL))
   1.259 +      throw new Error(ERR_CONTENT);
   1.260 +
   1.261 +    this._views = [];
   1.262 +
   1.263 +    // Set tooltip to label value if we don't have tooltip defined
   1.264 +    if (!this.tooltip)
   1.265 +      this.tooltip = this.label;
   1.266 +
   1.267 +    model.setEvents(this, WIDGET_EVENTS, options);
   1.268 +
   1.269 +    this.on('change', this._onChange.bind(this));
   1.270 +
   1.271 +    let self = this;
   1.272 +    this._port = EventEmitterTrait.create({
   1.273 +      emit: function () {
   1.274 +        let args = arguments;
   1.275 +        self._views.forEach(function(v) v.port.emit.apply(v.port, args));
   1.276 +      }
   1.277 +    });
   1.278 +    // expose wrapped port, that exposes only public properties.
   1.279 +    this._port._public = Cortex(this._port);
   1.280 +
   1.281 +    // Register this widget to browser manager in order to create new widget on
   1.282 +    // all new windows
   1.283 +    browserManager.addItem(this);
   1.284 +  },
   1.285 +
   1.286 +  _onChange: function _onChange(name, value) {
   1.287 +    // Set tooltip to label value if we don't have tooltip defined
   1.288 +    if (name == 'tooltip' && !value) {
   1.289 +      // we need to change tooltip again in order to change the value of the
   1.290 +      // attribute itself
   1.291 +      this.tooltip = this.label;
   1.292 +      return;
   1.293 +    }
   1.294 +
   1.295 +    // Forward attributes changes to WidgetViews
   1.296 +    if (['width', 'tooltip', 'content', 'contentURL'].indexOf(name) != -1) {
   1.297 +      this._views.forEach(function(v) v[name] = value);
   1.298 +    }
   1.299 +  },
   1.300 +
   1.301 +  _onEvent: function _onEvent(type, eventData) {
   1.302 +    this._emit(type, eventData);
   1.303 +  },
   1.304 +
   1.305 +  _createView: function _createView() {
   1.306 +    // Create a new WidgetView instance
   1.307 +    let view = WidgetView(this);
   1.308 +
   1.309 +    // Keep a reference to it
   1.310 +    this._views.push(view);
   1.311 +
   1.312 +    return view;
   1.313 +  },
   1.314 +
   1.315 +  // a WidgetView instance is destroyed
   1.316 +  _onViewDestroyed: function _onViewDestroyed(view) {
   1.317 +    let idx = this._views.indexOf(view);
   1.318 +    this._views.splice(idx, 1);
   1.319 +  },
   1.320 +
   1.321 +  /**
   1.322 +   * Called on browser window closed, to destroy related WidgetViews
   1.323 +   * @params {ChromeWindow} window
   1.324 +   *         Window that has been closed
   1.325 +   */
   1.326 +  _onWindowClosed: function _onWindowClosed(window) {
   1.327 +    for each (let view in this._views) {
   1.328 +      if (view._isInChromeWindow(window)) {
   1.329 +        view.destroy();
   1.330 +        break;
   1.331 +      }
   1.332 +    }
   1.333 +  },
   1.334 +
   1.335 +  /**
   1.336 +   * Get the WidgetView instance related to a BrowserWindow instance
   1.337 +   * @params {BrowserWindow} window
   1.338 +   *         BrowserWindow reference from "windows" module
   1.339 +   */
   1.340 +  getView: function getView(window) {
   1.341 +    for each (let view in this._views) {
   1.342 +      if (view._isInWindow(window)) {
   1.343 +        return view._public;
   1.344 +      }
   1.345 +    }
   1.346 +    return null;
   1.347 +  },
   1.348 +
   1.349 +  get port() this._port._public,
   1.350 +  set port(v) {}, // Work around Cortex failure with getter without setter
   1.351 +                  // See bug 653464
   1.352 +  _port: null,
   1.353 +
   1.354 +  postMessage: function postMessage(message) {
   1.355 +    this._views.forEach(function(v) v.postMessage(message));
   1.356 +  },
   1.357 +
   1.358 +  destroy: function destroy() {
   1.359 +    if (this.panel)
   1.360 +      this.panel.destroy();
   1.361 +
   1.362 +    // Dispatch destroy calls to views
   1.363 +    // we need to go backward as we remove items from this array in
   1.364 +    // _onViewDestroyed
   1.365 +    for (let i = this._views.length - 1; i >= 0; i--)
   1.366 +      this._views[i].destroy();
   1.367 +
   1.368 +    // Unregister widget to stop creating it over new windows
   1.369 +    // and allow creation of new widget with same id
   1.370 +    browserManager.removeItem(this);
   1.371 +  }
   1.372 +
   1.373 +}));
   1.374 +
   1.375 +// Widget constructor
   1.376 +const Widget = function Widget(options) {
   1.377 +  let w = WidgetTrait.create(Widget.prototype);
   1.378 +  w._initWidget(options);
   1.379 +
   1.380 +  // Return a Cortex of widget in order to hide private attributes like _onEvent
   1.381 +  let _public = Cortex(w);
   1.382 +  unload.ensure(_public, "destroy");
   1.383 +  return _public;
   1.384 +}
   1.385 +exports.Widget = Widget;
   1.386 +
   1.387 +
   1.388 +/**
   1.389 + * WidgetView is an instance of a widget for a specific window.
   1.390 + *
   1.391 + * This is an external API that can be retrieved by calling Widget.getView or
   1.392 + * by watching `attach` event on Widget.
   1.393 + */
   1.394 +const WidgetViewTrait = LightTrait.compose(EventEmitterTrait, LightTrait({
   1.395 +
   1.396 +  // Reference to the matching WidgetChrome
   1.397 +  // set right after constructor call
   1.398 +  _chrome: null,
   1.399 +
   1.400 +  // Public interface of the WidgetView, passed in `attach` event or in
   1.401 +  // Widget.getView
   1.402 +  _public: null,
   1.403 +
   1.404 +  _initWidgetView: function WidgetView__initWidgetView(baseWidget) {
   1.405 +    this._baseWidget = baseWidget;
   1.406 +
   1.407 +    model.setAttributes(this, widgetAttributes, baseWidget);
   1.408 +
   1.409 +    this.on('change', this._onChange.bind(this));
   1.410 +
   1.411 +    let self = this;
   1.412 +    this._port = EventEmitterTrait.create({
   1.413 +      emit: function () {
   1.414 +        if (!self._chrome)
   1.415 +          throw new Error(ERR_DESTROYED);
   1.416 +        self._chrome.update(self._baseWidget, "emit", arguments);
   1.417 +      }
   1.418 +    });
   1.419 +    // expose wrapped port, that exposes only public properties.
   1.420 +    this._port._public = Cortex(this._port);
   1.421 +
   1.422 +    this._public = Cortex(this);
   1.423 +  },
   1.424 +
   1.425 +  // Called by WidgetChrome, when the related Worker is applied to the document,
   1.426 +  // so that we can start sending events to it
   1.427 +  _onWorkerReady: function () {
   1.428 +    // Emit an `attach` event with a WidgetView instance without private attrs
   1.429 +    this._baseWidget._emit("attach", this._public);
   1.430 +  },
   1.431 +
   1.432 +  _onChange: function WidgetView__onChange(name, value) {
   1.433 +    if (name == 'tooltip' && !value) {
   1.434 +      this.tooltip = this.label;
   1.435 +      return;
   1.436 +    }
   1.437 +
   1.438 +    // Forward attributes changes to WidgetChrome instance
   1.439 +    if (['width', 'tooltip', 'content', 'contentURL'].indexOf(name) != -1) {
   1.440 +      this._chrome.update(this._baseWidget, name, value);
   1.441 +    }
   1.442 +  },
   1.443 +
   1.444 +  _onEvent: function WidgetView__onEvent(type, eventData, domNode) {
   1.445 +    // Dispatch event in view
   1.446 +    this._emit(type, eventData);
   1.447 +
   1.448 +    // And forward it to the main Widget object
   1.449 +    if ("click" == type || type.indexOf("mouse") == 0)
   1.450 +      this._baseWidget._onEvent(type, this._public);
   1.451 +    else
   1.452 +      this._baseWidget._onEvent(type, eventData);
   1.453 +
   1.454 +    // Special case for click events: if the widget doesn't have a click
   1.455 +    // handler, but it does have a panel, display the panel.
   1.456 +    if ("click" == type && !this._listeners("click").length && this.panel) {
   1.457 +      // This kind of ugly workaround, instead we should implement
   1.458 +      // `getNodeView` for the `Widget` class itself, but that's kind of
   1.459 +      // hard without cleaning things up.
   1.460 +      this.panel.show(null, getNodeView.implement({}, () => domNode));
   1.461 +    }
   1.462 +  },
   1.463 +
   1.464 +  _isInWindow: function WidgetView__isInWindow(window) {
   1.465 +    return windowsAPI.BrowserWindow({
   1.466 +      window: this._chrome.window
   1.467 +    }) == window;
   1.468 +  },
   1.469 +
   1.470 +  _isInChromeWindow: function WidgetView__isInChromeWindow(window) {
   1.471 +    return this._chrome.window == window;
   1.472 +  },
   1.473 +
   1.474 +  _onPortEvent: function WidgetView__onPortEvent(args) {
   1.475 +    let port = this._port;
   1.476 +    port._emit.apply(port, args);
   1.477 +    let basePort = this._baseWidget._port;
   1.478 +    basePort._emit.apply(basePort, args);
   1.479 +  },
   1.480 +
   1.481 +  get port() this._port._public,
   1.482 +  set port(v) {}, // Work around Cortex failure with getter without setter
   1.483 +                  // See bug 653464
   1.484 +  _port: null,
   1.485 +
   1.486 +  postMessage: function WidgetView_postMessage(message) {
   1.487 +    if (!this._chrome)
   1.488 +      throw new Error(ERR_DESTROYED);
   1.489 +    this._chrome.update(this._baseWidget, "postMessage", message);
   1.490 +  },
   1.491 +
   1.492 +  destroy: function WidgetView_destroy() {
   1.493 +    this._chrome.destroy();
   1.494 +    delete this._chrome;
   1.495 +    this._baseWidget._onViewDestroyed(this);
   1.496 +    this._emit("detach");
   1.497 +  }
   1.498 +
   1.499 +}));
   1.500 +
   1.501 +
   1.502 +const WidgetView = function WidgetView(baseWidget) {
   1.503 +  let w = WidgetViewTrait.create(WidgetView.prototype);
   1.504 +  w._initWidgetView(baseWidget);
   1.505 +  return w;
   1.506 +}
   1.507 +
   1.508 +
   1.509 +/**
   1.510 + * Keeps track of all browser windows.
   1.511 + * Exposes methods for adding/removing widgets
   1.512 + * across all open windows (and future ones).
   1.513 + * Create a new instance of BrowserWindow per window.
   1.514 + */
   1.515 +let browserManager = {
   1.516 +  items: [],
   1.517 +  windows: [],
   1.518 +
   1.519 +  // Registers the manager to listen for window openings and closings.  Note
   1.520 +  // that calling this method can cause onTrack to be called immediately if
   1.521 +  // there are open windows.
   1.522 +  init: function () {
   1.523 +    let windowTracker = new WindowTracker(this);
   1.524 +    unload.ensure(windowTracker);
   1.525 +  },
   1.526 +
   1.527 +  // Registers a window with the manager.  This is a WindowTracker callback.
   1.528 +  onTrack: function browserManager_onTrack(window) {
   1.529 +    if (isBrowser(window)) {
   1.530 +      let win = new BrowserWindow(window);
   1.531 +      win.addItems(this.items);
   1.532 +      this.windows.push(win);
   1.533 +    }
   1.534 +  },
   1.535 +
   1.536 +  // Unregisters a window from the manager.  It's told to undo all
   1.537 +  // modifications.  This is a WindowTracker callback.  Note that when
   1.538 +  // WindowTracker is unloaded, it calls onUntrack for every currently opened
   1.539 +  // window.  The browserManager therefore doesn't need to specially handle
   1.540 +  // unload itself, since unloading the browserManager means untracking all
   1.541 +  // currently opened windows.
   1.542 +  onUntrack: function browserManager_onUntrack(window) {
   1.543 +    if (isBrowser(window)) {
   1.544 +      this.items.forEach(function(i) i._onWindowClosed(window));
   1.545 +      for (let i = 0; i < this.windows.length; i++) {
   1.546 +        if (this.windows[i].window == window) {
   1.547 +          this.windows.splice(i, 1)[0];
   1.548 +          return;
   1.549 +        }
   1.550 +      }
   1.551 +
   1.552 +    }
   1.553 +  },
   1.554 +
   1.555 +  // Used to validate widget by browserManager before adding it,
   1.556 +  // in order to check input very early in widget constructor
   1.557 +  validate : function (item) {
   1.558 +    let idx = this.items.indexOf(item);
   1.559 +    if (idx > -1)
   1.560 +      throw new Error("The widget " + item + " has already been added.");
   1.561 +    if (item.id) {
   1.562 +      let sameId = this.items.filter(function(i) i.id == item.id);
   1.563 +      if (sameId.length > 0)
   1.564 +        throw new Error("This widget ID is already used: " + item.id);
   1.565 +    } else {
   1.566 +      item.id = this.items.length;
   1.567 +    }
   1.568 +  },
   1.569 +
   1.570 +  // Registers an item with the manager. It's added to all currently registered
   1.571 +  // windows, and when new windows are registered it will be added to them, too.
   1.572 +  addItem: function browserManager_addItem(item) {
   1.573 +    this.items.push(item);
   1.574 +    this.windows.forEach(function (w) w.addItems([item]));
   1.575 +  },
   1.576 +
   1.577 +  // Unregisters an item from the manager.  It's removed from all windows that
   1.578 +  // are currently registered.
   1.579 +  removeItem: function browserManager_removeItem(item) {
   1.580 +    let idx = this.items.indexOf(item);
   1.581 +    if (idx > -1)
   1.582 +      this.items.splice(idx, 1);
   1.583 +  },
   1.584 +  propagateCurrentset: function browserManager_propagateCurrentset(id, currentset) {
   1.585 +    this.windows.forEach(function (w) w.doc.getElementById(id).setAttribute("currentset", currentset));
   1.586 +  }
   1.587 +};
   1.588 +
   1.589 +
   1.590 +
   1.591 +/**
   1.592 + * Keeps track of a single browser window.
   1.593 + *
   1.594 + * This is where the core of how a widget's content is added to a window lives.
   1.595 + */
   1.596 +function BrowserWindow(window) {
   1.597 +  this.window = window;
   1.598 +  this.doc = window.document;
   1.599 +}
   1.600 +
   1.601 +BrowserWindow.prototype = {
   1.602 +  // Adds an array of items to the window.
   1.603 +  addItems: function BW_addItems(items) {
   1.604 +    items.forEach(this._addItemToWindow, this);
   1.605 +  },
   1.606 +
   1.607 +  _addItemToWindow: function BW__addItemToWindow(baseWidget) {
   1.608 +    // Create a WidgetView instance
   1.609 +    let widget = baseWidget._createView();
   1.610 +
   1.611 +    // Create a WidgetChrome instance
   1.612 +    let item = new WidgetChrome({
   1.613 +      widget: widget,
   1.614 +      doc: this.doc,
   1.615 +      window: this.window
   1.616 +    });
   1.617 +
   1.618 +    widget._chrome = item;
   1.619 +
   1.620 +    this._insertNodeInToolbar(item.node);
   1.621 +
   1.622 +    // We need to insert Widget DOM Node before finishing widget view creation
   1.623 +    // (because fill creates an iframe and tries to access its docShell)
   1.624 +    item.fill();
   1.625 +  },
   1.626 +
   1.627 +  _insertNodeInToolbar: function BW__insertNodeInToolbar(node) {
   1.628 +    // Add to the customization palette
   1.629 +    let toolbox = this.doc.getElementById("navigator-toolbox");
   1.630 +    let palette = toolbox.palette;
   1.631 +    palette.appendChild(node);
   1.632 +
   1.633 +    let { CustomizableUI } = this.window;
   1.634 +    let { id } = node;
   1.635 +
   1.636 +    let placement = CustomizableUI.getPlacementOfWidget(id);
   1.637 +
   1.638 +    if (!placement) {
   1.639 +      if (haveInserted(id) || isWide(node))
   1.640 +        return;
   1.641 +
   1.642 +      placement = {area: 'nav-bar', position: undefined};
   1.643 +      saveInserted(id);
   1.644 +    }
   1.645 +
   1.646 +    CustomizableUI.addWidgetToArea(id, placement.area, placement.position);
   1.647 +    CustomizableUI.ensureWidgetPlacedInWindow(id, this.window);
   1.648 +  }
   1.649 +}
   1.650 +
   1.651 +
   1.652 +/**
   1.653 + * Final Widget class that handles chrome DOM Node:
   1.654 + *  - create initial DOM nodes
   1.655 + *  - receive instruction from WidgetView through update method and update DOM
   1.656 + *  - watch for DOM events and forward them to WidgetView
   1.657 + */
   1.658 +function WidgetChrome(options) {
   1.659 +  this.window = options.window;
   1.660 +  this._doc = options.doc;
   1.661 +  this._widget = options.widget;
   1.662 +  this._symbiont = null; // set later
   1.663 +  this.node = null; // set later
   1.664 +
   1.665 +  this._createNode();
   1.666 +}
   1.667 +
   1.668 +// Update a property of a widget.
   1.669 +WidgetChrome.prototype.update = function WC_update(updatedItem, property, value) {
   1.670 +  switch(property) {
   1.671 +    case "contentURL":
   1.672 +    case "content":
   1.673 +      this.setContent();
   1.674 +      break;
   1.675 +    case "width":
   1.676 +      this.node.style.minWidth = value + "px";
   1.677 +      this.node.querySelector("iframe").style.width = value + "px";
   1.678 +      break;
   1.679 +    case "tooltip":
   1.680 +      this.node.setAttribute("tooltiptext", value);
   1.681 +      break;
   1.682 +    case "postMessage":
   1.683 +      this._symbiont.postMessage(value);
   1.684 +      break;
   1.685 +    case "emit":
   1.686 +      let port = this._symbiont.port;
   1.687 +      port.emit.apply(port, value);
   1.688 +      break;
   1.689 +  }
   1.690 +}
   1.691 +
   1.692 +// Add a widget to this window.
   1.693 +WidgetChrome.prototype._createNode = function WC__createNode() {
   1.694 +  // XUL element container for widget
   1.695 +  let node = this._doc.createElement("toolbaritem");
   1.696 +
   1.697 +  // Temporary work around require("self") failing on unit-test execution ...
   1.698 +  let jetpackID = "testID";
   1.699 +  try {
   1.700 +    jetpackID = require("./self").id;
   1.701 +  } catch(e) {}
   1.702 +
   1.703 +  // Compute an unique and stable widget id with jetpack id and widget.id
   1.704 +  let id = "widget:" + jetpackID + "-" + this._widget.id;
   1.705 +  node.setAttribute("id", id);
   1.706 +  node.setAttribute("label", this._widget.label);
   1.707 +  node.setAttribute("tooltiptext", this._widget.tooltip);
   1.708 +  node.setAttribute("align", "center");
   1.709 +  // Bug 626326: Prevent customize toolbar context menu to appear
   1.710 +  node.setAttribute("context", "");
   1.711 +
   1.712 +  // For use in styling by the browser
   1.713 +  node.setAttribute("sdkstylewidget", "true");
   1.714 +
   1.715 +  if (this._widget.width > AUSTRALIS_PANEL_WIDE_WIDGET_CUTOFF) {
   1.716 +    node.classList.add(AUSTRALIS_PANEL_WIDE_CLASSNAME);
   1.717 +  }
   1.718 +
   1.719 +  // TODO move into a stylesheet, configurable by consumers.
   1.720 +  // Either widget.style, exposing the style object, or a URL
   1.721 +  // (eg, can load local stylesheet file).
   1.722 +  node.setAttribute("style", [
   1.723 +      "overflow: hidden; margin: 1px 2px 1px 2px; padding: 0px;",
   1.724 +      "min-height: 16px;",
   1.725 +  ].join(""));
   1.726 +
   1.727 +  node.style.minWidth = this._widget.width + "px";
   1.728 +
   1.729 +  this.node = node;
   1.730 +}
   1.731 +
   1.732 +// Initial population of a widget's content.
   1.733 +WidgetChrome.prototype.fill = function WC_fill() {
   1.734 +  let { node, _doc: document } = this;
   1.735 +
   1.736 +  // Create element
   1.737 +  let iframe = document.createElement("iframe");
   1.738 +  iframe.setAttribute("type", "content");
   1.739 +  iframe.setAttribute("transparent", "transparent");
   1.740 +  iframe.style.overflow = "hidden";
   1.741 +  iframe.style.height = "16px";
   1.742 +  iframe.style.maxHeight = "16px";
   1.743 +  iframe.style.width = this._widget.width + "px";
   1.744 +  iframe.setAttribute("flex", "1");
   1.745 +  iframe.style.border = "none";
   1.746 +  iframe.style.padding = "0px";
   1.747 +
   1.748 +  // Do this early, because things like contentWindow are null
   1.749 +  // until the node is attached to a document.
   1.750 +  node.appendChild(iframe);
   1.751 +
   1.752 +  let label = document.createElement("label");
   1.753 +  label.setAttribute("value", this._widget.label);
   1.754 +  label.className = "toolbarbutton-text";
   1.755 +  label.setAttribute("crop", "right");
   1.756 +  label.setAttribute("flex", "1");
   1.757 +  node.appendChild(label);
   1.758 +
   1.759 +  // This toolbarbutton is created to provide a more consistent user experience
   1.760 +  // during customization, see:
   1.761 +  // https://bugzilla.mozilla.org/show_bug.cgi?id=959640
   1.762 +  let button = document.createElement("toolbarbutton");
   1.763 +  button.setAttribute("label", this._widget.label);
   1.764 +  button.setAttribute("crop", "right");
   1.765 +  button.className = "toolbarbutton-1 chromeclass-toolbar-additional";
   1.766 +  node.appendChild(button);
   1.767 +
   1.768 +  // add event handlers
   1.769 +  this.addEventHandlers();
   1.770 +
   1.771 +  // set content
   1.772 +  this.setContent();
   1.773 +}
   1.774 +
   1.775 +// Get widget content type.
   1.776 +WidgetChrome.prototype.getContentType = function WC_getContentType() {
   1.777 +  if (this._widget.content)
   1.778 +    return CONTENT_TYPE_HTML;
   1.779 +  return (this._widget.contentURL && /\.(jpg|gif|png|ico|svg)$/i.test(this._widget.contentURL))
   1.780 +    ? CONTENT_TYPE_IMAGE : CONTENT_TYPE_URI;
   1.781 +}
   1.782 +
   1.783 +// Set widget content.
   1.784 +WidgetChrome.prototype.setContent = function WC_setContent() {
   1.785 +  let type = this.getContentType();
   1.786 +  let contentURL = null;
   1.787 +
   1.788 +  switch (type) {
   1.789 +    case CONTENT_TYPE_HTML:
   1.790 +      contentURL = "data:text/html;charset=utf-8," + encodeURIComponent(this._widget.content);
   1.791 +      break;
   1.792 +    case CONTENT_TYPE_URI:
   1.793 +      contentURL = this._widget.contentURL;
   1.794 +      break;
   1.795 +    case CONTENT_TYPE_IMAGE:
   1.796 +      let imageURL = this._widget.contentURL;
   1.797 +      contentURL = "data:text/html;charset=utf-8,<html><body><img src='" +
   1.798 +                   encodeURI(imageURL) + "'></body></html>";
   1.799 +      break;
   1.800 +    default:
   1.801 +      throw new Error("The widget's type cannot be determined.");
   1.802 +  }
   1.803 +
   1.804 +  let iframe = this.node.firstElementChild;
   1.805 +
   1.806 +  let self = this;
   1.807 +  // Cleanup previously created symbiont (in case we are update content)
   1.808 +  if (this._symbiont)
   1.809 +    this._symbiont.destroy();
   1.810 +
   1.811 +  this._symbiont = Trait.compose(Symbiont.resolve({
   1.812 +    _onContentScriptEvent: "_onContentScriptEvent-not-used",
   1.813 +    _onInit: "_initSymbiont"
   1.814 +  }), {
   1.815 +    // Overload `Symbiont._onInit` in order to know when the related worker
   1.816 +    // is ready.
   1.817 +    _onInit: function () {
   1.818 +      this._initSymbiont();
   1.819 +      self._widget._onWorkerReady();
   1.820 +    },
   1.821 +    _onContentScriptEvent: function () {
   1.822 +      // Redirect events to WidgetView
   1.823 +      self._widget._onPortEvent(arguments);
   1.824 +    }
   1.825 +  })({
   1.826 +    frame: iframe,
   1.827 +    contentURL: contentURL,
   1.828 +    contentScriptFile: this._widget.contentScriptFile,
   1.829 +    contentScript: this._widget.contentScript,
   1.830 +    contentScriptWhen: this._widget.contentScriptWhen,
   1.831 +    contentScriptOptions: this._widget.contentScriptOptions,
   1.832 +    allow: this._widget.allow,
   1.833 +    onMessage: function(message) {
   1.834 +      setTimeout(function() {
   1.835 +        self._widget._onEvent("message", message);
   1.836 +      }, 0);
   1.837 +    }
   1.838 +  });
   1.839 +}
   1.840 +
   1.841 +// Detect if document consists of a single image.
   1.842 +WidgetChrome._isImageDoc = function WC__isImageDoc(doc) {
   1.843 +  return /*doc.body &&*/ doc.body.childNodes.length == 1 &&
   1.844 +         doc.body.firstElementChild &&
   1.845 +         doc.body.firstElementChild.tagName == "IMG";
   1.846 +}
   1.847 +
   1.848 +// Set up all supported events for a widget.
   1.849 +WidgetChrome.prototype.addEventHandlers = function WC_addEventHandlers() {
   1.850 +  let contentType = this.getContentType();
   1.851 +
   1.852 +  let self = this;
   1.853 +  let listener = function(e) {
   1.854 +    // Ignore event firings that target the iframe.
   1.855 +    if (e.target == self.node.firstElementChild)
   1.856 +      return;
   1.857 +
   1.858 +    // The widget only supports left-click for now,
   1.859 +    // so ignore all clicks (i.e. middle or right) except left ones.
   1.860 +    if (e.type == "click" && e.button !== 0)
   1.861 +      return;
   1.862 +
   1.863 +    // Proxy event to the widget
   1.864 +    setTimeout(function() {
   1.865 +      self._widget._onEvent(EVENTS[e.type], null, self.node);
   1.866 +    }, 0);
   1.867 +  };
   1.868 +
   1.869 +  this.eventListeners = {};
   1.870 +  let iframe = this.node.firstElementChild;
   1.871 +  for (let type in EVENTS) {
   1.872 +    iframe.addEventListener(type, listener, true, true);
   1.873 +
   1.874 +    // Store listeners for later removal
   1.875 +    this.eventListeners[type] = listener;
   1.876 +  }
   1.877 +
   1.878 +  // On document load, make modifications required for nice default
   1.879 +  // presentation.
   1.880 +  function loadListener(e) {
   1.881 +    let containerStyle = self.window.getComputedStyle(self.node.parentNode);
   1.882 +    // Ignore event firings that target the iframe
   1.883 +    if (e.target == iframe)
   1.884 +      return;
   1.885 +    // Ignore about:blank loads
   1.886 +    if (e.type == "load" && e.target.location == "about:blank")
   1.887 +      return;
   1.888 +
   1.889 +    // We may have had an unload event before that cleaned up the symbiont
   1.890 +    if (!self._symbiont)
   1.891 +      self.setContent();
   1.892 +
   1.893 +    let doc = e.target;
   1.894 +
   1.895 +    if (contentType == CONTENT_TYPE_IMAGE || WidgetChrome._isImageDoc(doc)) {
   1.896 +      // Force image content to size.
   1.897 +      // Add-on authors must size their images correctly.
   1.898 +      doc.body.firstElementChild.style.width = self._widget.width + "px";
   1.899 +      doc.body.firstElementChild.style.height = "16px";
   1.900 +    }
   1.901 +
   1.902 +    // Extend the add-on bar's default text styles to the widget.
   1.903 +    doc.body.style.color = containerStyle.color;
   1.904 +    doc.body.style.fontFamily = containerStyle.fontFamily;
   1.905 +    doc.body.style.fontSize = containerStyle.fontSize;
   1.906 +    doc.body.style.fontWeight = containerStyle.fontWeight;
   1.907 +    doc.body.style.textShadow = containerStyle.textShadow;
   1.908 +    // Allow all content to fill the box by default.
   1.909 +    doc.body.style.margin = "0";
   1.910 +  }
   1.911 +
   1.912 +  iframe.addEventListener("load", loadListener, true);
   1.913 +  this.eventListeners["load"] = loadListener;
   1.914 +
   1.915 +  // Register a listener to unload symbiont if the toolbaritem is moved
   1.916 +  // on user toolbars customization
   1.917 +  function unloadListener(e) {
   1.918 +    if (e.target.location == "about:blank")
   1.919 +      return;
   1.920 +    self._symbiont.destroy();
   1.921 +    self._symbiont = null;
   1.922 +    // This may fail but not always, it depends on how the node is
   1.923 +    // moved or removed
   1.924 +    try {
   1.925 +      self.setContent();
   1.926 +    } catch(e) {}
   1.927 +
   1.928 +  }
   1.929 +
   1.930 +  iframe.addEventListener("unload", unloadListener, true);
   1.931 +  this.eventListeners["unload"] = unloadListener;
   1.932 +}
   1.933 +
   1.934 +// Remove and unregister the widget from everything
   1.935 +WidgetChrome.prototype.destroy = function WC_destroy(removedItems) {
   1.936 +  // remove event listeners
   1.937 +  for (let type in this.eventListeners) {
   1.938 +    let listener = this.eventListeners[type];
   1.939 +    this.node.firstElementChild.removeEventListener(type, listener, true);
   1.940 +  }
   1.941 +  // remove dom node
   1.942 +  this.node.parentNode.removeChild(this.node);
   1.943 +  // cleanup symbiont
   1.944 +  this._symbiont.destroy();
   1.945 +  // cleanup itself
   1.946 +  this.eventListeners = null;
   1.947 +  this._widget = null;
   1.948 +  this._symbiont = null;
   1.949 +}
   1.950 +
   1.951 +// Init the browserManager only after setting prototypes and such above, because
   1.952 +// it will cause browserManager.onTrack to be called immediately if there are
   1.953 +// open windows.
   1.954 +browserManager.init();

mercurial