/* 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"; // The widget module currently supports only Firefox. // See: https://bugzilla.mozilla.org/show_bug.cgi?id=560716 module.metadata = { "stability": "stable", "engines": { "Firefox": "*" } }; // 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."; const INSERTION_PREF_ROOT = "extensions.sdk-widget-inserted."; // Supported events, mapping from DOM event names to our event names const EVENTS = { "click": "click", "mouseover": "mouseover", "mouseout": "mouseout", }; // In the Australis menu panel, normally widgets should be treated like // normal toolbarbuttons. If they're any wider than this margin, we'll // treat them as wide widgets instead, which fill up the width of the panel: const AUSTRALIS_PANEL_WIDE_WIDGET_CUTOFF = 70; 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 { getNodeView } = require("./view/core"); const prefs = require('./preferences/service'); // 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)); } } }; function saveInserted(widgetId) { prefs.set(INSERTION_PREF_ROOT + widgetId, true); } function haveInserted(widgetId) { return prefs.has(INSERTION_PREF_ROOT + widgetId); } /** * 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) { // In Australis, widgets may be positioned in an overflow panel or the // menu panel. // In such cases clicking this widget will hide the overflow/menu panel, // and the widget's panel will show instead. let anchor = domNode; let { CustomizableUI, window } = domNode.ownerDocument.defaultView; if (CustomizableUI) { ({anchor}) = CustomizableUI.getWidget(domNode.id).forWindow(window); // if `anchor` is not the `domNode` itself, it means the widget is // positioned in a panel, therefore we have to hide it before show // the widget's panel in the same anchor if (anchor !== domNode) CustomizableUI.hidePanelForNode(domNode); } // This kind of ugly workaround, instead we should implement // `getNodeView` for the `Widget` class itself, but that's kind of // hard without cleaning things up. this.panel.show(null, getNodeView.implement({}, () => anchor)); } }, _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); }, propagateCurrentset: function browserManager_propagateCurrentset(id, currentset) { this.windows.forEach(function (w) w.doc.getElementById(id).setAttribute("currentset", currentset)); } }; /** * 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); if (this.window.CustomizableUI) { let placement = this.window.CustomizableUI.getPlacementOfWidget(node.id); if (!placement) { if (haveInserted(node.id)) { return; } placement = {area: 'nav-bar', position: undefined}; saveInserted(node.id); } this.window.CustomizableUI.addWidgetToArea(node.id, placement.area, placement.position); this.window.CustomizableUI.ensureWidgetPlacedInWindow(node.id, this.window); return; } // 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 let needToPropagateCurrentset = false; if (!container) { if (haveInserted(node.id)) { return; } container = this.doc.getElementById("addon-bar"); saveInserted(node.id); needToPropagateCurrentset = true; // 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) { let set = container.currentSet; container.setAttribute("currentset", set); // Save DOM attribute in order to save position on new window opened this.window.document.persist(container.id, "currentset"); browserManager.propagateCurrentset(container.id, set); } } } /** * 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"); // 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", ""); // For use in styling by the browser node.setAttribute("sdkstylewidget", "true"); // Mark wide widgets as such: if (this.window.CustomizableUI && this._widget.width > AUSTRALIS_PANEL_WIDE_WIDGET_CUTOFF) { node.classList.add("panel-wide-item"); } // 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); var label = this._doc.createElement("label"); label.setAttribute("value", this._widget.label); label.className = "toolbarbutton-text"; label.setAttribute("crop", "right"); label.setAttribute("flex", "1"); this.node.appendChild(label); // 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|svg)$/i.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,
"; 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 &&*/ 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 all clicks (i.e. middle or right) except left ones. if (e.type == "click" && e.button !== 0) 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();