widget.js 31 KB

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