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;
29 if (anchor instanceof Ci.nsIDOMXULElement) {
32 return aBrowser.ownerDocument.getElementById(anchor);
38 * Notification object describes a single popup notification.
40 * @see PopupNotifications.show()
42 function Notification(id, message, anchorID, mainAction, secondaryActions,
43 browser, owner, options) {
45 this.message = message;
46 this.anchorID = anchorID;
47 this.mainAction = mainAction;
48 this.secondaryActions = secondaryActions || [];
49 this.browser = browser;
51 this.options = options || {};
54 Notification.prototype = {
60 secondaryActions: null,
67 * Removes the notification and updates the popup accordingly if needed.
69 remove: function Notification_remove() {
70 this.owner.remove(this);
74 let iconBox = this.owner.iconBox;
76 let anchorElement = getAnchorFromBrowser(this.browser);
81 if (!anchorElement && this.anchorID)
82 anchorElement = iconBox.querySelector("#"+this.anchorID);
84 // Use a default anchor icon if it's available
86 anchorElement = iconBox.querySelector("#default-notification-icon") ||
93 this.owner._reshowNotifications(this.anchorElement, this.browser);
98 * The PopupNotifications object manages popup notifications for a given browser
101 * window's <xul:tabbrowser/>. Used to observe tab switching events and
102 * for determining the active browser element.
104 * The <xul:panel/> element to use for notifications. The panel is
105 * populated with <popupnotification> children and displayed it as
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.
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;
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 = {
142 set iconBox(iconBox) {
143 // Remove the listeners on the old iconBox, if needed
145 this._iconBox.removeEventListener("click", this, false);
146 this._iconBox.removeEventListener("keypress", this, false);
148 this._iconBox = iconBox;
150 iconBox.addEventListener("click", this, false);
151 iconBox.addEventListener("keypress", this, false);
155 return this._iconBox;
159 * Enable or disable the opening/closing transition.
163 set transitionsEnabled(state) {
165 this.panel.removeAttribute("animate");
168 this.panel.setAttribute("animate", "false");
173 * Retrieve a Notification object associated with the browser/ID pair.
175 * The Notification ID to search for.
177 * The browser whose notifications should be searched. If null, the
178 * currently selected browser's notifications will be searched.
180 * @returns the corresponding Notification object, or null if no such
181 * notification exists.
183 getNotification: function PopupNotifications_getNotification(id, browser) {
185 let notifications = this._getNotificationsForBrowser(browser || this.tabbrowser.selectedBrowser);
186 notifications.some(function(x) x.id == id && (n = x));
191 * Adds a new popup notification.
193 * The <xul:browser> element associated with the notification. Must not
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
201 * The text to be displayed in the notification.
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.
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
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
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.
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
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
249 * "shown": notification has been shown (this can be fired
250 * multiple times as notifications are dismissed
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
263 * neverShow: Indicate that no popup should be shown for this
264 * notification. Useful for just showing the anchor icon.
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
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.
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.
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.
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;
292 throw "PopupNotifications_show: invalid browser";
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) {
321 this._update(notifications, notification.anchorElement, true);
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);
340 // Notify observers that we're not showing the popup (useful for testing)
341 this._notify("backgroundShow");
348 * Returns true if the notification popup is currently being displayed.
351 let panelState = this.panel.state;
353 return panelState == "showing" || panelState == "open";
357 * Called by the consumer to indicate that a browser's location has changed,
358 * so that we can update the active notifications accordingly.
360 locationChange: function PopupNotifications_locationChange(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 &&
371 if ("persistence" in notification.options &&
372 notification.options.persistence)
373 notification.options.persistence--;
377 // The persistence option allows a notification to persist across multiple
379 if ("persistence" in notification.options &&
380 notification.options.persistence) {
381 notification.options.persistence--;
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) {
391 this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED);
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
401 let anchorElement = notifications.length > 0 ? notifications[0].anchorElement : null;
403 anchorElement = getAnchorFromBrowser(aBrowser);
404 this._update(notifications, anchorElement);
409 * Removes a Notification.
410 * @param notification
411 * The Notification object to remove.
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);
422 handleEvent: function (aEvent) {
423 switch (aEvent.type) {
425 this._onPopupHidden(aEvent);
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 () {
438 this._onIconBoxCommand(aEvent);
443 ////////////////////////////////////////////////////////////////////////////////
445 ////////////////////////////////////////////////////////////////////////////////
447 _ignoreDismissal: null,
448 _currentAnchorElement: null,
451 * Gets notifications for the currently selected browser.
453 get _currentNotifications() {
454 return this.tabbrowser.selectedBrowser ? this._getNotificationsForBrowser(this.tabbrowser.selectedBrowser) : [];
457 _remove: function PopupNotifications_removeHelper(notification) {
458 // This notification may already be removed, in which case let's just fail
460 let notifications = this._getNotificationsForBrowser(notification.browser);
464 var index = notifications.indexOf(notification);
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);
477 * Dismisses the notification without removing it.
479 _dismiss: function PopupNotifications_dismiss() {
480 let browser = this.panel.firstChild &&
481 this.panel.firstChild.notification.browser;
482 this.panel.hidePopup();
488 * Hides the notification popup.
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;
503 * Removes all notifications from the notification popup.
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;
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);
536 _refreshPanel: function PopupNotifications_refreshPanel(notificationsToShow) {
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
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);
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();");
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();");
567 popupnotification.removeAttribute("buttonlabel");
568 popupnotification.removeAttribute("buttonaccesskey");
569 popupnotification.removeAttribute("buttoncommand");
570 popupnotification.removeAttribute("menucommand");
571 popupnotification.removeAttribute("closeitemcommand");
574 if (n.options.popupIconURL)
575 popupnotification.setAttribute("icon", n.options.popupIconURL);
576 if (n.options.learnMoreURL)
577 popupnotification.setAttribute("learnmoreurl", n.options.learnMoreURL);
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;
591 popupnotification.appendChild(item);
594 if (n.options.hideNotNow) {
595 popupnotification.setAttribute("hidenotnow", "true");
597 else if (n.secondaryActions.length) {
598 let closeItemSeparator = doc.createElementNS(XUL_NS, "menuseparator");
599 popupnotification.appendChild(closeItemSeparator);
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;
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);
620 if (!notificationsToShow.length)
623 this._refreshPanel(notificationsToShow);
625 if (this.isPanelOpen && this._currentAnchorElement == anchorElement) {
626 notificationsToShow.forEach(function (n) {
627 this._fireCallback(n, NOTIFICATION_EVENT_SHOWN);
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.)
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
640 let selectedTab = this.tabbrowser.selectedTab;
642 let bo = anchorElement.boxObject;
643 if (bo.height == 0 && bo.width == 0)
644 anchorElement = selectedTab; // hidden
646 anchorElement = selectedTab; // null
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();
658 this.panel.openPopup(anchorElement, "bottomcenter topleft");
659 notificationsToShow.forEach(function (n) {
660 this._fireCallback(n, NOTIFICATION_EVENT_SHOWN);
665 * Updates the notification state in response to window activation or tab
668 * @param notifications an array of Notification instances. if null,
669 * notifications will be retrieved off the current
671 * @param anchor is a XUL element that the notifications panel will be
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.
677 _update: function PopupNotifications_update(notifications, anchor, dismissShowing = false) {
678 let useIconBox = this.iconBox && (!anchor || anchor.parentNode == this.iconBox);
680 // hide icons of the previous tab.
684 let anchorElement = anchor, notificationsToShow = [];
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;
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;
700 this._showIcons(notifications);
701 this.iconBox.hidden = false;
702 } else if (anchorElement) {
703 this._updateAnchorIcon(notifications, anchorElement);
706 // Also filter out notifications that are for a different anchor.
707 notificationsToShow = notificationsToShow.filter(function (n) {
708 return n.anchorElement == anchorElement;
712 if (notificationsToShow.length > 0) {
713 this._showPanel(notificationsToShow, anchorElement);
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.
725 // Only hide the iconBox if we actually have no notifications (as opposed
726 // to not having any showable notifications)
727 if (!haveNotifications) {
729 this.iconBox.hidden = true;
730 else if (anchorElement)
731 anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
736 _updateAnchorIcon: function PopupNotifications_updateAnchorIcon(notifications,
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;
750 anchorElement.className = className;
754 _showIcons: function PopupNotifications_showIcons(aCurrentNotifications) {
755 for (let notification of aCurrentNotifications) {
756 let anchorElm = notification.anchorElement;
758 anchorElm.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
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);
771 * Gets and sets notifications for the browser.
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.
778 popupNotificationsMap.set(browser, notifications);
780 return notifications;
782 _setNotificationsForBrowser: function PopupNotifications_setNotifications(browser, notifications) {
783 popupNotificationsMap.set(browser, notifications);
784 return notifications;
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);
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)
801 if (type == "keypress" &&
802 !(event.charCode == Ci.nsIDOMKeyEvent.DOM_VK_SPACE ||
803 event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN))
806 if (this._currentNotifications.length == 0)
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);
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)
825 // ...and then show them.
826 this._update(notifications, anchor);
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;
836 if (ourNotifications.length > 0)
837 Cu.reportError("unable to swap notifications: otherBrowser doesn't support notifications");
840 let otherNotifications = other._getNotificationsForBrowser(otherBrowser);
841 if (ourNotifications.length < 1 && otherNotifications.length < 1) {
842 // No notification to swap.
846 otherNotifications = otherNotifications.filter(n => {
847 if (this._fireCallback(n, NOTIFICATION_EVENT_SWAPPING, ourBrowser)) {
848 n.browser = ourBrowser;
852 other._fireCallback(n, NOTIFICATION_EVENT_REMOVED);
856 ourNotifications = ourNotifications.filter(n => {
857 if (this._fireCallback(n, NOTIFICATION_EVENT_SWAPPING, otherBrowser)) {
858 n.browser = otherBrowser;
862 this._fireCallback(n, NOTIFICATION_EVENT_REMOVED);
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);
875 _fireCallback: function PopupNotifications_fireCallback(n, event, ...args) {
877 if (n.options.eventCallback)
878 return n.options.eventCallback.call(n, event, ...args);
880 Cu.reportError(error);
885 _onPopupHidden: function PopupNotifications_onPopupHidden(event) {
886 if (event.target != this.panel || this._ignoreDismissal)
889 let browser = this.panel.firstChild &&
890 this.panel.firstChild.notification.browser;
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)
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);
907 notificationObj.dismissed = true;
908 this._fireCallback(notificationObj, NOTIFICATION_EVENT_DISMISSED);
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;
925 while (parent && (parent = target.ownerDocument.getBindingParent(parent)))
926 notificationEl = parent;
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").
944 if (timeSinceShown < this.buttonDelay) {
945 Services.console.logStringMessage("PopupNotifications_onButtonCommand: " +
946 "Button click happened before the security delay: " +
947 timeSinceShown + "ms");
951 notification.mainAction.callback.call();
953 Cu.reportError(error);
956 if (notification.mainAction.dismiss) {
961 this._remove(notification);
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();
972 target.action.callback.call();
974 Cu.reportError(error);
977 if (target.action.dismiss) {
982 this._remove(target.notification);
986 _notify: function PopupNotifications_notify(topic) {
987 Services.obs.notifyObservers(null, "PopupNotifications-" + topic, "");