1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/addon-sdk/source/lib/sdk/widget.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,951 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 +"use strict"; 1.8 + 1.9 +// The widget module currently supports only Firefox. 1.10 +// See: https://bugzilla.mozilla.org/show_bug.cgi?id=560716 1.11 +module.metadata = { 1.12 + "stability": "deprecated", 1.13 + "engines": { 1.14 + "Firefox": "*" 1.15 + } 1.16 +}; 1.17 + 1.18 +// Widget content types 1.19 +const CONTENT_TYPE_URI = 1; 1.20 +const CONTENT_TYPE_HTML = 2; 1.21 +const CONTENT_TYPE_IMAGE = 3; 1.22 + 1.23 +const ERR_CONTENT = "No content or contentURL property found. Widgets must " 1.24 + + "have one or the other.", 1.25 + ERR_LABEL = "The widget must have a non-empty label property.", 1.26 + ERR_ID = "You have to specify a unique value for the id property of " + 1.27 + "your widget in order for the application to remember its " + 1.28 + "position.", 1.29 + ERR_DESTROYED = "The widget has been destroyed and can no longer be used."; 1.30 + 1.31 +const INSERTION_PREF_ROOT = "extensions.sdk-widget-inserted."; 1.32 + 1.33 +// Supported events, mapping from DOM event names to our event names 1.34 +const EVENTS = { 1.35 + "click": "click", 1.36 + "mouseover": "mouseover", 1.37 + "mouseout": "mouseout", 1.38 +}; 1.39 + 1.40 +// In the Australis menu panel, normally widgets should be treated like 1.41 +// normal toolbarbuttons. If they're any wider than this margin, we'll 1.42 +// treat them as wide widgets instead, which fill up the width of the panel: 1.43 +const AUSTRALIS_PANEL_WIDE_WIDGET_CUTOFF = 70; 1.44 +const AUSTRALIS_PANEL_WIDE_CLASSNAME = "panel-wide-item"; 1.45 + 1.46 +const { validateOptions } = require("./deprecated/api-utils"); 1.47 +const panels = require("./panel"); 1.48 +const { EventEmitter, EventEmitterTrait } = require("./deprecated/events"); 1.49 +const { Trait } = require("./deprecated/traits"); 1.50 +const LightTrait = require('./deprecated/light-traits').Trait; 1.51 +const { Loader, Symbiont } = require("./content/content"); 1.52 +const { Cortex } = require('./deprecated/cortex'); 1.53 +const windowsAPI = require("./windows"); 1.54 +const { WindowTracker } = require("./deprecated/window-utils"); 1.55 +const { isBrowser } = require("./window/utils"); 1.56 +const { setTimeout } = require("./timers"); 1.57 +const unload = require("./system/unload"); 1.58 +const { getNodeView } = require("./view/core"); 1.59 +const prefs = require('./preferences/service'); 1.60 + 1.61 +require("./util/deprecate").deprecateUsage( 1.62 + "The widget module is deprecated. " + 1.63 + "Please consider using the sdk/ui module instead." 1.64 +); 1.65 + 1.66 +// Data types definition 1.67 +const valid = { 1.68 + number: { is: ["null", "undefined", "number"] }, 1.69 + string: { is: ["null", "undefined", "string"] }, 1.70 + id: { 1.71 + is: ["string"], 1.72 + ok: function (v) v.length > 0, 1.73 + msg: ERR_ID, 1.74 + readonly: true 1.75 + }, 1.76 + label: { 1.77 + is: ["string"], 1.78 + ok: function (v) v.length > 0, 1.79 + msg: ERR_LABEL 1.80 + }, 1.81 + panel: { 1.82 + is: ["null", "undefined", "object"], 1.83 + ok: function(v) !v || v instanceof panels.Panel 1.84 + }, 1.85 + width: { 1.86 + is: ["null", "undefined", "number"], 1.87 + map: function (v) { 1.88 + if (null === v || undefined === v) v = 16; 1.89 + return v; 1.90 + }, 1.91 + defaultValue: 16 1.92 + }, 1.93 + allow: { 1.94 + is: ["null", "undefined", "object"], 1.95 + map: function (v) { 1.96 + if (!v) v = { script: true }; 1.97 + return v; 1.98 + }, 1.99 + get defaultValue() ({ script: true }) 1.100 + }, 1.101 +}; 1.102 + 1.103 +// Widgets attributes definition 1.104 +let widgetAttributes = { 1.105 + label: valid.label, 1.106 + id: valid.id, 1.107 + tooltip: valid.string, 1.108 + width: valid.width, 1.109 + content: valid.string, 1.110 + panel: valid.panel, 1.111 + allow: valid.allow 1.112 +}; 1.113 + 1.114 +// Import data definitions from loader, but don't compose with it as Model 1.115 +// functions allow us to recreate easily all Loader code. 1.116 +let loaderAttributes = require("./content/loader").validationAttributes; 1.117 +for (let i in loaderAttributes) 1.118 + widgetAttributes[i] = loaderAttributes[i]; 1.119 + 1.120 +widgetAttributes.contentURL.optional = true; 1.121 + 1.122 +// Widgets public events list, that are automatically binded in options object 1.123 +const WIDGET_EVENTS = [ 1.124 + "click", 1.125 + "mouseover", 1.126 + "mouseout", 1.127 + "error", 1.128 + "message", 1.129 + "attach" 1.130 +]; 1.131 + 1.132 +// `Model` utility functions that help creating these various Widgets objects 1.133 +let model = { 1.134 + 1.135 + // Validate one attribute using api-utils.js:validateOptions function 1.136 + _validate: function _validate(name, suspect, validation) { 1.137 + let $1 = {}; 1.138 + $1[name] = suspect; 1.139 + let $2 = {}; 1.140 + $2[name] = validation; 1.141 + return validateOptions($1, $2)[name]; 1.142 + }, 1.143 + 1.144 + /** 1.145 + * This method has two purposes: 1.146 + * 1/ Validate and define, on a given object, a set of attribute 1.147 + * 2/ Emit a "change" event on this object when an attribute is changed 1.148 + * 1.149 + * @params {Object} object 1.150 + * Object on which we can bind attributes on and watch for their changes. 1.151 + * This object must have an EventEmitter interface, or, at least `_emit` 1.152 + * method 1.153 + * @params {Object} attrs 1.154 + * Dictionary of attributes definition following api-utils:validateOptions 1.155 + * scheme 1.156 + * @params {Object} values 1.157 + * Dictionary of attributes default values 1.158 + */ 1.159 + setAttributes: function setAttributes(object, attrs, values) { 1.160 + let properties = {}; 1.161 + for (let name in attrs) { 1.162 + let value = values[name]; 1.163 + let req = attrs[name]; 1.164 + 1.165 + // Retrieve default value from typedef if the value is not defined 1.166 + if ((typeof value == "undefined" || value == null) && req.defaultValue) 1.167 + value = req.defaultValue; 1.168 + 1.169 + // Check for valid value if value is defined or mandatory 1.170 + if (!req.optional || typeof value != "undefined") 1.171 + value = model._validate(name, value, req); 1.172 + 1.173 + // In any case, define this property on `object` 1.174 + let property = null; 1.175 + if (req.readonly) { 1.176 + property = { 1.177 + value: value, 1.178 + writable: false, 1.179 + enumerable: true, 1.180 + configurable: false 1.181 + }; 1.182 + } 1.183 + else { 1.184 + property = model._createWritableProperty(name, value); 1.185 + } 1.186 + 1.187 + properties[name] = property; 1.188 + } 1.189 + Object.defineProperties(object, properties); 1.190 + }, 1.191 + 1.192 + // Generate ES5 property definition for a given attribute 1.193 + _createWritableProperty: function _createWritableProperty(name, value) { 1.194 + return { 1.195 + get: function () { 1.196 + return value; 1.197 + }, 1.198 + set: function (newValue) { 1.199 + value = newValue; 1.200 + // The main goal of all this Model stuff is here: 1.201 + // We want to forward all changes to some listeners 1.202 + this._emit("change", name, value); 1.203 + }, 1.204 + enumerable: true, 1.205 + configurable: false 1.206 + }; 1.207 + }, 1.208 + 1.209 + /** 1.210 + * Automagically register listeners in options dictionary 1.211 + * by detecting listener attributes with name starting with `on` 1.212 + * 1.213 + * @params {Object} object 1.214 + * Target object that need to follow EventEmitter interface, or, at least, 1.215 + * having `on` method. 1.216 + * @params {Array} events 1.217 + * List of events name to automatically bind. 1.218 + * @params {Object} listeners 1.219 + * Dictionary of event listener functions to register. 1.220 + */ 1.221 + setEvents: function setEvents(object, events, listeners) { 1.222 + for (let i = 0, l = events.length; i < l; i++) { 1.223 + let name = events[i]; 1.224 + let onName = "on" + name[0].toUpperCase() + name.substr(1); 1.225 + if (!listeners[onName]) 1.226 + continue; 1.227 + object.on(name, listeners[onName].bind(object)); 1.228 + } 1.229 + } 1.230 + 1.231 +}; 1.232 + 1.233 +function saveInserted(widgetId) { 1.234 + prefs.set(INSERTION_PREF_ROOT + widgetId, true); 1.235 +} 1.236 + 1.237 +function haveInserted(widgetId) { 1.238 + return prefs.has(INSERTION_PREF_ROOT + widgetId); 1.239 +} 1.240 + 1.241 +const isWide = node => node.classList.contains(AUSTRALIS_PANEL_WIDE_CLASSNAME); 1.242 + 1.243 +/** 1.244 + * Main Widget class: entry point of the widget API 1.245 + * 1.246 + * Allow to control all widget across all existing windows with a single object. 1.247 + * Widget.getView allow to retrieve a WidgetView instance to control a widget 1.248 + * specific to one window. 1.249 + */ 1.250 +const WidgetTrait = LightTrait.compose(EventEmitterTrait, LightTrait({ 1.251 + 1.252 + _initWidget: function _initWidget(options) { 1.253 + model.setAttributes(this, widgetAttributes, options); 1.254 + 1.255 + browserManager.validate(this); 1.256 + 1.257 + // We must have at least content or contentURL defined 1.258 + if (!(this.content || this.contentURL)) 1.259 + throw new Error(ERR_CONTENT); 1.260 + 1.261 + this._views = []; 1.262 + 1.263 + // Set tooltip to label value if we don't have tooltip defined 1.264 + if (!this.tooltip) 1.265 + this.tooltip = this.label; 1.266 + 1.267 + model.setEvents(this, WIDGET_EVENTS, options); 1.268 + 1.269 + this.on('change', this._onChange.bind(this)); 1.270 + 1.271 + let self = this; 1.272 + this._port = EventEmitterTrait.create({ 1.273 + emit: function () { 1.274 + let args = arguments; 1.275 + self._views.forEach(function(v) v.port.emit.apply(v.port, args)); 1.276 + } 1.277 + }); 1.278 + // expose wrapped port, that exposes only public properties. 1.279 + this._port._public = Cortex(this._port); 1.280 + 1.281 + // Register this widget to browser manager in order to create new widget on 1.282 + // all new windows 1.283 + browserManager.addItem(this); 1.284 + }, 1.285 + 1.286 + _onChange: function _onChange(name, value) { 1.287 + // Set tooltip to label value if we don't have tooltip defined 1.288 + if (name == 'tooltip' && !value) { 1.289 + // we need to change tooltip again in order to change the value of the 1.290 + // attribute itself 1.291 + this.tooltip = this.label; 1.292 + return; 1.293 + } 1.294 + 1.295 + // Forward attributes changes to WidgetViews 1.296 + if (['width', 'tooltip', 'content', 'contentURL'].indexOf(name) != -1) { 1.297 + this._views.forEach(function(v) v[name] = value); 1.298 + } 1.299 + }, 1.300 + 1.301 + _onEvent: function _onEvent(type, eventData) { 1.302 + this._emit(type, eventData); 1.303 + }, 1.304 + 1.305 + _createView: function _createView() { 1.306 + // Create a new WidgetView instance 1.307 + let view = WidgetView(this); 1.308 + 1.309 + // Keep a reference to it 1.310 + this._views.push(view); 1.311 + 1.312 + return view; 1.313 + }, 1.314 + 1.315 + // a WidgetView instance is destroyed 1.316 + _onViewDestroyed: function _onViewDestroyed(view) { 1.317 + let idx = this._views.indexOf(view); 1.318 + this._views.splice(idx, 1); 1.319 + }, 1.320 + 1.321 + /** 1.322 + * Called on browser window closed, to destroy related WidgetViews 1.323 + * @params {ChromeWindow} window 1.324 + * Window that has been closed 1.325 + */ 1.326 + _onWindowClosed: function _onWindowClosed(window) { 1.327 + for each (let view in this._views) { 1.328 + if (view._isInChromeWindow(window)) { 1.329 + view.destroy(); 1.330 + break; 1.331 + } 1.332 + } 1.333 + }, 1.334 + 1.335 + /** 1.336 + * Get the WidgetView instance related to a BrowserWindow instance 1.337 + * @params {BrowserWindow} window 1.338 + * BrowserWindow reference from "windows" module 1.339 + */ 1.340 + getView: function getView(window) { 1.341 + for each (let view in this._views) { 1.342 + if (view._isInWindow(window)) { 1.343 + return view._public; 1.344 + } 1.345 + } 1.346 + return null; 1.347 + }, 1.348 + 1.349 + get port() this._port._public, 1.350 + set port(v) {}, // Work around Cortex failure with getter without setter 1.351 + // See bug 653464 1.352 + _port: null, 1.353 + 1.354 + postMessage: function postMessage(message) { 1.355 + this._views.forEach(function(v) v.postMessage(message)); 1.356 + }, 1.357 + 1.358 + destroy: function destroy() { 1.359 + if (this.panel) 1.360 + this.panel.destroy(); 1.361 + 1.362 + // Dispatch destroy calls to views 1.363 + // we need to go backward as we remove items from this array in 1.364 + // _onViewDestroyed 1.365 + for (let i = this._views.length - 1; i >= 0; i--) 1.366 + this._views[i].destroy(); 1.367 + 1.368 + // Unregister widget to stop creating it over new windows 1.369 + // and allow creation of new widget with same id 1.370 + browserManager.removeItem(this); 1.371 + } 1.372 + 1.373 +})); 1.374 + 1.375 +// Widget constructor 1.376 +const Widget = function Widget(options) { 1.377 + let w = WidgetTrait.create(Widget.prototype); 1.378 + w._initWidget(options); 1.379 + 1.380 + // Return a Cortex of widget in order to hide private attributes like _onEvent 1.381 + let _public = Cortex(w); 1.382 + unload.ensure(_public, "destroy"); 1.383 + return _public; 1.384 +} 1.385 +exports.Widget = Widget; 1.386 + 1.387 + 1.388 +/** 1.389 + * WidgetView is an instance of a widget for a specific window. 1.390 + * 1.391 + * This is an external API that can be retrieved by calling Widget.getView or 1.392 + * by watching `attach` event on Widget. 1.393 + */ 1.394 +const WidgetViewTrait = LightTrait.compose(EventEmitterTrait, LightTrait({ 1.395 + 1.396 + // Reference to the matching WidgetChrome 1.397 + // set right after constructor call 1.398 + _chrome: null, 1.399 + 1.400 + // Public interface of the WidgetView, passed in `attach` event or in 1.401 + // Widget.getView 1.402 + _public: null, 1.403 + 1.404 + _initWidgetView: function WidgetView__initWidgetView(baseWidget) { 1.405 + this._baseWidget = baseWidget; 1.406 + 1.407 + model.setAttributes(this, widgetAttributes, baseWidget); 1.408 + 1.409 + this.on('change', this._onChange.bind(this)); 1.410 + 1.411 + let self = this; 1.412 + this._port = EventEmitterTrait.create({ 1.413 + emit: function () { 1.414 + if (!self._chrome) 1.415 + throw new Error(ERR_DESTROYED); 1.416 + self._chrome.update(self._baseWidget, "emit", arguments); 1.417 + } 1.418 + }); 1.419 + // expose wrapped port, that exposes only public properties. 1.420 + this._port._public = Cortex(this._port); 1.421 + 1.422 + this._public = Cortex(this); 1.423 + }, 1.424 + 1.425 + // Called by WidgetChrome, when the related Worker is applied to the document, 1.426 + // so that we can start sending events to it 1.427 + _onWorkerReady: function () { 1.428 + // Emit an `attach` event with a WidgetView instance without private attrs 1.429 + this._baseWidget._emit("attach", this._public); 1.430 + }, 1.431 + 1.432 + _onChange: function WidgetView__onChange(name, value) { 1.433 + if (name == 'tooltip' && !value) { 1.434 + this.tooltip = this.label; 1.435 + return; 1.436 + } 1.437 + 1.438 + // Forward attributes changes to WidgetChrome instance 1.439 + if (['width', 'tooltip', 'content', 'contentURL'].indexOf(name) != -1) { 1.440 + this._chrome.update(this._baseWidget, name, value); 1.441 + } 1.442 + }, 1.443 + 1.444 + _onEvent: function WidgetView__onEvent(type, eventData, domNode) { 1.445 + // Dispatch event in view 1.446 + this._emit(type, eventData); 1.447 + 1.448 + // And forward it to the main Widget object 1.449 + if ("click" == type || type.indexOf("mouse") == 0) 1.450 + this._baseWidget._onEvent(type, this._public); 1.451 + else 1.452 + this._baseWidget._onEvent(type, eventData); 1.453 + 1.454 + // Special case for click events: if the widget doesn't have a click 1.455 + // handler, but it does have a panel, display the panel. 1.456 + if ("click" == type && !this._listeners("click").length && this.panel) { 1.457 + // This kind of ugly workaround, instead we should implement 1.458 + // `getNodeView` for the `Widget` class itself, but that's kind of 1.459 + // hard without cleaning things up. 1.460 + this.panel.show(null, getNodeView.implement({}, () => domNode)); 1.461 + } 1.462 + }, 1.463 + 1.464 + _isInWindow: function WidgetView__isInWindow(window) { 1.465 + return windowsAPI.BrowserWindow({ 1.466 + window: this._chrome.window 1.467 + }) == window; 1.468 + }, 1.469 + 1.470 + _isInChromeWindow: function WidgetView__isInChromeWindow(window) { 1.471 + return this._chrome.window == window; 1.472 + }, 1.473 + 1.474 + _onPortEvent: function WidgetView__onPortEvent(args) { 1.475 + let port = this._port; 1.476 + port._emit.apply(port, args); 1.477 + let basePort = this._baseWidget._port; 1.478 + basePort._emit.apply(basePort, args); 1.479 + }, 1.480 + 1.481 + get port() this._port._public, 1.482 + set port(v) {}, // Work around Cortex failure with getter without setter 1.483 + // See bug 653464 1.484 + _port: null, 1.485 + 1.486 + postMessage: function WidgetView_postMessage(message) { 1.487 + if (!this._chrome) 1.488 + throw new Error(ERR_DESTROYED); 1.489 + this._chrome.update(this._baseWidget, "postMessage", message); 1.490 + }, 1.491 + 1.492 + destroy: function WidgetView_destroy() { 1.493 + this._chrome.destroy(); 1.494 + delete this._chrome; 1.495 + this._baseWidget._onViewDestroyed(this); 1.496 + this._emit("detach"); 1.497 + } 1.498 + 1.499 +})); 1.500 + 1.501 + 1.502 +const WidgetView = function WidgetView(baseWidget) { 1.503 + let w = WidgetViewTrait.create(WidgetView.prototype); 1.504 + w._initWidgetView(baseWidget); 1.505 + return w; 1.506 +} 1.507 + 1.508 + 1.509 +/** 1.510 + * Keeps track of all browser windows. 1.511 + * Exposes methods for adding/removing widgets 1.512 + * across all open windows (and future ones). 1.513 + * Create a new instance of BrowserWindow per window. 1.514 + */ 1.515 +let browserManager = { 1.516 + items: [], 1.517 + windows: [], 1.518 + 1.519 + // Registers the manager to listen for window openings and closings. Note 1.520 + // that calling this method can cause onTrack to be called immediately if 1.521 + // there are open windows. 1.522 + init: function () { 1.523 + let windowTracker = new WindowTracker(this); 1.524 + unload.ensure(windowTracker); 1.525 + }, 1.526 + 1.527 + // Registers a window with the manager. This is a WindowTracker callback. 1.528 + onTrack: function browserManager_onTrack(window) { 1.529 + if (isBrowser(window)) { 1.530 + let win = new BrowserWindow(window); 1.531 + win.addItems(this.items); 1.532 + this.windows.push(win); 1.533 + } 1.534 + }, 1.535 + 1.536 + // Unregisters a window from the manager. It's told to undo all 1.537 + // modifications. This is a WindowTracker callback. Note that when 1.538 + // WindowTracker is unloaded, it calls onUntrack for every currently opened 1.539 + // window. The browserManager therefore doesn't need to specially handle 1.540 + // unload itself, since unloading the browserManager means untracking all 1.541 + // currently opened windows. 1.542 + onUntrack: function browserManager_onUntrack(window) { 1.543 + if (isBrowser(window)) { 1.544 + this.items.forEach(function(i) i._onWindowClosed(window)); 1.545 + for (let i = 0; i < this.windows.length; i++) { 1.546 + if (this.windows[i].window == window) { 1.547 + this.windows.splice(i, 1)[0]; 1.548 + return; 1.549 + } 1.550 + } 1.551 + 1.552 + } 1.553 + }, 1.554 + 1.555 + // Used to validate widget by browserManager before adding it, 1.556 + // in order to check input very early in widget constructor 1.557 + validate : function (item) { 1.558 + let idx = this.items.indexOf(item); 1.559 + if (idx > -1) 1.560 + throw new Error("The widget " + item + " has already been added."); 1.561 + if (item.id) { 1.562 + let sameId = this.items.filter(function(i) i.id == item.id); 1.563 + if (sameId.length > 0) 1.564 + throw new Error("This widget ID is already used: " + item.id); 1.565 + } else { 1.566 + item.id = this.items.length; 1.567 + } 1.568 + }, 1.569 + 1.570 + // Registers an item with the manager. It's added to all currently registered 1.571 + // windows, and when new windows are registered it will be added to them, too. 1.572 + addItem: function browserManager_addItem(item) { 1.573 + this.items.push(item); 1.574 + this.windows.forEach(function (w) w.addItems([item])); 1.575 + }, 1.576 + 1.577 + // Unregisters an item from the manager. It's removed from all windows that 1.578 + // are currently registered. 1.579 + removeItem: function browserManager_removeItem(item) { 1.580 + let idx = this.items.indexOf(item); 1.581 + if (idx > -1) 1.582 + this.items.splice(idx, 1); 1.583 + }, 1.584 + propagateCurrentset: function browserManager_propagateCurrentset(id, currentset) { 1.585 + this.windows.forEach(function (w) w.doc.getElementById(id).setAttribute("currentset", currentset)); 1.586 + } 1.587 +}; 1.588 + 1.589 + 1.590 + 1.591 +/** 1.592 + * Keeps track of a single browser window. 1.593 + * 1.594 + * This is where the core of how a widget's content is added to a window lives. 1.595 + */ 1.596 +function BrowserWindow(window) { 1.597 + this.window = window; 1.598 + this.doc = window.document; 1.599 +} 1.600 + 1.601 +BrowserWindow.prototype = { 1.602 + // Adds an array of items to the window. 1.603 + addItems: function BW_addItems(items) { 1.604 + items.forEach(this._addItemToWindow, this); 1.605 + }, 1.606 + 1.607 + _addItemToWindow: function BW__addItemToWindow(baseWidget) { 1.608 + // Create a WidgetView instance 1.609 + let widget = baseWidget._createView(); 1.610 + 1.611 + // Create a WidgetChrome instance 1.612 + let item = new WidgetChrome({ 1.613 + widget: widget, 1.614 + doc: this.doc, 1.615 + window: this.window 1.616 + }); 1.617 + 1.618 + widget._chrome = item; 1.619 + 1.620 + this._insertNodeInToolbar(item.node); 1.621 + 1.622 + // We need to insert Widget DOM Node before finishing widget view creation 1.623 + // (because fill creates an iframe and tries to access its docShell) 1.624 + item.fill(); 1.625 + }, 1.626 + 1.627 + _insertNodeInToolbar: function BW__insertNodeInToolbar(node) { 1.628 + // Add to the customization palette 1.629 + let toolbox = this.doc.getElementById("navigator-toolbox"); 1.630 + let palette = toolbox.palette; 1.631 + palette.appendChild(node); 1.632 + 1.633 + let { CustomizableUI } = this.window; 1.634 + let { id } = node; 1.635 + 1.636 + let placement = CustomizableUI.getPlacementOfWidget(id); 1.637 + 1.638 + if (!placement) { 1.639 + if (haveInserted(id) || isWide(node)) 1.640 + return; 1.641 + 1.642 + placement = {area: 'nav-bar', position: undefined}; 1.643 + saveInserted(id); 1.644 + } 1.645 + 1.646 + CustomizableUI.addWidgetToArea(id, placement.area, placement.position); 1.647 + CustomizableUI.ensureWidgetPlacedInWindow(id, this.window); 1.648 + } 1.649 +} 1.650 + 1.651 + 1.652 +/** 1.653 + * Final Widget class that handles chrome DOM Node: 1.654 + * - create initial DOM nodes 1.655 + * - receive instruction from WidgetView through update method and update DOM 1.656 + * - watch for DOM events and forward them to WidgetView 1.657 + */ 1.658 +function WidgetChrome(options) { 1.659 + this.window = options.window; 1.660 + this._doc = options.doc; 1.661 + this._widget = options.widget; 1.662 + this._symbiont = null; // set later 1.663 + this.node = null; // set later 1.664 + 1.665 + this._createNode(); 1.666 +} 1.667 + 1.668 +// Update a property of a widget. 1.669 +WidgetChrome.prototype.update = function WC_update(updatedItem, property, value) { 1.670 + switch(property) { 1.671 + case "contentURL": 1.672 + case "content": 1.673 + this.setContent(); 1.674 + break; 1.675 + case "width": 1.676 + this.node.style.minWidth = value + "px"; 1.677 + this.node.querySelector("iframe").style.width = value + "px"; 1.678 + break; 1.679 + case "tooltip": 1.680 + this.node.setAttribute("tooltiptext", value); 1.681 + break; 1.682 + case "postMessage": 1.683 + this._symbiont.postMessage(value); 1.684 + break; 1.685 + case "emit": 1.686 + let port = this._symbiont.port; 1.687 + port.emit.apply(port, value); 1.688 + break; 1.689 + } 1.690 +} 1.691 + 1.692 +// Add a widget to this window. 1.693 +WidgetChrome.prototype._createNode = function WC__createNode() { 1.694 + // XUL element container for widget 1.695 + let node = this._doc.createElement("toolbaritem"); 1.696 + 1.697 + // Temporary work around require("self") failing on unit-test execution ... 1.698 + let jetpackID = "testID"; 1.699 + try { 1.700 + jetpackID = require("./self").id; 1.701 + } catch(e) {} 1.702 + 1.703 + // Compute an unique and stable widget id with jetpack id and widget.id 1.704 + let id = "widget:" + jetpackID + "-" + this._widget.id; 1.705 + node.setAttribute("id", id); 1.706 + node.setAttribute("label", this._widget.label); 1.707 + node.setAttribute("tooltiptext", this._widget.tooltip); 1.708 + node.setAttribute("align", "center"); 1.709 + // Bug 626326: Prevent customize toolbar context menu to appear 1.710 + node.setAttribute("context", ""); 1.711 + 1.712 + // For use in styling by the browser 1.713 + node.setAttribute("sdkstylewidget", "true"); 1.714 + 1.715 + if (this._widget.width > AUSTRALIS_PANEL_WIDE_WIDGET_CUTOFF) { 1.716 + node.classList.add(AUSTRALIS_PANEL_WIDE_CLASSNAME); 1.717 + } 1.718 + 1.719 + // TODO move into a stylesheet, configurable by consumers. 1.720 + // Either widget.style, exposing the style object, or a URL 1.721 + // (eg, can load local stylesheet file). 1.722 + node.setAttribute("style", [ 1.723 + "overflow: hidden; margin: 1px 2px 1px 2px; padding: 0px;", 1.724 + "min-height: 16px;", 1.725 + ].join("")); 1.726 + 1.727 + node.style.minWidth = this._widget.width + "px"; 1.728 + 1.729 + this.node = node; 1.730 +} 1.731 + 1.732 +// Initial population of a widget's content. 1.733 +WidgetChrome.prototype.fill = function WC_fill() { 1.734 + let { node, _doc: document } = this; 1.735 + 1.736 + // Create element 1.737 + let iframe = document.createElement("iframe"); 1.738 + iframe.setAttribute("type", "content"); 1.739 + iframe.setAttribute("transparent", "transparent"); 1.740 + iframe.style.overflow = "hidden"; 1.741 + iframe.style.height = "16px"; 1.742 + iframe.style.maxHeight = "16px"; 1.743 + iframe.style.width = this._widget.width + "px"; 1.744 + iframe.setAttribute("flex", "1"); 1.745 + iframe.style.border = "none"; 1.746 + iframe.style.padding = "0px"; 1.747 + 1.748 + // Do this early, because things like contentWindow are null 1.749 + // until the node is attached to a document. 1.750 + node.appendChild(iframe); 1.751 + 1.752 + let label = document.createElement("label"); 1.753 + label.setAttribute("value", this._widget.label); 1.754 + label.className = "toolbarbutton-text"; 1.755 + label.setAttribute("crop", "right"); 1.756 + label.setAttribute("flex", "1"); 1.757 + node.appendChild(label); 1.758 + 1.759 + // This toolbarbutton is created to provide a more consistent user experience 1.760 + // during customization, see: 1.761 + // https://bugzilla.mozilla.org/show_bug.cgi?id=959640 1.762 + let button = document.createElement("toolbarbutton"); 1.763 + button.setAttribute("label", this._widget.label); 1.764 + button.setAttribute("crop", "right"); 1.765 + button.className = "toolbarbutton-1 chromeclass-toolbar-additional"; 1.766 + node.appendChild(button); 1.767 + 1.768 + // add event handlers 1.769 + this.addEventHandlers(); 1.770 + 1.771 + // set content 1.772 + this.setContent(); 1.773 +} 1.774 + 1.775 +// Get widget content type. 1.776 +WidgetChrome.prototype.getContentType = function WC_getContentType() { 1.777 + if (this._widget.content) 1.778 + return CONTENT_TYPE_HTML; 1.779 + return (this._widget.contentURL && /\.(jpg|gif|png|ico|svg)$/i.test(this._widget.contentURL)) 1.780 + ? CONTENT_TYPE_IMAGE : CONTENT_TYPE_URI; 1.781 +} 1.782 + 1.783 +// Set widget content. 1.784 +WidgetChrome.prototype.setContent = function WC_setContent() { 1.785 + let type = this.getContentType(); 1.786 + let contentURL = null; 1.787 + 1.788 + switch (type) { 1.789 + case CONTENT_TYPE_HTML: 1.790 + contentURL = "data:text/html;charset=utf-8," + encodeURIComponent(this._widget.content); 1.791 + break; 1.792 + case CONTENT_TYPE_URI: 1.793 + contentURL = this._widget.contentURL; 1.794 + break; 1.795 + case CONTENT_TYPE_IMAGE: 1.796 + let imageURL = this._widget.contentURL; 1.797 + contentURL = "data:text/html;charset=utf-8,<html><body><img src='" + 1.798 + encodeURI(imageURL) + "'></body></html>"; 1.799 + break; 1.800 + default: 1.801 + throw new Error("The widget's type cannot be determined."); 1.802 + } 1.803 + 1.804 + let iframe = this.node.firstElementChild; 1.805 + 1.806 + let self = this; 1.807 + // Cleanup previously created symbiont (in case we are update content) 1.808 + if (this._symbiont) 1.809 + this._symbiont.destroy(); 1.810 + 1.811 + this._symbiont = Trait.compose(Symbiont.resolve({ 1.812 + _onContentScriptEvent: "_onContentScriptEvent-not-used", 1.813 + _onInit: "_initSymbiont" 1.814 + }), { 1.815 + // Overload `Symbiont._onInit` in order to know when the related worker 1.816 + // is ready. 1.817 + _onInit: function () { 1.818 + this._initSymbiont(); 1.819 + self._widget._onWorkerReady(); 1.820 + }, 1.821 + _onContentScriptEvent: function () { 1.822 + // Redirect events to WidgetView 1.823 + self._widget._onPortEvent(arguments); 1.824 + } 1.825 + })({ 1.826 + frame: iframe, 1.827 + contentURL: contentURL, 1.828 + contentScriptFile: this._widget.contentScriptFile, 1.829 + contentScript: this._widget.contentScript, 1.830 + contentScriptWhen: this._widget.contentScriptWhen, 1.831 + contentScriptOptions: this._widget.contentScriptOptions, 1.832 + allow: this._widget.allow, 1.833 + onMessage: function(message) { 1.834 + setTimeout(function() { 1.835 + self._widget._onEvent("message", message); 1.836 + }, 0); 1.837 + } 1.838 + }); 1.839 +} 1.840 + 1.841 +// Detect if document consists of a single image. 1.842 +WidgetChrome._isImageDoc = function WC__isImageDoc(doc) { 1.843 + return /*doc.body &&*/ doc.body.childNodes.length == 1 && 1.844 + doc.body.firstElementChild && 1.845 + doc.body.firstElementChild.tagName == "IMG"; 1.846 +} 1.847 + 1.848 +// Set up all supported events for a widget. 1.849 +WidgetChrome.prototype.addEventHandlers = function WC_addEventHandlers() { 1.850 + let contentType = this.getContentType(); 1.851 + 1.852 + let self = this; 1.853 + let listener = function(e) { 1.854 + // Ignore event firings that target the iframe. 1.855 + if (e.target == self.node.firstElementChild) 1.856 + return; 1.857 + 1.858 + // The widget only supports left-click for now, 1.859 + // so ignore all clicks (i.e. middle or right) except left ones. 1.860 + if (e.type == "click" && e.button !== 0) 1.861 + return; 1.862 + 1.863 + // Proxy event to the widget 1.864 + setTimeout(function() { 1.865 + self._widget._onEvent(EVENTS[e.type], null, self.node); 1.866 + }, 0); 1.867 + }; 1.868 + 1.869 + this.eventListeners = {}; 1.870 + let iframe = this.node.firstElementChild; 1.871 + for (let type in EVENTS) { 1.872 + iframe.addEventListener(type, listener, true, true); 1.873 + 1.874 + // Store listeners for later removal 1.875 + this.eventListeners[type] = listener; 1.876 + } 1.877 + 1.878 + // On document load, make modifications required for nice default 1.879 + // presentation. 1.880 + function loadListener(e) { 1.881 + let containerStyle = self.window.getComputedStyle(self.node.parentNode); 1.882 + // Ignore event firings that target the iframe 1.883 + if (e.target == iframe) 1.884 + return; 1.885 + // Ignore about:blank loads 1.886 + if (e.type == "load" && e.target.location == "about:blank") 1.887 + return; 1.888 + 1.889 + // We may have had an unload event before that cleaned up the symbiont 1.890 + if (!self._symbiont) 1.891 + self.setContent(); 1.892 + 1.893 + let doc = e.target; 1.894 + 1.895 + if (contentType == CONTENT_TYPE_IMAGE || WidgetChrome._isImageDoc(doc)) { 1.896 + // Force image content to size. 1.897 + // Add-on authors must size their images correctly. 1.898 + doc.body.firstElementChild.style.width = self._widget.width + "px"; 1.899 + doc.body.firstElementChild.style.height = "16px"; 1.900 + } 1.901 + 1.902 + // Extend the add-on bar's default text styles to the widget. 1.903 + doc.body.style.color = containerStyle.color; 1.904 + doc.body.style.fontFamily = containerStyle.fontFamily; 1.905 + doc.body.style.fontSize = containerStyle.fontSize; 1.906 + doc.body.style.fontWeight = containerStyle.fontWeight; 1.907 + doc.body.style.textShadow = containerStyle.textShadow; 1.908 + // Allow all content to fill the box by default. 1.909 + doc.body.style.margin = "0"; 1.910 + } 1.911 + 1.912 + iframe.addEventListener("load", loadListener, true); 1.913 + this.eventListeners["load"] = loadListener; 1.914 + 1.915 + // Register a listener to unload symbiont if the toolbaritem is moved 1.916 + // on user toolbars customization 1.917 + function unloadListener(e) { 1.918 + if (e.target.location == "about:blank") 1.919 + return; 1.920 + self._symbiont.destroy(); 1.921 + self._symbiont = null; 1.922 + // This may fail but not always, it depends on how the node is 1.923 + // moved or removed 1.924 + try { 1.925 + self.setContent(); 1.926 + } catch(e) {} 1.927 + 1.928 + } 1.929 + 1.930 + iframe.addEventListener("unload", unloadListener, true); 1.931 + this.eventListeners["unload"] = unloadListener; 1.932 +} 1.933 + 1.934 +// Remove and unregister the widget from everything 1.935 +WidgetChrome.prototype.destroy = function WC_destroy(removedItems) { 1.936 + // remove event listeners 1.937 + for (let type in this.eventListeners) { 1.938 + let listener = this.eventListeners[type]; 1.939 + this.node.firstElementChild.removeEventListener(type, listener, true); 1.940 + } 1.941 + // remove dom node 1.942 + this.node.parentNode.removeChild(this.node); 1.943 + // cleanup symbiont 1.944 + this._symbiont.destroy(); 1.945 + // cleanup itself 1.946 + this.eventListeners = null; 1.947 + this._widget = null; 1.948 + this._symbiont = null; 1.949 +} 1.950 + 1.951 +// Init the browserManager only after setting prototypes and such above, because 1.952 +// it will cause browserManager.onTrack to be called immediately if there are 1.953 +// open windows. 1.954 +browserManager.init();