michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: // The widget module currently supports only Firefox. michael@0: // See: https://bugzilla.mozilla.org/show_bug.cgi?id=560716 michael@0: module.metadata = { michael@0: "stability": "deprecated", michael@0: "engines": { michael@0: "Firefox": "*" michael@0: } michael@0: }; michael@0: michael@0: // Widget content types michael@0: const CONTENT_TYPE_URI = 1; michael@0: const CONTENT_TYPE_HTML = 2; michael@0: const CONTENT_TYPE_IMAGE = 3; michael@0: michael@0: const ERR_CONTENT = "No content or contentURL property found. Widgets must " michael@0: + "have one or the other.", michael@0: ERR_LABEL = "The widget must have a non-empty label property.", michael@0: ERR_ID = "You have to specify a unique value for the id property of " + michael@0: "your widget in order for the application to remember its " + michael@0: "position.", michael@0: ERR_DESTROYED = "The widget has been destroyed and can no longer be used."; michael@0: michael@0: const INSERTION_PREF_ROOT = "extensions.sdk-widget-inserted."; michael@0: michael@0: // Supported events, mapping from DOM event names to our event names michael@0: const EVENTS = { michael@0: "click": "click", michael@0: "mouseover": "mouseover", michael@0: "mouseout": "mouseout", michael@0: }; michael@0: michael@0: // In the Australis menu panel, normally widgets should be treated like michael@0: // normal toolbarbuttons. If they're any wider than this margin, we'll michael@0: // treat them as wide widgets instead, which fill up the width of the panel: michael@0: const AUSTRALIS_PANEL_WIDE_WIDGET_CUTOFF = 70; michael@0: const AUSTRALIS_PANEL_WIDE_CLASSNAME = "panel-wide-item"; michael@0: michael@0: const { validateOptions } = require("./deprecated/api-utils"); michael@0: const panels = require("./panel"); michael@0: const { EventEmitter, EventEmitterTrait } = require("./deprecated/events"); michael@0: const { Trait } = require("./deprecated/traits"); michael@0: const LightTrait = require('./deprecated/light-traits').Trait; michael@0: const { Loader, Symbiont } = require("./content/content"); michael@0: const { Cortex } = require('./deprecated/cortex'); michael@0: const windowsAPI = require("./windows"); michael@0: const { WindowTracker } = require("./deprecated/window-utils"); michael@0: const { isBrowser } = require("./window/utils"); michael@0: const { setTimeout } = require("./timers"); michael@0: const unload = require("./system/unload"); michael@0: const { getNodeView } = require("./view/core"); michael@0: const prefs = require('./preferences/service'); michael@0: michael@0: require("./util/deprecate").deprecateUsage( michael@0: "The widget module is deprecated. " + michael@0: "Please consider using the sdk/ui module instead." michael@0: ); michael@0: michael@0: // Data types definition michael@0: const valid = { michael@0: number: { is: ["null", "undefined", "number"] }, michael@0: string: { is: ["null", "undefined", "string"] }, michael@0: id: { michael@0: is: ["string"], michael@0: ok: function (v) v.length > 0, michael@0: msg: ERR_ID, michael@0: readonly: true michael@0: }, michael@0: label: { michael@0: is: ["string"], michael@0: ok: function (v) v.length > 0, michael@0: msg: ERR_LABEL michael@0: }, michael@0: panel: { michael@0: is: ["null", "undefined", "object"], michael@0: ok: function(v) !v || v instanceof panels.Panel michael@0: }, michael@0: width: { michael@0: is: ["null", "undefined", "number"], michael@0: map: function (v) { michael@0: if (null === v || undefined === v) v = 16; michael@0: return v; michael@0: }, michael@0: defaultValue: 16 michael@0: }, michael@0: allow: { michael@0: is: ["null", "undefined", "object"], michael@0: map: function (v) { michael@0: if (!v) v = { script: true }; michael@0: return v; michael@0: }, michael@0: get defaultValue() ({ script: true }) michael@0: }, michael@0: }; michael@0: michael@0: // Widgets attributes definition michael@0: let widgetAttributes = { michael@0: label: valid.label, michael@0: id: valid.id, michael@0: tooltip: valid.string, michael@0: width: valid.width, michael@0: content: valid.string, michael@0: panel: valid.panel, michael@0: allow: valid.allow michael@0: }; michael@0: michael@0: // Import data definitions from loader, but don't compose with it as Model michael@0: // functions allow us to recreate easily all Loader code. michael@0: let loaderAttributes = require("./content/loader").validationAttributes; michael@0: for (let i in loaderAttributes) michael@0: widgetAttributes[i] = loaderAttributes[i]; michael@0: michael@0: widgetAttributes.contentURL.optional = true; michael@0: michael@0: // Widgets public events list, that are automatically binded in options object michael@0: const WIDGET_EVENTS = [ michael@0: "click", michael@0: "mouseover", michael@0: "mouseout", michael@0: "error", michael@0: "message", michael@0: "attach" michael@0: ]; michael@0: michael@0: // `Model` utility functions that help creating these various Widgets objects michael@0: let model = { michael@0: michael@0: // Validate one attribute using api-utils.js:validateOptions function michael@0: _validate: function _validate(name, suspect, validation) { michael@0: let $1 = {}; michael@0: $1[name] = suspect; michael@0: let $2 = {}; michael@0: $2[name] = validation; michael@0: return validateOptions($1, $2)[name]; michael@0: }, michael@0: michael@0: /** michael@0: * This method has two purposes: michael@0: * 1/ Validate and define, on a given object, a set of attribute michael@0: * 2/ Emit a "change" event on this object when an attribute is changed michael@0: * michael@0: * @params {Object} object michael@0: * Object on which we can bind attributes on and watch for their changes. michael@0: * This object must have an EventEmitter interface, or, at least `_emit` michael@0: * method michael@0: * @params {Object} attrs michael@0: * Dictionary of attributes definition following api-utils:validateOptions michael@0: * scheme michael@0: * @params {Object} values michael@0: * Dictionary of attributes default values michael@0: */ michael@0: setAttributes: function setAttributes(object, attrs, values) { michael@0: let properties = {}; michael@0: for (let name in attrs) { michael@0: let value = values[name]; michael@0: let req = attrs[name]; michael@0: michael@0: // Retrieve default value from typedef if the value is not defined michael@0: if ((typeof value == "undefined" || value == null) && req.defaultValue) michael@0: value = req.defaultValue; michael@0: michael@0: // Check for valid value if value is defined or mandatory michael@0: if (!req.optional || typeof value != "undefined") michael@0: value = model._validate(name, value, req); michael@0: michael@0: // In any case, define this property on `object` michael@0: let property = null; michael@0: if (req.readonly) { michael@0: property = { michael@0: value: value, michael@0: writable: false, michael@0: enumerable: true, michael@0: configurable: false michael@0: }; michael@0: } michael@0: else { michael@0: property = model._createWritableProperty(name, value); michael@0: } michael@0: michael@0: properties[name] = property; michael@0: } michael@0: Object.defineProperties(object, properties); michael@0: }, michael@0: michael@0: // Generate ES5 property definition for a given attribute michael@0: _createWritableProperty: function _createWritableProperty(name, value) { michael@0: return { michael@0: get: function () { michael@0: return value; michael@0: }, michael@0: set: function (newValue) { michael@0: value = newValue; michael@0: // The main goal of all this Model stuff is here: michael@0: // We want to forward all changes to some listeners michael@0: this._emit("change", name, value); michael@0: }, michael@0: enumerable: true, michael@0: configurable: false michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Automagically register listeners in options dictionary michael@0: * by detecting listener attributes with name starting with `on` michael@0: * michael@0: * @params {Object} object michael@0: * Target object that need to follow EventEmitter interface, or, at least, michael@0: * having `on` method. michael@0: * @params {Array} events michael@0: * List of events name to automatically bind. michael@0: * @params {Object} listeners michael@0: * Dictionary of event listener functions to register. michael@0: */ michael@0: setEvents: function setEvents(object, events, listeners) { michael@0: for (let i = 0, l = events.length; i < l; i++) { michael@0: let name = events[i]; michael@0: let onName = "on" + name[0].toUpperCase() + name.substr(1); michael@0: if (!listeners[onName]) michael@0: continue; michael@0: object.on(name, listeners[onName].bind(object)); michael@0: } michael@0: } michael@0: michael@0: }; michael@0: michael@0: function saveInserted(widgetId) { michael@0: prefs.set(INSERTION_PREF_ROOT + widgetId, true); michael@0: } michael@0: michael@0: function haveInserted(widgetId) { michael@0: return prefs.has(INSERTION_PREF_ROOT + widgetId); michael@0: } michael@0: michael@0: const isWide = node => node.classList.contains(AUSTRALIS_PANEL_WIDE_CLASSNAME); michael@0: michael@0: /** michael@0: * Main Widget class: entry point of the widget API michael@0: * michael@0: * Allow to control all widget across all existing windows with a single object. michael@0: * Widget.getView allow to retrieve a WidgetView instance to control a widget michael@0: * specific to one window. michael@0: */ michael@0: const WidgetTrait = LightTrait.compose(EventEmitterTrait, LightTrait({ michael@0: michael@0: _initWidget: function _initWidget(options) { michael@0: model.setAttributes(this, widgetAttributes, options); michael@0: michael@0: browserManager.validate(this); michael@0: michael@0: // We must have at least content or contentURL defined michael@0: if (!(this.content || this.contentURL)) michael@0: throw new Error(ERR_CONTENT); michael@0: michael@0: this._views = []; michael@0: michael@0: // Set tooltip to label value if we don't have tooltip defined michael@0: if (!this.tooltip) michael@0: this.tooltip = this.label; michael@0: michael@0: model.setEvents(this, WIDGET_EVENTS, options); michael@0: michael@0: this.on('change', this._onChange.bind(this)); michael@0: michael@0: let self = this; michael@0: this._port = EventEmitterTrait.create({ michael@0: emit: function () { michael@0: let args = arguments; michael@0: self._views.forEach(function(v) v.port.emit.apply(v.port, args)); michael@0: } michael@0: }); michael@0: // expose wrapped port, that exposes only public properties. michael@0: this._port._public = Cortex(this._port); michael@0: michael@0: // Register this widget to browser manager in order to create new widget on michael@0: // all new windows michael@0: browserManager.addItem(this); michael@0: }, michael@0: michael@0: _onChange: function _onChange(name, value) { michael@0: // Set tooltip to label value if we don't have tooltip defined michael@0: if (name == 'tooltip' && !value) { michael@0: // we need to change tooltip again in order to change the value of the michael@0: // attribute itself michael@0: this.tooltip = this.label; michael@0: return; michael@0: } michael@0: michael@0: // Forward attributes changes to WidgetViews michael@0: if (['width', 'tooltip', 'content', 'contentURL'].indexOf(name) != -1) { michael@0: this._views.forEach(function(v) v[name] = value); michael@0: } michael@0: }, michael@0: michael@0: _onEvent: function _onEvent(type, eventData) { michael@0: this._emit(type, eventData); michael@0: }, michael@0: michael@0: _createView: function _createView() { michael@0: // Create a new WidgetView instance michael@0: let view = WidgetView(this); michael@0: michael@0: // Keep a reference to it michael@0: this._views.push(view); michael@0: michael@0: return view; michael@0: }, michael@0: michael@0: // a WidgetView instance is destroyed michael@0: _onViewDestroyed: function _onViewDestroyed(view) { michael@0: let idx = this._views.indexOf(view); michael@0: this._views.splice(idx, 1); michael@0: }, michael@0: michael@0: /** michael@0: * Called on browser window closed, to destroy related WidgetViews michael@0: * @params {ChromeWindow} window michael@0: * Window that has been closed michael@0: */ michael@0: _onWindowClosed: function _onWindowClosed(window) { michael@0: for each (let view in this._views) { michael@0: if (view._isInChromeWindow(window)) { michael@0: view.destroy(); michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Get the WidgetView instance related to a BrowserWindow instance michael@0: * @params {BrowserWindow} window michael@0: * BrowserWindow reference from "windows" module michael@0: */ michael@0: getView: function getView(window) { michael@0: for each (let view in this._views) { michael@0: if (view._isInWindow(window)) { michael@0: return view._public; michael@0: } michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: get port() this._port._public, michael@0: set port(v) {}, // Work around Cortex failure with getter without setter michael@0: // See bug 653464 michael@0: _port: null, michael@0: michael@0: postMessage: function postMessage(message) { michael@0: this._views.forEach(function(v) v.postMessage(message)); michael@0: }, michael@0: michael@0: destroy: function destroy() { michael@0: if (this.panel) michael@0: this.panel.destroy(); michael@0: michael@0: // Dispatch destroy calls to views michael@0: // we need to go backward as we remove items from this array in michael@0: // _onViewDestroyed michael@0: for (let i = this._views.length - 1; i >= 0; i--) michael@0: this._views[i].destroy(); michael@0: michael@0: // Unregister widget to stop creating it over new windows michael@0: // and allow creation of new widget with same id michael@0: browserManager.removeItem(this); michael@0: } michael@0: michael@0: })); michael@0: michael@0: // Widget constructor michael@0: const Widget = function Widget(options) { michael@0: let w = WidgetTrait.create(Widget.prototype); michael@0: w._initWidget(options); michael@0: michael@0: // Return a Cortex of widget in order to hide private attributes like _onEvent michael@0: let _public = Cortex(w); michael@0: unload.ensure(_public, "destroy"); michael@0: return _public; michael@0: } michael@0: exports.Widget = Widget; michael@0: michael@0: michael@0: /** michael@0: * WidgetView is an instance of a widget for a specific window. michael@0: * michael@0: * This is an external API that can be retrieved by calling Widget.getView or michael@0: * by watching `attach` event on Widget. michael@0: */ michael@0: const WidgetViewTrait = LightTrait.compose(EventEmitterTrait, LightTrait({ michael@0: michael@0: // Reference to the matching WidgetChrome michael@0: // set right after constructor call michael@0: _chrome: null, michael@0: michael@0: // Public interface of the WidgetView, passed in `attach` event or in michael@0: // Widget.getView michael@0: _public: null, michael@0: michael@0: _initWidgetView: function WidgetView__initWidgetView(baseWidget) { michael@0: this._baseWidget = baseWidget; michael@0: michael@0: model.setAttributes(this, widgetAttributes, baseWidget); michael@0: michael@0: this.on('change', this._onChange.bind(this)); michael@0: michael@0: let self = this; michael@0: this._port = EventEmitterTrait.create({ michael@0: emit: function () { michael@0: if (!self._chrome) michael@0: throw new Error(ERR_DESTROYED); michael@0: self._chrome.update(self._baseWidget, "emit", arguments); michael@0: } michael@0: }); michael@0: // expose wrapped port, that exposes only public properties. michael@0: this._port._public = Cortex(this._port); michael@0: michael@0: this._public = Cortex(this); michael@0: }, michael@0: michael@0: // Called by WidgetChrome, when the related Worker is applied to the document, michael@0: // so that we can start sending events to it michael@0: _onWorkerReady: function () { michael@0: // Emit an `attach` event with a WidgetView instance without private attrs michael@0: this._baseWidget._emit("attach", this._public); michael@0: }, michael@0: michael@0: _onChange: function WidgetView__onChange(name, value) { michael@0: if (name == 'tooltip' && !value) { michael@0: this.tooltip = this.label; michael@0: return; michael@0: } michael@0: michael@0: // Forward attributes changes to WidgetChrome instance michael@0: if (['width', 'tooltip', 'content', 'contentURL'].indexOf(name) != -1) { michael@0: this._chrome.update(this._baseWidget, name, value); michael@0: } michael@0: }, michael@0: michael@0: _onEvent: function WidgetView__onEvent(type, eventData, domNode) { michael@0: // Dispatch event in view michael@0: this._emit(type, eventData); michael@0: michael@0: // And forward it to the main Widget object michael@0: if ("click" == type || type.indexOf("mouse") == 0) michael@0: this._baseWidget._onEvent(type, this._public); michael@0: else michael@0: this._baseWidget._onEvent(type, eventData); michael@0: michael@0: // Special case for click events: if the widget doesn't have a click michael@0: // handler, but it does have a panel, display the panel. michael@0: if ("click" == type && !this._listeners("click").length && this.panel) { michael@0: // This kind of ugly workaround, instead we should implement michael@0: // `getNodeView` for the `Widget` class itself, but that's kind of michael@0: // hard without cleaning things up. michael@0: this.panel.show(null, getNodeView.implement({}, () => domNode)); michael@0: } michael@0: }, michael@0: michael@0: _isInWindow: function WidgetView__isInWindow(window) { michael@0: return windowsAPI.BrowserWindow({ michael@0: window: this._chrome.window michael@0: }) == window; michael@0: }, michael@0: michael@0: _isInChromeWindow: function WidgetView__isInChromeWindow(window) { michael@0: return this._chrome.window == window; michael@0: }, michael@0: michael@0: _onPortEvent: function WidgetView__onPortEvent(args) { michael@0: let port = this._port; michael@0: port._emit.apply(port, args); michael@0: let basePort = this._baseWidget._port; michael@0: basePort._emit.apply(basePort, args); michael@0: }, michael@0: michael@0: get port() this._port._public, michael@0: set port(v) {}, // Work around Cortex failure with getter without setter michael@0: // See bug 653464 michael@0: _port: null, michael@0: michael@0: postMessage: function WidgetView_postMessage(message) { michael@0: if (!this._chrome) michael@0: throw new Error(ERR_DESTROYED); michael@0: this._chrome.update(this._baseWidget, "postMessage", message); michael@0: }, michael@0: michael@0: destroy: function WidgetView_destroy() { michael@0: this._chrome.destroy(); michael@0: delete this._chrome; michael@0: this._baseWidget._onViewDestroyed(this); michael@0: this._emit("detach"); michael@0: } michael@0: michael@0: })); michael@0: michael@0: michael@0: const WidgetView = function WidgetView(baseWidget) { michael@0: let w = WidgetViewTrait.create(WidgetView.prototype); michael@0: w._initWidgetView(baseWidget); michael@0: return w; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Keeps track of all browser windows. michael@0: * Exposes methods for adding/removing widgets michael@0: * across all open windows (and future ones). michael@0: * Create a new instance of BrowserWindow per window. michael@0: */ michael@0: let browserManager = { michael@0: items: [], michael@0: windows: [], michael@0: michael@0: // Registers the manager to listen for window openings and closings. Note michael@0: // that calling this method can cause onTrack to be called immediately if michael@0: // there are open windows. michael@0: init: function () { michael@0: let windowTracker = new WindowTracker(this); michael@0: unload.ensure(windowTracker); michael@0: }, michael@0: michael@0: // Registers a window with the manager. This is a WindowTracker callback. michael@0: onTrack: function browserManager_onTrack(window) { michael@0: if (isBrowser(window)) { michael@0: let win = new BrowserWindow(window); michael@0: win.addItems(this.items); michael@0: this.windows.push(win); michael@0: } michael@0: }, michael@0: michael@0: // Unregisters a window from the manager. It's told to undo all michael@0: // modifications. This is a WindowTracker callback. Note that when michael@0: // WindowTracker is unloaded, it calls onUntrack for every currently opened michael@0: // window. The browserManager therefore doesn't need to specially handle michael@0: // unload itself, since unloading the browserManager means untracking all michael@0: // currently opened windows. michael@0: onUntrack: function browserManager_onUntrack(window) { michael@0: if (isBrowser(window)) { michael@0: this.items.forEach(function(i) i._onWindowClosed(window)); michael@0: for (let i = 0; i < this.windows.length; i++) { michael@0: if (this.windows[i].window == window) { michael@0: this.windows.splice(i, 1)[0]; michael@0: return; michael@0: } michael@0: } michael@0: michael@0: } michael@0: }, michael@0: michael@0: // Used to validate widget by browserManager before adding it, michael@0: // in order to check input very early in widget constructor michael@0: validate : function (item) { michael@0: let idx = this.items.indexOf(item); michael@0: if (idx > -1) michael@0: throw new Error("The widget " + item + " has already been added."); michael@0: if (item.id) { michael@0: let sameId = this.items.filter(function(i) i.id == item.id); michael@0: if (sameId.length > 0) michael@0: throw new Error("This widget ID is already used: " + item.id); michael@0: } else { michael@0: item.id = this.items.length; michael@0: } michael@0: }, michael@0: michael@0: // Registers an item with the manager. It's added to all currently registered michael@0: // windows, and when new windows are registered it will be added to them, too. michael@0: addItem: function browserManager_addItem(item) { michael@0: this.items.push(item); michael@0: this.windows.forEach(function (w) w.addItems([item])); michael@0: }, michael@0: michael@0: // Unregisters an item from the manager. It's removed from all windows that michael@0: // are currently registered. michael@0: removeItem: function browserManager_removeItem(item) { michael@0: let idx = this.items.indexOf(item); michael@0: if (idx > -1) michael@0: this.items.splice(idx, 1); michael@0: }, michael@0: propagateCurrentset: function browserManager_propagateCurrentset(id, currentset) { michael@0: this.windows.forEach(function (w) w.doc.getElementById(id).setAttribute("currentset", currentset)); michael@0: } michael@0: }; michael@0: michael@0: michael@0: michael@0: /** michael@0: * Keeps track of a single browser window. michael@0: * michael@0: * This is where the core of how a widget's content is added to a window lives. michael@0: */ michael@0: function BrowserWindow(window) { michael@0: this.window = window; michael@0: this.doc = window.document; michael@0: } michael@0: michael@0: BrowserWindow.prototype = { michael@0: // Adds an array of items to the window. michael@0: addItems: function BW_addItems(items) { michael@0: items.forEach(this._addItemToWindow, this); michael@0: }, michael@0: michael@0: _addItemToWindow: function BW__addItemToWindow(baseWidget) { michael@0: // Create a WidgetView instance michael@0: let widget = baseWidget._createView(); michael@0: michael@0: // Create a WidgetChrome instance michael@0: let item = new WidgetChrome({ michael@0: widget: widget, michael@0: doc: this.doc, michael@0: window: this.window michael@0: }); michael@0: michael@0: widget._chrome = item; michael@0: michael@0: this._insertNodeInToolbar(item.node); michael@0: michael@0: // We need to insert Widget DOM Node before finishing widget view creation michael@0: // (because fill creates an iframe and tries to access its docShell) michael@0: item.fill(); michael@0: }, michael@0: michael@0: _insertNodeInToolbar: function BW__insertNodeInToolbar(node) { michael@0: // Add to the customization palette michael@0: let toolbox = this.doc.getElementById("navigator-toolbox"); michael@0: let palette = toolbox.palette; michael@0: palette.appendChild(node); michael@0: michael@0: let { CustomizableUI } = this.window; michael@0: let { id } = node; michael@0: michael@0: let placement = CustomizableUI.getPlacementOfWidget(id); michael@0: michael@0: if (!placement) { michael@0: if (haveInserted(id) || isWide(node)) michael@0: return; michael@0: michael@0: placement = {area: 'nav-bar', position: undefined}; michael@0: saveInserted(id); michael@0: } michael@0: michael@0: CustomizableUI.addWidgetToArea(id, placement.area, placement.position); michael@0: CustomizableUI.ensureWidgetPlacedInWindow(id, this.window); michael@0: } michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Final Widget class that handles chrome DOM Node: michael@0: * - create initial DOM nodes michael@0: * - receive instruction from WidgetView through update method and update DOM michael@0: * - watch for DOM events and forward them to WidgetView michael@0: */ michael@0: function WidgetChrome(options) { michael@0: this.window = options.window; michael@0: this._doc = options.doc; michael@0: this._widget = options.widget; michael@0: this._symbiont = null; // set later michael@0: this.node = null; // set later michael@0: michael@0: this._createNode(); michael@0: } michael@0: michael@0: // Update a property of a widget. michael@0: WidgetChrome.prototype.update = function WC_update(updatedItem, property, value) { michael@0: switch(property) { michael@0: case "contentURL": michael@0: case "content": michael@0: this.setContent(); michael@0: break; michael@0: case "width": michael@0: this.node.style.minWidth = value + "px"; michael@0: this.node.querySelector("iframe").style.width = value + "px"; michael@0: break; michael@0: case "tooltip": michael@0: this.node.setAttribute("tooltiptext", value); michael@0: break; michael@0: case "postMessage": michael@0: this._symbiont.postMessage(value); michael@0: break; michael@0: case "emit": michael@0: let port = this._symbiont.port; michael@0: port.emit.apply(port, value); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: // Add a widget to this window. michael@0: WidgetChrome.prototype._createNode = function WC__createNode() { michael@0: // XUL element container for widget michael@0: let node = this._doc.createElement("toolbaritem"); michael@0: michael@0: // Temporary work around require("self") failing on unit-test execution ... michael@0: let jetpackID = "testID"; michael@0: try { michael@0: jetpackID = require("./self").id; michael@0: } catch(e) {} michael@0: michael@0: // Compute an unique and stable widget id with jetpack id and widget.id michael@0: let id = "widget:" + jetpackID + "-" + this._widget.id; michael@0: node.setAttribute("id", id); michael@0: node.setAttribute("label", this._widget.label); michael@0: node.setAttribute("tooltiptext", this._widget.tooltip); michael@0: node.setAttribute("align", "center"); michael@0: // Bug 626326: Prevent customize toolbar context menu to appear michael@0: node.setAttribute("context", ""); michael@0: michael@0: // For use in styling by the browser michael@0: node.setAttribute("sdkstylewidget", "true"); michael@0: michael@0: if (this._widget.width > AUSTRALIS_PANEL_WIDE_WIDGET_CUTOFF) { michael@0: node.classList.add(AUSTRALIS_PANEL_WIDE_CLASSNAME); michael@0: } michael@0: michael@0: // TODO move into a stylesheet, configurable by consumers. michael@0: // Either widget.style, exposing the style object, or a URL michael@0: // (eg, can load local stylesheet file). michael@0: node.setAttribute("style", [ michael@0: "overflow: hidden; margin: 1px 2px 1px 2px; padding: 0px;", michael@0: "min-height: 16px;", michael@0: ].join("")); michael@0: michael@0: node.style.minWidth = this._widget.width + "px"; michael@0: michael@0: this.node = node; michael@0: } michael@0: michael@0: // Initial population of a widget's content. michael@0: WidgetChrome.prototype.fill = function WC_fill() { michael@0: let { node, _doc: document } = this; michael@0: michael@0: // Create element michael@0: let iframe = document.createElement("iframe"); michael@0: iframe.setAttribute("type", "content"); michael@0: iframe.setAttribute("transparent", "transparent"); michael@0: iframe.style.overflow = "hidden"; michael@0: iframe.style.height = "16px"; michael@0: iframe.style.maxHeight = "16px"; michael@0: iframe.style.width = this._widget.width + "px"; michael@0: iframe.setAttribute("flex", "1"); michael@0: iframe.style.border = "none"; michael@0: iframe.style.padding = "0px"; michael@0: michael@0: // Do this early, because things like contentWindow are null michael@0: // until the node is attached to a document. michael@0: node.appendChild(iframe); michael@0: michael@0: let label = document.createElement("label"); michael@0: label.setAttribute("value", this._widget.label); michael@0: label.className = "toolbarbutton-text"; michael@0: label.setAttribute("crop", "right"); michael@0: label.setAttribute("flex", "1"); michael@0: node.appendChild(label); michael@0: michael@0: // This toolbarbutton is created to provide a more consistent user experience michael@0: // during customization, see: michael@0: // https://bugzilla.mozilla.org/show_bug.cgi?id=959640 michael@0: let button = document.createElement("toolbarbutton"); michael@0: button.setAttribute("label", this._widget.label); michael@0: button.setAttribute("crop", "right"); michael@0: button.className = "toolbarbutton-1 chromeclass-toolbar-additional"; michael@0: node.appendChild(button); michael@0: michael@0: // add event handlers michael@0: this.addEventHandlers(); michael@0: michael@0: // set content michael@0: this.setContent(); michael@0: } michael@0: michael@0: // Get widget content type. michael@0: WidgetChrome.prototype.getContentType = function WC_getContentType() { michael@0: if (this._widget.content) michael@0: return CONTENT_TYPE_HTML; michael@0: return (this._widget.contentURL && /\.(jpg|gif|png|ico|svg)$/i.test(this._widget.contentURL)) michael@0: ? CONTENT_TYPE_IMAGE : CONTENT_TYPE_URI; michael@0: } michael@0: michael@0: // Set widget content. michael@0: WidgetChrome.prototype.setContent = function WC_setContent() { michael@0: let type = this.getContentType(); michael@0: let contentURL = null; michael@0: michael@0: switch (type) { michael@0: case CONTENT_TYPE_HTML: michael@0: contentURL = "data:text/html;charset=utf-8," + encodeURIComponent(this._widget.content); michael@0: break; michael@0: case CONTENT_TYPE_URI: michael@0: contentURL = this._widget.contentURL; michael@0: break; michael@0: case CONTENT_TYPE_IMAGE: michael@0: let imageURL = this._widget.contentURL; michael@0: contentURL = "data:text/html;charset=utf-8,