Bumping manifests a=b2g-bump
[gecko.git] / toolkit / modules / PopupNotifications.jsm
blobaafed4ed8fc0abb15d8459b89fbfe4eef1784bf7
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 this.EXPORTED_SYMBOLS = ["PopupNotifications"];
7 var Cc = Components.classes, Ci = Components.interfaces, Cu = Components.utils;
9 Cu.import("resource://gre/modules/Services.jsm");
11 const NOTIFICATION_EVENT_DISMISSED = "dismissed";
12 const NOTIFICATION_EVENT_REMOVED = "removed";
13 const NOTIFICATION_EVENT_SHOWING = "showing";
14 const NOTIFICATION_EVENT_SHOWN = "shown";
15 const NOTIFICATION_EVENT_SWAPPING = "swapping";
17 const ICON_SELECTOR = ".notification-anchor-icon";
18 const ICON_ATTRIBUTE_SHOWING = "showing";
20 const PREF_SECURITY_DELAY = "security.notification_enable_delay";
22 let popupNotificationsMap = new WeakMap();
23 let gNotificationParents = new WeakMap;
25 function getAnchorFromBrowser(aBrowser) {
26   let anchor = aBrowser.getAttribute("popupnotificationanchor") ||
27                 aBrowser.popupnotificationanchor;
28   if (anchor) {
29     if (anchor instanceof Ci.nsIDOMXULElement) {
30       return anchor;
31     }
32     return aBrowser.ownerDocument.getElementById(anchor);
33   }
34   return null;
37 /**
38  * Notification object describes a single popup notification.
39  *
40  * @see PopupNotifications.show()
41  */
42 function Notification(id, message, anchorID, mainAction, secondaryActions,
43                       browser, owner, options) {
44   this.id = id;
45   this.message = message;
46   this.anchorID = anchorID;
47   this.mainAction = mainAction;
48   this.secondaryActions = secondaryActions || [];
49   this.browser = browser;
50   this.owner = owner;
51   this.options = options || {};
54 Notification.prototype = {
56   id: null,
57   message: null,
58   anchorID: null,
59   mainAction: null,
60   secondaryActions: null,
61   browser: null,
62   owner: null,
63   options: null,
64   timeShown: null,
66   /**
67    * Removes the notification and updates the popup accordingly if needed.
68    */
69   remove: function Notification_remove() {
70     this.owner.remove(this);
71   },
73   get anchorElement() {
74     let iconBox = this.owner.iconBox;
76     let anchorElement = getAnchorFromBrowser(this.browser);
78     if (!iconBox)
79       return anchorElement;
81     if (!anchorElement && this.anchorID)
82       anchorElement = iconBox.querySelector("#"+this.anchorID);
84     // Use a default anchor icon if it's available
85     if (!anchorElement)
86       anchorElement = iconBox.querySelector("#default-notification-icon") ||
87                       iconBox;
89     return anchorElement;
90   },
92   reshow: function() {
93     this.owner._reshowNotifications(this.anchorElement, this.browser);
94   }
97 /**
98  * The PopupNotifications object manages popup notifications for a given browser
99  * window.
100  * @param tabbrowser
101  *        window's <xul:tabbrowser/>. Used to observe tab switching events and
102  *        for determining the active browser element.
103  * @param panel
104  *        The <xul:panel/> element to use for notifications. The panel is
105  *        populated with <popupnotification> children and displayed it as
106  *        needed.
107  * @param iconBox
108  *        Reference to a container element that should be hidden or
109  *        unhidden when notifications are hidden or shown. It should be the
110  *        parent of anchor elements whose IDs are passed to show().
111  *        It is used as a fallback popup anchor if notifications specify
112  *        invalid or non-existent anchor IDs.
113  */
114 this.PopupNotifications = function PopupNotifications(tabbrowser, panel, iconBox) {
115   if (!(tabbrowser instanceof Ci.nsIDOMXULElement))
116     throw "Invalid tabbrowser";
117   if (iconBox && !(iconBox instanceof Ci.nsIDOMXULElement))
118     throw "Invalid iconBox";
119   if (!(panel instanceof Ci.nsIDOMXULElement))
120     throw "Invalid panel";
122   this.window = tabbrowser.ownerDocument.defaultView;
123   this.panel = panel;
124   this.tabbrowser = tabbrowser;
125   this.iconBox = iconBox;
126   this.buttonDelay = Services.prefs.getIntPref(PREF_SECURITY_DELAY);
128   this.panel.addEventListener("popuphidden", this, true);
130   this.window.addEventListener("activate", this, true);
131   if (this.tabbrowser.tabContainer)
132     this.tabbrowser.tabContainer.addEventListener("TabSelect", this, true);
135 PopupNotifications.prototype = {
137   window: null,
138   panel: null,
139   tabbrowser: null,
141   _iconBox: null,
142   set iconBox(iconBox) {
143     // Remove the listeners on the old iconBox, if needed
144     if (this._iconBox) {
145       this._iconBox.removeEventListener("click", this, false);
146       this._iconBox.removeEventListener("keypress", this, false);
147     }
148     this._iconBox = iconBox;
149     if (iconBox) {
150       iconBox.addEventListener("click", this, false);
151       iconBox.addEventListener("keypress", this, false);
152     }
153   },
154   get iconBox() {
155     return this._iconBox;
156   },
158   /**
159    * Enable or disable the opening/closing transition.
160    * @param state
161    *        Boolean state
162    */
163   set transitionsEnabled(state) {
164     if (state) {
165       this.panel.removeAttribute("animate");
166     }
167     else {
168       this.panel.setAttribute("animate", "false");
169     }
170   },
172   /**
173    * Retrieve a Notification object associated with the browser/ID pair.
174    * @param id
175    *        The Notification ID to search for.
176    * @param browser
177    *        The browser whose notifications should be searched. If null, the
178    *        currently selected browser's notifications will be searched.
179    *
180    * @returns the corresponding Notification object, or null if no such
181    *          notification exists.
182    */
183   getNotification: function PopupNotifications_getNotification(id, browser) {
184     let n = null;
185     let notifications = this._getNotificationsForBrowser(browser || this.tabbrowser.selectedBrowser);
186     notifications.some(function(x) x.id == id && (n = x));
187     return n;
188   },
190   /**
191    * Adds a new popup notification.
192    * @param browser
193    *        The <xul:browser> element associated with the notification. Must not
194    *        be null.
195    * @param id
196    *        A unique ID that identifies the type of notification (e.g.
197    *        "geolocation"). Only one notification with a given ID can be visible
198    *        at a time. If a notification already exists with the given ID, it
199    *        will be replaced.
200    * @param message
201    *        The text to be displayed in the notification.
202    * @param anchorID
203    *        The ID of the element that should be used as this notification
204    *        popup's anchor. May be null, in which case the notification will be
205    *        anchored to the iconBox.
206    * @param mainAction
207    *        A JavaScript object literal describing the notification button's
208    *        action. If present, it must have the following properties:
209    *          - label (string): the button's label.
210    *          - accessKey (string): the button's accessKey.
211    *          - callback (function): a callback to be invoked when the button is
212    *            pressed.
213    *          - [optional] dismiss (boolean): If this is true, the notification
214    *            will be dismissed instead of removed after running the callback.
215    *        If null, the notification will not have a button, and
216    *        secondaryActions will be ignored.
217    * @param secondaryActions
218    *        An optional JavaScript array describing the notification's alternate
219    *        actions. The array should contain objects with the same properties
220    *        as mainAction. These are used to populate the notification button's
221    *        dropdown menu.
222    * @param options
223    *        An options JavaScript object holding additional properties for the
224    *        notification. The following properties are currently supported:
225    *        persistence: An integer. The notification will not automatically
226    *                     dismiss for this many page loads.
227    *        timeout:     A time in milliseconds. The notification will not
228    *                     automatically dismiss before this time.
229    *        persistWhileVisible:
230    *                     A boolean. If true, a visible notification will always
231    *                     persist across location changes.
232    *        dismissed:   Whether the notification should be added as a dismissed
233    *                     notification. Dismissed notifications can be activated
234    *                     by clicking on their anchorElement.
235    *        eventCallback:
236    *                     Callback to be invoked when the notification changes
237    *                     state. The callback's first argument is a string
238    *                     identifying the state change:
239    *                     "dismissed": notification has been dismissed by the
240    *                                  user (e.g. by clicking away or switching
241    *                                  tabs)
242    *                     "removed": notification has been removed (due to
243    *                                location change or user action)
244    *                     "showing": notification is about to be shown
245    *                                (this can be fired multiple times as
246    *                                 notifications are dismissed and re-shown)
247    *                                If the callback returns true, the notification
248    *                                will be dismissed.
249    *                     "shown": notification has been shown (this can be fired
250    *                              multiple times as notifications are dismissed
251    *                              and re-shown)
252    *                     "swapping": the docshell of the browser that created
253    *                                 the notification is about to be swapped to
254    *                                 another browser. A second parameter contains
255    *                                 the browser that is receiving the docshell,
256    *                                 so that the event callback can transfer stuff
257    *                                 specific to this notification.
258    *                                 If the callback returns true, the notification
259    *                                 will be moved to the new browser.
260    *                                 If the callback isn't implemented, returns false,
261    *                                 or doesn't return any value, the notification
262    *                                 will be removed.
263    *        neverShow:   Indicate that no popup should be shown for this
264    *                     notification. Useful for just showing the anchor icon.
265    *        removeOnDismissal:
266    *                     Notifications with this parameter set to true will be
267    *                     removed when they would have otherwise been dismissed
268    *                     (i.e. any time the popup is closed due to user
269    *                     interaction).
270    *        hideNotNow:  If true, indicates that the 'Not Now' menuitem should
271    *                     not be shown. If 'Not Now' is hidden, it needs to be
272    *                     replaced by another 'do nothing' item, so providing at
273    *                     least one secondary action is required; and one of the
274    *                     actions needs to have the 'dismiss' property set to true.
275    *        popupIconURL:
276    *                     A string. URL of the image to be displayed in the popup.
277    *                     Normally specified in CSS using list-style-image and the
278    *                     .popup-notification-icon[popupid=...] selector.
279    *        learnMoreURL:
280    *                     A string URL. Setting this property will make the
281    *                     prompt display a "Learn More" link that, when clicked,
282    *                     opens the URL in a new tab.
283    * @returns the Notification object corresponding to the added notification.
284    */
285   show: function PopupNotifications_show(browser, id, message, anchorID,
286                                          mainAction, secondaryActions, options) {
287     function isInvalidAction(a) {
288       return !a || !(typeof(a.callback) == "function") || !a.label || !a.accessKey;
289     }
291     if (!browser)
292       throw "PopupNotifications_show: invalid browser";
293     if (!id)
294       throw "PopupNotifications_show: invalid ID";
295     if (mainAction && isInvalidAction(mainAction))
296       throw "PopupNotifications_show: invalid mainAction";
297     if (secondaryActions && secondaryActions.some(isInvalidAction))
298       throw "PopupNotifications_show: invalid secondaryActions";
299     if (options && options.hideNotNow &&
300         (!secondaryActions || !secondaryActions.length ||
301          !secondaryActions.concat(mainAction).some(action => action.dismiss)))
302       throw "PopupNotifications_show: 'Not Now' item hidden without replacement";
304     let notification = new Notification(id, message, anchorID, mainAction,
305                                         secondaryActions, browser, this, options);
307     if (options && options.dismissed)
308       notification.dismissed = true;
310     let existingNotification = this.getNotification(id, browser);
311     if (existingNotification)
312       this._remove(existingNotification);
314     let notifications = this._getNotificationsForBrowser(browser);
315     notifications.push(notification);
317     let isActive = this._isActiveBrowser(browser);
318     let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
319     if (isActive && fm.activeWindow == this.window) {
320       // show panel now
321       this._update(notifications, notification.anchorElement, true);
322     } else {
323       // Otherwise, update() will display the notification the next time the
324       // relevant tab/window is selected.
326       // If the tab is selected but the window is in the background, let the OS
327       // tell the user that there's a notification waiting in that window.
328       // At some point we might want to do something about background tabs here
329       // too. When the user switches to this window, we'll show the panel if
330       // this browser is a tab (thus showing the anchor icon). For
331       // non-tabbrowser browsers, we need to make the icon visible now or the
332       // user will not be able to open the panel.
333       if (!notification.dismissed && isActive) {
334         this.window.getAttention();
335         if (notification.anchorElement.parentNode != this.iconBox) {
336           this._updateAnchorIcon(notifications, notification.anchorElement);
337         }
338       }
340       // Notify observers that we're not showing the popup (useful for testing)
341       this._notify("backgroundShow");
342     }
344     return notification;
345   },
347   /**
348    * Returns true if the notification popup is currently being displayed.
349    */
350   get isPanelOpen() {
351     let panelState = this.panel.state;
353     return panelState == "showing" || panelState == "open";
354   },
356   /**
357    * Called by the consumer to indicate that a browser's location has changed,
358    * so that we can update the active notifications accordingly.
359    */
360   locationChange: function PopupNotifications_locationChange(aBrowser) {
361     if (!aBrowser)
362       throw "PopupNotifications_locationChange: invalid browser";
364     let notifications = this._getNotificationsForBrowser(aBrowser);
366     notifications = notifications.filter(function (notification) {
367       // The persistWhileVisible option allows an open notification to persist
368       // across location changes
369       if (notification.options.persistWhileVisible &&
370           this.isPanelOpen) {
371         if ("persistence" in notification.options &&
372           notification.options.persistence)
373           notification.options.persistence--;
374         return true;
375       }
377       // The persistence option allows a notification to persist across multiple
378       // page loads
379       if ("persistence" in notification.options &&
380           notification.options.persistence) {
381         notification.options.persistence--;
382         return true;
383       }
385       // The timeout option allows a notification to persist until a certain time
386       if ("timeout" in notification.options &&
387           Date.now() <= notification.options.timeout) {
388         return true;
389       }
391       this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED);
392       return false;
393     }, this);
395     this._setNotificationsForBrowser(aBrowser, notifications);
397     if (this._isActiveBrowser(aBrowser)) {
398       // get the anchor element if the browser has defined one so it will
399       // _update will handle both the tabs iconBox and non-tab permission
400       // anchors.
401       let anchorElement = notifications.length > 0 ? notifications[0].anchorElement : null;
402       if (!anchorElement)
403         anchorElement = getAnchorFromBrowser(aBrowser);
404       this._update(notifications, anchorElement);
405     }
406   },
408   /**
409    * Removes a Notification.
410    * @param notification
411    *        The Notification object to remove.
412    */
413   remove: function PopupNotifications_remove(notification) {
414     this._remove(notification);
416     if (this._isActiveBrowser(notification.browser)) {
417       let notifications = this._getNotificationsForBrowser(notification.browser);
418       this._update(notifications, notification.anchorElement);
419     }
420   },
422   handleEvent: function (aEvent) {
423     switch (aEvent.type) {
424       case "popuphidden":
425         this._onPopupHidden(aEvent);
426         break;
427       case "activate":
428       case "TabSelect":
429         let self = this;
430         // setTimeout(..., 0) needed, otherwise openPopup from "activate" event
431         // handler results in the popup being hidden again for some reason...
432         this.window.setTimeout(function () {
433           self._update();
434         }, 0);
435         break;
436       case "click":
437       case "keypress":
438         this._onIconBoxCommand(aEvent);
439         break;
440     }
441   },
443 ////////////////////////////////////////////////////////////////////////////////
444 // Utility methods
445 ////////////////////////////////////////////////////////////////////////////////
447   _ignoreDismissal: null,
448   _currentAnchorElement: null,
450   /**
451    * Gets notifications for the currently selected browser.
452    */
453   get _currentNotifications() {
454     return this.tabbrowser.selectedBrowser ? this._getNotificationsForBrowser(this.tabbrowser.selectedBrowser) : [];
455   },
457   _remove: function PopupNotifications_removeHelper(notification) {
458     // This notification may already be removed, in which case let's just fail
459     // silently.
460     let notifications = this._getNotificationsForBrowser(notification.browser);
461     if (!notifications)
462       return;
464     var index = notifications.indexOf(notification);
465     if (index == -1)
466       return;
468     if (this._isActiveBrowser(notification.browser))
469       notification.anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
471     // remove the notification
472     notifications.splice(index, 1);
473     this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED);
474   },
476   /**
477    * Dismisses the notification without removing it.
478    */
479   _dismiss: function PopupNotifications_dismiss() {
480     let browser = this.panel.firstChild &&
481                   this.panel.firstChild.notification.browser;
482     this.panel.hidePopup();
483     if (browser)
484       browser.focus();
485   },
487   /**
488    * Hides the notification popup.
489    */
490   _hidePanel: function PopupNotifications_hide() {
491     // We need to disable the closing animation when setting _ignoreDismissal
492     // to true, otherwise the popuphidden event will fire after we have set
493     // _ignoreDismissal back to false.
494     let transitionsEnabled = this.transitionsEnabled;
495     this.transitionsEnabled = false;
496     this._ignoreDismissal = true;
497     this.panel.hidePopup();
498     this._ignoreDismissal = false;
499     this.transitionsEnabled = transitionsEnabled;
500   },
502   /**
503    * Removes all notifications from the notification popup.
504    */
505   _clearPanel: function () {
506     let popupnotification;
507     while ((popupnotification = this.panel.lastChild)) {
508       this.panel.removeChild(popupnotification);
510       // If this notification was provided by the chrome document rather than
511       // created ad hoc, move it back to where we got it from.
512       let originalParent = gNotificationParents.get(popupnotification);
513       if (originalParent) {
514         popupnotification.notification = null;
516         // Remove nodes dynamically added to the notification's menu button
517         // in _refreshPanel. Keep popupnotificationcontent nodes; they are
518         // provided by the chrome document.
519         let contentNode = popupnotification.lastChild;
520         while (contentNode) {
521           let previousSibling = contentNode.previousSibling;
522           if (contentNode.nodeName != "popupnotificationcontent")
523             popupnotification.removeChild(contentNode);
524           contentNode = previousSibling;
525         }
527         // Re-hide the notification such that it isn't rendered in the chrome
528         // document. _refreshPanel will unhide it again when needed.
529         popupnotification.hidden = true;
531         originalParent.appendChild(popupnotification);
532       }
533     }
534   },
536   _refreshPanel: function PopupNotifications_refreshPanel(notificationsToShow) {
537     this._clearPanel();
539     const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
541     notificationsToShow.forEach(function (n) {
542       let doc = this.window.document;
544       // Append "-notification" to the ID to try to avoid ID conflicts with other stuff
545       // in the document.
546       let popupnotificationID = n.id + "-notification";
548       // If the chrome document provides a popupnotification with this id, use
549       // that. Otherwise create it ad-hoc.
550       let popupnotification = doc.getElementById(popupnotificationID);
551       if (popupnotification)
552         gNotificationParents.set(popupnotification, popupnotification.parentNode);
553       else
554         popupnotification = doc.createElementNS(XUL_NS, "popupnotification");
556       popupnotification.setAttribute("label", n.message);
557       popupnotification.setAttribute("id", popupnotificationID);
558       popupnotification.setAttribute("popupid", n.id);
559       popupnotification.setAttribute("closebuttoncommand", "PopupNotifications._dismiss();");
560       if (n.mainAction) {
561         popupnotification.setAttribute("buttonlabel", n.mainAction.label);
562         popupnotification.setAttribute("buttonaccesskey", n.mainAction.accessKey);
563         popupnotification.setAttribute("buttoncommand", "PopupNotifications._onButtonCommand(event);");
564         popupnotification.setAttribute("menucommand", "PopupNotifications._onMenuCommand(event);");
565         popupnotification.setAttribute("closeitemcommand", "PopupNotifications._dismiss();event.stopPropagation();");
566       } else {
567         popupnotification.removeAttribute("buttonlabel");
568         popupnotification.removeAttribute("buttonaccesskey");
569         popupnotification.removeAttribute("buttoncommand");
570         popupnotification.removeAttribute("menucommand");
571         popupnotification.removeAttribute("closeitemcommand");
572       }
574       if (n.options.popupIconURL)
575         popupnotification.setAttribute("icon", n.options.popupIconURL);
576       if (n.options.learnMoreURL)
577         popupnotification.setAttribute("learnmoreurl", n.options.learnMoreURL);
578       else
579         popupnotification.removeAttribute("learnmoreurl");
581       popupnotification.notification = n;
583       if (n.secondaryActions) {
584         n.secondaryActions.forEach(function (a) {
585           let item = doc.createElementNS(XUL_NS, "menuitem");
586           item.setAttribute("label", a.label);
587           item.setAttribute("accesskey", a.accessKey);
588           item.notification = n;
589           item.action = a;
591           popupnotification.appendChild(item);
592         }, this);
594         if (n.options.hideNotNow) {
595           popupnotification.setAttribute("hidenotnow", "true");
596         }
597         else if (n.secondaryActions.length) {
598           let closeItemSeparator = doc.createElementNS(XUL_NS, "menuseparator");
599           popupnotification.appendChild(closeItemSeparator);
600         }
601       }
603       this.panel.appendChild(popupnotification);
605       // The popupnotification may be hidden if we got it from the chrome
606       // document rather than creating it ad hoc.
607       popupnotification.hidden = false;
608     }, this);
609   },
611   _showPanel: function PopupNotifications_showPanel(notificationsToShow, anchorElement) {
612     this.panel.hidden = false;
614     notificationsToShow = notificationsToShow.filter(n => {
615       let dismiss = this._fireCallback(n, NOTIFICATION_EVENT_SHOWING);
616       if (dismiss)
617         n.dismissed = true;
618       return !dismiss;
619     });
620     if (!notificationsToShow.length)
621       return;
623     this._refreshPanel(notificationsToShow);
625     if (this.isPanelOpen && this._currentAnchorElement == anchorElement) {
626       notificationsToShow.forEach(function (n) {
627         this._fireCallback(n, NOTIFICATION_EVENT_SHOWN);
628       }, this);
629       return;
630     }
632     // If the panel is already open but we're changing anchors, we need to hide
633     // it first.  Otherwise it can appear in the wrong spot.  (_hidePanel is
634     // safe to call even if the panel is already hidden.)
635     this._hidePanel();
637     // If the anchor element is hidden or null, use the tab as the anchor. We
638     // only ever show notifications for the current browser, so we can just use
639     // the current tab.
640     let selectedTab = this.tabbrowser.selectedTab;
641     if (anchorElement) {
642       let bo = anchorElement.boxObject;
643       if (bo.height == 0 && bo.width == 0)
644         anchorElement = selectedTab; // hidden
645     } else {
646       anchorElement = selectedTab; // null
647     }
649     this._currentAnchorElement = anchorElement;
651     // On OS X and Linux we need a different panel arrow color for
652     // click-to-play plugins, so copy the popupid and use css.
653     this.panel.setAttribute("popupid", this.panel.firstChild.getAttribute("popupid"));
654     notificationsToShow.forEach(function (n) {
655       // Remember the time the notification was shown for the security delay.
656       n.timeShown = this.window.performance.now();
657     }, this);
658     this.panel.openPopup(anchorElement, "bottomcenter topleft");
659     notificationsToShow.forEach(function (n) {
660       this._fireCallback(n, NOTIFICATION_EVENT_SHOWN);
661     }, this);
662   },
664   /**
665    * Updates the notification state in response to window activation or tab
666    * selection changes.
667    *
668    * @param notifications an array of Notification instances. if null,
669    *                      notifications will be retrieved off the current
670    *                      browser tab
671    * @param anchor is a XUL element that the notifications panel will be
672    *                      anchored to
673    * @param dismissShowing if true, dismiss any currently visible notifications
674    *                       if there are no notifications to show. Otherwise,
675    *                       currently displayed notifications will be left alone.
676    */
677   _update: function PopupNotifications_update(notifications, anchor, dismissShowing = false) {
678     let useIconBox = this.iconBox && (!anchor || anchor.parentNode == this.iconBox);
679     if (useIconBox) {
680       // hide icons of the previous tab.
681       this._hideIcons();
682     }
684     let anchorElement = anchor, notificationsToShow = [];
685     if (!notifications)
686       notifications = this._currentNotifications;
687     let haveNotifications = notifications.length > 0;
688     if (haveNotifications) {
689       // Filter out notifications that have been dismissed.
690       notificationsToShow = notifications.filter(function (n) {
691         return !n.dismissed && !n.options.neverShow;
692       });
694       // If no anchor has been passed in, use the anchor of the first
695       // showable notification.
696       if (!anchorElement && notificationsToShow.length)
697         anchorElement = notificationsToShow[0].anchorElement;
699       if (useIconBox) {
700         this._showIcons(notifications);
701         this.iconBox.hidden = false;
702       } else if (anchorElement) {
703         this._updateAnchorIcon(notifications, anchorElement);
704       }
706       // Also filter out notifications that are for a different anchor.
707       notificationsToShow = notificationsToShow.filter(function (n) {
708         return n.anchorElement == anchorElement;
709       });
710     }
712     if (notificationsToShow.length > 0) {
713       this._showPanel(notificationsToShow, anchorElement);
714     } else {
715       // Notify observers that we're not showing the popup (useful for testing)
716       this._notify("updateNotShowing");
718       // Close the panel if there are no notifications to show.
719       // When called from PopupNotifications.show() we should never close the
720       // panel, however. It may just be adding a dismissed notification, in
721       // which case we want to continue showing any existing notifications.
722       if (!dismissShowing)
723         this._dismiss();
725       // Only hide the iconBox if we actually have no notifications (as opposed
726       // to not having any showable notifications)
727       if (!haveNotifications) {
728         if (useIconBox)
729           this.iconBox.hidden = true;
730         else if (anchorElement)
731           anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
732       }
733     }
734   },
736   _updateAnchorIcon: function PopupNotifications_updateAnchorIcon(notifications,
737                                                                   anchorElement) {
738     anchorElement.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
739     // Use the anchorID as a class along with the default icon class as a
740     // fallback if anchorID is not defined in CSS. We always use the first
741     // notifications icon, so in the case of multiple notifications we'll
742     // only use the default icon.
743     if (anchorElement.classList.contains("notification-anchor-icon")) {
744       // remove previous icon classes
745       let className = anchorElement.className.replace(/([-\w]+-notification-icon\s?)/g,"")
746       className = "default-notification-icon " + className;
747       if (notifications.length == 1) {
748         className = notifications[0].anchorID + " " + className;
749       }
750       anchorElement.className = className;
751     }
752   },
754   _showIcons: function PopupNotifications_showIcons(aCurrentNotifications) {
755     for (let notification of aCurrentNotifications) {
756       let anchorElm = notification.anchorElement;
757       if (anchorElm) {
758         anchorElm.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
759       }
760     }
761   },
763   _hideIcons: function PopupNotifications_hideIcons() {
764     let icons = this.iconBox.querySelectorAll(ICON_SELECTOR);
765     for (let icon of icons) {
766       icon.removeAttribute(ICON_ATTRIBUTE_SHOWING);
767     }
768   },
770   /**
771    * Gets and sets notifications for the browser.
772    */
773   _getNotificationsForBrowser: function PopupNotifications_getNotifications(browser) {
774     let notifications = popupNotificationsMap.get(browser);
775     if (!notifications) {
776       // Initialize the WeakMap for the browser so callers can reference/manipulate the array.
777       notifications = [];
778       popupNotificationsMap.set(browser, notifications);
779     }
780     return notifications;
781   },
782   _setNotificationsForBrowser: function PopupNotifications_setNotifications(browser, notifications) {
783     popupNotificationsMap.set(browser, notifications);
784     return notifications;
785   },
787   _isActiveBrowser: function (browser) {
788     // Note: This helper only exists, because in e10s builds,
789     // we can't access the docShell of a browser from chrome.
790     return browser.docShell
791       ? browser.docShell.isActive
792       : (this.window.gBrowser.selectedBrowser == browser);
793   },
795   _onIconBoxCommand: function PopupNotifications_onIconBoxCommand(event) {
796     // Left click, space or enter only
797     let type = event.type;
798     if (type == "click" && event.button != 0)
799       return;
801     if (type == "keypress" &&
802         !(event.charCode == Ci.nsIDOMKeyEvent.DOM_VK_SPACE ||
803           event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN))
804       return;
806     if (this._currentNotifications.length == 0)
807       return;
809     // Get the anchor that is the immediate child of the icon box
810     let anchor = event.target;
811     while (anchor && anchor.parentNode != this.iconBox)
812       anchor = anchor.parentNode;
814     this._reshowNotifications(anchor);
815   },
817   _reshowNotifications: function PopupNotifications_reshowNotifications(anchor, browser) {
818     // Mark notifications anchored to this anchor as un-dismissed
819     let notifications = this._getNotificationsForBrowser(browser || this.tabbrowser.selectedBrowser);
820     notifications.forEach(function (n) {
821       if (n.anchorElement == anchor)
822         n.dismissed = false;
823     });
825     // ...and then show them.
826     this._update(notifications, anchor);
827   },
829   _swapBrowserNotifications: function PopupNotifications_swapBrowserNoficications(ourBrowser, otherBrowser) {
830     // When swaping browser docshells (e.g. dragging tab to new window) we need
831     // to update our notification map.
833     let ourNotifications = this._getNotificationsForBrowser(ourBrowser);
834     let other = otherBrowser.ownerDocument.defaultView.PopupNotifications;
835     if (!other) {
836       if (ourNotifications.length > 0)
837         Cu.reportError("unable to swap notifications: otherBrowser doesn't support notifications");
838       return;
839     }
840     let otherNotifications = other._getNotificationsForBrowser(otherBrowser);
841     if (ourNotifications.length < 1 && otherNotifications.length < 1) {
842       // No notification to swap.
843       return;
844     }
846     otherNotifications = otherNotifications.filter(n => {
847       if (this._fireCallback(n, NOTIFICATION_EVENT_SWAPPING, ourBrowser)) {
848         n.browser = ourBrowser;
849         n.owner = this;
850         return true;
851       }
852       other._fireCallback(n, NOTIFICATION_EVENT_REMOVED);
853       return false;
854     });
856     ourNotifications = ourNotifications.filter(n => {
857       if (this._fireCallback(n, NOTIFICATION_EVENT_SWAPPING, otherBrowser)) {
858         n.browser = otherBrowser;
859         n.owner = other;
860         return true;
861       }
862       this._fireCallback(n, NOTIFICATION_EVENT_REMOVED);
863       return false;
864     });
866     this._setNotificationsForBrowser(otherBrowser, ourNotifications);
867     other._setNotificationsForBrowser(ourBrowser, otherNotifications);
869     if (otherNotifications.length > 0)
870       this._update(otherNotifications, otherNotifications[0].anchorElement);
871     if (ourNotifications.length > 0)
872       other._update(ourNotifications, ourNotifications[0].anchorElement);
873   },
875   _fireCallback: function PopupNotifications_fireCallback(n, event, ...args) {
876     try {
877       if (n.options.eventCallback)
878         return n.options.eventCallback.call(n, event, ...args);
879     } catch (error) {
880       Cu.reportError(error);
881     }
882     return undefined;
883   },
885   _onPopupHidden: function PopupNotifications_onPopupHidden(event) {
886     if (event.target != this.panel || this._ignoreDismissal)
887       return;
889     let browser = this.panel.firstChild &&
890                   this.panel.firstChild.notification.browser;
891     if (!browser)
892       return;
894     let notifications = this._getNotificationsForBrowser(browser);
895     // Mark notifications as dismissed and call dismissal callbacks
896     Array.forEach(this.panel.childNodes, function (nEl) {
897       let notificationObj = nEl.notification;
898       // Never call a dismissal handler on a notification that's been removed.
899       if (notifications.indexOf(notificationObj) == -1)
900         return;
902       // Do not mark the notification as dismissed or fire NOTIFICATION_EVENT_DISMISSED
903       // if the notification is removed.
904       if (notificationObj.options.removeOnDismissal)
905         this._remove(notificationObj);
906       else {
907         notificationObj.dismissed = true;
908         this._fireCallback(notificationObj, NOTIFICATION_EVENT_DISMISSED);
909       }
910     }, this);
912     this._clearPanel();
914     this._update();
915   },
917   _onButtonCommand: function PopupNotifications_onButtonCommand(event) {
918     // Need to find the associated notification object, which is a bit tricky
919     // since it isn't associated with the button directly - this is kind of
920     // gross and very dependent on the structure of the popupnotification
921     // binding's content.
922     let target = event.originalTarget;
923     let notificationEl;
924     let parent = target;
925     while (parent && (parent = target.ownerDocument.getBindingParent(parent)))
926       notificationEl = parent;
928     if (!notificationEl)
929       throw "PopupNotifications_onButtonCommand: couldn't find notification element";
931     if (!notificationEl.notification)
932       throw "PopupNotifications_onButtonCommand: couldn't find notification";
934     let notification = notificationEl.notification;
935     let timeSinceShown = this.window.performance.now() - notification.timeShown;
937     // Only report the first time mainAction is triggered and remember that this occurred.
938     if (!notification.timeMainActionFirstTriggered) {
939       notification.timeMainActionFirstTriggered = timeSinceShown;
940       Services.telemetry.getHistogramById("POPUP_NOTIFICATION_MAINACTION_TRIGGERED_MS").
941                          add(timeSinceShown);
942     }
944     if (timeSinceShown < this.buttonDelay) {
945       Services.console.logStringMessage("PopupNotifications_onButtonCommand: " +
946                                         "Button click happened before the security delay: " +
947                                         timeSinceShown + "ms");
948       return;
949     }
950     try {
951       notification.mainAction.callback.call();
952     } catch(error) {
953       Cu.reportError(error);
954     }
956     if (notification.mainAction.dismiss) {
957       this._dismiss();
958       return;
959     }
961     this._remove(notification);
962     this._update();
963   },
965   _onMenuCommand: function PopupNotifications_onMenuCommand(event) {
966     let target = event.originalTarget;
967     if (!target.action || !target.notification)
968       throw "menucommand target has no associated action/notification";
970     event.stopPropagation();
971     try {
972       target.action.callback.call();
973     } catch(error) {
974       Cu.reportError(error);
975     }
977     if (target.action.dismiss) {
978       this._dismiss();
979       return;
980     }
982     this._remove(target.notification);
983     this._update();
984   },
986   _notify: function PopupNotifications_notify(topic) {
987     Services.obs.notifyObservers(null, "PopupNotifications-" + topic, "");
988   },