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

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     4 "use strict";
     6 // The widget module currently supports only Firefox.
     7 // See: https://bugzilla.mozilla.org/show_bug.cgi?id=560716
     8 module.metadata = {
     9   "stability": "deprecated",
    10   "engines": {
    11     "Firefox": "*"
    12   }
    13 };
    15 // Widget content types
    16 const CONTENT_TYPE_URI    = 1;
    17 const CONTENT_TYPE_HTML   = 2;
    18 const CONTENT_TYPE_IMAGE  = 3;
    20 const ERR_CONTENT = "No content or contentURL property found. Widgets must "
    21                          + "have one or the other.",
    22       ERR_LABEL = "The widget must have a non-empty label property.",
    23       ERR_ID = "You have to specify a unique value for the id property of " +
    24                "your widget in order for the application to remember its " +
    25                "position.",
    26       ERR_DESTROYED = "The widget has been destroyed and can no longer be used.";
    28 const INSERTION_PREF_ROOT = "extensions.sdk-widget-inserted.";
    30 // Supported events, mapping from DOM event names to our event names
    31 const EVENTS = {
    32   "click": "click",
    33   "mouseover": "mouseover",
    34   "mouseout": "mouseout",
    35 };
    37 // In the Australis menu panel, normally widgets should be treated like
    38 // normal toolbarbuttons. If they're any wider than this margin, we'll
    39 // treat them as wide widgets instead, which fill up the width of the panel:
    40 const AUSTRALIS_PANEL_WIDE_WIDGET_CUTOFF = 70;
    41 const AUSTRALIS_PANEL_WIDE_CLASSNAME = "panel-wide-item";
    43 const { validateOptions } = require("./deprecated/api-utils");
    44 const panels = require("./panel");
    45 const { EventEmitter, EventEmitterTrait } = require("./deprecated/events");
    46 const { Trait } = require("./deprecated/traits");
    47 const LightTrait = require('./deprecated/light-traits').Trait;
    48 const { Loader, Symbiont } = require("./content/content");
    49 const { Cortex } = require('./deprecated/cortex');
    50 const windowsAPI = require("./windows");
    51 const { WindowTracker } = require("./deprecated/window-utils");
    52 const { isBrowser } = require("./window/utils");
    53 const { setTimeout } = require("./timers");
    54 const unload = require("./system/unload");
    55 const { getNodeView } = require("./view/core");
    56 const prefs = require('./preferences/service');
    58 require("./util/deprecate").deprecateUsage(
    59   "The widget module is deprecated.  " +
    60   "Please consider using the sdk/ui module instead."
    61 );
    63 // Data types definition
    64 const valid = {
    65   number: { is: ["null", "undefined", "number"] },
    66   string: { is: ["null", "undefined", "string"] },
    67   id: {
    68     is: ["string"],
    69     ok: function (v) v.length > 0,
    70     msg: ERR_ID,
    71     readonly: true
    72   },
    73   label: {
    74     is: ["string"],
    75     ok: function (v) v.length > 0,
    76     msg: ERR_LABEL
    77   },
    78   panel: {
    79     is: ["null", "undefined", "object"],
    80     ok: function(v) !v || v instanceof panels.Panel
    81   },
    82   width: {
    83     is: ["null", "undefined", "number"],
    84     map: function (v) {
    85       if (null === v || undefined === v) v = 16;
    86       return v;
    87     },
    88     defaultValue: 16
    89   },
    90   allow: {
    91     is: ["null", "undefined", "object"],
    92     map: function (v) {
    93       if (!v) v = { script: true };
    94       return v;
    95     },
    96     get defaultValue() ({ script: true })
    97   },
    98 };
   100 // Widgets attributes definition
   101 let widgetAttributes = {
   102   label: valid.label,
   103   id: valid.id,
   104   tooltip: valid.string,
   105   width: valid.width,
   106   content: valid.string,
   107   panel: valid.panel,
   108   allow: valid.allow
   109 };
   111 // Import data definitions from loader, but don't compose with it as Model
   112 // functions allow us to recreate easily all Loader code.
   113 let loaderAttributes = require("./content/loader").validationAttributes;
   114 for (let i in loaderAttributes)
   115   widgetAttributes[i] = loaderAttributes[i];
   117 widgetAttributes.contentURL.optional = true;
   119 // Widgets public events list, that are automatically binded in options object
   120 const WIDGET_EVENTS = [
   121   "click",
   122   "mouseover",
   123   "mouseout",
   124   "error",
   125   "message",
   126   "attach"
   127 ];
   129 // `Model` utility functions that help creating these various Widgets objects
   130 let model = {
   132   // Validate one attribute using api-utils.js:validateOptions function
   133   _validate: function _validate(name, suspect, validation) {
   134     let $1 = {};
   135     $1[name] = suspect;
   136     let $2 = {};
   137     $2[name] = validation;
   138     return validateOptions($1, $2)[name];
   139   },
   141   /**
   142    * This method has two purposes:
   143    * 1/ Validate and define, on a given object, a set of attribute
   144    * 2/ Emit a "change" event on this object when an attribute is changed
   145    *
   146    * @params {Object} object
   147    *    Object on which we can bind attributes on and watch for their changes.
   148    *    This object must have an EventEmitter interface, or, at least `_emit`
   149    *    method
   150    * @params {Object} attrs
   151    *    Dictionary of attributes definition following api-utils:validateOptions
   152    *    scheme
   153    * @params {Object} values
   154    *    Dictionary of attributes default values
   155    */
   156   setAttributes: function setAttributes(object, attrs, values) {
   157     let properties = {};
   158     for (let name in attrs) {
   159       let value = values[name];
   160       let req = attrs[name];
   162       // Retrieve default value from typedef if the value is not defined
   163       if ((typeof value == "undefined" || value == null) && req.defaultValue)
   164         value = req.defaultValue;
   166       // Check for valid value if value is defined or mandatory
   167       if (!req.optional || typeof value != "undefined")
   168         value = model._validate(name, value, req);
   170       // In any case, define this property on `object`
   171       let property = null;
   172       if (req.readonly) {
   173         property = {
   174           value: value,
   175           writable: false,
   176           enumerable: true,
   177           configurable: false
   178         };
   179       }
   180       else {
   181         property = model._createWritableProperty(name, value);
   182       }
   184       properties[name] = property;
   185     }
   186     Object.defineProperties(object, properties);
   187   },
   189   // Generate ES5 property definition for a given attribute
   190   _createWritableProperty: function _createWritableProperty(name, value) {
   191     return {
   192       get: function () {
   193         return value;
   194       },
   195       set: function (newValue) {
   196         value = newValue;
   197         // The main goal of all this Model stuff is here:
   198         // We want to forward all changes to some listeners
   199         this._emit("change", name, value);
   200       },
   201       enumerable: true,
   202       configurable: false
   203     };
   204   },
   206   /**
   207    * Automagically register listeners in options dictionary
   208    * by detecting listener attributes with name starting with `on`
   209    *
   210    * @params {Object} object
   211    *    Target object that need to follow EventEmitter interface, or, at least,
   212    *    having `on` method.
   213    * @params {Array} events
   214    *    List of events name to automatically bind.
   215    * @params {Object} listeners
   216    *    Dictionary of event listener functions to register.
   217    */
   218   setEvents: function setEvents(object, events, listeners) {
   219     for (let i = 0, l = events.length; i < l; i++) {
   220       let name = events[i];
   221       let onName = "on" + name[0].toUpperCase() + name.substr(1);
   222       if (!listeners[onName])
   223         continue;
   224       object.on(name, listeners[onName].bind(object));
   225     }
   226   }
   228 };
   230 function saveInserted(widgetId) {
   231   prefs.set(INSERTION_PREF_ROOT + widgetId, true);
   232 }
   234 function haveInserted(widgetId) {
   235   return prefs.has(INSERTION_PREF_ROOT + widgetId);
   236 }
   238 const isWide = node => node.classList.contains(AUSTRALIS_PANEL_WIDE_CLASSNAME);
   240 /**
   241  * Main Widget class: entry point of the widget API
   242  *
   243  * Allow to control all widget across all existing windows with a single object.
   244  * Widget.getView allow to retrieve a WidgetView instance to control a widget
   245  * specific to one window.
   246  */
   247 const WidgetTrait = LightTrait.compose(EventEmitterTrait, LightTrait({
   249   _initWidget: function _initWidget(options) {
   250     model.setAttributes(this, widgetAttributes, options);
   252     browserManager.validate(this);
   254     // We must have at least content or contentURL defined
   255     if (!(this.content || this.contentURL))
   256       throw new Error(ERR_CONTENT);
   258     this._views = [];
   260     // Set tooltip to label value if we don't have tooltip defined
   261     if (!this.tooltip)
   262       this.tooltip = this.label;
   264     model.setEvents(this, WIDGET_EVENTS, options);
   266     this.on('change', this._onChange.bind(this));
   268     let self = this;
   269     this._port = EventEmitterTrait.create({
   270       emit: function () {
   271         let args = arguments;
   272         self._views.forEach(function(v) v.port.emit.apply(v.port, args));
   273       }
   274     });
   275     // expose wrapped port, that exposes only public properties.
   276     this._port._public = Cortex(this._port);
   278     // Register this widget to browser manager in order to create new widget on
   279     // all new windows
   280     browserManager.addItem(this);
   281   },
   283   _onChange: function _onChange(name, value) {
   284     // Set tooltip to label value if we don't have tooltip defined
   285     if (name == 'tooltip' && !value) {
   286       // we need to change tooltip again in order to change the value of the
   287       // attribute itself
   288       this.tooltip = this.label;
   289       return;
   290     }
   292     // Forward attributes changes to WidgetViews
   293     if (['width', 'tooltip', 'content', 'contentURL'].indexOf(name) != -1) {
   294       this._views.forEach(function(v) v[name] = value);
   295     }
   296   },
   298   _onEvent: function _onEvent(type, eventData) {
   299     this._emit(type, eventData);
   300   },
   302   _createView: function _createView() {
   303     // Create a new WidgetView instance
   304     let view = WidgetView(this);
   306     // Keep a reference to it
   307     this._views.push(view);
   309     return view;
   310   },
   312   // a WidgetView instance is destroyed
   313   _onViewDestroyed: function _onViewDestroyed(view) {
   314     let idx = this._views.indexOf(view);
   315     this._views.splice(idx, 1);
   316   },
   318   /**
   319    * Called on browser window closed, to destroy related WidgetViews
   320    * @params {ChromeWindow} window
   321    *         Window that has been closed
   322    */
   323   _onWindowClosed: function _onWindowClosed(window) {
   324     for each (let view in this._views) {
   325       if (view._isInChromeWindow(window)) {
   326         view.destroy();
   327         break;
   328       }
   329     }
   330   },
   332   /**
   333    * Get the WidgetView instance related to a BrowserWindow instance
   334    * @params {BrowserWindow} window
   335    *         BrowserWindow reference from "windows" module
   336    */
   337   getView: function getView(window) {
   338     for each (let view in this._views) {
   339       if (view._isInWindow(window)) {
   340         return view._public;
   341       }
   342     }
   343     return null;
   344   },
   346   get port() this._port._public,
   347   set port(v) {}, // Work around Cortex failure with getter without setter
   348                   // See bug 653464
   349   _port: null,
   351   postMessage: function postMessage(message) {
   352     this._views.forEach(function(v) v.postMessage(message));
   353   },
   355   destroy: function destroy() {
   356     if (this.panel)
   357       this.panel.destroy();
   359     // Dispatch destroy calls to views
   360     // we need to go backward as we remove items from this array in
   361     // _onViewDestroyed
   362     for (let i = this._views.length - 1; i >= 0; i--)
   363       this._views[i].destroy();
   365     // Unregister widget to stop creating it over new windows
   366     // and allow creation of new widget with same id
   367     browserManager.removeItem(this);
   368   }
   370 }));
   372 // Widget constructor
   373 const Widget = function Widget(options) {
   374   let w = WidgetTrait.create(Widget.prototype);
   375   w._initWidget(options);
   377   // Return a Cortex of widget in order to hide private attributes like _onEvent
   378   let _public = Cortex(w);
   379   unload.ensure(_public, "destroy");
   380   return _public;
   381 }
   382 exports.Widget = Widget;
   385 /**
   386  * WidgetView is an instance of a widget for a specific window.
   387  *
   388  * This is an external API that can be retrieved by calling Widget.getView or
   389  * by watching `attach` event on Widget.
   390  */
   391 const WidgetViewTrait = LightTrait.compose(EventEmitterTrait, LightTrait({
   393   // Reference to the matching WidgetChrome
   394   // set right after constructor call
   395   _chrome: null,
   397   // Public interface of the WidgetView, passed in `attach` event or in
   398   // Widget.getView
   399   _public: null,
   401   _initWidgetView: function WidgetView__initWidgetView(baseWidget) {
   402     this._baseWidget = baseWidget;
   404     model.setAttributes(this, widgetAttributes, baseWidget);
   406     this.on('change', this._onChange.bind(this));
   408     let self = this;
   409     this._port = EventEmitterTrait.create({
   410       emit: function () {
   411         if (!self._chrome)
   412           throw new Error(ERR_DESTROYED);
   413         self._chrome.update(self._baseWidget, "emit", arguments);
   414       }
   415     });
   416     // expose wrapped port, that exposes only public properties.
   417     this._port._public = Cortex(this._port);
   419     this._public = Cortex(this);
   420   },
   422   // Called by WidgetChrome, when the related Worker is applied to the document,
   423   // so that we can start sending events to it
   424   _onWorkerReady: function () {
   425     // Emit an `attach` event with a WidgetView instance without private attrs
   426     this._baseWidget._emit("attach", this._public);
   427   },
   429   _onChange: function WidgetView__onChange(name, value) {
   430     if (name == 'tooltip' && !value) {
   431       this.tooltip = this.label;
   432       return;
   433     }
   435     // Forward attributes changes to WidgetChrome instance
   436     if (['width', 'tooltip', 'content', 'contentURL'].indexOf(name) != -1) {
   437       this._chrome.update(this._baseWidget, name, value);
   438     }
   439   },
   441   _onEvent: function WidgetView__onEvent(type, eventData, domNode) {
   442     // Dispatch event in view
   443     this._emit(type, eventData);
   445     // And forward it to the main Widget object
   446     if ("click" == type || type.indexOf("mouse") == 0)
   447       this._baseWidget._onEvent(type, this._public);
   448     else
   449       this._baseWidget._onEvent(type, eventData);
   451     // Special case for click events: if the widget doesn't have a click
   452     // handler, but it does have a panel, display the panel.
   453     if ("click" == type && !this._listeners("click").length && this.panel) {
   454       // This kind of ugly workaround, instead we should implement
   455       // `getNodeView` for the `Widget` class itself, but that's kind of
   456       // hard without cleaning things up.
   457       this.panel.show(null, getNodeView.implement({}, () => domNode));
   458     }
   459   },
   461   _isInWindow: function WidgetView__isInWindow(window) {
   462     return windowsAPI.BrowserWindow({
   463       window: this._chrome.window
   464     }) == window;
   465   },
   467   _isInChromeWindow: function WidgetView__isInChromeWindow(window) {
   468     return this._chrome.window == window;
   469   },
   471   _onPortEvent: function WidgetView__onPortEvent(args) {
   472     let port = this._port;
   473     port._emit.apply(port, args);
   474     let basePort = this._baseWidget._port;
   475     basePort._emit.apply(basePort, args);
   476   },
   478   get port() this._port._public,
   479   set port(v) {}, // Work around Cortex failure with getter without setter
   480                   // See bug 653464
   481   _port: null,
   483   postMessage: function WidgetView_postMessage(message) {
   484     if (!this._chrome)
   485       throw new Error(ERR_DESTROYED);
   486     this._chrome.update(this._baseWidget, "postMessage", message);
   487   },
   489   destroy: function WidgetView_destroy() {
   490     this._chrome.destroy();
   491     delete this._chrome;
   492     this._baseWidget._onViewDestroyed(this);
   493     this._emit("detach");
   494   }
   496 }));
   499 const WidgetView = function WidgetView(baseWidget) {
   500   let w = WidgetViewTrait.create(WidgetView.prototype);
   501   w._initWidgetView(baseWidget);
   502   return w;
   503 }
   506 /**
   507  * Keeps track of all browser windows.
   508  * Exposes methods for adding/removing widgets
   509  * across all open windows (and future ones).
   510  * Create a new instance of BrowserWindow per window.
   511  */
   512 let browserManager = {
   513   items: [],
   514   windows: [],
   516   // Registers the manager to listen for window openings and closings.  Note
   517   // that calling this method can cause onTrack to be called immediately if
   518   // there are open windows.
   519   init: function () {
   520     let windowTracker = new WindowTracker(this);
   521     unload.ensure(windowTracker);
   522   },
   524   // Registers a window with the manager.  This is a WindowTracker callback.
   525   onTrack: function browserManager_onTrack(window) {
   526     if (isBrowser(window)) {
   527       let win = new BrowserWindow(window);
   528       win.addItems(this.items);
   529       this.windows.push(win);
   530     }
   531   },
   533   // Unregisters a window from the manager.  It's told to undo all
   534   // modifications.  This is a WindowTracker callback.  Note that when
   535   // WindowTracker is unloaded, it calls onUntrack for every currently opened
   536   // window.  The browserManager therefore doesn't need to specially handle
   537   // unload itself, since unloading the browserManager means untracking all
   538   // currently opened windows.
   539   onUntrack: function browserManager_onUntrack(window) {
   540     if (isBrowser(window)) {
   541       this.items.forEach(function(i) i._onWindowClosed(window));
   542       for (let i = 0; i < this.windows.length; i++) {
   543         if (this.windows[i].window == window) {
   544           this.windows.splice(i, 1)[0];
   545           return;
   546         }
   547       }
   549     }
   550   },
   552   // Used to validate widget by browserManager before adding it,
   553   // in order to check input very early in widget constructor
   554   validate : function (item) {
   555     let idx = this.items.indexOf(item);
   556     if (idx > -1)
   557       throw new Error("The widget " + item + " has already been added.");
   558     if (item.id) {
   559       let sameId = this.items.filter(function(i) i.id == item.id);
   560       if (sameId.length > 0)
   561         throw new Error("This widget ID is already used: " + item.id);
   562     } else {
   563       item.id = this.items.length;
   564     }
   565   },
   567   // Registers an item with the manager. It's added to all currently registered
   568   // windows, and when new windows are registered it will be added to them, too.
   569   addItem: function browserManager_addItem(item) {
   570     this.items.push(item);
   571     this.windows.forEach(function (w) w.addItems([item]));
   572   },
   574   // Unregisters an item from the manager.  It's removed from all windows that
   575   // are currently registered.
   576   removeItem: function browserManager_removeItem(item) {
   577     let idx = this.items.indexOf(item);
   578     if (idx > -1)
   579       this.items.splice(idx, 1);
   580   },
   581   propagateCurrentset: function browserManager_propagateCurrentset(id, currentset) {
   582     this.windows.forEach(function (w) w.doc.getElementById(id).setAttribute("currentset", currentset));
   583   }
   584 };
   588 /**
   589  * Keeps track of a single browser window.
   590  *
   591  * This is where the core of how a widget's content is added to a window lives.
   592  */
   593 function BrowserWindow(window) {
   594   this.window = window;
   595   this.doc = window.document;
   596 }
   598 BrowserWindow.prototype = {
   599   // Adds an array of items to the window.
   600   addItems: function BW_addItems(items) {
   601     items.forEach(this._addItemToWindow, this);
   602   },
   604   _addItemToWindow: function BW__addItemToWindow(baseWidget) {
   605     // Create a WidgetView instance
   606     let widget = baseWidget._createView();
   608     // Create a WidgetChrome instance
   609     let item = new WidgetChrome({
   610       widget: widget,
   611       doc: this.doc,
   612       window: this.window
   613     });
   615     widget._chrome = item;
   617     this._insertNodeInToolbar(item.node);
   619     // We need to insert Widget DOM Node before finishing widget view creation
   620     // (because fill creates an iframe and tries to access its docShell)
   621     item.fill();
   622   },
   624   _insertNodeInToolbar: function BW__insertNodeInToolbar(node) {
   625     // Add to the customization palette
   626     let toolbox = this.doc.getElementById("navigator-toolbox");
   627     let palette = toolbox.palette;
   628     palette.appendChild(node);
   630     let { CustomizableUI } = this.window;
   631     let { id } = node;
   633     let placement = CustomizableUI.getPlacementOfWidget(id);
   635     if (!placement) {
   636       if (haveInserted(id) || isWide(node))
   637         return;
   639       placement = {area: 'nav-bar', position: undefined};
   640       saveInserted(id);
   641     }
   643     CustomizableUI.addWidgetToArea(id, placement.area, placement.position);
   644     CustomizableUI.ensureWidgetPlacedInWindow(id, this.window);
   645   }
   646 }
   649 /**
   650  * Final Widget class that handles chrome DOM Node:
   651  *  - create initial DOM nodes
   652  *  - receive instruction from WidgetView through update method and update DOM
   653  *  - watch for DOM events and forward them to WidgetView
   654  */
   655 function WidgetChrome(options) {
   656   this.window = options.window;
   657   this._doc = options.doc;
   658   this._widget = options.widget;
   659   this._symbiont = null; // set later
   660   this.node = null; // set later
   662   this._createNode();
   663 }
   665 // Update a property of a widget.
   666 WidgetChrome.prototype.update = function WC_update(updatedItem, property, value) {
   667   switch(property) {
   668     case "contentURL":
   669     case "content":
   670       this.setContent();
   671       break;
   672     case "width":
   673       this.node.style.minWidth = value + "px";
   674       this.node.querySelector("iframe").style.width = value + "px";
   675       break;
   676     case "tooltip":
   677       this.node.setAttribute("tooltiptext", value);
   678       break;
   679     case "postMessage":
   680       this._symbiont.postMessage(value);
   681       break;
   682     case "emit":
   683       let port = this._symbiont.port;
   684       port.emit.apply(port, value);
   685       break;
   686   }
   687 }
   689 // Add a widget to this window.
   690 WidgetChrome.prototype._createNode = function WC__createNode() {
   691   // XUL element container for widget
   692   let node = this._doc.createElement("toolbaritem");
   694   // Temporary work around require("self") failing on unit-test execution ...
   695   let jetpackID = "testID";
   696   try {
   697     jetpackID = require("./self").id;
   698   } catch(e) {}
   700   // Compute an unique and stable widget id with jetpack id and widget.id
   701   let id = "widget:" + jetpackID + "-" + this._widget.id;
   702   node.setAttribute("id", id);
   703   node.setAttribute("label", this._widget.label);
   704   node.setAttribute("tooltiptext", this._widget.tooltip);
   705   node.setAttribute("align", "center");
   706   // Bug 626326: Prevent customize toolbar context menu to appear
   707   node.setAttribute("context", "");
   709   // For use in styling by the browser
   710   node.setAttribute("sdkstylewidget", "true");
   712   if (this._widget.width > AUSTRALIS_PANEL_WIDE_WIDGET_CUTOFF) {
   713     node.classList.add(AUSTRALIS_PANEL_WIDE_CLASSNAME);
   714   }
   716   // TODO move into a stylesheet, configurable by consumers.
   717   // Either widget.style, exposing the style object, or a URL
   718   // (eg, can load local stylesheet file).
   719   node.setAttribute("style", [
   720       "overflow: hidden; margin: 1px 2px 1px 2px; padding: 0px;",
   721       "min-height: 16px;",
   722   ].join(""));
   724   node.style.minWidth = this._widget.width + "px";
   726   this.node = node;
   727 }
   729 // Initial population of a widget's content.
   730 WidgetChrome.prototype.fill = function WC_fill() {
   731   let { node, _doc: document } = this;
   733   // Create element
   734   let iframe = document.createElement("iframe");
   735   iframe.setAttribute("type", "content");
   736   iframe.setAttribute("transparent", "transparent");
   737   iframe.style.overflow = "hidden";
   738   iframe.style.height = "16px";
   739   iframe.style.maxHeight = "16px";
   740   iframe.style.width = this._widget.width + "px";
   741   iframe.setAttribute("flex", "1");
   742   iframe.style.border = "none";
   743   iframe.style.padding = "0px";
   745   // Do this early, because things like contentWindow are null
   746   // until the node is attached to a document.
   747   node.appendChild(iframe);
   749   let label = document.createElement("label");
   750   label.setAttribute("value", this._widget.label);
   751   label.className = "toolbarbutton-text";
   752   label.setAttribute("crop", "right");
   753   label.setAttribute("flex", "1");
   754   node.appendChild(label);
   756   // This toolbarbutton is created to provide a more consistent user experience
   757   // during customization, see:
   758   // https://bugzilla.mozilla.org/show_bug.cgi?id=959640
   759   let button = document.createElement("toolbarbutton");
   760   button.setAttribute("label", this._widget.label);
   761   button.setAttribute("crop", "right");
   762   button.className = "toolbarbutton-1 chromeclass-toolbar-additional";
   763   node.appendChild(button);
   765   // add event handlers
   766   this.addEventHandlers();
   768   // set content
   769   this.setContent();
   770 }
   772 // Get widget content type.
   773 WidgetChrome.prototype.getContentType = function WC_getContentType() {
   774   if (this._widget.content)
   775     return CONTENT_TYPE_HTML;
   776   return (this._widget.contentURL && /\.(jpg|gif|png|ico|svg)$/i.test(this._widget.contentURL))
   777     ? CONTENT_TYPE_IMAGE : CONTENT_TYPE_URI;
   778 }
   780 // Set widget content.
   781 WidgetChrome.prototype.setContent = function WC_setContent() {
   782   let type = this.getContentType();
   783   let contentURL = null;
   785   switch (type) {
   786     case CONTENT_TYPE_HTML:
   787       contentURL = "data:text/html;charset=utf-8," + encodeURIComponent(this._widget.content);
   788       break;
   789     case CONTENT_TYPE_URI:
   790       contentURL = this._widget.contentURL;
   791       break;
   792     case CONTENT_TYPE_IMAGE:
   793       let imageURL = this._widget.contentURL;
   794       contentURL = "data:text/html;charset=utf-8,<html><body><img src='" +
   795                    encodeURI(imageURL) + "'></body></html>";
   796       break;
   797     default:
   798       throw new Error("The widget's type cannot be determined.");
   799   }
   801   let iframe = this.node.firstElementChild;
   803   let self = this;
   804   // Cleanup previously created symbiont (in case we are update content)
   805   if (this._symbiont)
   806     this._symbiont.destroy();
   808   this._symbiont = Trait.compose(Symbiont.resolve({
   809     _onContentScriptEvent: "_onContentScriptEvent-not-used",
   810     _onInit: "_initSymbiont"
   811   }), {
   812     // Overload `Symbiont._onInit` in order to know when the related worker
   813     // is ready.
   814     _onInit: function () {
   815       this._initSymbiont();
   816       self._widget._onWorkerReady();
   817     },
   818     _onContentScriptEvent: function () {
   819       // Redirect events to WidgetView
   820       self._widget._onPortEvent(arguments);
   821     }
   822   })({
   823     frame: iframe,
   824     contentURL: contentURL,
   825     contentScriptFile: this._widget.contentScriptFile,
   826     contentScript: this._widget.contentScript,
   827     contentScriptWhen: this._widget.contentScriptWhen,
   828     contentScriptOptions: this._widget.contentScriptOptions,
   829     allow: this._widget.allow,
   830     onMessage: function(message) {
   831       setTimeout(function() {
   832         self._widget._onEvent("message", message);
   833       }, 0);
   834     }
   835   });
   836 }
   838 // Detect if document consists of a single image.
   839 WidgetChrome._isImageDoc = function WC__isImageDoc(doc) {
   840   return /*doc.body &&*/ doc.body.childNodes.length == 1 &&
   841          doc.body.firstElementChild &&
   842          doc.body.firstElementChild.tagName == "IMG";
   843 }
   845 // Set up all supported events for a widget.
   846 WidgetChrome.prototype.addEventHandlers = function WC_addEventHandlers() {
   847   let contentType = this.getContentType();
   849   let self = this;
   850   let listener = function(e) {
   851     // Ignore event firings that target the iframe.
   852     if (e.target == self.node.firstElementChild)
   853       return;
   855     // The widget only supports left-click for now,
   856     // so ignore all clicks (i.e. middle or right) except left ones.
   857     if (e.type == "click" && e.button !== 0)
   858       return;
   860     // Proxy event to the widget
   861     setTimeout(function() {
   862       self._widget._onEvent(EVENTS[e.type], null, self.node);
   863     }, 0);
   864   };
   866   this.eventListeners = {};
   867   let iframe = this.node.firstElementChild;
   868   for (let type in EVENTS) {
   869     iframe.addEventListener(type, listener, true, true);
   871     // Store listeners for later removal
   872     this.eventListeners[type] = listener;
   873   }
   875   // On document load, make modifications required for nice default
   876   // presentation.
   877   function loadListener(e) {
   878     let containerStyle = self.window.getComputedStyle(self.node.parentNode);
   879     // Ignore event firings that target the iframe
   880     if (e.target == iframe)
   881       return;
   882     // Ignore about:blank loads
   883     if (e.type == "load" && e.target.location == "about:blank")
   884       return;
   886     // We may have had an unload event before that cleaned up the symbiont
   887     if (!self._symbiont)
   888       self.setContent();
   890     let doc = e.target;
   892     if (contentType == CONTENT_TYPE_IMAGE || WidgetChrome._isImageDoc(doc)) {
   893       // Force image content to size.
   894       // Add-on authors must size their images correctly.
   895       doc.body.firstElementChild.style.width = self._widget.width + "px";
   896       doc.body.firstElementChild.style.height = "16px";
   897     }
   899     // Extend the add-on bar's default text styles to the widget.
   900     doc.body.style.color = containerStyle.color;
   901     doc.body.style.fontFamily = containerStyle.fontFamily;
   902     doc.body.style.fontSize = containerStyle.fontSize;
   903     doc.body.style.fontWeight = containerStyle.fontWeight;
   904     doc.body.style.textShadow = containerStyle.textShadow;
   905     // Allow all content to fill the box by default.
   906     doc.body.style.margin = "0";
   907   }
   909   iframe.addEventListener("load", loadListener, true);
   910   this.eventListeners["load"] = loadListener;
   912   // Register a listener to unload symbiont if the toolbaritem is moved
   913   // on user toolbars customization
   914   function unloadListener(e) {
   915     if (e.target.location == "about:blank")
   916       return;
   917     self._symbiont.destroy();
   918     self._symbiont = null;
   919     // This may fail but not always, it depends on how the node is
   920     // moved or removed
   921     try {
   922       self.setContent();
   923     } catch(e) {}
   925   }
   927   iframe.addEventListener("unload", unloadListener, true);
   928   this.eventListeners["unload"] = unloadListener;
   929 }
   931 // Remove and unregister the widget from everything
   932 WidgetChrome.prototype.destroy = function WC_destroy(removedItems) {
   933   // remove event listeners
   934   for (let type in this.eventListeners) {
   935     let listener = this.eventListeners[type];
   936     this.node.firstElementChild.removeEventListener(type, listener, true);
   937   }
   938   // remove dom node
   939   this.node.parentNode.removeChild(this.node);
   940   // cleanup symbiont
   941   this._symbiont.destroy();
   942   // cleanup itself
   943   this.eventListeners = null;
   944   this._widget = null;
   945   this._symbiont = null;
   946 }
   948 // Init the browserManager only after setting prototypes and such above, because
   949 // it will cause browserManager.onTrack to be called immediately if there are
   950 // open windows.
   951 browserManager.init();

mercurial