addon-sdk/source/lib/sdk/widget.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

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.

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

mercurial