1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/modules/PopupNotifications.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,975 @@ 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 + 1.8 +this.EXPORTED_SYMBOLS = ["PopupNotifications"]; 1.9 + 1.10 +var Cc = Components.classes, Ci = Components.interfaces, Cu = Components.utils; 1.11 + 1.12 +Cu.import("resource://gre/modules/Services.jsm"); 1.13 + 1.14 +const NOTIFICATION_EVENT_DISMISSED = "dismissed"; 1.15 +const NOTIFICATION_EVENT_REMOVED = "removed"; 1.16 +const NOTIFICATION_EVENT_SHOWING = "showing"; 1.17 +const NOTIFICATION_EVENT_SHOWN = "shown"; 1.18 +const NOTIFICATION_EVENT_SWAPPING = "swapping"; 1.19 + 1.20 +const ICON_SELECTOR = ".notification-anchor-icon"; 1.21 +const ICON_ATTRIBUTE_SHOWING = "showing"; 1.22 + 1.23 +const PREF_SECURITY_DELAY = "security.notification_enable_delay"; 1.24 + 1.25 +let popupNotificationsMap = new WeakMap(); 1.26 +let gNotificationParents = new WeakMap; 1.27 + 1.28 +function getAnchorFromBrowser(aBrowser) { 1.29 + let anchor = aBrowser.getAttribute("popupnotificationanchor") || 1.30 + aBrowser.popupnotificationanchor; 1.31 + if (anchor) { 1.32 + if (anchor instanceof Ci.nsIDOMXULElement) { 1.33 + return anchor; 1.34 + } 1.35 + return aBrowser.ownerDocument.getElementById(anchor); 1.36 + } 1.37 + return null; 1.38 +} 1.39 + 1.40 +/** 1.41 + * Notification object describes a single popup notification. 1.42 + * 1.43 + * @see PopupNotifications.show() 1.44 + */ 1.45 +function Notification(id, message, anchorID, mainAction, secondaryActions, 1.46 + browser, owner, options) { 1.47 + this.id = id; 1.48 + this.message = message; 1.49 + this.anchorID = anchorID; 1.50 + this.mainAction = mainAction; 1.51 + this.secondaryActions = secondaryActions || []; 1.52 + this.browser = browser; 1.53 + this.owner = owner; 1.54 + this.options = options || {}; 1.55 +} 1.56 + 1.57 +Notification.prototype = { 1.58 + 1.59 + id: null, 1.60 + message: null, 1.61 + anchorID: null, 1.62 + mainAction: null, 1.63 + secondaryActions: null, 1.64 + browser: null, 1.65 + owner: null, 1.66 + options: null, 1.67 + timeShown: null, 1.68 + 1.69 + /** 1.70 + * Removes the notification and updates the popup accordingly if needed. 1.71 + */ 1.72 + remove: function Notification_remove() { 1.73 + this.owner.remove(this); 1.74 + }, 1.75 + 1.76 + get anchorElement() { 1.77 + let iconBox = this.owner.iconBox; 1.78 + 1.79 + let anchorElement = getAnchorFromBrowser(this.browser); 1.80 + 1.81 + if (!iconBox) 1.82 + return anchorElement; 1.83 + 1.84 + if (!anchorElement && this.anchorID) 1.85 + anchorElement = iconBox.querySelector("#"+this.anchorID); 1.86 + 1.87 + // Use a default anchor icon if it's available 1.88 + if (!anchorElement) 1.89 + anchorElement = iconBox.querySelector("#default-notification-icon") || 1.90 + iconBox; 1.91 + 1.92 + return anchorElement; 1.93 + }, 1.94 + 1.95 + reshow: function() { 1.96 + this.owner._reshowNotifications(this.anchorElement, this.browser); 1.97 + } 1.98 +}; 1.99 + 1.100 +/** 1.101 + * The PopupNotifications object manages popup notifications for a given browser 1.102 + * window. 1.103 + * @param tabbrowser 1.104 + * window's <xul:tabbrowser/>. Used to observe tab switching events and 1.105 + * for determining the active browser element. 1.106 + * @param panel 1.107 + * The <xul:panel/> element to use for notifications. The panel is 1.108 + * populated with <popupnotification> children and displayed it as 1.109 + * needed. 1.110 + * @param iconBox 1.111 + * Reference to a container element that should be hidden or 1.112 + * unhidden when notifications are hidden or shown. It should be the 1.113 + * parent of anchor elements whose IDs are passed to show(). 1.114 + * It is used as a fallback popup anchor if notifications specify 1.115 + * invalid or non-existent anchor IDs. 1.116 + */ 1.117 +this.PopupNotifications = function PopupNotifications(tabbrowser, panel, iconBox) { 1.118 + if (!(tabbrowser instanceof Ci.nsIDOMXULElement)) 1.119 + throw "Invalid tabbrowser"; 1.120 + if (iconBox && !(iconBox instanceof Ci.nsIDOMXULElement)) 1.121 + throw "Invalid iconBox"; 1.122 + if (!(panel instanceof Ci.nsIDOMXULElement)) 1.123 + throw "Invalid panel"; 1.124 + 1.125 + this.window = tabbrowser.ownerDocument.defaultView; 1.126 + this.panel = panel; 1.127 + this.tabbrowser = tabbrowser; 1.128 + this.iconBox = iconBox; 1.129 + this.buttonDelay = Services.prefs.getIntPref(PREF_SECURITY_DELAY); 1.130 + 1.131 + this.panel.addEventListener("popuphidden", this, true); 1.132 + 1.133 + this.window.addEventListener("activate", this, true); 1.134 + if (this.tabbrowser.tabContainer) 1.135 + this.tabbrowser.tabContainer.addEventListener("TabSelect", this, true); 1.136 +} 1.137 + 1.138 +PopupNotifications.prototype = { 1.139 + 1.140 + window: null, 1.141 + panel: null, 1.142 + tabbrowser: null, 1.143 + 1.144 + _iconBox: null, 1.145 + set iconBox(iconBox) { 1.146 + // Remove the listeners on the old iconBox, if needed 1.147 + if (this._iconBox) { 1.148 + this._iconBox.removeEventListener("click", this, false); 1.149 + this._iconBox.removeEventListener("keypress", this, false); 1.150 + } 1.151 + this._iconBox = iconBox; 1.152 + if (iconBox) { 1.153 + iconBox.addEventListener("click", this, false); 1.154 + iconBox.addEventListener("keypress", this, false); 1.155 + } 1.156 + }, 1.157 + get iconBox() { 1.158 + return this._iconBox; 1.159 + }, 1.160 + 1.161 + /** 1.162 + * Enable or disable the opening/closing transition. 1.163 + * @param state 1.164 + * Boolean state 1.165 + */ 1.166 + set transitionsEnabled(state) { 1.167 + if (state) { 1.168 + this.panel.removeAttribute("animate"); 1.169 + } 1.170 + else { 1.171 + this.panel.setAttribute("animate", "false"); 1.172 + } 1.173 + }, 1.174 + 1.175 + /** 1.176 + * Retrieve a Notification object associated with the browser/ID pair. 1.177 + * @param id 1.178 + * The Notification ID to search for. 1.179 + * @param browser 1.180 + * The browser whose notifications should be searched. If null, the 1.181 + * currently selected browser's notifications will be searched. 1.182 + * 1.183 + * @returns the corresponding Notification object, or null if no such 1.184 + * notification exists. 1.185 + */ 1.186 + getNotification: function PopupNotifications_getNotification(id, browser) { 1.187 + let n = null; 1.188 + let notifications = this._getNotificationsForBrowser(browser || this.tabbrowser.selectedBrowser); 1.189 + notifications.some(function(x) x.id == id && (n = x)); 1.190 + return n; 1.191 + }, 1.192 + 1.193 + /** 1.194 + * Adds a new popup notification. 1.195 + * @param browser 1.196 + * The <xul:browser> element associated with the notification. Must not 1.197 + * be null. 1.198 + * @param id 1.199 + * A unique ID that identifies the type of notification (e.g. 1.200 + * "geolocation"). Only one notification with a given ID can be visible 1.201 + * at a time. If a notification already exists with the given ID, it 1.202 + * will be replaced. 1.203 + * @param message 1.204 + * The text to be displayed in the notification. 1.205 + * @param anchorID 1.206 + * The ID of the element that should be used as this notification 1.207 + * popup's anchor. May be null, in which case the notification will be 1.208 + * anchored to the iconBox. 1.209 + * @param mainAction 1.210 + * A JavaScript object literal describing the notification button's 1.211 + * action. If present, it must have the following properties: 1.212 + * - label (string): the button's label. 1.213 + * - accessKey (string): the button's accessKey. 1.214 + * - callback (function): a callback to be invoked when the button is 1.215 + * pressed. 1.216 + * - [optional] dismiss (boolean): If this is true, the notification 1.217 + * will be dismissed instead of removed after running the callback. 1.218 + * If null, the notification will not have a button, and 1.219 + * secondaryActions will be ignored. 1.220 + * @param secondaryActions 1.221 + * An optional JavaScript array describing the notification's alternate 1.222 + * actions. The array should contain objects with the same properties 1.223 + * as mainAction. These are used to populate the notification button's 1.224 + * dropdown menu. 1.225 + * @param options 1.226 + * An options JavaScript object holding additional properties for the 1.227 + * notification. The following properties are currently supported: 1.228 + * persistence: An integer. The notification will not automatically 1.229 + * dismiss for this many page loads. 1.230 + * timeout: A time in milliseconds. The notification will not 1.231 + * automatically dismiss before this time. 1.232 + * persistWhileVisible: 1.233 + * A boolean. If true, a visible notification will always 1.234 + * persist across location changes. 1.235 + * dismissed: Whether the notification should be added as a dismissed 1.236 + * notification. Dismissed notifications can be activated 1.237 + * by clicking on their anchorElement. 1.238 + * eventCallback: 1.239 + * Callback to be invoked when the notification changes 1.240 + * state. The callback's first argument is a string 1.241 + * identifying the state change: 1.242 + * "dismissed": notification has been dismissed by the 1.243 + * user (e.g. by clicking away or switching 1.244 + * tabs) 1.245 + * "removed": notification has been removed (due to 1.246 + * location change or user action) 1.247 + * "showing": notification is about to be shown 1.248 + * (this can be fired multiple times as 1.249 + * notifications are dismissed and re-shown) 1.250 + * If the callback returns true, the notification 1.251 + * will be dismissed. 1.252 + * "shown": notification has been shown (this can be fired 1.253 + * multiple times as notifications are dismissed 1.254 + * and re-shown) 1.255 + * "swapping": the docshell of the browser that created 1.256 + * the notification is about to be swapped to 1.257 + * another browser. A second parameter contains 1.258 + * the browser that is receiving the docshell, 1.259 + * so that the event callback can transfer stuff 1.260 + * specific to this notification. 1.261 + * If the callback returns true, the notification 1.262 + * will be moved to the new browser. 1.263 + * If the callback isn't implemented, returns false, 1.264 + * or doesn't return any value, the notification 1.265 + * will be removed. 1.266 + * neverShow: Indicate that no popup should be shown for this 1.267 + * notification. Useful for just showing the anchor icon. 1.268 + * removeOnDismissal: 1.269 + * Notifications with this parameter set to true will be 1.270 + * removed when they would have otherwise been dismissed 1.271 + * (i.e. any time the popup is closed due to user 1.272 + * interaction). 1.273 + * hideNotNow: If true, indicates that the 'Not Now' menuitem should 1.274 + * not be shown. If 'Not Now' is hidden, it needs to be 1.275 + * replaced by another 'do nothing' item, so providing at 1.276 + * least one secondary action is required; and one of the 1.277 + * actions needs to have the 'dismiss' property set to true. 1.278 + * popupIconURL: 1.279 + * A string. URL of the image to be displayed in the popup. 1.280 + * Normally specified in CSS using list-style-image and the 1.281 + * .popup-notification-icon[popupid=...] selector. 1.282 + * learnMoreURL: 1.283 + * A string URL. Setting this property will make the 1.284 + * prompt display a "Learn More" link that, when clicked, 1.285 + * opens the URL in a new tab. 1.286 + * @returns the Notification object corresponding to the added notification. 1.287 + */ 1.288 + show: function PopupNotifications_show(browser, id, message, anchorID, 1.289 + mainAction, secondaryActions, options) { 1.290 + function isInvalidAction(a) { 1.291 + return !a || !(typeof(a.callback) == "function") || !a.label || !a.accessKey; 1.292 + } 1.293 + 1.294 + if (!browser) 1.295 + throw "PopupNotifications_show: invalid browser"; 1.296 + if (!id) 1.297 + throw "PopupNotifications_show: invalid ID"; 1.298 + if (mainAction && isInvalidAction(mainAction)) 1.299 + throw "PopupNotifications_show: invalid mainAction"; 1.300 + if (secondaryActions && secondaryActions.some(isInvalidAction)) 1.301 + throw "PopupNotifications_show: invalid secondaryActions"; 1.302 + if (options && options.hideNotNow && 1.303 + (!secondaryActions || !secondaryActions.length || 1.304 + !secondaryActions.concat(mainAction).some(action => action.dismiss))) 1.305 + throw "PopupNotifications_show: 'Not Now' item hidden without replacement"; 1.306 + 1.307 + let notification = new Notification(id, message, anchorID, mainAction, 1.308 + secondaryActions, browser, this, options); 1.309 + 1.310 + if (options && options.dismissed) 1.311 + notification.dismissed = true; 1.312 + 1.313 + let existingNotification = this.getNotification(id, browser); 1.314 + if (existingNotification) 1.315 + this._remove(existingNotification); 1.316 + 1.317 + let notifications = this._getNotificationsForBrowser(browser); 1.318 + notifications.push(notification); 1.319 + 1.320 + let isActive = this._isActiveBrowser(browser); 1.321 + let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); 1.322 + if (isActive && fm.activeWindow == this.window) { 1.323 + // show panel now 1.324 + this._update(notifications, notification.anchorElement, true); 1.325 + } else { 1.326 + // Otherwise, update() will display the notification the next time the 1.327 + // relevant tab/window is selected. 1.328 + 1.329 + // If the tab is selected but the window is in the background, let the OS 1.330 + // tell the user that there's a notification waiting in that window. 1.331 + // At some point we might want to do something about background tabs here 1.332 + // too. When the user switches to this window, we'll show the panel if 1.333 + // this browser is a tab (thus showing the anchor icon). For 1.334 + // non-tabbrowser browsers, we need to make the icon visible now or the 1.335 + // user will not be able to open the panel. 1.336 + if (!notification.dismissed && isActive) { 1.337 + this.window.getAttention(); 1.338 + if (notification.anchorElement.parentNode != this.iconBox) { 1.339 + this._updateAnchorIcon(notifications, notification.anchorElement); 1.340 + } 1.341 + } 1.342 + 1.343 + // Notify observers that we're not showing the popup (useful for testing) 1.344 + this._notify("backgroundShow"); 1.345 + } 1.346 + 1.347 + return notification; 1.348 + }, 1.349 + 1.350 + /** 1.351 + * Returns true if the notification popup is currently being displayed. 1.352 + */ 1.353 + get isPanelOpen() { 1.354 + let panelState = this.panel.state; 1.355 + 1.356 + return panelState == "showing" || panelState == "open"; 1.357 + }, 1.358 + 1.359 + /** 1.360 + * Called by the consumer to indicate that a browser's location has changed, 1.361 + * so that we can update the active notifications accordingly. 1.362 + */ 1.363 + locationChange: function PopupNotifications_locationChange(aBrowser) { 1.364 + if (!aBrowser) 1.365 + throw "PopupNotifications_locationChange: invalid browser"; 1.366 + 1.367 + let notifications = this._getNotificationsForBrowser(aBrowser); 1.368 + 1.369 + notifications = notifications.filter(function (notification) { 1.370 + // The persistWhileVisible option allows an open notification to persist 1.371 + // across location changes 1.372 + if (notification.options.persistWhileVisible && 1.373 + this.isPanelOpen) { 1.374 + if ("persistence" in notification.options && 1.375 + notification.options.persistence) 1.376 + notification.options.persistence--; 1.377 + return true; 1.378 + } 1.379 + 1.380 + // The persistence option allows a notification to persist across multiple 1.381 + // page loads 1.382 + if ("persistence" in notification.options && 1.383 + notification.options.persistence) { 1.384 + notification.options.persistence--; 1.385 + return true; 1.386 + } 1.387 + 1.388 + // The timeout option allows a notification to persist until a certain time 1.389 + if ("timeout" in notification.options && 1.390 + Date.now() <= notification.options.timeout) { 1.391 + return true; 1.392 + } 1.393 + 1.394 + this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED); 1.395 + return false; 1.396 + }, this); 1.397 + 1.398 + this._setNotificationsForBrowser(aBrowser, notifications); 1.399 + 1.400 + if (this._isActiveBrowser(aBrowser)) { 1.401 + // get the anchor element if the browser has defined one so it will 1.402 + // _update will handle both the tabs iconBox and non-tab permission 1.403 + // anchors. 1.404 + let anchorElement = notifications.length > 0 ? notifications[0].anchorElement : null; 1.405 + if (!anchorElement) 1.406 + anchorElement = getAnchorFromBrowser(aBrowser); 1.407 + this._update(notifications, anchorElement); 1.408 + } 1.409 + }, 1.410 + 1.411 + /** 1.412 + * Removes a Notification. 1.413 + * @param notification 1.414 + * The Notification object to remove. 1.415 + */ 1.416 + remove: function PopupNotifications_remove(notification) { 1.417 + this._remove(notification); 1.418 + 1.419 + if (this._isActiveBrowser(notification.browser)) { 1.420 + let notifications = this._getNotificationsForBrowser(notification.browser); 1.421 + this._update(notifications, notification.anchorElement); 1.422 + } 1.423 + }, 1.424 + 1.425 + handleEvent: function (aEvent) { 1.426 + switch (aEvent.type) { 1.427 + case "popuphidden": 1.428 + this._onPopupHidden(aEvent); 1.429 + break; 1.430 + case "activate": 1.431 + case "TabSelect": 1.432 + let self = this; 1.433 + // setTimeout(..., 0) needed, otherwise openPopup from "activate" event 1.434 + // handler results in the popup being hidden again for some reason... 1.435 + this.window.setTimeout(function () { 1.436 + self._update(); 1.437 + }, 0); 1.438 + break; 1.439 + case "click": 1.440 + case "keypress": 1.441 + this._onIconBoxCommand(aEvent); 1.442 + break; 1.443 + } 1.444 + }, 1.445 + 1.446 +//////////////////////////////////////////////////////////////////////////////// 1.447 +// Utility methods 1.448 +//////////////////////////////////////////////////////////////////////////////// 1.449 + 1.450 + _ignoreDismissal: null, 1.451 + _currentAnchorElement: null, 1.452 + 1.453 + /** 1.454 + * Gets notifications for the currently selected browser. 1.455 + */ 1.456 + get _currentNotifications() { 1.457 + return this.tabbrowser.selectedBrowser ? this._getNotificationsForBrowser(this.tabbrowser.selectedBrowser) : []; 1.458 + }, 1.459 + 1.460 + _remove: function PopupNotifications_removeHelper(notification) { 1.461 + // This notification may already be removed, in which case let's just fail 1.462 + // silently. 1.463 + let notifications = this._getNotificationsForBrowser(notification.browser); 1.464 + if (!notifications) 1.465 + return; 1.466 + 1.467 + var index = notifications.indexOf(notification); 1.468 + if (index == -1) 1.469 + return; 1.470 + 1.471 + if (this._isActiveBrowser(notification.browser)) 1.472 + notification.anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING); 1.473 + 1.474 + // remove the notification 1.475 + notifications.splice(index, 1); 1.476 + this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED); 1.477 + }, 1.478 + 1.479 + /** 1.480 + * Dismisses the notification without removing it. 1.481 + */ 1.482 + _dismiss: function PopupNotifications_dismiss() { 1.483 + let browser = this.panel.firstChild && 1.484 + this.panel.firstChild.notification.browser; 1.485 + this.panel.hidePopup(); 1.486 + if (browser) 1.487 + browser.focus(); 1.488 + }, 1.489 + 1.490 + /** 1.491 + * Hides the notification popup. 1.492 + */ 1.493 + _hidePanel: function PopupNotifications_hide() { 1.494 + this._ignoreDismissal = true; 1.495 + this.panel.hidePopup(); 1.496 + this._ignoreDismissal = false; 1.497 + }, 1.498 + 1.499 + /** 1.500 + * Removes all notifications from the notification popup. 1.501 + */ 1.502 + _clearPanel: function () { 1.503 + let popupnotification; 1.504 + while ((popupnotification = this.panel.lastChild)) { 1.505 + this.panel.removeChild(popupnotification); 1.506 + 1.507 + // If this notification was provided by the chrome document rather than 1.508 + // created ad hoc, move it back to where we got it from. 1.509 + let originalParent = gNotificationParents.get(popupnotification); 1.510 + if (originalParent) { 1.511 + popupnotification.notification = null; 1.512 + 1.513 + // Remove nodes dynamically added to the notification's menu button 1.514 + // in _refreshPanel. Keep popupnotificationcontent nodes; they are 1.515 + // provided by the chrome document. 1.516 + let contentNode = popupnotification.lastChild; 1.517 + while (contentNode) { 1.518 + let previousSibling = contentNode.previousSibling; 1.519 + if (contentNode.nodeName != "popupnotificationcontent") 1.520 + popupnotification.removeChild(contentNode); 1.521 + contentNode = previousSibling; 1.522 + } 1.523 + 1.524 + // Re-hide the notification such that it isn't rendered in the chrome 1.525 + // document. _refreshPanel will unhide it again when needed. 1.526 + popupnotification.hidden = true; 1.527 + 1.528 + originalParent.appendChild(popupnotification); 1.529 + } 1.530 + } 1.531 + }, 1.532 + 1.533 + _refreshPanel: function PopupNotifications_refreshPanel(notificationsToShow) { 1.534 + this._clearPanel(); 1.535 + 1.536 + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; 1.537 + 1.538 + notificationsToShow.forEach(function (n) { 1.539 + let doc = this.window.document; 1.540 + 1.541 + // Append "-notification" to the ID to try to avoid ID conflicts with other stuff 1.542 + // in the document. 1.543 + let popupnotificationID = n.id + "-notification"; 1.544 + 1.545 + // If the chrome document provides a popupnotification with this id, use 1.546 + // that. Otherwise create it ad-hoc. 1.547 + let popupnotification = doc.getElementById(popupnotificationID); 1.548 + if (popupnotification) 1.549 + gNotificationParents.set(popupnotification, popupnotification.parentNode); 1.550 + else 1.551 + popupnotification = doc.createElementNS(XUL_NS, "popupnotification"); 1.552 + 1.553 + popupnotification.setAttribute("label", n.message); 1.554 + popupnotification.setAttribute("id", popupnotificationID); 1.555 + popupnotification.setAttribute("popupid", n.id); 1.556 + popupnotification.setAttribute("closebuttoncommand", "PopupNotifications._dismiss();"); 1.557 + if (n.mainAction) { 1.558 + popupnotification.setAttribute("buttonlabel", n.mainAction.label); 1.559 + popupnotification.setAttribute("buttonaccesskey", n.mainAction.accessKey); 1.560 + popupnotification.setAttribute("buttoncommand", "PopupNotifications._onButtonCommand(event);"); 1.561 + popupnotification.setAttribute("menucommand", "PopupNotifications._onMenuCommand(event);"); 1.562 + popupnotification.setAttribute("closeitemcommand", "PopupNotifications._dismiss();event.stopPropagation();"); 1.563 + } else { 1.564 + popupnotification.removeAttribute("buttonlabel"); 1.565 + popupnotification.removeAttribute("buttonaccesskey"); 1.566 + popupnotification.removeAttribute("buttoncommand"); 1.567 + popupnotification.removeAttribute("menucommand"); 1.568 + popupnotification.removeAttribute("closeitemcommand"); 1.569 + } 1.570 + 1.571 + if (n.options.popupIconURL) 1.572 + popupnotification.setAttribute("icon", n.options.popupIconURL); 1.573 + if (n.options.learnMoreURL) 1.574 + popupnotification.setAttribute("learnmoreurl", n.options.learnMoreURL); 1.575 + else 1.576 + popupnotification.removeAttribute("learnmoreurl"); 1.577 + 1.578 + popupnotification.notification = n; 1.579 + 1.580 + if (n.secondaryActions) { 1.581 + n.secondaryActions.forEach(function (a) { 1.582 + let item = doc.createElementNS(XUL_NS, "menuitem"); 1.583 + item.setAttribute("label", a.label); 1.584 + item.setAttribute("accesskey", a.accessKey); 1.585 + item.notification = n; 1.586 + item.action = a; 1.587 + 1.588 + popupnotification.appendChild(item); 1.589 + }, this); 1.590 + 1.591 + if (n.options.hideNotNow) { 1.592 + popupnotification.setAttribute("hidenotnow", "true"); 1.593 + } 1.594 + else if (n.secondaryActions.length) { 1.595 + let closeItemSeparator = doc.createElementNS(XUL_NS, "menuseparator"); 1.596 + popupnotification.appendChild(closeItemSeparator); 1.597 + } 1.598 + } 1.599 + 1.600 + this.panel.appendChild(popupnotification); 1.601 + 1.602 + // The popupnotification may be hidden if we got it from the chrome 1.603 + // document rather than creating it ad hoc. 1.604 + popupnotification.hidden = false; 1.605 + }, this); 1.606 + }, 1.607 + 1.608 + _showPanel: function PopupNotifications_showPanel(notificationsToShow, anchorElement) { 1.609 + this.panel.hidden = false; 1.610 + 1.611 + notificationsToShow = notificationsToShow.filter(n => { 1.612 + let dismiss = this._fireCallback(n, NOTIFICATION_EVENT_SHOWING); 1.613 + if (dismiss) 1.614 + n.dismissed = true; 1.615 + return !dismiss; 1.616 + }); 1.617 + if (!notificationsToShow.length) 1.618 + return; 1.619 + 1.620 + this._refreshPanel(notificationsToShow); 1.621 + 1.622 + if (this.isPanelOpen && this._currentAnchorElement == anchorElement) 1.623 + return; 1.624 + 1.625 + // If the panel is already open but we're changing anchors, we need to hide 1.626 + // it first. Otherwise it can appear in the wrong spot. (_hidePanel is 1.627 + // safe to call even if the panel is already hidden.) 1.628 + this._hidePanel(); 1.629 + 1.630 + // If the anchor element is hidden or null, use the tab as the anchor. We 1.631 + // only ever show notifications for the current browser, so we can just use 1.632 + // the current tab. 1.633 + let selectedTab = this.tabbrowser.selectedTab; 1.634 + if (anchorElement) { 1.635 + let bo = anchorElement.boxObject; 1.636 + if (bo.height == 0 && bo.width == 0) 1.637 + anchorElement = selectedTab; // hidden 1.638 + } else { 1.639 + anchorElement = selectedTab; // null 1.640 + } 1.641 + 1.642 + this._currentAnchorElement = anchorElement; 1.643 + 1.644 + // On OS X and Linux we need a different panel arrow color for 1.645 + // click-to-play plugins, so copy the popupid and use css. 1.646 + this.panel.setAttribute("popupid", this.panel.firstChild.getAttribute("popupid")); 1.647 + notificationsToShow.forEach(function (n) { 1.648 + // Remember the time the notification was shown for the security delay. 1.649 + n.timeShown = this.window.performance.now(); 1.650 + }, this); 1.651 + this.panel.openPopup(anchorElement, "bottomcenter topleft"); 1.652 + notificationsToShow.forEach(function (n) { 1.653 + this._fireCallback(n, NOTIFICATION_EVENT_SHOWN); 1.654 + }, this); 1.655 + }, 1.656 + 1.657 + /** 1.658 + * Updates the notification state in response to window activation or tab 1.659 + * selection changes. 1.660 + * 1.661 + * @param notifications an array of Notification instances. if null, 1.662 + * notifications will be retrieved off the current 1.663 + * browser tab 1.664 + * @param anchor is a XUL element that the notifications panel will be 1.665 + * anchored to 1.666 + * @param dismissShowing if true, dismiss any currently visible notifications 1.667 + * if there are no notifications to show. Otherwise, 1.668 + * currently displayed notifications will be left alone. 1.669 + */ 1.670 + _update: function PopupNotifications_update(notifications, anchor, dismissShowing = false) { 1.671 + let useIconBox = this.iconBox && (!anchor || anchor.parentNode == this.iconBox); 1.672 + if (useIconBox) { 1.673 + // hide icons of the previous tab. 1.674 + this._hideIcons(); 1.675 + } 1.676 + 1.677 + let anchorElement = anchor, notificationsToShow = []; 1.678 + if (!notifications) 1.679 + notifications = this._currentNotifications; 1.680 + let haveNotifications = notifications.length > 0; 1.681 + if (haveNotifications) { 1.682 + // Only show the notifications that have the passed-in anchor (or the 1.683 + // first notification's anchor, if none was passed in). Other 1.684 + // notifications will be shown once these are dismissed. 1.685 + anchorElement = anchor || notifications[0].anchorElement; 1.686 + 1.687 + if (useIconBox) { 1.688 + this._showIcons(notifications); 1.689 + this.iconBox.hidden = false; 1.690 + } else if (anchorElement) { 1.691 + this._updateAnchorIcon(notifications, anchorElement); 1.692 + } 1.693 + 1.694 + // Also filter out notifications that have been dismissed. 1.695 + notificationsToShow = notifications.filter(function (n) { 1.696 + return !n.dismissed && n.anchorElement == anchorElement && 1.697 + !n.options.neverShow; 1.698 + }); 1.699 + } 1.700 + 1.701 + if (notificationsToShow.length > 0) { 1.702 + this._showPanel(notificationsToShow, anchorElement); 1.703 + } else { 1.704 + // Notify observers that we're not showing the popup (useful for testing) 1.705 + this._notify("updateNotShowing"); 1.706 + 1.707 + // Close the panel if there are no notifications to show. 1.708 + // When called from PopupNotifications.show() we should never close the 1.709 + // panel, however. It may just be adding a dismissed notification, in 1.710 + // which case we want to continue showing any existing notifications. 1.711 + if (!dismissShowing) 1.712 + this._dismiss(); 1.713 + 1.714 + // Only hide the iconBox if we actually have no notifications (as opposed 1.715 + // to not having any showable notifications) 1.716 + if (!haveNotifications) { 1.717 + if (useIconBox) 1.718 + this.iconBox.hidden = true; 1.719 + else if (anchorElement) 1.720 + anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING); 1.721 + } 1.722 + } 1.723 + }, 1.724 + 1.725 + _updateAnchorIcon: function PopupNotifications_updateAnchorIcon(notifications, 1.726 + anchorElement) { 1.727 + anchorElement.setAttribute(ICON_ATTRIBUTE_SHOWING, "true"); 1.728 + // Use the anchorID as a class along with the default icon class as a 1.729 + // fallback if anchorID is not defined in CSS. We always use the first 1.730 + // notifications icon, so in the case of multiple notifications we'll 1.731 + // only use the default icon. 1.732 + if (anchorElement.classList.contains("notification-anchor-icon")) { 1.733 + // remove previous icon classes 1.734 + let className = anchorElement.className.replace(/([-\w]+-notification-icon\s?)/g,"") 1.735 + className = "default-notification-icon " + className; 1.736 + if (notifications.length == 1) { 1.737 + className = notifications[0].anchorID + " " + className; 1.738 + } 1.739 + anchorElement.className = className; 1.740 + } 1.741 + }, 1.742 + 1.743 + _showIcons: function PopupNotifications_showIcons(aCurrentNotifications) { 1.744 + for (let notification of aCurrentNotifications) { 1.745 + let anchorElm = notification.anchorElement; 1.746 + if (anchorElm) { 1.747 + anchorElm.setAttribute(ICON_ATTRIBUTE_SHOWING, "true"); 1.748 + } 1.749 + } 1.750 + }, 1.751 + 1.752 + _hideIcons: function PopupNotifications_hideIcons() { 1.753 + let icons = this.iconBox.querySelectorAll(ICON_SELECTOR); 1.754 + for (let icon of icons) { 1.755 + icon.removeAttribute(ICON_ATTRIBUTE_SHOWING); 1.756 + } 1.757 + }, 1.758 + 1.759 + /** 1.760 + * Gets and sets notifications for the browser. 1.761 + */ 1.762 + _getNotificationsForBrowser: function PopupNotifications_getNotifications(browser) { 1.763 + let notifications = popupNotificationsMap.get(browser); 1.764 + if (!notifications) { 1.765 + // Initialize the WeakMap for the browser so callers can reference/manipulate the array. 1.766 + notifications = []; 1.767 + popupNotificationsMap.set(browser, notifications); 1.768 + } 1.769 + return notifications; 1.770 + }, 1.771 + _setNotificationsForBrowser: function PopupNotifications_setNotifications(browser, notifications) { 1.772 + popupNotificationsMap.set(browser, notifications); 1.773 + return notifications; 1.774 + }, 1.775 + 1.776 + _isActiveBrowser: function (browser) { 1.777 + // Note: This helper only exists, because in e10s builds, 1.778 + // we can't access the docShell of a browser from chrome. 1.779 + return browser.docShell 1.780 + ? browser.docShell.isActive 1.781 + : (this.window.gBrowser.selectedBrowser == browser); 1.782 + }, 1.783 + 1.784 + _onIconBoxCommand: function PopupNotifications_onIconBoxCommand(event) { 1.785 + // Left click, space or enter only 1.786 + let type = event.type; 1.787 + if (type == "click" && event.button != 0) 1.788 + return; 1.789 + 1.790 + if (type == "keypress" && 1.791 + !(event.charCode == Ci.nsIDOMKeyEvent.DOM_VK_SPACE || 1.792 + event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN)) 1.793 + return; 1.794 + 1.795 + if (this._currentNotifications.length == 0) 1.796 + return; 1.797 + 1.798 + // Get the anchor that is the immediate child of the icon box 1.799 + let anchor = event.target; 1.800 + while (anchor && anchor.parentNode != this.iconBox) 1.801 + anchor = anchor.parentNode; 1.802 + 1.803 + this._reshowNotifications(anchor); 1.804 + }, 1.805 + 1.806 + _reshowNotifications: function PopupNotifications_reshowNotifications(anchor, browser) { 1.807 + // Mark notifications anchored to this anchor as un-dismissed 1.808 + let notifications = this._getNotificationsForBrowser(browser || this.tabbrowser.selectedBrowser); 1.809 + notifications.forEach(function (n) { 1.810 + if (n.anchorElement == anchor) 1.811 + n.dismissed = false; 1.812 + }); 1.813 + 1.814 + // ...and then show them. 1.815 + this._update(notifications, anchor); 1.816 + }, 1.817 + 1.818 + _swapBrowserNotifications: function PopupNotifications_swapBrowserNoficications(ourBrowser, otherBrowser) { 1.819 + // When swaping browser docshells (e.g. dragging tab to new window) we need 1.820 + // to update our notification map. 1.821 + 1.822 + let ourNotifications = this._getNotificationsForBrowser(ourBrowser); 1.823 + let other = otherBrowser.ownerDocument.defaultView.PopupNotifications; 1.824 + if (!other) { 1.825 + if (ourNotifications.length > 0) 1.826 + Cu.reportError("unable to swap notifications: otherBrowser doesn't support notifications"); 1.827 + return; 1.828 + } 1.829 + let otherNotifications = other._getNotificationsForBrowser(otherBrowser); 1.830 + if (ourNotifications.length < 1 && otherNotifications.length < 1) { 1.831 + // No notification to swap. 1.832 + return; 1.833 + } 1.834 + 1.835 + otherNotifications = otherNotifications.filter(n => { 1.836 + if (this._fireCallback(n, NOTIFICATION_EVENT_SWAPPING, ourBrowser)) { 1.837 + n.browser = ourBrowser; 1.838 + n.owner = this; 1.839 + return true; 1.840 + } 1.841 + other._fireCallback(n, NOTIFICATION_EVENT_REMOVED); 1.842 + return false; 1.843 + }); 1.844 + 1.845 + ourNotifications = ourNotifications.filter(n => { 1.846 + if (this._fireCallback(n, NOTIFICATION_EVENT_SWAPPING, otherBrowser)) { 1.847 + n.browser = otherBrowser; 1.848 + n.owner = other; 1.849 + return true; 1.850 + } 1.851 + this._fireCallback(n, NOTIFICATION_EVENT_REMOVED); 1.852 + return false; 1.853 + }); 1.854 + 1.855 + this._setNotificationsForBrowser(otherBrowser, ourNotifications); 1.856 + other._setNotificationsForBrowser(ourBrowser, otherNotifications); 1.857 + 1.858 + if (otherNotifications.length > 0) 1.859 + this._update(otherNotifications, otherNotifications[0].anchorElement); 1.860 + if (ourNotifications.length > 0) 1.861 + other._update(ourNotifications, ourNotifications[0].anchorElement); 1.862 + }, 1.863 + 1.864 + _fireCallback: function PopupNotifications_fireCallback(n, event, ...args) { 1.865 + try { 1.866 + if (n.options.eventCallback) 1.867 + return n.options.eventCallback.call(n, event, ...args); 1.868 + } catch (error) { 1.869 + Cu.reportError(error); 1.870 + } 1.871 + return undefined; 1.872 + }, 1.873 + 1.874 + _onPopupHidden: function PopupNotifications_onPopupHidden(event) { 1.875 + if (event.target != this.panel || this._ignoreDismissal) 1.876 + return; 1.877 + 1.878 + let browser = this.panel.firstChild && 1.879 + this.panel.firstChild.notification.browser; 1.880 + if (!browser) 1.881 + return; 1.882 + 1.883 + let notifications = this._getNotificationsForBrowser(browser); 1.884 + // Mark notifications as dismissed and call dismissal callbacks 1.885 + Array.forEach(this.panel.childNodes, function (nEl) { 1.886 + let notificationObj = nEl.notification; 1.887 + // Never call a dismissal handler on a notification that's been removed. 1.888 + if (notifications.indexOf(notificationObj) == -1) 1.889 + return; 1.890 + 1.891 + // Do not mark the notification as dismissed or fire NOTIFICATION_EVENT_DISMISSED 1.892 + // if the notification is removed. 1.893 + if (notificationObj.options.removeOnDismissal) 1.894 + this._remove(notificationObj); 1.895 + else { 1.896 + notificationObj.dismissed = true; 1.897 + this._fireCallback(notificationObj, NOTIFICATION_EVENT_DISMISSED); 1.898 + } 1.899 + }, this); 1.900 + 1.901 + this._clearPanel(); 1.902 + 1.903 + this._update(); 1.904 + }, 1.905 + 1.906 + _onButtonCommand: function PopupNotifications_onButtonCommand(event) { 1.907 + // Need to find the associated notification object, which is a bit tricky 1.908 + // since it isn't associated with the button directly - this is kind of 1.909 + // gross and very dependent on the structure of the popupnotification 1.910 + // binding's content. 1.911 + let target = event.originalTarget; 1.912 + let notificationEl; 1.913 + let parent = target; 1.914 + while (parent && (parent = target.ownerDocument.getBindingParent(parent))) 1.915 + notificationEl = parent; 1.916 + 1.917 + if (!notificationEl) 1.918 + throw "PopupNotifications_onButtonCommand: couldn't find notification element"; 1.919 + 1.920 + if (!notificationEl.notification) 1.921 + throw "PopupNotifications_onButtonCommand: couldn't find notification"; 1.922 + 1.923 + let notification = notificationEl.notification; 1.924 + let timeSinceShown = this.window.performance.now() - notification.timeShown; 1.925 + 1.926 + // Only report the first time mainAction is triggered and remember that this occurred. 1.927 + if (!notification.timeMainActionFirstTriggered) { 1.928 + notification.timeMainActionFirstTriggered = timeSinceShown; 1.929 + Services.telemetry.getHistogramById("POPUP_NOTIFICATION_MAINACTION_TRIGGERED_MS"). 1.930 + add(timeSinceShown); 1.931 + } 1.932 + 1.933 + if (timeSinceShown < this.buttonDelay) { 1.934 + Services.console.logStringMessage("PopupNotifications_onButtonCommand: " + 1.935 + "Button click happened before the security delay: " + 1.936 + timeSinceShown + "ms"); 1.937 + return; 1.938 + } 1.939 + try { 1.940 + notification.mainAction.callback.call(); 1.941 + } catch(error) { 1.942 + Cu.reportError(error); 1.943 + } 1.944 + 1.945 + if (notification.mainAction.dismiss) { 1.946 + this._dismiss(); 1.947 + return; 1.948 + } 1.949 + 1.950 + this._remove(notification); 1.951 + this._update(); 1.952 + }, 1.953 + 1.954 + _onMenuCommand: function PopupNotifications_onMenuCommand(event) { 1.955 + let target = event.originalTarget; 1.956 + if (!target.action || !target.notification) 1.957 + throw "menucommand target has no associated action/notification"; 1.958 + 1.959 + event.stopPropagation(); 1.960 + try { 1.961 + target.action.callback.call(); 1.962 + } catch(error) { 1.963 + Cu.reportError(error); 1.964 + } 1.965 + 1.966 + if (target.action.dismiss) { 1.967 + this._dismiss(); 1.968 + return; 1.969 + } 1.970 + 1.971 + this._remove(target.notification); 1.972 + this._update(); 1.973 + }, 1.974 + 1.975 + _notify: function PopupNotifications_notify(topic) { 1.976 + Services.obs.notifyObservers(null, "PopupNotifications-" + topic, ""); 1.977 + }, 1.978 +};