|
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 |
|
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 }; |
|
14 |
|
15 // Widget content types |
|
16 const CONTENT_TYPE_URI = 1; |
|
17 const CONTENT_TYPE_HTML = 2; |
|
18 const CONTENT_TYPE_IMAGE = 3; |
|
19 |
|
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."; |
|
27 |
|
28 const INSERTION_PREF_ROOT = "extensions.sdk-widget-inserted."; |
|
29 |
|
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 }; |
|
36 |
|
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"; |
|
42 |
|
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'); |
|
57 |
|
58 require("./util/deprecate").deprecateUsage( |
|
59 "The widget module is deprecated. " + |
|
60 "Please consider using the sdk/ui module instead." |
|
61 ); |
|
62 |
|
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 }; |
|
99 |
|
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 }; |
|
110 |
|
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]; |
|
116 |
|
117 widgetAttributes.contentURL.optional = true; |
|
118 |
|
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 ]; |
|
128 |
|
129 // `Model` utility functions that help creating these various Widgets objects |
|
130 let model = { |
|
131 |
|
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 }, |
|
140 |
|
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]; |
|
161 |
|
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; |
|
165 |
|
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); |
|
169 |
|
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 } |
|
183 |
|
184 properties[name] = property; |
|
185 } |
|
186 Object.defineProperties(object, properties); |
|
187 }, |
|
188 |
|
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 }, |
|
205 |
|
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 } |
|
227 |
|
228 }; |
|
229 |
|
230 function saveInserted(widgetId) { |
|
231 prefs.set(INSERTION_PREF_ROOT + widgetId, true); |
|
232 } |
|
233 |
|
234 function haveInserted(widgetId) { |
|
235 return prefs.has(INSERTION_PREF_ROOT + widgetId); |
|
236 } |
|
237 |
|
238 const isWide = node => node.classList.contains(AUSTRALIS_PANEL_WIDE_CLASSNAME); |
|
239 |
|
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({ |
|
248 |
|
249 _initWidget: function _initWidget(options) { |
|
250 model.setAttributes(this, widgetAttributes, options); |
|
251 |
|
252 browserManager.validate(this); |
|
253 |
|
254 // We must have at least content or contentURL defined |
|
255 if (!(this.content || this.contentURL)) |
|
256 throw new Error(ERR_CONTENT); |
|
257 |
|
258 this._views = []; |
|
259 |
|
260 // Set tooltip to label value if we don't have tooltip defined |
|
261 if (!this.tooltip) |
|
262 this.tooltip = this.label; |
|
263 |
|
264 model.setEvents(this, WIDGET_EVENTS, options); |
|
265 |
|
266 this.on('change', this._onChange.bind(this)); |
|
267 |
|
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); |
|
277 |
|
278 // Register this widget to browser manager in order to create new widget on |
|
279 // all new windows |
|
280 browserManager.addItem(this); |
|
281 }, |
|
282 |
|
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 } |
|
291 |
|
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 }, |
|
297 |
|
298 _onEvent: function _onEvent(type, eventData) { |
|
299 this._emit(type, eventData); |
|
300 }, |
|
301 |
|
302 _createView: function _createView() { |
|
303 // Create a new WidgetView instance |
|
304 let view = WidgetView(this); |
|
305 |
|
306 // Keep a reference to it |
|
307 this._views.push(view); |
|
308 |
|
309 return view; |
|
310 }, |
|
311 |
|
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 }, |
|
317 |
|
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 }, |
|
331 |
|
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 }, |
|
345 |
|
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, |
|
350 |
|
351 postMessage: function postMessage(message) { |
|
352 this._views.forEach(function(v) v.postMessage(message)); |
|
353 }, |
|
354 |
|
355 destroy: function destroy() { |
|
356 if (this.panel) |
|
357 this.panel.destroy(); |
|
358 |
|
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(); |
|
364 |
|
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 } |
|
369 |
|
370 })); |
|
371 |
|
372 // Widget constructor |
|
373 const Widget = function Widget(options) { |
|
374 let w = WidgetTrait.create(Widget.prototype); |
|
375 w._initWidget(options); |
|
376 |
|
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; |
|
383 |
|
384 |
|
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({ |
|
392 |
|
393 // Reference to the matching WidgetChrome |
|
394 // set right after constructor call |
|
395 _chrome: null, |
|
396 |
|
397 // Public interface of the WidgetView, passed in `attach` event or in |
|
398 // Widget.getView |
|
399 _public: null, |
|
400 |
|
401 _initWidgetView: function WidgetView__initWidgetView(baseWidget) { |
|
402 this._baseWidget = baseWidget; |
|
403 |
|
404 model.setAttributes(this, widgetAttributes, baseWidget); |
|
405 |
|
406 this.on('change', this._onChange.bind(this)); |
|
407 |
|
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); |
|
418 |
|
419 this._public = Cortex(this); |
|
420 }, |
|
421 |
|
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 }, |
|
428 |
|
429 _onChange: function WidgetView__onChange(name, value) { |
|
430 if (name == 'tooltip' && !value) { |
|
431 this.tooltip = this.label; |
|
432 return; |
|
433 } |
|
434 |
|
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 }, |
|
440 |
|
441 _onEvent: function WidgetView__onEvent(type, eventData, domNode) { |
|
442 // Dispatch event in view |
|
443 this._emit(type, eventData); |
|
444 |
|
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); |
|
450 |
|
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 }, |
|
460 |
|
461 _isInWindow: function WidgetView__isInWindow(window) { |
|
462 return windowsAPI.BrowserWindow({ |
|
463 window: this._chrome.window |
|
464 }) == window; |
|
465 }, |
|
466 |
|
467 _isInChromeWindow: function WidgetView__isInChromeWindow(window) { |
|
468 return this._chrome.window == window; |
|
469 }, |
|
470 |
|
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 }, |
|
477 |
|
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, |
|
482 |
|
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 }, |
|
488 |
|
489 destroy: function WidgetView_destroy() { |
|
490 this._chrome.destroy(); |
|
491 delete this._chrome; |
|
492 this._baseWidget._onViewDestroyed(this); |
|
493 this._emit("detach"); |
|
494 } |
|
495 |
|
496 })); |
|
497 |
|
498 |
|
499 const WidgetView = function WidgetView(baseWidget) { |
|
500 let w = WidgetViewTrait.create(WidgetView.prototype); |
|
501 w._initWidgetView(baseWidget); |
|
502 return w; |
|
503 } |
|
504 |
|
505 |
|
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: [], |
|
515 |
|
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 }, |
|
523 |
|
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 }, |
|
532 |
|
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 } |
|
548 |
|
549 } |
|
550 }, |
|
551 |
|
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 }, |
|
566 |
|
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 }, |
|
573 |
|
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 }; |
|
585 |
|
586 |
|
587 |
|
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 } |
|
597 |
|
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 }, |
|
603 |
|
604 _addItemToWindow: function BW__addItemToWindow(baseWidget) { |
|
605 // Create a WidgetView instance |
|
606 let widget = baseWidget._createView(); |
|
607 |
|
608 // Create a WidgetChrome instance |
|
609 let item = new WidgetChrome({ |
|
610 widget: widget, |
|
611 doc: this.doc, |
|
612 window: this.window |
|
613 }); |
|
614 |
|
615 widget._chrome = item; |
|
616 |
|
617 this._insertNodeInToolbar(item.node); |
|
618 |
|
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 }, |
|
623 |
|
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); |
|
629 |
|
630 let { CustomizableUI } = this.window; |
|
631 let { id } = node; |
|
632 |
|
633 let placement = CustomizableUI.getPlacementOfWidget(id); |
|
634 |
|
635 if (!placement) { |
|
636 if (haveInserted(id) || isWide(node)) |
|
637 return; |
|
638 |
|
639 placement = {area: 'nav-bar', position: undefined}; |
|
640 saveInserted(id); |
|
641 } |
|
642 |
|
643 CustomizableUI.addWidgetToArea(id, placement.area, placement.position); |
|
644 CustomizableUI.ensureWidgetPlacedInWindow(id, this.window); |
|
645 } |
|
646 } |
|
647 |
|
648 |
|
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 |
|
661 |
|
662 this._createNode(); |
|
663 } |
|
664 |
|
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 } |
|
688 |
|
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"); |
|
693 |
|
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) {} |
|
699 |
|
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", ""); |
|
708 |
|
709 // For use in styling by the browser |
|
710 node.setAttribute("sdkstylewidget", "true"); |
|
711 |
|
712 if (this._widget.width > AUSTRALIS_PANEL_WIDE_WIDGET_CUTOFF) { |
|
713 node.classList.add(AUSTRALIS_PANEL_WIDE_CLASSNAME); |
|
714 } |
|
715 |
|
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("")); |
|
723 |
|
724 node.style.minWidth = this._widget.width + "px"; |
|
725 |
|
726 this.node = node; |
|
727 } |
|
728 |
|
729 // Initial population of a widget's content. |
|
730 WidgetChrome.prototype.fill = function WC_fill() { |
|
731 let { node, _doc: document } = this; |
|
732 |
|
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"; |
|
744 |
|
745 // Do this early, because things like contentWindow are null |
|
746 // until the node is attached to a document. |
|
747 node.appendChild(iframe); |
|
748 |
|
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); |
|
755 |
|
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); |
|
764 |
|
765 // add event handlers |
|
766 this.addEventHandlers(); |
|
767 |
|
768 // set content |
|
769 this.setContent(); |
|
770 } |
|
771 |
|
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 } |
|
779 |
|
780 // Set widget content. |
|
781 WidgetChrome.prototype.setContent = function WC_setContent() { |
|
782 let type = this.getContentType(); |
|
783 let contentURL = null; |
|
784 |
|
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 } |
|
800 |
|
801 let iframe = this.node.firstElementChild; |
|
802 |
|
803 let self = this; |
|
804 // Cleanup previously created symbiont (in case we are update content) |
|
805 if (this._symbiont) |
|
806 this._symbiont.destroy(); |
|
807 |
|
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 } |
|
837 |
|
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 } |
|
844 |
|
845 // Set up all supported events for a widget. |
|
846 WidgetChrome.prototype.addEventHandlers = function WC_addEventHandlers() { |
|
847 let contentType = this.getContentType(); |
|
848 |
|
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; |
|
854 |
|
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; |
|
859 |
|
860 // Proxy event to the widget |
|
861 setTimeout(function() { |
|
862 self._widget._onEvent(EVENTS[e.type], null, self.node); |
|
863 }, 0); |
|
864 }; |
|
865 |
|
866 this.eventListeners = {}; |
|
867 let iframe = this.node.firstElementChild; |
|
868 for (let type in EVENTS) { |
|
869 iframe.addEventListener(type, listener, true, true); |
|
870 |
|
871 // Store listeners for later removal |
|
872 this.eventListeners[type] = listener; |
|
873 } |
|
874 |
|
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; |
|
885 |
|
886 // We may have had an unload event before that cleaned up the symbiont |
|
887 if (!self._symbiont) |
|
888 self.setContent(); |
|
889 |
|
890 let doc = e.target; |
|
891 |
|
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 } |
|
898 |
|
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 } |
|
908 |
|
909 iframe.addEventListener("load", loadListener, true); |
|
910 this.eventListeners["load"] = loadListener; |
|
911 |
|
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) {} |
|
924 |
|
925 } |
|
926 |
|
927 iframe.addEventListener("unload", unloadListener, true); |
|
928 this.eventListeners["unload"] = unloadListener; |
|
929 } |
|
930 |
|
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 } |
|
947 |
|
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(); |