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 import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs";
6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
8 const NOTIFICATION_EVENT_DISMISSED = "dismissed";
9 const NOTIFICATION_EVENT_REMOVED = "removed";
10 const NOTIFICATION_EVENT_SHOWING = "showing";
11 const NOTIFICATION_EVENT_SHOWN = "shown";
12 const NOTIFICATION_EVENT_SWAPPING = "swapping";
14 const ICON_SELECTOR = ".notification-anchor-icon";
15 const ICON_ATTRIBUTE_SHOWING = "showing";
16 const ICON_ANCHOR_ATTRIBUTE = "popupnotificationanchor";
18 const PREF_SECURITY_DELAY = "security.notification_enable_delay";
19 const FULLSCREEN_TRANSITION_TIME_SHOWN_OFFSET_MS = 2000;
21 // Enumerated values for the POPUP_NOTIFICATION_STATS telemetry histogram.
22 const TELEMETRY_STAT_OFFERED = 0;
23 const TELEMETRY_STAT_ACTION_1 = 1;
24 const TELEMETRY_STAT_ACTION_2 = 2;
25 // const TELEMETRY_STAT_ACTION_3 = 3;
26 const TELEMETRY_STAT_ACTION_LAST = 4;
27 // const TELEMETRY_STAT_DISMISSAL_CLICK_ELSEWHERE = 5;
28 const TELEMETRY_STAT_REMOVAL_LEAVE_PAGE = 6;
29 // const TELEMETRY_STAT_DISMISSAL_CLOSE_BUTTON = 7;
30 const TELEMETRY_STAT_OPEN_SUBMENU = 10;
31 const TELEMETRY_STAT_LEARN_MORE = 11;
33 const TELEMETRY_STAT_REOPENED_OFFSET = 20;
36 XPCOMUtils.defineLazyPreferenceGetter(lazy, "buttonDelay", PREF_SECURITY_DELAY);
38 var popupNotificationsMap = new WeakMap();
39 var gNotificationParents = new WeakMap();
41 function getAnchorFromBrowser(aBrowser, aAnchorID) {
42 let attrPrefix = aAnchorID ? aAnchorID.replace("notification-icon", "") : "";
44 aBrowser.getAttribute(attrPrefix + ICON_ANCHOR_ATTRIBUTE) ||
45 aBrowser[attrPrefix + ICON_ANCHOR_ATTRIBUTE] ||
46 aBrowser.getAttribute(ICON_ANCHOR_ATTRIBUTE) ||
47 aBrowser[ICON_ANCHOR_ATTRIBUTE];
49 if (ChromeUtils.getClassName(anchor) == "XULElement") {
52 return aBrowser.ownerDocument.getElementById(anchor);
58 * Given a DOM node inside a <popupnotification>, return the parent <popupnotification>.
60 function getNotificationFromElement(aElement) {
61 return aElement.closest("popupnotification");
65 * Notification object describes a single popup notification.
67 * @see PopupNotifications.show()
69 function Notification(
80 this.message = message;
81 this.anchorID = anchorID;
82 this.mainAction = mainAction;
83 this.secondaryActions = secondaryActions || [];
84 this.browser = browser;
86 this.options = options || {};
88 this._dismissed = false;
89 // Will become a boolean when manually toggled by the user.
90 this._checkboxChecked = null;
91 this.wasDismissed = false;
92 this.recordedTelemetryStats = new Set();
93 this.isPrivate = PrivateBrowsingUtils.isWindowPrivate(
94 this.browser.ownerGlobal
96 this.timeCreated = this.owner.window.performance.now();
99 Notification.prototype = {
104 secondaryActions: null,
111 * Indicates whether the notification is currently dismissed.
113 set dismissed(value) {
114 this._dismissed = value;
116 // Keep the dismissal into account when recording telemetry.
117 this.wasDismissed = true;
121 return this._dismissed;
125 * Removes the notification and updates the popup accordingly if needed.
127 remove: function Notification_remove() {
128 this.owner.remove(this);
131 get anchorElement() {
132 let iconBox = this.owner.iconBox;
134 let anchorElement = getAnchorFromBrowser(this.browser, this.anchorID);
136 return anchorElement;
139 if (!anchorElement && this.anchorID) {
140 anchorElement = iconBox.querySelector("#" + this.anchorID);
143 // Use a default anchor icon if it's available
144 if (!anchorElement) {
146 iconBox.querySelector("#default-notification-icon") || iconBox;
149 return anchorElement;
153 this.owner._reshowNotifications(this.anchorElement, this.browser);
157 * Adds a value to the specified histogram, that must be keyed by ID.
159 _recordTelemetry(histogramId, value) {
160 if (this.isPrivate && !this.options.recordTelemetryInPrivateBrowsing) {
161 // The reason why we don't record telemetry in private windows is because
162 // the available actions can be different from regular mode. The main
163 // difference is that all of the persistent permission options like
164 // "Always remember" aren't there, so they really need to be handled
165 // separately to avoid skewing results. For notifications with the same
166 // choices, there would be no reason not to record in private windows as
167 // well, but it's just simpler to use the same check for everything.
170 let histogram = Services.telemetry.getKeyedHistogramById(histogramId);
171 histogram.add("(all)", value);
172 histogram.add(this.id, value);
176 * Adds an enumerated value to the POPUP_NOTIFICATION_STATS histogram,
177 * ensuring that it is recorded at most once for each distinct Notification.
179 * Statistics for reopened notifications are recorded in separate buckets.
182 * One of the TELEMETRY_STAT_ constants.
184 _recordTelemetryStat(value) {
185 if (this.wasDismissed) {
186 value += TELEMETRY_STAT_REOPENED_OFFSET;
188 if (!this.recordedTelemetryStats.has(value)) {
189 this.recordedTelemetryStats.add(value);
190 this._recordTelemetry("POPUP_NOTIFICATION_STATS", value);
196 * The PopupNotifications object manages popup notifications for a given browser
199 * window's TabBrowser. Used to observe tab switching events and
200 * for determining the active browser element.
202 * The <xul:panel/> element to use for notifications. The panel is
203 * populated with <popupnotification> children and displayed it as
206 * Reference to a container element that should be hidden or
207 * unhidden when notifications are hidden or shown. It should be the
208 * parent of anchor elements whose IDs are passed to show().
209 * It is used as a fallback popup anchor if notifications specify
210 * invalid or non-existent anchor IDs.
212 * An optional object with the following optional properties:
215 * If this function returns true, then all notifications are
216 * suppressed for this window. This state is checked on construction
217 * and when the "anchorVisibilityChange" method is called.
218 * getVisibleAnchorElement(anchorElement):
219 * A function which takes an anchor element as input and should return
220 * either the anchor if it's visible, a fallback anchor element, or if
221 * no fallback exists, a null element.
224 export function PopupNotifications(tabbrowser, panel, iconBox, options = {}) {
226 throw new Error("Invalid tabbrowser");
228 if (iconBox && ChromeUtils.getClassName(iconBox) != "XULElement") {
229 throw new Error("Invalid iconBox");
231 if (ChromeUtils.getClassName(panel) != "XULPopupElement") {
232 throw new Error("Invalid panel");
235 this._shouldSuppress = options.shouldSuppress || (() => false);
236 this._suppress = this._shouldSuppress();
238 this._getVisibleAnchorElement = options.getVisibleAnchorElement;
240 this.window = tabbrowser.ownerGlobal;
242 this.tabbrowser = tabbrowser;
243 this.iconBox = iconBox;
245 // panel itself has a listener in the bubble phase and this listener
246 // needs to be called after that, so use bubble phase here.
247 this.panel.addEventListener("popuphidden", this);
248 this.panel.addEventListener("popuppositioned", this);
249 this.panel.classList.add("popup-notification-panel", "panel-no-padding");
251 // This listener will be attached to the chrome window whenever a notification
252 // is showing, to allow the user to dismiss notifications using the escape key.
253 this._handleWindowKeyPress = aEvent => {
254 if (aEvent.keyCode != aEvent.DOM_VK_ESCAPE) {
258 // Esc key cancels the topmost notification, if there is one.
259 let notification = this.panel.firstElementChild;
264 let doc = this.window.document;
265 let focusedElement = Services.focus.focusedElement;
267 // If the chrome window has a focused element, let it handle the ESC key instead.
270 focusedElement == doc.body ||
271 focusedElement == this.tabbrowser.selectedBrowser ||
272 // Ignore focused elements inside the notification.
273 notification.contains(focusedElement)
275 let escAction = notification.notification.options.escAction;
276 this._onButtonEvent(aEvent, escAction, "esc-press", notification);
277 // Without this preventDefault call, the event will be sent to the content page
278 // and our event listener might be called again after receiving a reply from
279 // the content process, which could accidentally dismiss another notification.
280 aEvent.preventDefault();
284 let documentElement = this.window.document.documentElement;
285 let locationBarHidden = documentElement
286 .getAttribute("chromehidden")
287 .includes("location");
288 let isFullscreen = !!this.window.document.fullscreenElement;
290 this.panel.setAttribute("followanchor", !locationBarHidden && !isFullscreen);
292 // There are no anchor icons in DOM fullscreen mode, but we would
293 // still like to show the popup notification. To avoid an infinite
294 // loop of showing and hiding, we have to disable followanchor
295 // (which hides the element without an anchor) in fullscreen.
296 this.window.addEventListener(
297 "MozDOMFullscreen:Entered",
299 this.panel.setAttribute("followanchor", "false");
303 this.window.addEventListener(
304 "MozDOMFullscreen:Exited",
306 this.panel.setAttribute("followanchor", !locationBarHidden);
311 Services.obs.addObserver(this, "fullscreen-transition-start");
313 this.window.addEventListener("unload", () => {
314 Services.obs.removeObserver(this, "fullscreen-transition-start");
317 this.window.addEventListener("activate", this, true);
318 if (this.tabbrowser.tabContainer) {
319 this.tabbrowser.tabContainer.addEventListener("TabSelect", this, true);
321 this.tabbrowser.tabContainer.addEventListener("TabClose", aEvent => {
322 // If the tab was just closed and we have notifications associated with it,
323 // then the notifications were closed because of the tab removal. We need to
324 // record this event in telemetry and fire the removal callback.
325 this.nextRemovalReason = TELEMETRY_STAT_REMOVAL_LEAVE_PAGE;
326 let notifications = this._getNotificationsForBrowser(
327 aEvent.target.linkedBrowser
329 for (let notification of notifications) {
332 NOTIFICATION_EVENT_REMOVED,
333 this.nextRemovalReason
335 notification._recordTelemetryStat(this.nextRemovalReason);
341 PopupNotifications.prototype = {
347 set iconBox(iconBox) {
348 // Remove the listeners on the old iconBox, if needed
350 this._iconBox.removeEventListener("click", this);
351 this._iconBox.removeEventListener("keypress", this);
353 this._iconBox = iconBox;
355 iconBox.addEventListener("click", this);
356 iconBox.addEventListener("keypress", this);
360 return this._iconBox;
363 observe(subject, topic) {
364 if (topic == "fullscreen-transition-start") {
365 // Extend security delay if the panel is open.
366 if (this.isPanelOpen) {
367 let notification = this.panel.firstChild?.notification;
369 this._extendSecurityDelay([notification]);
376 * Retrieve one or many Notification object/s associated with the browser/ID pair.
377 * @param {string|string[]} id
378 * The Notification ID or an array of IDs to search for.
380 * The browser whose notifications should be searched. If null, the
381 * currently selected browser's notifications will be searched.
383 * @returns {Notification|Notification[]|null} If passed a single id, returns the corresponding Notification object, or null if no such
384 * notification exists.
385 * If passed an id array, returns an array of Notification objects which match the ids.
387 getNotification: function PopupNotifications_getNotification(id, browser) {
388 let notifications = this._getNotificationsForBrowser(
389 browser || this.tabbrowser.selectedBrowser
391 if (Array.isArray(id)) {
392 return notifications.filter(x => id.includes(x.id));
394 return notifications.find(x => x.id == id) || null;
398 * Adds a new popup notification.
400 * The <xul:browser> element associated with the notification. Must not
403 * A unique ID that identifies the type of notification (e.g.
404 * "geolocation"). Only one notification with a given ID can be visible
405 * at a time. If a notification already exists with the given ID, it
408 * A string containing the text to be displayed as the notification
409 * header. The string may optionally contain one or two "<>" as a
410 * placeholder which is later replaced by a host name or an addon name
411 * that is formatted to look bold, in which case the options.name
412 * property (as well as options.secondName if passing a "<>" and a "{}"
413 * placeholder) needs to be specified. "<>" will be considered as the
414 * first and "{}" as the second placeholder.
416 * The ID of the element that should be used as this notification
417 * popup's anchor. May be null, in which case the notification will be
418 * anchored to the iconBox.
420 * A JavaScript object literal describing the notification button's
421 * action. If present, it must have the following properties:
422 * - label (string): the button's label.
423 * - accessKey (string): the button's accessKey.
424 * - callback (function): a callback to be invoked when the button is
425 * pressed, is passed an object that contains the following fields:
426 * - checkboxChecked: (boolean) If the optional checkbox is checked.
427 * - source: (string): the source of the action that initiated the
429 * - "button" if popup buttons were directly activated, or
430 * - "esc-press" if the user pressed the escape key, or
431 * - "menucommand" if a menu was activated.
432 * - [optional] dismiss (boolean): If this is true, the notification
433 * will be dismissed instead of removed after running the callback.
434 * - [optional] disabled (boolean): If this is true, the button
436 * If null, the notification will have a default "OK" action button
437 * that can be used to dismiss the popup and secondaryActions will be ignored.
438 * @param secondaryActions
439 * An optional JavaScript array describing the notification's alternate
440 * actions. The array should contain objects with the same properties
441 * as mainAction. These are used to populate the notification button's
444 * An options JavaScript object holding additional properties for the
445 * notification. The following properties are currently supported:
446 * persistence: An integer. The notification will not automatically
447 * dismiss for this many page loads.
448 * timeout: A time in milliseconds. The notification will not
449 * automatically dismiss before this time.
450 * persistWhileVisible:
451 * A boolean. If true, a visible notification will always
452 * persist across location changes.
453 * persistent: A boolean. If true, the notification will always
454 * persist even across tab and app changes (but not across
455 * location changes), until the user accepts or rejects
456 * the request. The notification will never be implicitly
458 * dismissed: Whether the notification should be added as a dismissed
459 * notification. Dismissed notifications can be activated
460 * by clicking on their anchorElement.
461 * autofocus: Whether the notification should be autofocused on
462 * showing, stealing focus from any other focused element.
464 * Callback to be invoked when the notification changes
465 * state. The callback's first argument is a string
466 * identifying the state change:
467 * "dismissed": notification has been dismissed by the
468 * user (e.g. by clicking away or switching
470 * "removed": notification has been removed (due to
471 * location change or user action)
472 * "showing": notification is about to be shown
473 * (this can be fired multiple times as
474 * notifications are dismissed and re-shown)
475 * If the callback returns true, the notification
477 * "shown": notification has been shown (this can be fired
478 * multiple times as notifications are dismissed
480 * "swapping": the docshell of the browser that created
481 * the notification is about to be swapped to
482 * another browser. A second parameter contains
483 * the browser that is receiving the docshell,
484 * so that the event callback can transfer stuff
485 * specific to this notification.
486 * If the callback returns true, the notification
487 * will be moved to the new browser.
488 * If the callback isn't implemented, returns false,
489 * or doesn't return any value, the notification
491 * neverShow: Indicate that no popup should be shown for this
492 * notification. Useful for just showing the anchor icon.
494 * Notifications with this parameter set to true will be
495 * removed when they would have otherwise been dismissed
496 * (i.e. any time the popup is closed due to user
498 * hideClose: Indicate that the little close button in the corner of
499 * the panel should be hidden.
500 * checkbox: An object that allows you to add a checkbox and
501 * control its behavior with these fields:
503 * (required) Label to be shown next to the checkbox.
505 * (optional) Whether the checkbox should be checked
506 * by default. Defaults to false.
508 * (optional) An object that allows you to customize
509 * the notification state when the checkbox is checked.
511 * (optional) Whether the mainAction is disabled.
514 * (optional) A (warning) text that is shown below the
515 * checkbox. Pass null to hide.
517 * (optional) An object that allows you to customize
518 * the notification state when the checkbox is not checked.
519 * Has the same attributes as checkedState.
521 * A string. A class (or space separated list of classes)
522 * that will be applied to the icon in the popup so that
523 * several notifications using the same panel can use
526 * A string. URL of the image to be displayed in the popup.
528 * A string URL. Setting this property will make the
529 * prompt display a "Learn More" link that, when clicked,
530 * opens the URL in a new tab.
532 * The nsIURI of the page the notification came
533 * from. If present, this will be displayed above the message.
534 * If the nsIURI represents a file, the path will be displayed,
535 * otherwise the hostPort will be displayed.
537 * An optional string formatted to look bold and used in the
538 * notifiation description header text. Usually a host name or
541 * An optional string formatted to look bold and used in the
542 * notification description header text. Usually a host name or
543 * addon name. This is similar to name, and only used in case
544 * where message contains a "<>" and a "{}" placeholder. "<>"
545 * is considered the first and "{}" is considered the second
548 * An optional string indicating the action to take when the
549 * Esc key is pressed. This should be set to the name of the
550 * command to run. If not provided, "secondarybuttoncommand"
553 * An optional string value which will be given to the
554 * extraAttr attribute on the notification's anchorElement
556 * An optional object containing popup options passed to
557 * `openPopup()` when defined.
558 * recordTelemetryInPrivateBrowsing:
559 * An optional boolean indicating whether popup telemetry
560 * should be recorded in private browsing windows. By default,
561 * telemetry is NOT recorded in PBM, because the available
562 * options for persistent permission notifications are
563 * different between normal and PBM windows, potentially
564 * skewing the data. But for notifications that do not differ
565 * in PBM, this option can be used to ensure that popups in
566 * both PBM and normal windows record the same interactions.
567 * @returns the Notification object corresponding to the added notification.
569 show: function PopupNotifications_show(
578 function isInvalidAction(a) {
580 !a || !(typeof a.callback == "function") || !a.label || !a.accessKey
585 throw new Error("PopupNotifications_show: invalid browser");
588 throw new Error("PopupNotifications_show: invalid ID");
590 if (mainAction && isInvalidAction(mainAction)) {
591 throw new Error("PopupNotifications_show: invalid mainAction");
593 if (secondaryActions && secondaryActions.some(isInvalidAction)) {
594 throw new Error("PopupNotifications_show: invalid secondaryActions");
597 let notification = new Notification(
609 let escAction = options.escAction;
611 escAction != "buttoncommand" &&
612 escAction != "secondarybuttoncommand"
614 escAction = "secondarybuttoncommand";
616 notification.options.escAction = escAction;
619 if (options && options.dismissed) {
620 notification.dismissed = true;
623 let existingNotification = this.getNotification(id, browser);
624 if (existingNotification) {
625 this._remove(existingNotification);
628 let notifications = this._getNotificationsForBrowser(browser);
629 notifications.push(notification);
631 let isActiveBrowser = this._isActiveBrowser(browser);
632 let isActiveWindow = Services.focus.activeWindow == this.window;
634 if (isActiveBrowser) {
635 if (isActiveWindow) {
636 // Autofocus if the notification requests focus.
637 if (options && !options.dismissed && options.autofocus) {
638 this.panel.removeAttribute("noautofocus");
640 this.panel.setAttribute("noautofocus", "true");
646 new Set([notification.anchorElement]),
650 // indicate attention and update the icon if necessary
651 if (!notification.dismissed) {
652 this.window.getAttention();
654 this._updateAnchorIcons(
656 this._getAnchorsForNotifications(
658 notification.anchorElement
661 this._notify("backgroundShow");
664 // Notify observers that we're not showing the popup (useful for testing)
665 this._notify("backgroundShow");
672 * Returns true if the notification popup is currently being displayed.
675 let panelState = this.panel.state;
677 return panelState == "showing" || panelState == "open";
681 * Called by the consumer to indicate that the open panel should
682 * temporarily be hidden while the given panel is showing.
684 suppressWhileOpen(panel) {
685 this._hidePanel().catch(console.error);
686 panel.addEventListener("popuphidden", aEvent => {
692 * Called by the consumer to indicate that a browser's location has changed,
693 * so that we can update the active notifications accordingly.
695 locationChange: function PopupNotifications_locationChange(aBrowser) {
697 throw new Error("PopupNotifications_locationChange: invalid browser");
700 let notifications = this._getNotificationsForBrowser(aBrowser);
702 this.nextRemovalReason = TELEMETRY_STAT_REMOVAL_LEAVE_PAGE;
704 notifications = notifications.filter(function (notification) {
705 // The persistWhileVisible option allows an open notification to persist
706 // across location changes
707 if (notification.options.persistWhileVisible && this.isPanelOpen) {
709 "persistence" in notification.options &&
710 notification.options.persistence
712 notification.options.persistence--;
717 // The persistence option allows a notification to persist across multiple
720 "persistence" in notification.options &&
721 notification.options.persistence
723 notification.options.persistence--;
727 // The timeout option allows a notification to persist until a certain time
729 "timeout" in notification.options &&
730 Date.now() <= notification.options.timeout
735 notification._recordTelemetryStat(this.nextRemovalReason);
738 NOTIFICATION_EVENT_REMOVED,
739 this.nextRemovalReason
744 this._setNotificationsForBrowser(aBrowser, notifications);
746 if (this._isActiveBrowser(aBrowser)) {
747 this.anchorVisibilityChange();
752 * Called by the consumer to indicate that the visibility of the notification
753 * anchors may have changed, but the location has not changed. This also
754 * checks whether all notifications are suppressed for this window.
756 * Calling this method may result in the "showing" and "shown" events for
757 * visible notifications to be invoked even if the anchor has not changed.
759 anchorVisibilityChange() {
760 let suppress = this._shouldSuppress();
762 // If notifications are not suppressed, always update the visibility.
763 this._suppress = false;
764 let notifications = this._getNotificationsForBrowser(
765 this.tabbrowser.selectedBrowser
769 this._getAnchorsForNotifications(
771 getAnchorFromBrowser(this.tabbrowser.selectedBrowser)
777 // Notifications are suppressed, ensure that the panel is hidden.
778 if (!this._suppress) {
779 this._suppress = true;
780 this._hidePanel().catch(console.error);
785 * Removes one or many Notifications.
786 * @param {Notification|Notification[]} notification - The Notification object/s to remove.
787 * @param {Boolean} [isCancel] - Whether to signal, in the notification event, that removal
788 * should be treated as cancel. This is currently used to cancel permission requests
789 * when their Notifications are removed.
791 remove: function PopupNotifications_remove(notification, isCancel = false) {
792 let notificationArray = Array.isArray(notification)
797 notificationArray.forEach(n => {
798 this._remove(n, isCancel);
799 if (!activeBrowser && this._isActiveBrowser(n.browser)) {
800 activeBrowser = n.browser;
805 let browserNotifications =
806 this._getNotificationsForBrowser(activeBrowser);
807 this._update(browserNotifications);
811 handleEvent(aEvent) {
812 switch (aEvent.type) {
814 this._onPopupHidden(aEvent);
817 case "popuppositioned":
818 if (this.isPanelOpen) {
819 for (let elt of this.panel.children) {
820 elt.notification.timeShown = Math.max(
821 this.window.performance.now(),
822 elt.notification.timeShown ?? 0
829 // setTimeout(..., 0) needed, otherwise openPopup from "activate" event
830 // handler results in the popup being hidden again for some reason...
831 this.window.setTimeout(() => {
832 this._suppress = this._shouldSuppress();
838 this._onIconBoxCommand(aEvent);
845 _ignoreDismissal: null,
846 _currentAnchorElement: null,
849 * Gets notifications for the currently selected browser.
851 get _currentNotifications() {
852 return this.tabbrowser.selectedBrowser
853 ? this._getNotificationsForBrowser(this.tabbrowser.selectedBrowser)
857 _remove: function PopupNotifications_removeHelper(
861 // This notification may already be removed, in which case let's just fail
863 let notifications = this._getNotificationsForBrowser(notification.browser);
864 if (!notifications) {
868 var index = notifications.indexOf(notification);
873 if (this._isActiveBrowser(notification.browser)) {
874 notification.anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
877 // remove the notification
878 notifications.splice(index, 1);
881 NOTIFICATION_EVENT_REMOVED,
882 this.nextRemovalReason,
888 * Dismisses the notification without removing it.
890 * @param {Event} the event associated with the user interaction that
891 * caused the dismissal
892 * @param {boolean} whether to disable persistent status. Normally,
893 * persistent prompts can not be dismissed. You can
894 * use this argument to force dismissal.
896 _dismiss: function PopupNotifications_dismiss(
898 disablePersistent = false
900 if (disablePersistent) {
901 let notificationEl = getNotificationFromElement(event.target);
902 if (notificationEl) {
903 notificationEl.notification.options.persistent = false;
908 this.panel.firstElementChild &&
909 this.panel.firstElementChild.notification.browser;
910 this.panel.hidePopup();
917 * Hides the notification popup.
919 _hidePanel: function PopupNotifications_hide() {
920 if (this.panel.state == "closed") {
921 return Promise.resolve();
923 if (this._ignoreDismissal) {
924 return this._ignoreDismissal.promise;
926 let deferred = Promise.withResolvers();
927 this._ignoreDismissal = deferred;
928 this.panel.hidePopup();
929 return deferred.promise;
933 * Removes all notifications from the notification popup.
936 let popupnotification;
937 while ((popupnotification = this.panel.lastElementChild)) {
938 this.panel.removeChild(popupnotification);
940 // If this notification was provided by the chrome document rather than
941 // created ad hoc, move it back to where we got it from.
942 let originalParent = gNotificationParents.get(popupnotification);
943 if (originalParent) {
944 popupnotification.notification = null;
946 // Re-hide the notification such that it isn't rendered in the chrome
947 // document. _refreshPanel will unhide it again when needed.
948 popupnotification.hidden = true;
950 originalParent.appendChild(popupnotification);
956 * Formats the notification description message before we display it
957 * and splits it into three parts if the message contains "<>" as
961 * The Notification object which contains the message to format.
963 * @returns a Javascript object that has the following properties:
964 * start: A start label string containing the first part of the message.
965 * It may contain the whole string if the description message
966 * does not have "<>" as a placeholder. For example, local
967 * file URIs with description messages that don't display hostnames.
968 * name: A string that is formatted to look bold. It replaces the
969 * placeholder with the options.name property from the notification
970 * object which is usually an addon name or a host name.
971 * end: The last part of the description message.
973 _formatDescriptionMessage(n) {
975 let array = n.message.split(/<>|{}/);
976 text.start = array[0] || "";
977 text.name = n.options.name || "";
978 text.end = array[1] || "";
979 if (array.length == 3) {
980 text.secondName = n.options.secondName || "";
981 text.secondEnd = array[2] || "";
983 // name and secondName should be in logical positions. Swap them in case
984 // the second placeholder came before the first one in the original string.
985 if (n.message.indexOf("{}") < n.message.indexOf("<>")) {
987 text.name = text.secondName;
988 text.secondName = tmp;
990 } else if (array.length > 3) {
992 "Unexpected array length encountered in " +
993 "_formatDescriptionMessage: ",
1000 _refreshPanel: function PopupNotifications_refreshPanel(notificationsToShow) {
1003 notificationsToShow.forEach(function (n) {
1004 let doc = this.window.document;
1006 // Append "-notification" to the ID to try to avoid ID conflicts with other stuff
1008 let popupnotificationID = n.id + "-notification";
1010 // If the chrome document provides a popupnotification with this id, use
1011 // that. Otherwise create it ad-hoc.
1012 let popupnotification = doc.getElementById(popupnotificationID);
1013 if (popupnotification) {
1014 gNotificationParents.set(
1016 popupnotification.parentNode
1019 popupnotification = doc.createXULElement("popupnotification");
1022 // Create the notification description element.
1023 let desc = this._formatDescriptionMessage(n);
1024 popupnotification.setAttribute("label", desc.start);
1025 popupnotification.setAttribute("name", desc.name);
1026 popupnotification.setAttribute("endlabel", desc.end);
1027 if ("secondName" in desc && "secondEnd" in desc) {
1028 popupnotification.setAttribute("secondname", desc.secondName);
1029 popupnotification.setAttribute("secondendlabel", desc.secondEnd);
1031 popupnotification.removeAttribute("secondname");
1032 popupnotification.removeAttribute("secondendlabel");
1035 if (n.options.hintText) {
1036 popupnotification.setAttribute("hinttext", n.options.hintText);
1038 popupnotification.removeAttribute("hinttext");
1041 popupnotification.setAttribute("id", popupnotificationID);
1042 popupnotification.setAttribute("popupid", n.id);
1043 popupnotification.setAttribute(
1045 "PopupNotifications._onCommand(event);"
1047 popupnotification.setAttribute(
1048 "closebuttoncommand",
1049 `PopupNotifications._dismiss(event, true);`
1052 popupnotification.toggleAttribute(
1054 !!(n.options.popupIconURL || n.options.popupIconClass)
1058 popupnotification.setAttribute("buttonlabel", n.mainAction.label);
1059 popupnotification.setAttribute(
1061 n.mainAction.accessKey
1063 popupnotification.setAttribute(
1065 "PopupNotifications._onButtonEvent(event, 'buttoncommand');"
1067 popupnotification.setAttribute(
1068 "dropmarkerpopupshown",
1069 "PopupNotifications._onButtonEvent(event, 'dropmarkerpopupshown');"
1071 popupnotification.setAttribute(
1073 "PopupNotifications._onButtonEvent(event, 'learnmoreclick');"
1075 popupnotification.setAttribute(
1077 "PopupNotifications._onMenuCommand(event);"
1080 // Enable the default button to let the user close the popup if the close button is hidden
1081 popupnotification.setAttribute(
1083 "PopupNotifications._onButtonEvent(event, 'buttoncommand');"
1085 popupnotification.toggleAttribute("buttonhighlight", true);
1086 popupnotification.removeAttribute("buttonlabel");
1087 popupnotification.removeAttribute("buttonaccesskey");
1088 popupnotification.removeAttribute("dropmarkerpopupshown");
1089 popupnotification.removeAttribute("learnmoreclick");
1090 popupnotification.removeAttribute("menucommand");
1093 let classes = "popup-notification-icon";
1094 if (n.options.popupIconClass) {
1095 classes += " " + n.options.popupIconClass;
1097 popupnotification.setAttribute("iconclass", classes);
1099 if (n.options.popupIconURL) {
1100 popupnotification.setAttribute("icon", n.options.popupIconURL);
1102 popupnotification.removeAttribute("icon");
1105 if (n.options.learnMoreURL) {
1106 popupnotification.setAttribute("learnmoreurl", n.options.learnMoreURL);
1108 popupnotification.removeAttribute("learnmoreurl");
1111 if (n.options.displayURI) {
1114 if (n.options.displayURI instanceof Ci.nsIFileURL) {
1115 uri = n.options.displayURI.pathQueryRef;
1118 uri = n.options.displayURI.hostPort;
1120 uri = n.options.displayURI.spec;
1123 popupnotification.setAttribute("origin", uri);
1126 popupnotification.removeAttribute("origin");
1129 popupnotification.removeAttribute("origin");
1132 if (n.options.hideClose) {
1133 popupnotification.setAttribute("closebuttonhidden", "true");
1136 popupnotification.notification = n;
1139 if (n.mainAction && n.secondaryActions && n.secondaryActions.length) {
1140 let telemetryStatId = TELEMETRY_STAT_ACTION_2;
1142 let secondaryAction = n.secondaryActions[0];
1143 popupnotification.setAttribute(
1144 "secondarybuttonlabel",
1145 secondaryAction.label
1147 popupnotification.setAttribute(
1148 "secondarybuttonaccesskey",
1149 secondaryAction.accessKey
1151 popupnotification.setAttribute(
1152 "secondarybuttoncommand",
1153 "PopupNotifications._onButtonEvent(event, 'secondarybuttoncommand');"
1156 for (let i = 1; i < n.secondaryActions.length; i++) {
1157 let action = n.secondaryActions[i];
1158 let item = doc.createXULElement("menuitem");
1159 item.setAttribute("label", action.label);
1160 item.setAttribute("accesskey", action.accessKey);
1161 item.notification = n;
1162 item.action = action;
1164 menuitems.push(item);
1166 // We can only record a limited number of actions in telemetry. If
1167 // there are more, the latest are all recorded in the last bucket.
1168 item.action.telemetryStatId = telemetryStatId;
1169 if (telemetryStatId < TELEMETRY_STAT_ACTION_LAST) {
1173 popupnotification.setAttribute("secondarybuttonhidden", "false");
1175 popupnotification.setAttribute("secondarybuttonhidden", "true");
1177 popupnotification.setAttribute(
1179 n.secondaryActions.length < 2 ? "true" : "false"
1182 let checkbox = n.options.checkbox;
1183 if (checkbox && checkbox.label) {
1185 n._checkboxChecked != null ? n._checkboxChecked : !!checkbox.checked;
1186 popupnotification.checkboxState = {
1188 label: checkbox.label,
1192 this._setNotificationUIState(
1194 checkbox.checkedState
1197 this._setNotificationUIState(
1199 checkbox.uncheckedState
1203 popupnotification.checkboxState = null;
1204 // Reset the UI state to avoid previous state bleeding into this prompt.
1205 this._setNotificationUIState(popupnotification);
1208 this.panel.appendChild(popupnotification);
1210 // The popupnotification may be hidden if we got it from the chrome
1211 // document rather than creating it ad hoc.
1212 popupnotification.show();
1214 popupnotification.menupopup.textContent = "";
1215 popupnotification.menupopup.append(...menuitems);
1219 _setNotificationUIState(notification, state = {}) {
1220 let mainAction = notification.notification.mainAction;
1222 (mainAction && mainAction.disabled) ||
1223 state.disableMainAction ||
1224 notification.hasAttribute("invalidselection")
1226 notification.setAttribute("mainactiondisabled", "true");
1228 notification.removeAttribute("mainactiondisabled");
1230 if (state.warningLabel) {
1231 notification.setAttribute("warninglabel", state.warningLabel);
1232 notification.removeAttribute("warninghidden");
1234 notification.setAttribute("warninghidden", "true");
1238 _extendSecurityDelay(notifications) {
1239 let now = this.window.performance.now();
1240 notifications.forEach(n => {
1241 n.timeShown = now + FULLSCREEN_TRANSITION_TIME_SHOWN_OFFSET_MS;
1245 _showPanel: function PopupNotifications_showPanel(
1246 notificationsToShow,
1249 this.panel.hidden = false;
1251 notificationsToShow = notificationsToShow.filter(n => {
1252 if (anchorElement != n.anchorElement) {
1256 let dismiss = this._fireCallback(n, NOTIFICATION_EVENT_SHOWING);
1262 if (!notificationsToShow.length) {
1266 let notificationIds = notificationsToShow.map(n => n.id);
1268 this._refreshPanel(notificationsToShow);
1270 // The element the PopupNotification should anchor to might not be visible.
1271 // Check its visibility using a callback that returns the same anchor
1272 // element if its visible, or a fallback option that is visible.
1273 // If no fallbacks are visible, it should return null.
1274 if (this._getVisibleAnchorElement) {
1275 anchorElement = this._getVisibleAnchorElement(anchorElement);
1277 // In case _getVisibleAnchorElement provided a non-visible element.
1278 if (!anchorElement?.checkVisibility()) {
1279 // We only ever show notifications for the current browser,
1280 // so we can just use the current tab.
1281 anchorElement = this.tabbrowser.selectedTab;
1282 if (!anchorElement?.checkVisibility()) {
1283 // If we're in an entirely chromeless environment, set the anchorElement
1284 // to null and let openPopup show the notification at (0,0) later.
1285 anchorElement = null;
1289 // Remember the time the notification was shown for the security delay.
1290 notificationsToShow.forEach(
1292 (n.timeShown = Math.max(
1293 this.window.performance.now(),
1298 if (this.isPanelOpen && this._currentAnchorElement == anchorElement) {
1299 notificationsToShow.forEach(function (n) {
1300 this._fireCallback(n, NOTIFICATION_EVENT_SHOWN);
1303 // Make sure we update the noautohide attribute on the panel, in case it changed.
1304 if (notificationsToShow.some(n => n.options.persistent)) {
1305 this.panel.setAttribute("noautohide", "true");
1307 this.panel.removeAttribute("noautohide");
1310 // Let tests know that the panel was updated and what notifications it was
1311 // updated with so that tests can wait for the correct notifications to be
1313 let event = new this.window.CustomEvent("PanelUpdated", {
1314 detail: notificationIds,
1316 this.panel.dispatchEvent(event);
1320 // If the panel is already open but we're changing anchors, we need to hide
1321 // it first. Otherwise it can appear in the wrong spot. (_hidePanel is
1322 // safe to call even if the panel is already hidden.)
1323 this._hidePanel().then(() => {
1324 this._currentAnchorElement = anchorElement;
1326 if (notificationsToShow.some(n => n.options.persistent)) {
1327 this.panel.setAttribute("noautohide", "true");
1329 this.panel.removeAttribute("noautohide");
1332 notificationsToShow.forEach(function (n) {
1333 // Record that the notification was actually displayed on screen.
1334 // Notifications that were opened a second time or that were originally
1335 // shown with "options.dismissed" will be recorded in a separate bucket.
1336 n._recordTelemetryStat(TELEMETRY_STAT_OFFERED);
1339 // We're about to open the panel while in a full screen transition. Extend
1340 // the security delay.
1341 if (this.window.isInFullScreenTransition) {
1342 this._extendSecurityDelay(notificationsToShow);
1345 let target = this.panel;
1346 if (target.parentNode) {
1347 // NOTIFICATION_EVENT_SHOWN should be fired for the panel before
1348 // anyone listening for popupshown on the panel gets run. Otherwise,
1349 // the panel will not be initialized when the popupshown event
1351 // By targeting the panel's parent and using a capturing listener, we
1352 // can have our listener called before others waiting for the panel to
1353 // be shown (which probably expect the panel to be fully initialized)
1354 target = target.parentNode;
1356 if (this._popupshownListener) {
1357 target.removeEventListener(
1359 this._popupshownListener,
1363 this._popupshownListener = function (e) {
1364 target.removeEventListener(
1366 this._popupshownListener,
1369 this._popupshownListener = null;
1371 notificationsToShow.forEach(function (n) {
1372 this._fireCallback(n, NOTIFICATION_EVENT_SHOWN);
1374 // These notifications are used by tests to know when all the processing
1375 // required to display the panel has happened.
1376 this.panel.dispatchEvent(new this.window.CustomEvent("Shown"));
1377 let event = new this.window.CustomEvent("PanelUpdated", {
1378 detail: notificationIds,
1380 this.panel.dispatchEvent(event);
1382 this._popupshownListener = this._popupshownListener.bind(this);
1383 target.addEventListener("popupshown", this._popupshownListener, true);
1385 let popupOptions = notificationsToShow.findLast(
1386 n => n.options?.popupOptions
1387 )?.options?.popupOptions;
1389 this.panel.openPopup(anchorElement, popupOptions);
1391 this.panel.openPopup(anchorElement, "bottomleft topleft", 0, 0);
1397 * Updates the notification state in response to window activation or tab
1398 * selection changes.
1400 * @param notifications an array of Notification instances. if null,
1401 * notifications will be retrieved off the current
1403 * @param anchors is a XUL element or a Set of XUL elements that the
1404 * notifications panel(s) will be anchored to.
1405 * @param dismissShowing if true, dismiss any currently visible notifications
1406 * if there are no notifications to show. Otherwise,
1407 * currently displayed notifications will be left alone.
1409 _update: function PopupNotifications_update(
1411 anchors = new Set(),
1412 dismissShowing = false
1414 if (ChromeUtils.getClassName(anchors) == "XULElement") {
1415 anchors = new Set([anchors]);
1418 if (!notifications) {
1419 notifications = this._currentNotifications;
1422 let haveNotifications = !!notifications.length;
1423 if (!anchors.size && haveNotifications) {
1424 anchors = this._getAnchorsForNotifications(notifications);
1427 let useIconBox = !!this.iconBox;
1428 if (useIconBox && anchors.size) {
1429 for (let anchor of anchors) {
1430 if (anchor.parentNode == this.iconBox) {
1438 // Filter out notifications that have been dismissed, unless they are
1439 // persistent. Also check if we should not show any notification.
1440 let notificationsToShow = [];
1441 if (!this._suppress) {
1442 notificationsToShow = notifications.filter(
1443 n => (!n.dismissed || n.options.persistent) && !n.options.neverShow
1448 // Hide icons of the previous tab.
1452 if (haveNotifications) {
1453 // Also filter out notifications that are for a different anchor.
1454 notificationsToShow = notificationsToShow.filter(function (n) {
1455 return anchors.has(n.anchorElement);
1459 this._showIcons(notifications);
1460 this.iconBox.hidden = false;
1461 // Make sure that panels can only be attached to anchors of shown
1462 // notifications inside an iconBox.
1463 anchors = this._getAnchorsForNotifications(notificationsToShow);
1464 } else if (anchors.size) {
1465 this._updateAnchorIcons(notifications, anchors);
1469 if (notificationsToShow.length) {
1470 let anchorElement = anchors.values().next().value;
1471 if (anchorElement) {
1472 this._showPanel(notificationsToShow, anchorElement);
1475 // Setup a capturing event listener on the whole window to catch the
1476 // escape key while persistent notifications are visible.
1477 this.window.addEventListener(
1479 this._handleWindowKeyPress,
1483 // Notify observers that we're not showing the popup (useful for testing)
1484 this._notify("updateNotShowing");
1486 // Close the panel if there are no notifications to show.
1487 // When called from PopupNotifications.show() we should never close the
1488 // panel, however. It may just be adding a dismissed notification, in
1489 // which case we want to continue showing any existing notifications.
1490 if (!dismissShowing) {
1494 // Only hide the iconBox if we actually have no notifications (as opposed
1495 // to not having any showable notifications)
1496 if (!haveNotifications) {
1498 this.iconBox.hidden = true;
1499 } else if (anchors.size) {
1500 for (let anchorElement of anchors) {
1501 anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
1506 // Stop listening to keyboard events for notifications.
1507 this.window.removeEventListener(
1509 this._handleWindowKeyPress,
1515 _updateAnchorIcons: function PopupNotifications_updateAnchorIcons(
1519 for (let anchorElement of anchorElements) {
1520 anchorElement.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
1524 _showIcons: function PopupNotifications_showIcons(aCurrentNotifications) {
1525 for (let notification of aCurrentNotifications) {
1526 let anchorElm = notification.anchorElement;
1528 anchorElm.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
1530 if (notification.options.extraAttr) {
1531 anchorElm.setAttribute("extraAttr", notification.options.extraAttr);
1533 anchorElm.removeAttribute("extraAttr");
1539 _hideIcons: function PopupNotifications_hideIcons() {
1540 let icons = this.iconBox.querySelectorAll(ICON_SELECTOR);
1541 for (let icon of icons) {
1542 icon.removeAttribute(ICON_ATTRIBUTE_SHOWING);
1547 * Gets and sets notifications for the browser.
1549 _getNotificationsForBrowser: function PopupNotifications_getNotifications(
1552 let notifications = popupNotificationsMap.get(browser);
1553 if (!notifications) {
1554 // Initialize the WeakMap for the browser so callers can reference/manipulate the array.
1556 popupNotificationsMap.set(browser, notifications);
1558 return notifications;
1560 _setNotificationsForBrowser: function PopupNotifications_setNotifications(
1564 popupNotificationsMap.set(browser, notifications);
1565 return notifications;
1568 _getAnchorsForNotifications:
1569 function PopupNotifications_getAnchorsForNotifications(
1573 let anchors = new Set();
1574 for (let notification of notifications) {
1575 if (notification.anchorElement) {
1576 anchors.add(notification.anchorElement);
1579 if (defaultAnchor && !anchors.size) {
1580 anchors.add(defaultAnchor);
1585 _isActiveBrowser(browser) {
1586 // We compare on frameLoader instead of just comparing the
1587 // selectedBrowser and browser directly because browser tabs in
1588 // Responsive Design Mode put the actual web content into a
1589 // mozbrowser iframe and proxy property read/write and method
1590 // calls from the tab to that iframe. This is so that attempts
1591 // to reload the tab end up reloading the content in
1592 // Responsive Design Mode, and not the Responsive Design Mode
1595 // This means that PopupNotifications can come up from a browser
1596 // in Responsive Design Mode, but the selectedBrowser will not match
1597 // the browser being passed into this function, despite the browser
1598 // actually being within the selected tab. We workaround this by
1599 // comparing frameLoader instead, which is proxied from the outer
1600 // <xul:browser> to the inner mozbrowser <iframe>.
1601 return this.tabbrowser.selectedBrowser.frameLoader == browser.frameLoader;
1604 _onIconBoxCommand: function PopupNotifications_onIconBoxCommand(event) {
1605 // Left click, space or enter only
1606 let type = event.type;
1607 if (type == "click" && event.button != 0) {
1612 type == "keypress" &&
1614 event.charCode == event.DOM_VK_SPACE ||
1615 event.keyCode == event.DOM_VK_RETURN
1621 if (!this._currentNotifications.length) {
1625 event.stopPropagation();
1627 // Get the anchor that is the immediate child of the icon box
1628 let anchor = event.target;
1629 while (anchor && anchor.parentNode != this.iconBox) {
1630 anchor = anchor.parentNode;
1637 // If the panel is not closed, and the anchor is different, immediately mark all
1638 // active notifications for the previous anchor as dismissed
1639 if (this.panel.state != "closed" && anchor != this._currentAnchorElement) {
1640 this._dismissOrRemoveCurrentNotifications();
1643 // Avoid reshowing notifications that are already shown and have not been dismissed.
1644 if (this.panel.state == "closed" || anchor != this._currentAnchorElement) {
1645 // As soon as the panel is shown, focus the first element in the selected notification.
1646 this.panel.addEventListener(
1649 this.window.document.commandDispatcher.advanceFocusIntoSubtree(
1655 this._reshowNotifications(anchor);
1657 // Focus the first element in the selected notification.
1658 this.window.document.commandDispatcher.advanceFocusIntoSubtree(
1664 _reshowNotifications: function PopupNotifications_reshowNotifications(
1668 // Mark notifications anchored to this anchor as un-dismissed
1669 browser = browser || this.tabbrowser.selectedBrowser;
1670 let notifications = this._getNotificationsForBrowser(browser);
1671 notifications.forEach(function (n) {
1672 if (n.anchorElement == anchor) {
1673 n.dismissed = false;
1677 if (this._isActiveBrowser(browser)) {
1678 // ...and then show them.
1679 this._update(notifications, anchor);
1683 _swapBrowserNotifications:
1684 function PopupNotifications_swapBrowserNoficications(
1688 // When swaping browser docshells (e.g. dragging tab to new window) we need
1689 // to update our notification map.
1691 let ourNotifications = this._getNotificationsForBrowser(ourBrowser);
1692 let other = otherBrowser.ownerGlobal.PopupNotifications;
1694 if (ourNotifications.length) {
1696 "unable to swap notifications: otherBrowser doesn't support notifications"
1701 let otherNotifications = other._getNotificationsForBrowser(otherBrowser);
1702 if (ourNotifications.length < 1 && otherNotifications.length < 1) {
1703 // No notification to swap.
1707 otherNotifications = otherNotifications.filter(n => {
1708 if (this._fireCallback(n, NOTIFICATION_EVENT_SWAPPING, ourBrowser)) {
1709 n.browser = ourBrowser;
1713 other._fireCallback(
1715 NOTIFICATION_EVENT_REMOVED,
1716 this.nextRemovalReason
1721 ourNotifications = ourNotifications.filter(n => {
1722 if (this._fireCallback(n, NOTIFICATION_EVENT_SWAPPING, otherBrowser)) {
1723 n.browser = otherBrowser;
1729 NOTIFICATION_EVENT_REMOVED,
1730 this.nextRemovalReason
1735 this._setNotificationsForBrowser(otherBrowser, ourNotifications);
1736 other._setNotificationsForBrowser(ourBrowser, otherNotifications);
1738 if (otherNotifications.length) {
1739 this._update(otherNotifications);
1741 if (ourNotifications.length) {
1742 other._update(ourNotifications);
1746 _fireCallback: function PopupNotifications_fireCallback(n, event, ...args) {
1748 if (n.options.eventCallback) {
1749 return n.options.eventCallback.call(n, event, ...args);
1752 console.error(error);
1757 _onPopupHidden: function PopupNotifications_onPopupHidden(event) {
1758 if (event.target != this.panel) {
1762 // It's possible that a popupnotification set `aria-describedby` on the
1763 // panel element in its eventCallback function. If so, we'll clear that out
1764 // before showing the next notification.
1765 this.panel.removeAttribute("aria-describedby");
1767 // We may have removed the "noautofocus" attribute before showing the panel
1768 // if the notification specified it wants to autofocus on first show.
1769 // When the panel is closed, we have to restore the attribute to its default
1770 // value, so we don't autofocus it if it's subsequently opened from a different code path.
1771 this.panel.setAttribute("noautofocus", "true");
1773 // Handle the case where the panel was closed programmatically.
1774 if (this._ignoreDismissal) {
1775 this._ignoreDismissal.resolve();
1776 this._ignoreDismissal = null;
1780 this._dismissOrRemoveCurrentNotifications();
1787 _dismissOrRemoveCurrentNotifications() {
1789 this.panel.firstElementChild &&
1790 this.panel.firstElementChild.notification.browser;
1795 let notifications = this._getNotificationsForBrowser(browser);
1796 // Mark notifications as dismissed and call dismissal callbacks
1797 for (let nEl of this.panel.children) {
1798 let notificationObj = nEl.notification;
1799 // Never call a dismissal handler on a notification that's been removed.
1800 if (!notifications.includes(notificationObj)) {
1804 // Record the time of the first notification dismissal if the main action
1805 // was not triggered in the meantime.
1806 let timeSinceShown =
1807 this.window.performance.now() - notificationObj.timeShown;
1809 !notificationObj.wasDismissed &&
1810 !notificationObj.recordedTelemetryMainAction
1812 notificationObj._recordTelemetry(
1813 "POPUP_NOTIFICATION_DISMISSAL_MS",
1818 // Do not mark the notification as dismissed or fire NOTIFICATION_EVENT_DISMISSED
1819 // if the notification is removed.
1820 if (notificationObj.options.removeOnDismissal) {
1821 notificationObj._recordTelemetryStat(this.nextRemovalReason);
1822 this._remove(notificationObj);
1824 notificationObj.dismissed = true;
1825 this._fireCallback(notificationObj, NOTIFICATION_EVENT_DISMISSED);
1830 _onCheckboxCommand(event) {
1831 let notificationEl = getNotificationFromElement(event.originalTarget);
1832 let checked = notificationEl.checkbox.checked;
1833 let notification = notificationEl.notification;
1835 // Save checkbox state to be able to persist it when re-opening the doorhanger.
1836 notification._checkboxChecked = checked;
1839 this._setNotificationUIState(
1841 notification.options.checkbox.checkedState
1844 this._setNotificationUIState(
1846 notification.options.checkbox.uncheckedState
1849 event.stopPropagation();
1853 // Ignore events from buttons as they are submitting and so don't need checks
1854 if (event.originalTarget.localName == "button") {
1857 let notificationEl = getNotificationFromElement(event.target);
1859 let notification = notificationEl.notification;
1860 if (!notification.options.checkbox) {
1861 this._setNotificationUIState(notificationEl);
1865 if (notificationEl.checkbox.checked) {
1866 this._setNotificationUIState(
1868 notification.options.checkbox.checkedState
1871 this._setNotificationUIState(
1873 notification.options.checkbox.uncheckedState
1878 _onButtonEvent(event, type, source = "button", notificationEl = null) {
1879 if (!notificationEl) {
1880 notificationEl = getNotificationFromElement(event.originalTarget);
1883 if (!notificationEl) {
1885 "PopupNotifications._onButtonEvent: couldn't find notification element"
1889 if (!notificationEl.notification) {
1891 "PopupNotifications._onButtonEvent: couldn't find notification"
1895 let notification = notificationEl.notification;
1897 // Receiving a button event means the notification should have been shown.
1898 // Make sure that timeShown is always set to ensure we don't break the
1899 // security delay calculation below.
1900 if (!notification.timeShown) {
1902 "_onButtonEvent: notification.timeShown is unset. Setting to now.",
1905 notification.timeShown = this.window.performance.now();
1908 if (type == "dropmarkerpopupshown") {
1909 notification._recordTelemetryStat(TELEMETRY_STAT_OPEN_SUBMENU);
1913 if (type == "learnmoreclick") {
1914 notification._recordTelemetryStat(TELEMETRY_STAT_LEARN_MORE);
1918 if (type == "buttoncommand") {
1919 // Record the total timing of the main action since the notification was
1920 // created, even if the notification was dismissed in the meantime.
1921 let timeSinceCreated =
1922 this.window.performance.now() - notification.timeCreated;
1923 if (!notification.recordedTelemetryMainAction) {
1924 notification.recordedTelemetryMainAction = true;
1925 notification._recordTelemetry(
1926 "POPUP_NOTIFICATION_MAIN_ACTION_MS",
1932 if (type == "buttoncommand" || type == "secondarybuttoncommand") {
1933 if (Services.focus.activeWindow != this.window) {
1934 Services.console.logStringMessage(
1935 "PopupNotifications._onButtonEvent: " +
1936 "Button click happened before the window was focused"
1938 this.window.focus();
1942 let now = this.window.performance.now();
1943 let timeSinceShown = now - notification.timeShown;
1944 if (timeSinceShown < lazy.buttonDelay) {
1945 Services.console.logStringMessage(
1946 "PopupNotifications._onButtonEvent: " +
1947 "Button click happened before the security delay: " +
1951 notification.timeShown = Math.max(now, notification.timeShown);
1956 let action = notification.mainAction;
1957 let telemetryStatId = TELEMETRY_STAT_ACTION_1;
1959 if (type == "secondarybuttoncommand") {
1960 action = notification.secondaryActions[0];
1961 telemetryStatId = TELEMETRY_STAT_ACTION_2;
1964 notification._recordTelemetryStat(telemetryStatId);
1968 action.callback.call(undefined, {
1969 checkboxChecked: notificationEl.checkbox.checked,
1974 console.error(error);
1977 if (action.dismiss) {
1983 this._remove(notification);
1987 _onMenuCommand: function PopupNotifications_onMenuCommand(event) {
1988 let target = event.originalTarget;
1989 if (!target.action || !target.notification) {
1991 "menucommand target has no associated action/notification"
1995 let notificationEl = getNotificationFromElement(target);
1996 event.stopPropagation();
1998 target.notification._recordTelemetryStat(target.action.telemetryStatId);
2001 target.action.callback.call(undefined, {
2002 checkboxChecked: notificationEl.checkbox.checked,
2003 source: "menucommand",
2006 console.error(error);
2009 if (target.action.dismiss) {
2014 this._remove(target.notification);
2018 _notify: function PopupNotifications_notify(topic) {
2019 Services.obs.notifyObservers(null, "PopupNotifications-" + topic);