aboutsummaryrefslogtreecommitdiff
path: root/tools/addon-sdk-1.12/lib/sdk/widget.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/addon-sdk-1.12/lib/sdk/widget.js')
-rw-r--r--tools/addon-sdk-1.12/lib/sdk/widget.js938
1 files changed, 938 insertions, 0 deletions
diff --git a/tools/addon-sdk-1.12/lib/sdk/widget.js b/tools/addon-sdk-1.12/lib/sdk/widget.js
new file mode 100644
index 0000000..f1236aa
--- /dev/null
+++ b/tools/addon-sdk-1.12/lib/sdk/widget.js
@@ -0,0 +1,938 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.metadata = {
+ "stability": "stable"
+};
+
+// Widget content types
+const CONTENT_TYPE_URI = 1;
+const CONTENT_TYPE_HTML = 2;
+const CONTENT_TYPE_IMAGE = 3;
+
+const ERR_CONTENT = "No content or contentURL property found. Widgets must "
+ + "have one or the other.",
+ ERR_LABEL = "The widget must have a non-empty label property.",
+ ERR_ID = "You have to specify a unique value for the id property of " +
+ "your widget in order for the application to remember its " +
+ "position.",
+ ERR_DESTROYED = "The widget has been destroyed and can no longer be used.";
+
+// Supported events, mapping from DOM event names to our event names
+const EVENTS = {
+ "click": "click",
+ "mouseover": "mouseover",
+ "mouseout": "mouseout",
+};
+
+if (!require("./system/xul-app").is("Firefox")) {
+ throw new Error([
+ "The widget module currently supports only Firefox. In the future ",
+ "it will support other applications. Please see ",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=560716 for more information."
+ ].join(""));
+}
+
+const { validateOptions } = require("./deprecated/api-utils");
+const panels = require("./panel");
+const { EventEmitter, EventEmitterTrait } = require("./deprecated/events");
+const { Trait } = require("./deprecated/traits");
+const LightTrait = require('./deprecated/light-traits').Trait;
+const { Loader, Symbiont } = require("./content/content");
+const { Cortex } = require('./deprecated/cortex');
+const windowsAPI = require("./windows");
+const { WindowTracker } = require("./deprecated/window-utils");
+const { isBrowser } = require("./window/utils");
+const { setTimeout } = require("./timers");
+const unload = require("./system/unload");
+const { uuid } = require("./util/uuid");
+
+// Data types definition
+const valid = {
+ number: { is: ["null", "undefined", "number"] },
+ string: { is: ["null", "undefined", "string"] },
+ id: {
+ is: ["string"],
+ ok: function (v) v.length > 0,
+ msg: ERR_ID,
+ readonly: true
+ },
+ label: {
+ is: ["string"],
+ ok: function (v) v.length > 0,
+ msg: ERR_LABEL
+ },
+ panel: {
+ is: ["null", "undefined", "object"],
+ ok: function(v) !v || v instanceof panels.Panel
+ },
+ width: {
+ is: ["null", "undefined", "number"],
+ map: function (v) {
+ if (null === v || undefined === v) v = 16;
+ return v;
+ },
+ defaultValue: 16
+ },
+ allow: {
+ is: ["null", "undefined", "object"],
+ map: function (v) {
+ if (!v) v = { script: true };
+ return v;
+ },
+ get defaultValue() ({ script: true })
+ },
+};
+
+// Widgets attributes definition
+let widgetAttributes = {
+ label: valid.label,
+ id: valid.id,
+ tooltip: valid.string,
+ width: valid.width,
+ content: valid.string,
+ panel: valid.panel,
+ allow: valid.allow
+};
+
+// Import data definitions from loader, but don't compose with it as Model
+// functions allow us to recreate easily all Loader code.
+let loaderAttributes = require("./content/loader").validationAttributes;
+for (let i in loaderAttributes)
+ widgetAttributes[i] = loaderAttributes[i];
+
+widgetAttributes.contentURL.optional = true;
+
+// Widgets public events list, that are automatically binded in options object
+const WIDGET_EVENTS = [
+ "click",
+ "mouseover",
+ "mouseout",
+ "error",
+ "message",
+ "attach"
+];
+
+// `Model` utility functions that help creating these various Widgets objects
+let model = {
+
+ // Validate one attribute using api-utils.js:validateOptions function
+ _validate: function _validate(name, suspect, validation) {
+ let $1 = {};
+ $1[name] = suspect;
+ let $2 = {};
+ $2[name] = validation;
+ return validateOptions($1, $2)[name];
+ },
+
+ /**
+ * This method has two purposes:
+ * 1/ Validate and define, on a given object, a set of attribute
+ * 2/ Emit a "change" event on this object when an attribute is changed
+ *
+ * @params {Object} object
+ * Object on which we can bind attributes on and watch for their changes.
+ * This object must have an EventEmitter interface, or, at least `_emit`
+ * method
+ * @params {Object} attrs
+ * Dictionary of attributes definition following api-utils:validateOptions
+ * scheme
+ * @params {Object} values
+ * Dictionary of attributes default values
+ */
+ setAttributes: function setAttributes(object, attrs, values) {
+ let properties = {};
+ for (let name in attrs) {
+ let value = values[name];
+ let req = attrs[name];
+
+ // Retrieve default value from typedef if the value is not defined
+ if ((typeof value == "undefined" || value == null) && req.defaultValue)
+ value = req.defaultValue;
+
+ // Check for valid value if value is defined or mandatory
+ if (!req.optional || typeof value != "undefined")
+ value = model._validate(name, value, req);
+
+ // In any case, define this property on `object`
+ let property = null;
+ if (req.readonly) {
+ property = {
+ value: value,
+ writable: false,
+ enumerable: true,
+ configurable: false
+ };
+ }
+ else {
+ property = model._createWritableProperty(name, value);
+ }
+
+ properties[name] = property;
+ }
+ Object.defineProperties(object, properties);
+ },
+
+ // Generate ES5 property definition for a given attribute
+ _createWritableProperty: function _createWritableProperty(name, value) {
+ return {
+ get: function () {
+ return value;
+ },
+ set: function (newValue) {
+ value = newValue;
+ // The main goal of all this Model stuff is here:
+ // We want to forward all changes to some listeners
+ this._emit("change", name, value);
+ },
+ enumerable: true,
+ configurable: false
+ };
+ },
+
+ /**
+ * Automagically register listeners in options dictionary
+ * by detecting listener attributes with name starting with `on`
+ *
+ * @params {Object} object
+ * Target object that need to follow EventEmitter interface, or, at least,
+ * having `on` method.
+ * @params {Array} events
+ * List of events name to automatically bind.
+ * @params {Object} listeners
+ * Dictionary of event listener functions to register.
+ */
+ setEvents: function setEvents(object, events, listeners) {
+ for (let i = 0, l = events.length; i < l; i++) {
+ let name = events[i];
+ let onName = "on" + name[0].toUpperCase() + name.substr(1);
+ if (!listeners[onName])
+ continue;
+ object.on(name, listeners[onName].bind(object));
+ }
+ }
+
+};
+
+
+/**
+ * Main Widget class: entry point of the widget API
+ *
+ * Allow to control all widget across all existing windows with a single object.
+ * Widget.getView allow to retrieve a WidgetView instance to control a widget
+ * specific to one window.
+ */
+const WidgetTrait = LightTrait.compose(EventEmitterTrait, LightTrait({
+
+ _initWidget: function _initWidget(options) {
+ model.setAttributes(this, widgetAttributes, options);
+
+ browserManager.validate(this);
+
+ // We must have at least content or contentURL defined
+ if (!(this.content || this.contentURL))
+ throw new Error(ERR_CONTENT);
+
+ this._views = [];
+
+ // Set tooltip to label value if we don't have tooltip defined
+ if (!this.tooltip)
+ this.tooltip = this.label;
+
+ model.setEvents(this, WIDGET_EVENTS, options);
+
+ this.on('change', this._onChange.bind(this));
+
+ let self = this;
+ this._port = EventEmitterTrait.create({
+ emit: function () {
+ let args = arguments;
+ self._views.forEach(function(v) v.port.emit.apply(v.port, args));
+ }
+ });
+ // expose wrapped port, that exposes only public properties.
+ this._port._public = Cortex(this._port);
+
+ // Register this widget to browser manager in order to create new widget on
+ // all new windows
+ browserManager.addItem(this);
+ },
+
+ _onChange: function _onChange(name, value) {
+ // Set tooltip to label value if we don't have tooltip defined
+ if (name == 'tooltip' && !value) {
+ // we need to change tooltip again in order to change the value of the
+ // attribute itself
+ this.tooltip = this.label;
+ return;
+ }
+
+ // Forward attributes changes to WidgetViews
+ if (['width', 'tooltip', 'content', 'contentURL'].indexOf(name) != -1) {
+ this._views.forEach(function(v) v[name] = value);
+ }
+ },
+
+ _onEvent: function _onEvent(type, eventData) {
+ this._emit(type, eventData);
+ },
+
+ _createView: function _createView() {
+ // Create a new WidgetView instance
+ let view = WidgetView(this);
+
+ // Keep a reference to it
+ this._views.push(view);
+
+ return view;
+ },
+
+ // a WidgetView instance is destroyed
+ _onViewDestroyed: function _onViewDestroyed(view) {
+ let idx = this._views.indexOf(view);
+ this._views.splice(idx, 1);
+ },
+
+ /**
+ * Called on browser window closed, to destroy related WidgetViews
+ * @params {ChromeWindow} window
+ * Window that has been closed
+ */
+ _onWindowClosed: function _onWindowClosed(window) {
+ for each (let view in this._views) {
+ if (view._isInChromeWindow(window)) {
+ view.destroy();
+ break;
+ }
+ }
+ },
+
+ /**
+ * Get the WidgetView instance related to a BrowserWindow instance
+ * @params {BrowserWindow} window
+ * BrowserWindow reference from "windows" module
+ */
+ getView: function getView(window) {
+ for each (let view in this._views) {
+ if (view._isInWindow(window)) {
+ return view._public;
+ }
+ }
+ return null;
+ },
+
+ get port() this._port._public,
+ set port(v) {}, // Work around Cortex failure with getter without setter
+ // See bug 653464
+ _port: null,
+
+ postMessage: function postMessage(message) {
+ this._views.forEach(function(v) v.postMessage(message));
+ },
+
+ destroy: function destroy() {
+ if (this.panel)
+ this.panel.destroy();
+
+ // Dispatch destroy calls to views
+ // we need to go backward as we remove items from this array in
+ // _onViewDestroyed
+ for (let i = this._views.length - 1; i >= 0; i--)
+ this._views[i].destroy();
+
+ // Unregister widget to stop creating it over new windows
+ // and allow creation of new widget with same id
+ browserManager.removeItem(this);
+ }
+
+}));
+
+// Widget constructor
+const Widget = function Widget(options) {
+ let w = WidgetTrait.create(Widget.prototype);
+ w._initWidget(options);
+
+ // Return a Cortex of widget in order to hide private attributes like _onEvent
+ let _public = Cortex(w);
+ unload.ensure(_public, "destroy");
+ return _public;
+}
+exports.Widget = Widget;
+
+
+
+/**
+ * WidgetView is an instance of a widget for a specific window.
+ *
+ * This is an external API that can be retrieved by calling Widget.getView or
+ * by watching `attach` event on Widget.
+ */
+const WidgetViewTrait = LightTrait.compose(EventEmitterTrait, LightTrait({
+
+ // Reference to the matching WidgetChrome
+ // set right after constructor call
+ _chrome: null,
+
+ // Public interface of the WidgetView, passed in `attach` event or in
+ // Widget.getView
+ _public: null,
+
+ _initWidgetView: function WidgetView__initWidgetView(baseWidget) {
+ this._baseWidget = baseWidget;
+
+ model.setAttributes(this, widgetAttributes, baseWidget);
+
+ this.on('change', this._onChange.bind(this));
+
+ let self = this;
+ this._port = EventEmitterTrait.create({
+ emit: function () {
+ if (!self._chrome)
+ throw new Error(ERR_DESTROYED);
+ self._chrome.update(self._baseWidget, "emit", arguments);
+ }
+ });
+ // expose wrapped port, that exposes only public properties.
+ this._port._public = Cortex(this._port);
+
+ this._public = Cortex(this);
+ },
+
+ // Called by WidgetChrome, when the related Worker is applied to the document,
+ // so that we can start sending events to it
+ _onWorkerReady: function () {
+ // Emit an `attach` event with a WidgetView instance without private attrs
+ this._baseWidget._emit("attach", this._public);
+ },
+
+ _onChange: function WidgetView__onChange(name, value) {
+ if (name == 'tooltip' && !value) {
+ this.tooltip = this.label;
+ return;
+ }
+
+ // Forward attributes changes to WidgetChrome instance
+ if (['width', 'tooltip', 'content', 'contentURL'].indexOf(name) != -1) {
+ this._chrome.update(this._baseWidget, name, value);
+ }
+ },
+
+ _onEvent: function WidgetView__onEvent(type, eventData, domNode) {
+ // Dispatch event in view
+ this._emit(type, eventData);
+
+ // And forward it to the main Widget object
+ if ("click" == type || type.indexOf("mouse") == 0)
+ this._baseWidget._onEvent(type, this._public);
+ else
+ this._baseWidget._onEvent(type, eventData);
+
+ // Special case for click events: if the widget doesn't have a click
+ // handler, but it does have a panel, display the panel.
+ if ("click" == type && !this._listeners("click").length && this.panel)
+ this.panel.show(domNode);
+ },
+
+ _isInWindow: function WidgetView__isInWindow(window) {
+ return windowsAPI.BrowserWindow({
+ window: this._chrome.window
+ }) == window;
+ },
+
+ _isInChromeWindow: function WidgetView__isInChromeWindow(window) {
+ return this._chrome.window == window;
+ },
+
+ _onPortEvent: function WidgetView__onPortEvent(args) {
+ let port = this._port;
+ port._emit.apply(port, args);
+ let basePort = this._baseWidget._port;
+ basePort._emit.apply(basePort, args);
+ },
+
+ get port() this._port._public,
+ set port(v) {}, // Work around Cortex failure with getter without setter
+ // See bug 653464
+ _port: null,
+
+ postMessage: function WidgetView_postMessage(message) {
+ if (!this._chrome)
+ throw new Error(ERR_DESTROYED);
+ this._chrome.update(this._baseWidget, "postMessage", message);
+ },
+
+ destroy: function WidgetView_destroy() {
+ this._chrome.destroy();
+ delete this._chrome;
+ this._baseWidget._onViewDestroyed(this);
+ this._emit("detach");
+ }
+
+}));
+
+const WidgetView = function WidgetView(baseWidget) {
+ let w = WidgetViewTrait.create(WidgetView.prototype);
+ w._initWidgetView(baseWidget);
+ return w;
+}
+
+
+
+/**
+ * Keeps track of all browser windows.
+ * Exposes methods for adding/removing widgets
+ * across all open windows (and future ones).
+ * Create a new instance of BrowserWindow per window.
+ */
+let browserManager = {
+ items: [],
+ windows: [],
+
+ // Registers the manager to listen for window openings and closings. Note
+ // that calling this method can cause onTrack to be called immediately if
+ // there are open windows.
+ init: function () {
+ let windowTracker = new WindowTracker(this);
+ unload.ensure(windowTracker);
+ },
+
+ // Registers a window with the manager. This is a WindowTracker callback.
+ onTrack: function browserManager_onTrack(window) {
+ if (isBrowser(window)) {
+ let win = new BrowserWindow(window);
+ win.addItems(this.items);
+ this.windows.push(win);
+ }
+ },
+
+ // Unregisters a window from the manager. It's told to undo all
+ // modifications. This is a WindowTracker callback. Note that when
+ // WindowTracker is unloaded, it calls onUntrack for every currently opened
+ // window. The browserManager therefore doesn't need to specially handle
+ // unload itself, since unloading the browserManager means untracking all
+ // currently opened windows.
+ onUntrack: function browserManager_onUntrack(window) {
+ if (isBrowser(window)) {
+ this.items.forEach(function(i) i._onWindowClosed(window));
+ for (let i = 0; i < this.windows.length; i++) {
+ if (this.windows[i].window == window) {
+ this.windows.splice(i, 1)[0];
+ return;
+ }
+ }
+
+ }
+ },
+
+ // Used to validate widget by browserManager before adding it,
+ // in order to check input very early in widget constructor
+ validate : function (item) {
+ let idx = this.items.indexOf(item);
+ if (idx > -1)
+ throw new Error("The widget " + item + " has already been added.");
+ if (item.id) {
+ let sameId = this.items.filter(function(i) i.id == item.id);
+ if (sameId.length > 0)
+ throw new Error("This widget ID is already used: " + item.id);
+ } else {
+ item.id = this.items.length;
+ }
+ },
+
+ // Registers an item with the manager. It's added to all currently registered
+ // windows, and when new windows are registered it will be added to them, too.
+ addItem: function browserManager_addItem(item) {
+ this.items.push(item);
+ this.windows.forEach(function (w) w.addItems([item]));
+ },
+
+ // Unregisters an item from the manager. It's removed from all windows that
+ // are currently registered.
+ removeItem: function browserManager_removeItem(item) {
+ let idx = this.items.indexOf(item);
+ if (idx > -1)
+ this.items.splice(idx, 1);
+ }
+};
+
+
+
+/**
+ * Keeps track of a single browser window.
+ *
+ * This is where the core of how a widget's content is added to a window lives.
+ */
+function BrowserWindow(window) {
+ this.window = window;
+ this.doc = window.document;
+}
+
+BrowserWindow.prototype = {
+
+ // Adds an array of items to the window.
+ addItems: function BW_addItems(items) {
+ items.forEach(this._addItemToWindow, this);
+ },
+
+ _addItemToWindow: function BW__addItemToWindow(baseWidget) {
+ // Create a WidgetView instance
+ let widget = baseWidget._createView();
+
+ // Create a WidgetChrome instance
+ let item = new WidgetChrome({
+ widget: widget,
+ doc: this.doc,
+ window: this.window
+ });
+
+ widget._chrome = item;
+
+ this._insertNodeInToolbar(item.node);
+
+ // We need to insert Widget DOM Node before finishing widget view creation
+ // (because fill creates an iframe and tries to access its docShell)
+ item.fill();
+ },
+
+ _insertNodeInToolbar: function BW__insertNodeInToolbar(node) {
+ // Add to the customization palette
+ let toolbox = this.doc.getElementById("navigator-toolbox");
+ let palette = toolbox.palette;
+ palette.appendChild(node);
+
+ // Search for widget toolbar by reading toolbar's currentset attribute
+ let container = null;
+ let toolbars = this.doc.getElementsByTagName("toolbar");
+ let id = node.getAttribute("id");
+ for (let i = 0, l = toolbars.length; i < l; i++) {
+ let toolbar = toolbars[i];
+ if (toolbar.getAttribute("currentset").indexOf(id) == -1)
+ continue;
+ container = toolbar;
+ }
+
+ // if widget isn't in any toolbar, add it to the addon-bar
+ // TODO: we may want some "first-launch" module to do this only on very
+ // first execution
+ if (!container) {
+ container = this.doc.getElementById("addon-bar");
+ // TODO: find a way to make the following code work when we use "cfx run":
+ // http://mxr.mozilla.org/mozilla-central/source/browser/base/content/browser.js#8586
+ // until then, force display of addon bar directly from sdk code
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=627484
+ if (container.collapsed)
+ this.window.toggleAddonBar();
+ }
+
+ // Now retrieve a reference to the next toolbar item
+ // by reading currentset attribute on the toolbar
+ let nextNode = null;
+ let currentSet = container.getAttribute("currentset");
+ let ids = (currentSet == "__empty") ? [] : currentSet.split(",");
+ let idx = ids.indexOf(id);
+ if (idx != -1) {
+ for (let i = idx; i < ids.length; i++) {
+ nextNode = this.doc.getElementById(ids[i]);
+ if (nextNode)
+ break;
+ }
+ }
+
+ // Finally insert our widget in the right toolbar and in the right position
+ container.insertItem(id, nextNode, null, false);
+
+ // Update DOM in order to save position: which toolbar, and which position
+ // in this toolbar. But only do this the first time we add it to the toolbar
+ // Otherwise, this code will collide with other instance of Widget module
+ // during Firefox startup. See bug 685929.
+ if (ids.indexOf(id) == -1) {
+ container.setAttribute("currentset", container.currentSet);
+ // Save DOM attribute in order to save position on new window opened
+ this.window.document.persist(container.id, "currentset");
+ }
+ }
+}
+
+
+/**
+ * Final Widget class that handles chrome DOM Node:
+ * - create initial DOM nodes
+ * - receive instruction from WidgetView through update method and update DOM
+ * - watch for DOM events and forward them to WidgetView
+ */
+function WidgetChrome(options) {
+ this.window = options.window;
+ this._doc = options.doc;
+ this._widget = options.widget;
+ this._symbiont = null; // set later
+ this.node = null; // set later
+
+ this._createNode();
+}
+
+// Update a property of a widget.
+WidgetChrome.prototype.update = function WC_update(updatedItem, property, value) {
+ switch(property) {
+ case "contentURL":
+ case "content":
+ this.setContent();
+ break;
+ case "width":
+ this.node.style.minWidth = value + "px";
+ this.node.querySelector("iframe").style.width = value + "px";
+ break;
+ case "tooltip":
+ this.node.setAttribute("tooltiptext", value);
+ break;
+ case "postMessage":
+ this._symbiont.postMessage(value);
+ break;
+ case "emit":
+ let port = this._symbiont.port;
+ port.emit.apply(port, value);
+ break;
+ }
+}
+
+// Add a widget to this window.
+WidgetChrome.prototype._createNode = function WC__createNode() {
+ // XUL element container for widget
+ let node = this._doc.createElement("toolbaritem");
+ let guid = String(uuid());
+
+ // Temporary work around require("self") failing on unit-test execution ...
+ let jetpackID = "testID";
+ try {
+ jetpackID = require("./self").id;
+ } catch(e) {}
+
+ // Compute an unique and stable widget id with jetpack id and widget.id
+ let id = "widget:" + jetpackID + "-" + this._widget.id;
+ node.setAttribute("id", id);
+ node.setAttribute("label", this._widget.label);
+ node.setAttribute("tooltiptext", this._widget.tooltip);
+ node.setAttribute("align", "center");
+ // Bug 626326: Prevent customize toolbar context menu to appear
+ node.setAttribute("context", "");
+
+ // TODO move into a stylesheet, configurable by consumers.
+ // Either widget.style, exposing the style object, or a URL
+ // (eg, can load local stylesheet file).
+ node.setAttribute("style", [
+ "overflow: hidden; margin: 1px 2px 1px 2px; padding: 0px;",
+ "min-height: 16px;",
+ ].join(""));
+
+ node.style.minWidth = this._widget.width + "px";
+
+ this.node = node;
+}
+
+// Initial population of a widget's content.
+WidgetChrome.prototype.fill = function WC_fill() {
+ // Create element
+ var iframe = this._doc.createElement("iframe");
+ iframe.setAttribute("type", "content");
+ iframe.setAttribute("transparent", "transparent");
+ iframe.style.overflow = "hidden";
+ iframe.style.height = "16px";
+ iframe.style.maxHeight = "16px";
+ iframe.style.width = this._widget.width + "px";
+ iframe.setAttribute("flex", "1");
+ iframe.style.border = "none";
+ iframe.style.padding = "0px";
+
+ // Do this early, because things like contentWindow are null
+ // until the node is attached to a document.
+ this.node.appendChild(iframe);
+
+ // add event handlers
+ this.addEventHandlers();
+
+ // set content
+ this.setContent();
+}
+
+// Get widget content type.
+WidgetChrome.prototype.getContentType = function WC_getContentType() {
+ if (this._widget.content)
+ return CONTENT_TYPE_HTML;
+ return (this._widget.contentURL && /\.(jpg|gif|png|ico)$/.test(this._widget.contentURL))
+ ? CONTENT_TYPE_IMAGE : CONTENT_TYPE_URI;
+}
+
+// Set widget content.
+WidgetChrome.prototype.setContent = function WC_setContent() {
+ let type = this.getContentType();
+ let contentURL = null;
+
+ switch (type) {
+ case CONTENT_TYPE_HTML:
+ contentURL = "data:text/html;charset=utf-8," + encodeURIComponent(this._widget.content);
+ break;
+ case CONTENT_TYPE_URI:
+ contentURL = this._widget.contentURL;
+ break;
+ case CONTENT_TYPE_IMAGE:
+ let imageURL = this._widget.contentURL;
+ contentURL = "data:text/html;charset=utf-8,<html><body><img src='" +
+ encodeURI(imageURL) + "'></body></html>";
+ break;
+ default:
+ throw new Error("The widget's type cannot be determined.");
+ }
+
+ let iframe = this.node.firstElementChild;
+
+ let self = this;
+ // Cleanup previously created symbiont (in case we are update content)
+ if (this._symbiont)
+ this._symbiont.destroy();
+
+ this._symbiont = Trait.compose(Symbiont.resolve({
+ _onContentScriptEvent: "_onContentScriptEvent-not-used",
+ _onInit: "_initSymbiont"
+ }), {
+ // Overload `Symbiont._onInit` in order to know when the related worker
+ // is ready.
+ _onInit: function () {
+ this._initSymbiont();
+ self._widget._onWorkerReady();
+ },
+ _onContentScriptEvent: function () {
+ // Redirect events to WidgetView
+ self._widget._onPortEvent(arguments);
+ }
+ })({
+ frame: iframe,
+ contentURL: contentURL,
+ contentScriptFile: this._widget.contentScriptFile,
+ contentScript: this._widget.contentScript,
+ contentScriptWhen: this._widget.contentScriptWhen,
+ contentScriptOptions: this._widget.contentScriptOptions,
+ allow: this._widget.allow,
+ onMessage: function(message) {
+ setTimeout(function() {
+ self._widget._onEvent("message", message);
+ }, 0);
+ }
+ });
+}
+
+// Detect if document consists of a single image.
+WidgetChrome._isImageDoc = function WC__isImageDoc(doc) {
+ return doc.body.childNodes.length == 1 &&
+ doc.body.firstElementChild &&
+ doc.body.firstElementChild.tagName == "IMG";
+}
+
+// Set up all supported events for a widget.
+WidgetChrome.prototype.addEventHandlers = function WC_addEventHandlers() {
+ let contentType = this.getContentType();
+
+ let self = this;
+ let listener = function(e) {
+ // Ignore event firings that target the iframe.
+ if (e.target == self.node.firstElementChild)
+ return;
+
+ // The widget only supports left-click for now,
+ // so ignore right-clicks.
+ if (e.type == "click" && e.button == 2)
+ return;
+
+ // Proxy event to the widget
+ setTimeout(function() {
+ self._widget._onEvent(EVENTS[e.type], null, self.node);
+ }, 0);
+ };
+
+ this.eventListeners = {};
+ let iframe = this.node.firstElementChild;
+ for (let type in EVENTS) {
+ iframe.addEventListener(type, listener, true, true);
+
+ // Store listeners for later removal
+ this.eventListeners[type] = listener;
+ }
+
+ // On document load, make modifications required for nice default
+ // presentation.
+ function loadListener(e) {
+ let containerStyle = self.window.getComputedStyle(self.node.parentNode);
+ // Ignore event firings that target the iframe
+ if (e.target == iframe)
+ return;
+ // Ignore about:blank loads
+ if (e.type == "load" && e.target.location == "about:blank")
+ return;
+
+ // We may have had an unload event before that cleaned up the symbiont
+ if (!self._symbiont)
+ self.setContent();
+
+ let doc = e.target;
+ if (contentType == CONTENT_TYPE_IMAGE || WidgetChrome._isImageDoc(doc)) {
+ // Force image content to size.
+ // Add-on authors must size their images correctly.
+ doc.body.firstElementChild.style.width = self._widget.width + "px";
+ doc.body.firstElementChild.style.height = "16px";
+ }
+
+ // Extend the add-on bar's default text styles to the widget.
+ doc.body.style.color = containerStyle.color;
+ doc.body.style.fontFamily = containerStyle.fontFamily;
+ doc.body.style.fontSize = containerStyle.fontSize;
+ doc.body.style.fontWeight = containerStyle.fontWeight;
+ doc.body.style.textShadow = containerStyle.textShadow;
+ // Allow all content to fill the box by default.
+ doc.body.style.margin = "0";
+ }
+ iframe.addEventListener("load", loadListener, true);
+ this.eventListeners["load"] = loadListener;
+
+ // Register a listener to unload symbiont if the toolbaritem is moved
+ // on user toolbars customization
+ function unloadListener(e) {
+ if (e.target.location == "about:blank")
+ return;
+ self._symbiont.destroy();
+ self._symbiont = null;
+ // This may fail but not always, it depends on how the node is
+ // moved or removed
+ try {
+ self.setContent();
+ } catch(e) {}
+
+ }
+
+ iframe.addEventListener("unload", unloadListener, true);
+ this.eventListeners["unload"] = unloadListener;
+}
+
+// Remove and unregister the widget from everything
+WidgetChrome.prototype.destroy = function WC_destroy(removedItems) {
+ // remove event listeners
+ for (let type in this.eventListeners) {
+ let listener = this.eventListeners[type];
+ this.node.firstElementChild.removeEventListener(type, listener, true);
+ }
+ // remove dom node
+ this.node.parentNode.removeChild(this.node);
+ // cleanup symbiont
+ this._symbiont.destroy();
+ // cleanup itself
+ this.eventListeners = null;
+ this._widget = null;
+ this._symbiont = null;
+}
+
+// Init the browserManager only after setting prototypes and such above, because
+// it will cause browserManager.onTrack to be called immediately if there are
+// open windows.
+browserManager.init();