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