12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007 |
- /* 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,<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 &&*/ 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();
|