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 var EXPORTED_SYMBOLS = ["PopupNotifications"];
7 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
8 const { PrivateBrowsingUtils } = ChromeUtils.import(
9 "resource://gre/modules/PrivateBrowsingUtils.jsm"
11 const { PromiseUtils } = ChromeUtils.import(
12 "resource://gre/modules/PromiseUtils.jsm"
15 const NOTIFICATION_EVENT_DISMISSED = "dismissed";
16 const NOTIFICATION_EVENT_REMOVED = "removed";
17 const NOTIFICATION_EVENT_SHOWING = "showing";
18 const NOTIFICATION_EVENT_SHOWN = "shown";
19 const NOTIFICATION_EVENT_SWAPPING = "swapping";
21 const ICON_SELECTOR = ".notification-anchor-icon";
22 const ICON_ATTRIBUTE_SHOWING = "showing";
23 const ICON_ANCHOR_ATTRIBUTE = "popupnotificationanchor";
25 const PREF_SECURITY_DELAY = "security.notification_enable_delay";
27 // Enumerated values for the POPUP_NOTIFICATION_STATS telemetry histogram.
28 const TELEMETRY_STAT_OFFERED = 0;
29 const TELEMETRY_STAT_ACTION_1 = 1;
30 const TELEMETRY_STAT_ACTION_2 = 2;
31 // const TELEMETRY_STAT_ACTION_3 = 3;
32 const TELEMETRY_STAT_ACTION_LAST = 4;
33 // const TELEMETRY_STAT_DISMISSAL_CLICK_ELSEWHERE = 5;
34 const TELEMETRY_STAT_REMOVAL_LEAVE_PAGE = 6;
35 // const TELEMETRY_STAT_DISMISSAL_CLOSE_BUTTON = 7;
36 const TELEMETRY_STAT_OPEN_SUBMENU = 10;
37 const TELEMETRY_STAT_LEARN_MORE = 11;
39 const TELEMETRY_STAT_REOPENED_OFFSET = 20;
41 var popupNotificationsMap = new WeakMap();
42 var gNotificationParents = new WeakMap();
44 function getAnchorFromBrowser(aBrowser, aAnchorID) {
45 let attrPrefix = aAnchorID ? aAnchorID.replace("notification-icon", "") : "";
47 aBrowser.getAttribute(attrPrefix + ICON_ANCHOR_ATTRIBUTE) ||
48 aBrowser[attrPrefix + ICON_ANCHOR_ATTRIBUTE] ||
49 aBrowser.getAttribute(ICON_ANCHOR_ATTRIBUTE) ||
50 aBrowser[ICON_ANCHOR_ATTRIBUTE];
52 if (ChromeUtils.getClassName(anchor) == "XULElement") {
55 return aBrowser.ownerDocument.getElementById(anchor);
61 * Given a DOM node inside a <popupnotification>, return the parent <popupnotification>.
63 function getNotificationFromElement(aElement) {
64 return aElement.closest("popupnotification");
68 * Notification object describes a single popup notification.
70 * @see PopupNotifications.show()
72 function Notification(
83 this.message = message;
84 this.anchorID = anchorID;
85 this.mainAction = mainAction;
86 this.secondaryActions = secondaryActions || [];
87 this.browser = browser;
89 this.options = options || {};
91 this._dismissed = false;
92 // Will become a boolean when manually toggled by the user.
93 this._checkboxChecked = null;
94 this.wasDismissed = false;
95 this.recordedTelemetryStats = new Set();
96 this.isPrivate = PrivateBrowsingUtils.isWindowPrivate(
97 this.browser.ownerGlobal
99 this.timeCreated = this.owner.window.performance.now();
102 Notification.prototype = {
107 secondaryActions: null,
114 * Indicates whether the notification is currently dismissed.
116 set dismissed(value) {
117 this._dismissed = value;
119 // Keep the dismissal into account when recording telemetry.
120 this.wasDismissed = true;
124 return this._dismissed;
128 * Removes the notification and updates the popup accordingly if needed.
130 remove: function Notification_remove() {
131 this.owner.remove(this);
134 get anchorElement() {
135 let iconBox = this.owner.iconBox;
137 let anchorElement = getAnchorFromBrowser(this.browser, this.anchorID);
139 return anchorElement;
142 if (!anchorElement && this.anchorID) {
143 anchorElement = iconBox.querySelector("#" + this.anchorID);
146 // Use a default anchor icon if it's available
147 if (!anchorElement) {
149 iconBox.querySelector("#default-notification-icon") || iconBox;
152 return anchorElement;
156 this.owner._reshowNotifications(this.anchorElement, this.browser);
160 * Adds a value to the specified histogram, that must be keyed by ID.
162 _recordTelemetry(histogramId, value) {
163 if (this.isPrivate) {
164 // The reason why we don't record telemetry in private windows is because
165 // the available actions can be different from regular mode. The main
166 // difference is that all of the persistent permission options like
167 // "Always remember" aren't there, so they really need to be handled
168 // separately to avoid skewing results. For notifications with the same
169 // choices, there would be no reason not to record in private windows as
170 // well, but it's just simpler to use the same check for everything.
173 let histogram = Services.telemetry.getKeyedHistogramById(histogramId);
174 histogram.add("(all)", value);
175 histogram.add(this.id, value);
179 * Adds an enumerated value to the POPUP_NOTIFICATION_STATS histogram,
180 * ensuring that it is recorded at most once for each distinct Notification.
182 * Statistics for reopened notifications are recorded in separate buckets.
185 * One of the TELEMETRY_STAT_ constants.
187 _recordTelemetryStat(value) {
188 if (this.wasDismissed) {
189 value += TELEMETRY_STAT_REOPENED_OFFSET;
191 if (!this.recordedTelemetryStats.has(value)) {
192 this.recordedTelemetryStats.add(value);
193 this._recordTelemetry("POPUP_NOTIFICATION_STATS", value);
199 * The PopupNotifications object manages popup notifications for a given browser
202 * window's TabBrowser. Used to observe tab switching events and
203 * for determining the active browser element.
205 * The <xul:panel/> element to use for notifications. The panel is
206 * populated with <popupnotification> children and displayed it as
209 * Reference to a container element that should be hidden or
210 * unhidden when notifications are hidden or shown. It should be the
211 * parent of anchor elements whose IDs are passed to show().
212 * It is used as a fallback popup anchor if notifications specify
213 * invalid or non-existent anchor IDs.
215 * An optional object with the following optional properties:
218 * If this function returns true, then all notifications are
219 * suppressed for this window. This state is checked on construction
220 * and when the "anchorVisibilityChange" method is called.
223 function PopupNotifications(tabbrowser, panel, iconBox, options = {}) {
225 throw new Error("Invalid tabbrowser");
227 if (iconBox && ChromeUtils.getClassName(iconBox) != "XULElement") {
228 throw new Error("Invalid iconBox");
230 if (ChromeUtils.getClassName(panel) != "XULPopupElement") {
231 throw new Error("Invalid panel");
234 this._shouldSuppress = options.shouldSuppress || (() => false);
235 this._suppress = this._shouldSuppress();
237 this.window = tabbrowser.ownerGlobal;
239 this.tabbrowser = tabbrowser;
240 this.iconBox = iconBox;
241 this.buttonDelay = Services.prefs.getIntPref(PREF_SECURITY_DELAY);
243 this.panel.addEventListener("popuphidden", this, true);
244 this.panel.classList.add("popup-notification-panel", "panel-no-padding");
246 // This listener will be attached to the chrome window whenever a notification
247 // is showing, to allow the user to dismiss notifications using the escape key.
248 this._handleWindowKeyPress = aEvent => {
249 if (aEvent.keyCode != aEvent.DOM_VK_ESCAPE) {
253 // Esc key cancels the topmost notification, if there is one.
254 let notification = this.panel.firstElementChild;
259 let doc = this.window.document;
260 let focusedElement = Services.focus.focusedElement;
262 // If the chrome window has a focused element, let it handle the ESC key instead.
265 focusedElement == doc.body ||
266 focusedElement == this.tabbrowser.selectedBrowser ||
267 // Ignore focused elements inside the notification.
268 notification.contains(focusedElement)
270 let escAction = notification.notification.options.escAction;
271 this._onButtonEvent(aEvent, escAction, "esc-press", notification);
275 let documentElement = this.window.document.documentElement;
276 let locationBarHidden = documentElement
277 .getAttribute("chromehidden")
278 .includes("location");
279 let isFullscreen = !!this.window.document.fullscreenElement;
281 this.panel.setAttribute("followanchor", !locationBarHidden && !isFullscreen);
283 // There are no anchor icons in DOM fullscreen mode, but we would
284 // still like to show the popup notification. To avoid an infinite
285 // loop of showing and hiding, we have to disable followanchor
286 // (which hides the element without an anchor) in fullscreen.
287 this.window.addEventListener(
288 "MozDOMFullscreen:Entered",
290 this.panel.setAttribute("followanchor", "false");
294 this.window.addEventListener(
295 "MozDOMFullscreen:Exited",
297 this.panel.setAttribute("followanchor", !locationBarHidden);
302 this.window.addEventListener("activate", this, true);
303 if (this.tabbrowser.tabContainer) {
304 this.tabbrowser.tabContainer.addEventListener("TabSelect", this, true);
306 this.tabbrowser.tabContainer.addEventListener("TabClose", aEvent => {
307 // If the tab was just closed and we have notifications associated with it,
308 // then the notifications were closed because of the tab removal. We need to
309 // record this event in telemetry and fire the removal callback.
310 this.nextRemovalReason = TELEMETRY_STAT_REMOVAL_LEAVE_PAGE;
311 let notifications = this._getNotificationsForBrowser(
312 aEvent.target.linkedBrowser
314 for (let notification of notifications) {
317 NOTIFICATION_EVENT_REMOVED,
318 this.nextRemovalReason
320 notification._recordTelemetryStat(this.nextRemovalReason);
326 PopupNotifications.prototype = {
332 set iconBox(iconBox) {
333 // Remove the listeners on the old iconBox, if needed
335 this._iconBox.removeEventListener("click", this);
336 this._iconBox.removeEventListener("keypress", this);
338 this._iconBox = iconBox;
340 iconBox.addEventListener("click", this);
341 iconBox.addEventListener("keypress", this);
345 return this._iconBox;
349 * Retrieve one or many Notification object/s associated with the browser/ID pair.
350 * @param {string|string[]} id
351 * The Notification ID or an array of IDs to search for.
353 * The browser whose notifications should be searched. If null, the
354 * currently selected browser's notifications will be searched.
356 * @returns {Notification|Notification[]|null} If passed a single id, returns the corresponding Notification object, or null if no such
357 * notification exists.
358 * If passed an id array, returns an array of Notification objects which match the ids.
360 getNotification: function PopupNotifications_getNotification(id, browser) {
361 let notifications = this._getNotificationsForBrowser(
362 browser || this.tabbrowser.selectedBrowser
364 if (Array.isArray(id)) {
365 return notifications.filter(x => id.includes(x.id));
367 return notifications.find(x => x.id == id) || null;
371 * Adds a new popup notification.
373 * The <xul:browser> element associated with the notification. Must not
376 * A unique ID that identifies the type of notification (e.g.
377 * "geolocation"). Only one notification with a given ID can be visible
378 * at a time. If a notification already exists with the given ID, it
381 * A string containing the text to be displayed as the notification
382 * header. The string may optionally contain one or two "<>" as a
383 * placeholder which is later replaced by a host name or an addon name
384 * that is formatted to look bold, in which case the options.name
385 * property (as well as options.secondName if passing a "<>" and a "{}"
386 * placeholder) needs to be specified. "<>" will be considered as the
387 * first and "{}" as the second placeholder.
389 * The ID of the element that should be used as this notification
390 * popup's anchor. May be null, in which case the notification will be
391 * anchored to the iconBox.
393 * A JavaScript object literal describing the notification button's
394 * action. If present, it must have the following properties:
395 * - label (string): the button's label.
396 * - accessKey (string): the button's accessKey.
397 * - callback (function): a callback to be invoked when the button is
398 * pressed, is passed an object that contains the following fields:
399 * - checkboxChecked: (boolean) If the optional checkbox is checked.
400 * - source: (string): the source of the action that initiated the
402 * - "button" if popup buttons were directly activated, or
403 * - "esc-press" if the user pressed the escape key, or
404 * - "menucommand" if a menu was activated.
405 * - [optional] dismiss (boolean): If this is true, the notification
406 * will be dismissed instead of removed after running the callback.
407 * - [optional] disabled (boolean): If this is true, the button
409 * - [optional] disableHighlight (boolean): If this is true, the button
410 * will not apply the default highlight style.
411 * If null, the notification will have a default "OK" action button
412 * that can be used to dismiss the popup and secondaryActions will be ignored.
413 * @param secondaryActions
414 * An optional JavaScript array describing the notification's alternate
415 * actions. The array should contain objects with the same properties
416 * as mainAction. These are used to populate the notification button's
419 * An options JavaScript object holding additional properties for the
420 * notification. The following properties are currently supported:
421 * persistence: An integer. The notification will not automatically
422 * dismiss for this many page loads.
423 * timeout: A time in milliseconds. The notification will not
424 * automatically dismiss before this time.
425 * persistWhileVisible:
426 * A boolean. If true, a visible notification will always
427 * persist across location changes.
428 * persistent: A boolean. If true, the notification will always
429 * persist even across tab and app changes (but not across
430 * location changes), until the user accepts or rejects
431 * the request. The notification will never be implicitly
433 * dismissed: Whether the notification should be added as a dismissed
434 * notification. Dismissed notifications can be activated
435 * by clicking on their anchorElement.
436 * autofocus: Whether the notification should be autofocused on
437 * showing, stealing focus from any other focused element.
439 * Callback to be invoked when the notification changes
440 * state. The callback's first argument is a string
441 * identifying the state change:
442 * "dismissed": notification has been dismissed by the
443 * user (e.g. by clicking away or switching
445 * "removed": notification has been removed (due to
446 * location change or user action)
447 * "showing": notification is about to be shown
448 * (this can be fired multiple times as
449 * notifications are dismissed and re-shown)
450 * If the callback returns true, the notification
452 * "shown": notification has been shown (this can be fired
453 * multiple times as notifications are dismissed
455 * "swapping": the docshell of the browser that created
456 * the notification is about to be swapped to
457 * another browser. A second parameter contains
458 * the browser that is receiving the docshell,
459 * so that the event callback can transfer stuff
460 * specific to this notification.
461 * If the callback returns true, the notification
462 * will be moved to the new browser.
463 * If the callback isn't implemented, returns false,
464 * or doesn't return any value, the notification
466 * neverShow: Indicate that no popup should be shown for this
467 * notification. Useful for just showing the anchor icon.
469 * Notifications with this parameter set to true will be
470 * removed when they would have otherwise been dismissed
471 * (i.e. any time the popup is closed due to user
473 * hideClose: Indicate that the little close button in the corner of
474 * the panel should be hidden.
475 * checkbox: An object that allows you to add a checkbox and
476 * control its behavior with these fields:
478 * (required) Label to be shown next to the checkbox.
480 * (optional) Whether the checkbox should be checked
481 * by default. Defaults to false.
483 * (optional) An object that allows you to customize
484 * the notification state when the checkbox is checked.
486 * (optional) Whether the mainAction is disabled.
489 * (optional) A (warning) text that is shown below the
490 * checkbox. Pass null to hide.
492 * (optional) An object that allows you to customize
493 * the notification state when the checkbox is not checked.
494 * Has the same attributes as checkedState.
496 * A string. A class (or space separated list of classes)
497 * that will be applied to the icon in the popup so that
498 * several notifications using the same panel can use
501 * A string. URL of the image to be displayed in the popup.
502 * Normally specified in CSS using list-style-image and the
503 * .popup-notification-icon[popupid=...] selector.
505 * A string URL. Setting this property will make the
506 * prompt display a "Learn More" link that, when clicked,
507 * opens the URL in a new tab.
509 * The nsIURI of the page the notification came
510 * from. If present, this will be displayed above the message.
511 * If the nsIURI represents a file, the path will be displayed,
512 * otherwise the hostPort will be displayed.
514 * An optional string formatted to look bold and used in the
515 * notifiation description header text. Usually a host name or
518 * An optional string formatted to look bold and used in the
519 * notification description header text. Usually a host name or
520 * addon name. This is similar to name, and only used in case
521 * where message contains a "<>" and a "{}" placeholder. "<>"
522 * is considered the first and "{}" is considered the second
525 * An optional string indicating the action to take when the
526 * Esc key is pressed. This should be set to the name of the
527 * command to run. If not provided, "secondarybuttoncommand"
530 * An optional string value which will be given to the
531 * extraAttr attribute on the notification's anchorElement
532 * @returns the Notification object corresponding to the added notification.
534 show: function PopupNotifications_show(
543 function isInvalidAction(a) {
545 !a || !(typeof a.callback == "function") || !a.label || !a.accessKey
550 throw new Error("PopupNotifications_show: invalid browser");
553 throw new Error("PopupNotifications_show: invalid ID");
555 if (mainAction && isInvalidAction(mainAction)) {
556 throw new Error("PopupNotifications_show: invalid mainAction");
558 if (secondaryActions && secondaryActions.some(isInvalidAction)) {
559 throw new Error("PopupNotifications_show: invalid secondaryActions");
562 let notification = new Notification(
574 let escAction = options.escAction;
576 escAction != "buttoncommand" &&
577 escAction != "secondarybuttoncommand"
579 escAction = "secondarybuttoncommand";
581 notification.options.escAction = escAction;
584 if (options && options.dismissed) {
585 notification.dismissed = true;
588 let existingNotification = this.getNotification(id, browser);
589 if (existingNotification) {
590 this._remove(existingNotification);
593 let notifications = this._getNotificationsForBrowser(browser);
594 notifications.push(notification);
596 let isActiveBrowser = this._isActiveBrowser(browser);
597 let isActiveWindow = Services.focus.activeWindow == this.window;
599 if (isActiveBrowser) {
600 if (isActiveWindow) {
601 // Autofocus if the notification requests focus.
602 if (options && !options.dismissed && options.autofocus) {
603 this.panel.removeAttribute("noautofocus");
605 this.panel.setAttribute("noautofocus", "true");
611 new Set([notification.anchorElement]),
615 // indicate attention and update the icon if necessary
616 if (!notification.dismissed) {
617 this.window.getAttention();
619 this._updateAnchorIcons(
621 this._getAnchorsForNotifications(
623 notification.anchorElement
626 this._notify("backgroundShow");
629 // Notify observers that we're not showing the popup (useful for testing)
630 this._notify("backgroundShow");
637 * Returns true if the notification popup is currently being displayed.
640 let panelState = this.panel.state;
642 return panelState == "showing" || panelState == "open";
646 * Called by the consumer to indicate that a browser's location has changed,
647 * so that we can update the active notifications accordingly.
649 locationChange: function PopupNotifications_locationChange(aBrowser) {
651 throw new Error("PopupNotifications_locationChange: invalid browser");
654 let notifications = this._getNotificationsForBrowser(aBrowser);
656 this.nextRemovalReason = TELEMETRY_STAT_REMOVAL_LEAVE_PAGE;
658 notifications = notifications.filter(function(notification) {
659 // The persistWhileVisible option allows an open notification to persist
660 // across location changes
661 if (notification.options.persistWhileVisible && this.isPanelOpen) {
663 "persistence" in notification.options &&
664 notification.options.persistence
666 notification.options.persistence--;
671 // The persistence option allows a notification to persist across multiple
674 "persistence" in notification.options &&
675 notification.options.persistence
677 notification.options.persistence--;
681 // The timeout option allows a notification to persist until a certain time
683 "timeout" in notification.options &&
684 Date.now() <= notification.options.timeout
689 notification._recordTelemetryStat(this.nextRemovalReason);
692 NOTIFICATION_EVENT_REMOVED,
693 this.nextRemovalReason
698 this._setNotificationsForBrowser(aBrowser, notifications);
700 if (this._isActiveBrowser(aBrowser)) {
701 this.anchorVisibilityChange();
706 * Called by the consumer to indicate that the visibility of the notification
707 * anchors may have changed, but the location has not changed. This also
708 * checks whether all notifications are suppressed for this window.
710 * Calling this method may result in the "showing" and "shown" events for
711 * visible notifications to be invoked even if the anchor has not changed.
713 anchorVisibilityChange() {
714 let suppress = this._shouldSuppress();
716 // If notifications are not suppressed, always update the visibility.
717 this._suppress = false;
718 let notifications = this._getNotificationsForBrowser(
719 this.tabbrowser.selectedBrowser
723 this._getAnchorsForNotifications(
725 getAnchorFromBrowser(this.tabbrowser.selectedBrowser)
731 // Notifications are suppressed, ensure that the panel is hidden.
732 if (!this._suppress) {
733 this._suppress = true;
734 this._hidePanel().catch(Cu.reportError);
739 * Removes one or many Notifications.
740 * @param {Notification|Notification[]} notification - The Notification object/s to remove.
741 * @param {Boolean} [isCancel] - Whether to signal, in the notification event, that removal
742 * should be treated as cancel. This is currently used to cancel permission requests
743 * when their Notifications are removed.
745 remove: function PopupNotifications_remove(notification, isCancel = false) {
746 let notificationArray = Array.isArray(notification)
751 notificationArray.forEach(n => {
752 this._remove(n, isCancel);
753 if (!activeBrowser && this._isActiveBrowser(n.browser)) {
754 activeBrowser = n.browser;
759 let browserNotifications = this._getNotificationsForBrowser(
762 this._update(browserNotifications);
766 handleEvent(aEvent) {
767 switch (aEvent.type) {
769 this._onPopupHidden(aEvent);
772 if (this.isPanelOpen) {
773 for (let elt of this.panel.children) {
774 elt.notification.timeShown = this.window.performance.now();
781 // setTimeout(..., 0) needed, otherwise openPopup from "activate" event
782 // handler results in the popup being hidden again for some reason...
783 this.window.setTimeout(function() {
789 this._onIconBoxCommand(aEvent);
796 _ignoreDismissal: null,
797 _currentAnchorElement: null,
800 * Gets notifications for the currently selected browser.
802 get _currentNotifications() {
803 return this.tabbrowser.selectedBrowser
804 ? this._getNotificationsForBrowser(this.tabbrowser.selectedBrowser)
808 _remove: function PopupNotifications_removeHelper(
812 // This notification may already be removed, in which case let's just fail
814 let notifications = this._getNotificationsForBrowser(notification.browser);
815 if (!notifications) {
819 var index = notifications.indexOf(notification);
824 if (this._isActiveBrowser(notification.browser)) {
825 notification.anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
828 // remove the notification
829 notifications.splice(index, 1);
832 NOTIFICATION_EVENT_REMOVED,
833 this.nextRemovalReason,
839 * Dismisses the notification without removing it.
841 * @param {Event} the event associated with the user interaction that
842 * caused the dismissal
843 * @param {boolean} whether to disable persistent status. Normally,
844 * persistent prompts can not be dismissed. You can
845 * use this argument to force dismissal.
847 _dismiss: function PopupNotifications_dismiss(
849 disablePersistent = false
851 if (disablePersistent) {
852 let notificationEl = getNotificationFromElement(event.target);
853 if (notificationEl) {
854 notificationEl.notification.options.persistent = false;
859 this.panel.firstElementChild &&
860 this.panel.firstElementChild.notification.browser;
861 this.panel.hidePopup();
868 * Hides the notification popup.
870 _hidePanel: function PopupNotifications_hide() {
871 if (this.panel.state == "closed") {
872 return Promise.resolve();
874 if (this._ignoreDismissal) {
875 return this._ignoreDismissal.promise;
877 let deferred = PromiseUtils.defer();
878 this._ignoreDismissal = deferred;
879 this.panel.hidePopup();
880 return deferred.promise;
884 * Removes all notifications from the notification popup.
887 let popupnotification;
888 while ((popupnotification = this.panel.lastElementChild)) {
889 this.panel.removeChild(popupnotification);
891 // If this notification was provided by the chrome document rather than
892 // created ad hoc, move it back to where we got it from.
893 let originalParent = gNotificationParents.get(popupnotification);
894 if (originalParent) {
895 popupnotification.notification = null;
897 // Re-hide the notification such that it isn't rendered in the chrome
898 // document. _refreshPanel will unhide it again when needed.
899 popupnotification.hidden = true;
901 originalParent.appendChild(popupnotification);
907 * Formats the notification description message before we display it
908 * and splits it into three parts if the message contains "<>" as
912 * The Notification object which contains the message to format.
914 * @returns a Javascript object that has the following properties:
915 * start: A start label string containing the first part of the message.
916 * It may contain the whole string if the description message
917 * does not have "<>" as a placeholder. For example, local
918 * file URIs with description messages that don't display hostnames.
919 * name: A string that is formatted to look bold. It replaces the
920 * placeholder with the options.name property from the notification
921 * object which is usually an addon name or a host name.
922 * end: The last part of the description message.
924 _formatDescriptionMessage(n) {
926 let array = n.message.split(/<>|{}/);
927 text.start = array[0] || "";
928 text.name = n.options.name || "";
929 text.end = array[1] || "";
930 if (array.length == 3) {
931 text.secondName = n.options.secondName || "";
932 text.secondEnd = array[2] || "";
934 // name and secondName should be in logical positions. Swap them in case
935 // the second placeholder came before the first one in the original string.
936 if (n.message.indexOf("{}") < n.message.indexOf("<>")) {
938 text.name = text.secondName;
939 text.secondName = tmp;
941 } else if (array.length > 3) {
943 "Unexpected array length encountered in " +
944 "_formatDescriptionMessage: " +
951 _refreshPanel: function PopupNotifications_refreshPanel(notificationsToShow) {
954 notificationsToShow.forEach(function(n) {
955 let doc = this.window.document;
957 // Append "-notification" to the ID to try to avoid ID conflicts with other stuff
959 let popupnotificationID = n.id + "-notification";
961 // If the chrome document provides a popupnotification with this id, use
962 // that. Otherwise create it ad-hoc.
963 let popupnotification = doc.getElementById(popupnotificationID);
964 if (popupnotification) {
965 gNotificationParents.set(
967 popupnotification.parentNode
970 popupnotification = doc.createXULElement("popupnotification");
973 // Create the notification description element.
974 let desc = this._formatDescriptionMessage(n);
975 popupnotification.setAttribute("label", desc.start);
976 popupnotification.setAttribute("name", desc.name);
977 popupnotification.setAttribute("endlabel", desc.end);
978 if ("secondName" in desc && "secondEnd" in desc) {
979 popupnotification.setAttribute("secondname", desc.secondName);
980 popupnotification.setAttribute("secondendlabel", desc.secondEnd);
982 popupnotification.removeAttribute("secondname");
983 popupnotification.removeAttribute("secondendlabel");
986 popupnotification.setAttribute("id", popupnotificationID);
987 popupnotification.setAttribute("popupid", n.id);
988 popupnotification.setAttribute(
990 "PopupNotifications._onCommand(event);"
992 popupnotification.setAttribute(
993 "closebuttoncommand",
994 `PopupNotifications._dismiss(event, true);`
997 popupnotification.setAttribute("buttonlabel", n.mainAction.label);
998 popupnotification.setAttribute(
1000 n.mainAction.accessKey
1002 popupnotification.toggleAttribute(
1004 !n.mainAction.disableHighlight
1006 popupnotification.setAttribute(
1008 "PopupNotifications._onButtonEvent(event, 'buttoncommand');"
1010 popupnotification.setAttribute(
1011 "dropmarkerpopupshown",
1012 "PopupNotifications._onButtonEvent(event, 'dropmarkerpopupshown');"
1014 popupnotification.setAttribute(
1016 "PopupNotifications._onButtonEvent(event, 'learnmoreclick');"
1018 popupnotification.setAttribute(
1020 "PopupNotifications._onMenuCommand(event);"
1023 // Enable the default button to let the user close the popup if the close button is hidden
1024 popupnotification.setAttribute(
1026 "PopupNotifications._onButtonEvent(event, 'buttoncommand');"
1028 popupnotification.toggleAttribute("buttonhighlight", true);
1029 popupnotification.removeAttribute("buttonlabel");
1030 popupnotification.removeAttribute("buttonaccesskey");
1031 popupnotification.removeAttribute("dropmarkerpopupshown");
1032 popupnotification.removeAttribute("learnmoreclick");
1033 popupnotification.removeAttribute("menucommand");
1036 let classes = "popup-notification-icon";
1037 if (n.options.popupIconClass) {
1038 classes += " " + n.options.popupIconClass;
1040 popupnotification.setAttribute("iconclass", classes);
1042 if (n.options.popupIconURL) {
1043 popupnotification.setAttribute("icon", n.options.popupIconURL);
1045 popupnotification.removeAttribute("icon");
1048 if (n.options.learnMoreURL) {
1049 popupnotification.setAttribute("learnmoreurl", n.options.learnMoreURL);
1051 popupnotification.removeAttribute("learnmoreurl");
1054 if (n.options.displayURI) {
1057 if (n.options.displayURI instanceof Ci.nsIFileURL) {
1058 uri = n.options.displayURI.pathQueryRef;
1061 uri = n.options.displayURI.hostPort;
1063 uri = n.options.displayURI.spec;
1066 popupnotification.setAttribute("origin", uri);
1069 popupnotification.removeAttribute("origin");
1072 popupnotification.removeAttribute("origin");
1075 if (n.options.hideClose) {
1076 popupnotification.setAttribute("closebuttonhidden", "true");
1079 popupnotification.notification = n;
1082 if (n.mainAction && n.secondaryActions && n.secondaryActions.length) {
1083 let telemetryStatId = TELEMETRY_STAT_ACTION_2;
1085 let secondaryAction = n.secondaryActions[0];
1086 popupnotification.setAttribute(
1087 "secondarybuttonlabel",
1088 secondaryAction.label
1090 popupnotification.setAttribute(
1091 "secondarybuttonaccesskey",
1092 secondaryAction.accessKey
1094 popupnotification.setAttribute(
1095 "secondarybuttoncommand",
1096 "PopupNotifications._onButtonEvent(event, 'secondarybuttoncommand');"
1099 for (let i = 1; i < n.secondaryActions.length; i++) {
1100 let action = n.secondaryActions[i];
1101 let item = doc.createXULElement("menuitem");
1102 item.setAttribute("label", action.label);
1103 item.setAttribute("accesskey", action.accessKey);
1104 item.notification = n;
1105 item.action = action;
1107 menuitems.push(item);
1109 // We can only record a limited number of actions in telemetry. If
1110 // there are more, the latest are all recorded in the last bucket.
1111 item.action.telemetryStatId = telemetryStatId;
1112 if (telemetryStatId < TELEMETRY_STAT_ACTION_LAST) {
1116 popupnotification.setAttribute("secondarybuttonhidden", "false");
1118 popupnotification.setAttribute("secondarybuttonhidden", "true");
1120 popupnotification.setAttribute(
1122 n.secondaryActions.length < 2 ? "true" : "false"
1125 let checkbox = n.options.checkbox;
1126 if (checkbox && checkbox.label) {
1128 n._checkboxChecked != null ? n._checkboxChecked : !!checkbox.checked;
1129 popupnotification.checkboxState = {
1131 label: checkbox.label,
1135 this._setNotificationUIState(
1137 checkbox.checkedState
1140 this._setNotificationUIState(
1142 checkbox.uncheckedState
1146 popupnotification.checkboxState = null;
1147 // Reset the UI state to avoid previous state bleeding into this prompt.
1148 this._setNotificationUIState(popupnotification);
1151 this.panel.appendChild(popupnotification);
1153 // The popupnotification may be hidden if we got it from the chrome
1154 // document rather than creating it ad hoc.
1155 popupnotification.show();
1157 popupnotification.menupopup.textContent = "";
1158 popupnotification.menupopup.append(...menuitems);
1162 _setNotificationUIState(notification, state = {}) {
1163 let mainAction = notification.notification.mainAction;
1165 (mainAction && mainAction.disabled) ||
1166 state.disableMainAction ||
1167 notification.hasAttribute("invalidselection")
1169 notification.setAttribute("mainactiondisabled", "true");
1171 notification.removeAttribute("mainactiondisabled");
1173 if (state.warningLabel) {
1174 notification.setAttribute("warninglabel", state.warningLabel);
1175 notification.removeAttribute("warninghidden");
1177 notification.setAttribute("warninghidden", "true");
1181 _showPanel: function PopupNotifications_showPanel(
1182 notificationsToShow,
1185 this.panel.hidden = false;
1187 notificationsToShow = notificationsToShow.filter(n => {
1188 if (anchorElement != n.anchorElement) {
1192 let dismiss = this._fireCallback(n, NOTIFICATION_EVENT_SHOWING);
1198 if (!notificationsToShow.length) {
1201 let notificationIds = notificationsToShow.map(n => n.id);
1203 this._refreshPanel(notificationsToShow);
1205 function isNullOrHidden(elem) {
1210 let anchorRect = elem.getBoundingClientRect();
1211 return anchorRect.width == 0 && anchorRect.height == 0;
1214 // If the anchor element is hidden or null, fall back to the identity icon.
1215 if (isNullOrHidden(anchorElement)) {
1216 anchorElement = this.window.document.getElementById("identity-icon");
1218 if (isNullOrHidden(anchorElement)) {
1219 anchorElement = this.window.document.getElementById(
1220 "urlbar-search-button"
1224 // If the identity and search icons are not available in this window, use
1225 // the tab as the anchor. We only ever show notifications for the current
1226 // browser, so we can just use the current tab.
1227 if (isNullOrHidden(anchorElement)) {
1228 anchorElement = this.tabbrowser.selectedTab;
1230 // If we're in an entirely chromeless environment, set the anchorElement
1231 // to null and let openPopup show the notification at (0,0) later.
1232 if (isNullOrHidden(anchorElement)) {
1233 anchorElement = null;
1238 if (this.isPanelOpen && this._currentAnchorElement == anchorElement) {
1239 notificationsToShow.forEach(function(n) {
1240 this._fireCallback(n, NOTIFICATION_EVENT_SHOWN);
1243 // Make sure we update the noautohide attribute on the panel, in case it changed.
1244 if (notificationsToShow.some(n => n.options.persistent)) {
1245 this.panel.setAttribute("noautohide", "true");
1247 this.panel.removeAttribute("noautohide");
1250 // Let tests know that the panel was updated and what notifications it was
1251 // updated with so that tests can wait for the correct notifications to be
1253 let event = new this.window.CustomEvent("PanelUpdated", {
1254 detail: notificationIds,
1256 this.panel.dispatchEvent(event);
1260 // If the panel is already open but we're changing anchors, we need to hide
1261 // it first. Otherwise it can appear in the wrong spot. (_hidePanel is
1262 // safe to call even if the panel is already hidden.)
1263 this._hidePanel().then(() => {
1264 this._currentAnchorElement = anchorElement;
1266 if (notificationsToShow.some(n => n.options.persistent)) {
1267 this.panel.setAttribute("noautohide", "true");
1269 this.panel.removeAttribute("noautohide");
1272 notificationsToShow.forEach(function(n) {
1273 // Record that the notification was actually displayed on screen.
1274 // Notifications that were opened a second time or that were originally
1275 // shown with "options.dismissed" will be recorded in a separate bucket.
1276 n._recordTelemetryStat(TELEMETRY_STAT_OFFERED);
1277 // Remember the time the notification was shown for the security delay.
1278 n.timeShown = this.window.performance.now();
1281 let target = this.panel;
1282 if (target.parentNode) {
1283 // NOTIFICATION_EVENT_SHOWN should be fired for the panel before
1284 // anyone listening for popupshown on the panel gets run. Otherwise,
1285 // the panel will not be initialized when the popupshown event
1287 // By targeting the panel's parent and using a capturing listener, we
1288 // can have our listener called before others waiting for the panel to
1289 // be shown (which probably expect the panel to be fully initialized)
1290 target = target.parentNode;
1292 if (this._popupshownListener) {
1293 target.removeEventListener(
1295 this._popupshownListener,
1299 this._popupshownListener = function(e) {
1300 target.removeEventListener(
1302 this._popupshownListener,
1305 this._popupshownListener = null;
1307 notificationsToShow.forEach(function(n) {
1308 this._fireCallback(n, NOTIFICATION_EVENT_SHOWN);
1310 // These notifications are used by tests to know when all the processing
1311 // required to display the panel has happened.
1312 this.panel.dispatchEvent(new this.window.CustomEvent("Shown"));
1313 let event = new this.window.CustomEvent("PanelUpdated", {
1314 detail: notificationIds,
1316 this.panel.dispatchEvent(event);
1318 this._popupshownListener = this._popupshownListener.bind(this);
1319 target.addEventListener("popupshown", this._popupshownListener, true);
1321 this.panel.openPopup(anchorElement, "bottomcenter topleft", 0, 0);
1326 * Updates the notification state in response to window activation or tab
1327 * selection changes.
1329 * @param notifications an array of Notification instances. if null,
1330 * notifications will be retrieved off the current
1332 * @param anchors is a XUL element or a Set of XUL elements that the
1333 * notifications panel(s) will be anchored to.
1334 * @param dismissShowing if true, dismiss any currently visible notifications
1335 * if there are no notifications to show. Otherwise,
1336 * currently displayed notifications will be left alone.
1338 _update: function PopupNotifications_update(
1340 anchors = new Set(),
1341 dismissShowing = false
1343 if (ChromeUtils.getClassName(anchors) == "XULElement") {
1344 anchors = new Set([anchors]);
1347 if (!notifications) {
1348 notifications = this._currentNotifications;
1351 let haveNotifications = !!notifications.length;
1352 if (!anchors.size && haveNotifications) {
1353 anchors = this._getAnchorsForNotifications(notifications);
1356 let useIconBox = !!this.iconBox;
1357 if (useIconBox && anchors.size) {
1358 for (let anchor of anchors) {
1359 if (anchor.parentNode == this.iconBox) {
1367 // Filter out notifications that have been dismissed, unless they are
1368 // persistent. Also check if we should not show any notification.
1369 let notificationsToShow = [];
1370 if (!this._suppress) {
1371 notificationsToShow = notifications.filter(
1372 n => (!n.dismissed || n.options.persistent) && !n.options.neverShow
1377 // Hide icons of the previous tab.
1381 if (haveNotifications) {
1382 // Also filter out notifications that are for a different anchor.
1383 notificationsToShow = notificationsToShow.filter(function(n) {
1384 return anchors.has(n.anchorElement);
1388 this._showIcons(notifications);
1389 this.iconBox.hidden = false;
1390 // Make sure that panels can only be attached to anchors of shown
1391 // notifications inside an iconBox.
1392 anchors = this._getAnchorsForNotifications(notificationsToShow);
1393 } else if (anchors.size) {
1394 this._updateAnchorIcons(notifications, anchors);
1398 if (notificationsToShow.length) {
1399 let anchorElement = anchors.values().next().value;
1400 if (anchorElement) {
1401 this._showPanel(notificationsToShow, anchorElement);
1404 // Setup a capturing event listener on the whole window to catch the
1405 // escape key while persistent notifications are visible.
1406 this.window.addEventListener(
1408 this._handleWindowKeyPress,
1412 // Notify observers that we're not showing the popup (useful for testing)
1413 this._notify("updateNotShowing");
1415 // Close the panel if there are no notifications to show.
1416 // When called from PopupNotifications.show() we should never close the
1417 // panel, however. It may just be adding a dismissed notification, in
1418 // which case we want to continue showing any existing notifications.
1419 if (!dismissShowing) {
1423 // Only hide the iconBox if we actually have no notifications (as opposed
1424 // to not having any showable notifications)
1425 if (!haveNotifications) {
1427 this.iconBox.hidden = true;
1428 } else if (anchors.size) {
1429 for (let anchorElement of anchors) {
1430 anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
1435 // Stop listening to keyboard events for notifications.
1436 this.window.removeEventListener(
1438 this._handleWindowKeyPress,
1444 _updateAnchorIcons: function PopupNotifications_updateAnchorIcons(
1448 for (let anchorElement of anchorElements) {
1449 anchorElement.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
1453 _showIcons: function PopupNotifications_showIcons(aCurrentNotifications) {
1454 for (let notification of aCurrentNotifications) {
1455 let anchorElm = notification.anchorElement;
1457 anchorElm.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
1459 if (notification.options.extraAttr) {
1460 anchorElm.setAttribute("extraAttr", notification.options.extraAttr);
1466 _hideIcons: function PopupNotifications_hideIcons() {
1467 let icons = this.iconBox.querySelectorAll(ICON_SELECTOR);
1468 for (let icon of icons) {
1469 icon.removeAttribute(ICON_ATTRIBUTE_SHOWING);
1474 * Gets and sets notifications for the browser.
1476 _getNotificationsForBrowser: function PopupNotifications_getNotifications(
1479 let notifications = popupNotificationsMap.get(browser);
1480 if (!notifications) {
1481 // Initialize the WeakMap for the browser so callers can reference/manipulate the array.
1483 popupNotificationsMap.set(browser, notifications);
1485 return notifications;
1487 _setNotificationsForBrowser: function PopupNotifications_setNotifications(
1491 popupNotificationsMap.set(browser, notifications);
1492 return notifications;
1495 _getAnchorsForNotifications: function PopupNotifications_getAnchorsForNotifications(
1499 let anchors = new Set();
1500 for (let notification of notifications) {
1501 if (notification.anchorElement) {
1502 anchors.add(notification.anchorElement);
1505 if (defaultAnchor && !anchors.size) {
1506 anchors.add(defaultAnchor);
1511 _isActiveBrowser(browser) {
1512 // We compare on frameLoader instead of just comparing the
1513 // selectedBrowser and browser directly because browser tabs in
1514 // Responsive Design Mode put the actual web content into a
1515 // mozbrowser iframe and proxy property read/write and method
1516 // calls from the tab to that iframe. This is so that attempts
1517 // to reload the tab end up reloading the content in
1518 // Responsive Design Mode, and not the Responsive Design Mode
1521 // This means that PopupNotifications can come up from a browser
1522 // in Responsive Design Mode, but the selectedBrowser will not match
1523 // the browser being passed into this function, despite the browser
1524 // actually being within the selected tab. We workaround this by
1525 // comparing frameLoader instead, which is proxied from the outer
1526 // <xul:browser> to the inner mozbrowser <iframe>.
1527 return this.tabbrowser.selectedBrowser.frameLoader == browser.frameLoader;
1530 _onIconBoxCommand: function PopupNotifications_onIconBoxCommand(event) {
1531 // Left click, space or enter only
1532 let type = event.type;
1533 if (type == "click" && event.button != 0) {
1538 type == "keypress" &&
1540 event.charCode == event.DOM_VK_SPACE ||
1541 event.keyCode == event.DOM_VK_RETURN
1547 if (!this._currentNotifications.length) {
1551 event.stopPropagation();
1553 // Get the anchor that is the immediate child of the icon box
1554 let anchor = event.target;
1555 while (anchor && anchor.parentNode != this.iconBox) {
1556 anchor = anchor.parentNode;
1563 // If the panel is not closed, and the anchor is different, immediately mark all
1564 // active notifications for the previous anchor as dismissed
1565 if (this.panel.state != "closed" && anchor != this._currentAnchorElement) {
1566 this._dismissOrRemoveCurrentNotifications();
1569 // Avoid reshowing notifications that are already shown and have not been dismissed.
1570 if (this.panel.state == "closed" || anchor != this._currentAnchorElement) {
1571 // As soon as the panel is shown, focus the first element in the selected notification.
1572 this.panel.addEventListener(
1575 this.window.document.commandDispatcher.advanceFocusIntoSubtree(
1581 this._reshowNotifications(anchor);
1583 // Focus the first element in the selected notification.
1584 this.window.document.commandDispatcher.advanceFocusIntoSubtree(
1590 _reshowNotifications: function PopupNotifications_reshowNotifications(
1594 // Mark notifications anchored to this anchor as un-dismissed
1595 browser = browser || this.tabbrowser.selectedBrowser;
1596 let notifications = this._getNotificationsForBrowser(browser);
1597 notifications.forEach(function(n) {
1598 if (n.anchorElement == anchor) {
1599 n.dismissed = false;
1603 if (this._isActiveBrowser(browser)) {
1604 // ...and then show them.
1605 this._update(notifications, anchor);
1609 _swapBrowserNotifications: function PopupNotifications_swapBrowserNoficications(
1613 // When swaping browser docshells (e.g. dragging tab to new window) we need
1614 // to update our notification map.
1616 let ourNotifications = this._getNotificationsForBrowser(ourBrowser);
1617 let other = otherBrowser.ownerGlobal.PopupNotifications;
1619 if (ourNotifications.length) {
1621 "unable to swap notifications: otherBrowser doesn't support notifications"
1626 let otherNotifications = other._getNotificationsForBrowser(otherBrowser);
1627 if (ourNotifications.length < 1 && otherNotifications.length < 1) {
1628 // No notification to swap.
1632 otherNotifications = otherNotifications.filter(n => {
1633 if (this._fireCallback(n, NOTIFICATION_EVENT_SWAPPING, ourBrowser)) {
1634 n.browser = ourBrowser;
1638 other._fireCallback(
1640 NOTIFICATION_EVENT_REMOVED,
1641 this.nextRemovalReason
1646 ourNotifications = ourNotifications.filter(n => {
1647 if (this._fireCallback(n, NOTIFICATION_EVENT_SWAPPING, otherBrowser)) {
1648 n.browser = otherBrowser;
1652 this._fireCallback(n, NOTIFICATION_EVENT_REMOVED, this.nextRemovalReason);
1656 this._setNotificationsForBrowser(otherBrowser, ourNotifications);
1657 other._setNotificationsForBrowser(ourBrowser, otherNotifications);
1659 if (otherNotifications.length) {
1660 this._update(otherNotifications);
1662 if (ourNotifications.length) {
1663 other._update(ourNotifications);
1667 _fireCallback: function PopupNotifications_fireCallback(n, event, ...args) {
1669 if (n.options.eventCallback) {
1670 return n.options.eventCallback.call(n, event, ...args);
1673 Cu.reportError(error);
1678 _onPopupHidden: function PopupNotifications_onPopupHidden(event) {
1679 if (event.target != this.panel) {
1683 // We may have removed the "noautofocus" attribute before showing the panel
1684 // if the notification specified it wants to autofocus on first show.
1685 // When the panel is closed, we have to restore the attribute to its default
1686 // value, so we don't autofocus it if it's subsequently opened from a different code path.
1687 this.panel.setAttribute("noautofocus", "true");
1689 // Handle the case where the panel was closed programmatically.
1690 if (this._ignoreDismissal) {
1691 this._ignoreDismissal.resolve();
1692 this._ignoreDismissal = null;
1696 this._dismissOrRemoveCurrentNotifications();
1703 _dismissOrRemoveCurrentNotifications() {
1705 this.panel.firstElementChild &&
1706 this.panel.firstElementChild.notification.browser;
1711 let notifications = this._getNotificationsForBrowser(browser);
1712 // Mark notifications as dismissed and call dismissal callbacks
1713 for (let nEl of this.panel.children) {
1714 let notificationObj = nEl.notification;
1715 // Never call a dismissal handler on a notification that's been removed.
1716 if (!notifications.includes(notificationObj)) {
1720 // Record the time of the first notification dismissal if the main action
1721 // was not triggered in the meantime.
1722 let timeSinceShown =
1723 this.window.performance.now() - notificationObj.timeShown;
1725 !notificationObj.wasDismissed &&
1726 !notificationObj.recordedTelemetryMainAction
1728 notificationObj._recordTelemetry(
1729 "POPUP_NOTIFICATION_DISMISSAL_MS",
1734 // Do not mark the notification as dismissed or fire NOTIFICATION_EVENT_DISMISSED
1735 // if the notification is removed.
1736 if (notificationObj.options.removeOnDismissal) {
1737 notificationObj._recordTelemetryStat(this.nextRemovalReason);
1738 this._remove(notificationObj);
1740 notificationObj.dismissed = true;
1741 this._fireCallback(notificationObj, NOTIFICATION_EVENT_DISMISSED);
1746 _onCheckboxCommand(event) {
1747 let notificationEl = getNotificationFromElement(event.originalTarget);
1748 let checked = notificationEl.checkbox.checked;
1749 let notification = notificationEl.notification;
1751 // Save checkbox state to be able to persist it when re-opening the doorhanger.
1752 notification._checkboxChecked = checked;
1755 this._setNotificationUIState(
1757 notification.options.checkbox.checkedState
1760 this._setNotificationUIState(
1762 notification.options.checkbox.uncheckedState
1765 event.stopPropagation();
1769 // Ignore events from buttons as they are submitting and so don't need checks
1770 if (event.originalTarget.localName == "button") {
1773 let notificationEl = getNotificationFromElement(event.target);
1775 let notification = notificationEl.notification;
1776 if (!notification.options.checkbox) {
1777 this._setNotificationUIState(notificationEl);
1781 if (notificationEl.checkbox.checked) {
1782 this._setNotificationUIState(
1784 notification.options.checkbox.checkedState
1787 this._setNotificationUIState(
1789 notification.options.checkbox.uncheckedState
1794 _onButtonEvent(event, type, source = "button", notificationEl = null) {
1795 if (!notificationEl) {
1796 notificationEl = getNotificationFromElement(event.originalTarget);
1799 if (!notificationEl) {
1801 "PopupNotifications._onButtonEvent: couldn't find notification element"
1805 if (!notificationEl.notification) {
1807 "PopupNotifications._onButtonEvent: couldn't find notification"
1811 let notification = notificationEl.notification;
1813 if (type == "dropmarkerpopupshown") {
1814 notification._recordTelemetryStat(TELEMETRY_STAT_OPEN_SUBMENU);
1818 if (type == "learnmoreclick") {
1819 notification._recordTelemetryStat(TELEMETRY_STAT_LEARN_MORE);
1823 if (type == "buttoncommand") {
1824 // Record the total timing of the main action since the notification was
1825 // created, even if the notification was dismissed in the meantime.
1826 let timeSinceCreated =
1827 this.window.performance.now() - notification.timeCreated;
1828 if (!notification.recordedTelemetryMainAction) {
1829 notification.recordedTelemetryMainAction = true;
1830 notification._recordTelemetry(
1831 "POPUP_NOTIFICATION_MAIN_ACTION_MS",
1837 if (type == "buttoncommand" || type == "secondarybuttoncommand") {
1838 if (Services.focus.activeWindow != this.window) {
1839 Services.console.logStringMessage(
1840 "PopupNotifications._onButtonEvent: " +
1841 "Button click happened before the window was focused"
1843 this.window.focus();
1847 let timeSinceShown =
1848 this.window.performance.now() - notification.timeShown;
1849 if (timeSinceShown < this.buttonDelay) {
1850 Services.console.logStringMessage(
1851 "PopupNotifications._onButtonEvent: " +
1852 "Button click happened before the security delay: " +
1860 let action = notification.mainAction;
1861 let telemetryStatId = TELEMETRY_STAT_ACTION_1;
1863 if (type == "secondarybuttoncommand") {
1864 action = notification.secondaryActions[0];
1865 telemetryStatId = TELEMETRY_STAT_ACTION_2;
1868 notification._recordTelemetryStat(telemetryStatId);
1872 action.callback.call(undefined, {
1873 checkboxChecked: notificationEl.checkbox.checked,
1878 Cu.reportError(error);
1881 if (action.dismiss) {
1887 this._remove(notification);
1891 _onMenuCommand: function PopupNotifications_onMenuCommand(event) {
1892 let target = event.originalTarget;
1893 if (!target.action || !target.notification) {
1895 "menucommand target has no associated action/notification"
1899 let notificationEl = getNotificationFromElement(target);
1900 event.stopPropagation();
1902 target.notification._recordTelemetryStat(target.action.telemetryStatId);
1905 target.action.callback.call(undefined, {
1906 checkboxChecked: notificationEl.checkbox.checked,
1907 source: "menucommand",
1910 Cu.reportError(error);
1913 if (target.action.dismiss) {
1918 this._remove(target.notification);
1922 _notify: function PopupNotifications_notify(topic) {
1923 Services.obs.notifyObservers(null, "PopupNotifications-" + topic);