Backed out 3 changesets (bug 1890718) for failing sevaral UI related bc tests. CLOSED...
[gecko.git] / browser / components / customizableui / content / panelUI.js
blobcb32085fd70967ce8e8a2d5155fb543cd50d030b
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 file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 ChromeUtils.defineESModuleGetters(this, {
6   AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs",
7   NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
8   PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
9 });
11 /**
12  * Maintains the state and dispatches events for the main menu panel.
13  */
15 const PanelUI = {
16   /** Panel events that we listen for. **/
17   get kEvents() {
18     return ["popupshowing", "popupshown", "popuphiding", "popuphidden"];
19   },
20   /**
21    * Used for lazily getting and memoizing elements from the document. Lazy
22    * getters are set in init, and memoizing happens after the first retrieval.
23    */
24   get kElements() {
25     return {
26       multiView: "appMenu-multiView",
27       menuButton: "PanelUI-menu-button",
28       panel: "appMenu-popup",
29       overflowFixedList: "widget-overflow-fixed-list",
30       overflowPanel: "widget-overflow",
31       navbar: "nav-bar",
32     };
33   },
35   _initialized: false,
36   _notifications: null,
37   _notificationPanel: null,
39   init(shouldSuppress) {
40     this._shouldSuppress = shouldSuppress;
41     this._initElements();
43     this.menuButton.addEventListener("mousedown", this);
44     this.menuButton.addEventListener("keypress", this);
46     Services.obs.addObserver(this, "fullscreen-nav-toolbox");
47     Services.obs.addObserver(this, "appMenu-notifications");
48     Services.obs.addObserver(this, "show-update-progress");
50     XPCOMUtils.defineLazyPreferenceGetter(
51       this,
52       "autoHideToolbarInFullScreen",
53       "browser.fullscreen.autohide",
54       false,
55       (pref, previousValue, newValue) => {
56         // On OSX, or with autohide preffed off, MozDOMFullscreen is the only
57         // event we care about, since fullscreen should behave just like non
58         // fullscreen. Otherwise, we don't want to listen to these because
59         // we'd just be spamming ourselves with both of them whenever a user
60         // opened a video.
61         if (newValue) {
62           window.removeEventListener("MozDOMFullscreen:Entered", this);
63           window.removeEventListener("MozDOMFullscreen:Exited", this);
64           window.addEventListener("fullscreen", this);
65         } else {
66           window.addEventListener("MozDOMFullscreen:Entered", this);
67           window.addEventListener("MozDOMFullscreen:Exited", this);
68           window.removeEventListener("fullscreen", this);
69         }
71         this.updateNotifications(false);
72       },
73       autoHidePref => autoHidePref && Services.appinfo.OS !== "Darwin"
74     );
76     if (this.autoHideToolbarInFullScreen) {
77       window.addEventListener("fullscreen", this);
78     } else {
79       window.addEventListener("MozDOMFullscreen:Entered", this);
80       window.addEventListener("MozDOMFullscreen:Exited", this);
81     }
83     window.addEventListener("activate", this);
84     CustomizableUI.addListener(this);
86     // We do this sync on init because in order to have the overflow button show up
87     // we need to know whether anything is in the permanent panel area.
88     this.overflowFixedList.hidden = false;
89     // Also unhide the separator. We use CSS to hide/show it based on the panel's content.
90     this.overflowFixedList.previousElementSibling.hidden = false;
91     CustomizableUI.registerPanelNode(
92       this.overflowFixedList,
93       CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
94     );
95     this.updateOverflowStatus();
97     Services.obs.notifyObservers(
98       null,
99       "appMenu-notifications-request",
100       "refresh"
101     );
103     this._initialized = true;
104   },
106   _initElements() {
107     for (let [k, v] of Object.entries(this.kElements)) {
108       // Need to do fresh let-bindings per iteration
109       let getKey = k;
110       let id = v;
111       this.__defineGetter__(getKey, function () {
112         delete this[getKey];
113         return (this[getKey] = document.getElementById(id));
114       });
115     }
116   },
118   _eventListenersAdded: false,
119   _ensureEventListenersAdded() {
120     if (this._eventListenersAdded) {
121       return;
122     }
123     this._addEventListeners();
124   },
126   _addEventListeners() {
127     for (let event of this.kEvents) {
128       this.panel.addEventListener(event, this);
129     }
131     PanelMultiView.getViewNode(document, "PanelUI-helpView").addEventListener(
132       "ViewShowing",
133       this._onHelpViewShow
134     );
135     this._eventListenersAdded = true;
136   },
138   _removeEventListeners() {
139     for (let event of this.kEvents) {
140       this.panel.removeEventListener(event, this);
141     }
142     PanelMultiView.getViewNode(
143       document,
144       "PanelUI-helpView"
145     ).removeEventListener("ViewShowing", this._onHelpViewShow);
146     this._eventListenersAdded = false;
147   },
149   uninit() {
150     this._removeEventListeners();
152     if (this._notificationPanel) {
153       for (let event of this.kEvents) {
154         this.notificationPanel.removeEventListener(event, this);
155       }
156     }
158     Services.obs.removeObserver(this, "fullscreen-nav-toolbox");
159     Services.obs.removeObserver(this, "appMenu-notifications");
160     Services.obs.removeObserver(this, "show-update-progress");
162     window.removeEventListener("MozDOMFullscreen:Entered", this);
163     window.removeEventListener("MozDOMFullscreen:Exited", this);
164     window.removeEventListener("fullscreen", this);
165     window.removeEventListener("activate", this);
166     this.menuButton.removeEventListener("mousedown", this);
167     this.menuButton.removeEventListener("keypress", this);
168     CustomizableUI.removeListener(this);
169   },
171   /**
172    * Opens the menu panel if it's closed, or closes it if it's
173    * open.
174    *
175    * @param aEvent the event that triggers the toggle.
176    */
177   toggle(aEvent) {
178     // Don't show the panel if the window is in customization mode,
179     // since this button doubles as an exit path for the user in this case.
180     if (document.documentElement.hasAttribute("customizing")) {
181       return;
182     }
183     this._ensureEventListenersAdded();
184     if (this.panel.state == "open") {
185       this.hide();
186     } else if (this.panel.state == "closed") {
187       this.show(aEvent);
188     }
189   },
191   /**
192    * Opens the menu panel. If the event target has a child with the
193    * toolbarbutton-icon attribute, the panel will be anchored on that child.
194    * Otherwise, the panel is anchored on the event target itself.
195    *
196    * @param aEvent the event (if any) that triggers showing the menu.
197    */
198   show(aEvent) {
199     this._ensureShortcutsShown();
200     (async () => {
201       await this.ensureReady();
203       if (
204         this.panel.state == "open" ||
205         document.documentElement.hasAttribute("customizing")
206       ) {
207         return;
208       }
210       let domEvent = null;
211       if (aEvent && aEvent.type != "command") {
212         domEvent = aEvent;
213       }
215       let anchor = this._getPanelAnchor(this.menuButton);
216       await PanelMultiView.openPopup(this.panel, anchor, {
217         triggerEvent: domEvent,
218       });
219     })().catch(console.error);
220   },
222   /**
223    * If the menu panel is being shown, hide it.
224    */
225   hide() {
226     if (document.documentElement.hasAttribute("customizing")) {
227       return;
228     }
230     PanelMultiView.hidePopup(this.panel);
231   },
233   observe(subject, topic, status) {
234     switch (topic) {
235       case "fullscreen-nav-toolbox":
236         if (this._notifications) {
237           this.updateNotifications(false);
238         }
239         break;
240       case "appMenu-notifications":
241         // Don't initialize twice.
242         if (status == "init" && this._notifications) {
243           break;
244         }
245         this._notifications = AppMenuNotifications.notifications;
246         this.updateNotifications(true);
247         break;
248       case "show-update-progress":
249         openAboutDialog();
250         break;
251     }
252   },
254   handleEvent(aEvent) {
255     // Ignore context menus and menu button menus showing and hiding:
256     if (aEvent.type.startsWith("popup") && aEvent.target != this.panel) {
257       return;
258     }
259     switch (aEvent.type) {
260       case "popupshowing":
261         updateEditUIVisibility();
262       // Fall through
263       case "popupshown":
264         if (aEvent.type == "popupshown") {
265           CustomizableUI.addPanelCloseListeners(this.panel);
266         }
267       // Fall through
268       case "popuphiding":
269         if (aEvent.type == "popuphiding") {
270           updateEditUIVisibility();
271         }
272       // Fall through
273       case "popuphidden":
274         this.updateNotifications();
275         this._updatePanelButton(aEvent.target);
276         if (aEvent.type == "popuphidden") {
277           CustomizableUI.removePanelCloseListeners(this.panel);
278         }
279         break;
280       case "mousedown":
281         // On Mac, ctrl-click will send a context menu event from the widget, so
282         // we don't want to bring up the panel when ctrl key is pressed.
283         if (
284           aEvent.button == 0 &&
285           (AppConstants.platform != "macosx" || !aEvent.ctrlKey)
286         ) {
287           this.toggle(aEvent);
288         }
289         break;
290       case "keypress":
291         if (aEvent.key == " " || aEvent.key == "Enter") {
292           this.toggle(aEvent);
293           aEvent.stopPropagation();
294         }
295         break;
296       case "MozDOMFullscreen:Entered":
297       case "MozDOMFullscreen:Exited":
298       case "fullscreen":
299       case "activate":
300         this.updateNotifications();
301         break;
302     }
303   },
305   get isReady() {
306     return !!this._isReady;
307   },
309   get isNotificationPanelOpen() {
310     let panelState = this.notificationPanel.state;
312     return panelState == "showing" || panelState == "open";
313   },
315   /**
316    * Registering the menu panel is done lazily for performance reasons. This
317    * method is exposed so that CustomizationMode can force panel-readyness in the
318    * event that customization mode is started before the panel has been opened
319    * by the user.
320    *
321    * @param aCustomizing (optional) set to true if this was called while entering
322    *        customization mode. If that's the case, we trust that customization
323    *        mode will handle calling beginBatchUpdate and endBatchUpdate.
324    *
325    * @return a Promise that resolves once the panel is ready to roll.
326    */
327   async ensureReady() {
328     if (this._isReady) {
329       return;
330     }
332     await window.delayedStartupPromise;
333     this._ensureEventListenersAdded();
334     this.panel.hidden = false;
335     this._isReady = true;
336   },
338   /**
339    * Switch the panel to the help view if it's not already
340    * in that view.
341    */
342   showHelpView(aAnchor) {
343     this._ensureEventListenersAdded();
344     this.multiView.showSubView("PanelUI-helpView", aAnchor);
345   },
347   /**
348    * Switch the panel to the "More Tools" view.
349    *
350    * @param moreTools The panel showing the "More Tools" view.
351    */
352   showMoreToolsPanel(moreTools) {
353     this.showSubView("appmenu-moreTools", moreTools);
355     // Notify DevTools the panel view is showing and need it to populate the
356     // "Browser Tools" section of the panel. We notify the observer setup by
357     // DevTools because we want to ensure the same menuitem list is shared
358     // between both the AppMenu and toolbar button views.
359     let view = document.getElementById("appmenu-developer-tools-view");
360     Services.obs.notifyObservers(view, "web-developer-tools-view-showing");
361   },
363   /**
364    * Shows a subview in the panel with a given ID.
365    *
366    * @param aViewId the ID of the subview to show.
367    * @param aAnchor the element that spawned the subview.
368    * @param aEvent the event triggering the view showing.
369    */
370   async showSubView(aViewId, aAnchor, aEvent) {
371     if (aEvent) {
372       // On Mac, ctrl-click will send a context menu event from the widget, so
373       // we don't want to bring up the panel when ctrl key is pressed.
374       if (
375         aEvent.type == "mousedown" &&
376         (aEvent.button != 0 ||
377           (AppConstants.platform == "macosx" && aEvent.ctrlKey))
378       ) {
379         return;
380       }
381       if (
382         aEvent.type == "keypress" &&
383         aEvent.key != " " &&
384         aEvent.key != "Enter"
385       ) {
386         return;
387       }
388     }
390     this._ensureEventListenersAdded();
392     let viewNode = PanelMultiView.getViewNode(document, aViewId);
393     if (!viewNode) {
394       console.error("Could not show panel subview with id: ", aViewId);
395       return;
396     }
398     if (!aAnchor) {
399       console.error(
400         "Expected an anchor when opening subview with id: ",
401         aViewId
402       );
403       return;
404     }
406     this.ensurePanicViewInitialized(viewNode);
408     let container = aAnchor.closest("panelmultiview");
409     if (container && !viewNode.hasAttribute("disallowSubView")) {
410       container.showSubView(aViewId, aAnchor);
411     } else if (!aAnchor.open) {
412       aAnchor.open = true;
414       let tempPanel = document.createXULElement("panel");
415       tempPanel.setAttribute("type", "arrow");
416       tempPanel.setAttribute("id", "customizationui-widget-panel");
417       if (viewNode.hasAttribute("neverhidden")) {
418         tempPanel.setAttribute("neverhidden", "true");
419       }
421       tempPanel.setAttribute("class", "cui-widget-panel panel-no-padding");
422       tempPanel.setAttribute("viewId", aViewId);
423       if (aAnchor.getAttribute("tabspecific")) {
424         tempPanel.setAttribute("tabspecific", true);
425       }
426       if (aAnchor.getAttribute("locationspecific")) {
427         tempPanel.setAttribute("locationspecific", true);
428       }
429       if (this._disableAnimations) {
430         tempPanel.setAttribute("animate", "false");
431       }
432       tempPanel.setAttribute("context", "");
433       document
434         .getElementById(CustomizableUI.AREA_NAVBAR)
435         .appendChild(tempPanel);
437       let multiView = document.createXULElement("panelmultiview");
438       multiView.setAttribute("id", "customizationui-widget-multiview");
439       multiView.setAttribute("viewCacheId", "appMenu-viewCache");
440       multiView.setAttribute("mainViewId", viewNode.id);
441       multiView.appendChild(viewNode);
442       tempPanel.appendChild(multiView);
443       viewNode.classList.add("cui-widget-panelview", "PanelUI-subView");
445       let viewShown = false;
446       let panelRemover = event => {
447         // Avoid bubbled events triggering the panel closing.
448         if (event && event.target != tempPanel) {
449           return;
450         }
451         viewNode.classList.remove("cui-widget-panelview");
452         if (viewShown) {
453           CustomizableUI.removePanelCloseListeners(tempPanel);
454           tempPanel.removeEventListener("popuphidden", panelRemover);
455         }
456         aAnchor.open = false;
458         PanelMultiView.removePopup(tempPanel);
459       };
461       if (aAnchor.parentNode.id == "PersonalToolbar") {
462         tempPanel.classList.add("bookmarks-toolbar");
463       }
465       let anchor = this._getPanelAnchor(aAnchor);
467       if (aAnchor != anchor && aAnchor.id) {
468         anchor.setAttribute("consumeanchor", aAnchor.id);
469       }
471       try {
472         viewShown = await PanelMultiView.openPopup(tempPanel, anchor, {
473           position: "bottomright topright",
474           triggerEvent: aEvent,
475         });
476       } catch (ex) {
477         console.error(ex);
478       }
480       if (viewShown) {
481         CustomizableUI.addPanelCloseListeners(tempPanel);
482         tempPanel.addEventListener("popuphidden", panelRemover);
483       } else {
484         panelRemover();
485       }
486     }
487   },
489   /**
490    * Adds FTL before appending the panic view markup to the main DOM.
491    *
492    * @param {panelview} panelView The Panic View panelview.
493    */
494   ensurePanicViewInitialized(panelView) {
495     if (panelView.id != "PanelUI-panicView" || panelView._initialized) {
496       return;
497     }
499     if (!this.panic) {
500       this.panic = panelView;
501     }
503     MozXULElement.insertFTLIfNeeded("browser/panicButton.ftl");
504     panelView._initialized = true;
505   },
507   /**
508    * NB: The enable- and disableSingleSubviewPanelAnimations methods only
509    * affect the hiding/showing animations of single-subview panels (tempPanel
510    * in the showSubView method).
511    */
512   disableSingleSubviewPanelAnimations() {
513     this._disableAnimations = true;
514   },
516   enableSingleSubviewPanelAnimations() {
517     this._disableAnimations = false;
518   },
520   updateOverflowStatus() {
521     let hasKids = this.overflowFixedList.hasChildNodes();
522     if (hasKids && !this.navbar.hasAttribute("nonemptyoverflow")) {
523       this.navbar.setAttribute("nonemptyoverflow", "true");
524       this.overflowPanel.setAttribute("hasfixeditems", "true");
525     } else if (!hasKids && this.navbar.hasAttribute("nonemptyoverflow")) {
526       PanelMultiView.hidePopup(this.overflowPanel);
527       this.overflowPanel.removeAttribute("hasfixeditems");
528       this.navbar.removeAttribute("nonemptyoverflow");
529     }
530   },
532   onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
533     if (aContainer == this.overflowFixedList) {
534       this.updateOverflowStatus();
535     }
536   },
538   onAreaReset(aArea, aContainer) {
539     if (aContainer == this.overflowFixedList) {
540       this.updateOverflowStatus();
541     }
542   },
544   /**
545    * Sets the anchor node into the open or closed state, depending
546    * on the state of the panel.
547    */
548   _updatePanelButton() {
549     let { state } = this.panel;
550     if (state == "open" || state == "showing") {
551       this.menuButton.open = true;
552       document.l10n.setAttributes(
553         this.menuButton,
554         "appmenu-menu-button-opened2"
555       );
556     } else {
557       this.menuButton.open = false;
558       document.l10n.setAttributes(
559         this.menuButton,
560         "appmenu-menu-button-closed2"
561       );
562     }
563   },
565   _onHelpViewShow() {
566     // Call global menu setup function
567     buildHelpMenu();
569     let helpMenu = document.getElementById("menu_HelpPopup");
570     let items = this.getElementsByTagName("vbox")[0];
571     let attrs = [
572       "command",
573       "oncommand",
574       "onclick",
575       "key",
576       "disabled",
577       "accesskey",
578       "label",
579     ];
581     // Remove all buttons from the view
582     while (items.firstChild) {
583       items.firstChild.remove();
584     }
586     // Add the current set of menuitems of the Help menu to this view
587     let menuItems = Array.prototype.slice.call(
588       helpMenu.getElementsByTagName("menuitem")
589     );
590     let fragment = document.createDocumentFragment();
591     for (let node of menuItems) {
592       if (node.hidden) {
593         continue;
594       }
595       let button = document.createXULElement("toolbarbutton");
596       // Copy specific attributes from a menuitem of the Help menu
597       for (let attrName of attrs) {
598         if (!node.hasAttribute(attrName)) {
599           continue;
600         }
601         button.setAttribute(attrName, node.getAttribute(attrName));
602       }
604       // We have AppMenu-specific strings for the Help menu. By convention,
605       // their localization IDs are set on "appmenu-data-l10n-id" attributes.
606       let l10nId = node.getAttribute("appmenu-data-l10n-id");
607       if (l10nId) {
608         document.l10n.setAttributes(button, l10nId);
609       }
611       if (node.id) {
612         button.id = "appMenu_" + node.id;
613       }
615       button.classList.add("subviewbutton");
616       fragment.appendChild(button);
617     }
619     // The Enterprise Support menu item has a different location than its
620     // placement in the menubar, so we need to specify it here.
621     let helpPolicySupport = fragment.querySelector(
622       "#appMenu_helpPolicySupport"
623     );
624     if (helpPolicySupport) {
625       fragment.insertBefore(
626         helpPolicySupport,
627         fragment.querySelector("#appMenu_menu_HelpPopup_reportPhishingtoolmenu")
628           .nextSibling
629       );
630     }
632     items.appendChild(fragment);
633   },
635   _hidePopup() {
636     if (!this._notificationPanel) {
637       return;
638     }
640     if (this.isNotificationPanelOpen) {
641       this.notificationPanel.hidePopup();
642     }
643   },
645   /**
646    * Selects and marks an item by id from the main view. The ids are an array,
647    * the first in the main view and the later ids in subsequent subviews that
648    * become marked when the user opens the subview. The subview marking is
649    * cancelled if a different subview is opened.
650    */
651   async selectAndMarkItem(itemIds) {
652     // This shouldn't really occur, but return early just in case.
653     if (document.documentElement.hasAttribute("customizing")) {
654       return;
655     }
657     // This function was triggered from a button while the menu was
658     // already open, so the panel should be in the process of hiding.
659     // Wait for the panel to hide first, then reopen it.
660     if (this.panel.state == "hiding") {
661       await new Promise(resolve => {
662         this.panel.addEventListener("popuphidden", resolve, { once: true });
663       });
664     }
666     if (this.panel.state != "open") {
667       await new Promise(resolve => {
668         this.panel.addEventListener("ViewShown", resolve, { once: true });
669         this.show();
670       });
671     }
673     let currentView;
675     let viewShownCB = event => {
676       viewHidingCB();
678       if (itemIds.length) {
679         let subItem = window.document.getElementById(itemIds[0]);
680         if (event.target.id == subItem?.closest("panelview")?.id) {
681           Services.tm.dispatchToMainThread(() => {
682             markItem(event.target);
683           });
684         } else {
685           itemIds = [];
686         }
687       }
688     };
690     let viewHidingCB = () => {
691       if (currentView) {
692         currentView.ignoreMouseMove = false;
693       }
694       currentView = null;
695     };
697     let popupHiddenCB = () => {
698       viewHidingCB();
699       this.panel.removeEventListener("ViewShown", viewShownCB);
700     };
702     let markItem = viewNode => {
703       let id = itemIds.shift();
704       let item = window.document.getElementById(id);
705       item.setAttribute("tabindex", "-1");
707       currentView = PanelView.forNode(viewNode);
708       currentView.selectedElement = item;
709       currentView.focusSelectedElement(true);
711       // Prevent the mouse from changing the highlight temporarily.
712       // This flag gets removed when the view is hidden or a key
713       // is pressed.
714       currentView.ignoreMouseMove = true;
716       if (itemIds.length) {
717         this.panel.addEventListener("ViewShown", viewShownCB, { once: true });
718       }
719       this.panel.addEventListener("ViewHiding", viewHidingCB, { once: true });
720     };
722     this.panel.addEventListener("popuphidden", popupHiddenCB, { once: true });
723     markItem(this.mainView);
724   },
726   updateNotifications(notificationsChanged) {
727     let notifications = this._notifications;
728     if (!notifications || !notifications.length) {
729       if (notificationsChanged) {
730         this._clearAllNotifications();
731         this._hidePopup();
732       }
733       return;
734     }
736     if (
737       (window.fullScreen && FullScreen.navToolboxHidden) ||
738       document.fullscreenElement ||
739       this._shouldSuppress()
740     ) {
741       this._hidePopup();
742       return;
743     }
745     let doorhangers = notifications.filter(
746       n => !n.dismissed && !n.options.badgeOnly
747     );
749     if (this.panel.state == "showing" || this.panel.state == "open") {
750       // If the menu is already showing, then we need to dismiss all
751       // notifications since we don't want their doorhangers competing for
752       // attention. Don't hide the badge though; it isn't really in competition
753       // with anything.
754       doorhangers.forEach(n => {
755         n.dismissed = true;
756         if (n.options.onDismissed) {
757           n.options.onDismissed(window);
758         }
759       });
760       this._hidePopup();
761       if (!notifications[0].options.badgeOnly) {
762         this._showBannerItem(notifications[0]);
763       }
764     } else if (doorhangers.length) {
765       // Only show the doorhanger if the window is focused and not fullscreen
766       if (
767         (window.fullScreen && this.autoHideToolbarInFullScreen) ||
768         Services.focus.activeWindow !== window
769       ) {
770         this._hidePopup();
771         this._showBadge(doorhangers[0]);
772         this._showBannerItem(doorhangers[0]);
773       } else {
774         this._clearBadge();
775         this._showNotificationPanel(doorhangers[0]);
776       }
777     } else {
778       this._hidePopup();
779       this._showBadge(notifications[0]);
780       this._showBannerItem(notifications[0]);
781     }
782   },
784   _showNotificationPanel(notification) {
785     this._refreshNotificationPanel(notification);
787     if (this.isNotificationPanelOpen) {
788       return;
789     }
791     if (notification.options.beforeShowDoorhanger) {
792       notification.options.beforeShowDoorhanger(document);
793     }
795     let anchor = this._getPanelAnchor(this.menuButton);
797     // Insert Fluent files when needed before notification is opened
798     MozXULElement.insertFTLIfNeeded("branding/brand.ftl");
799     MozXULElement.insertFTLIfNeeded("browser/appMenuNotifications.ftl");
801     // After Fluent files are loaded into document replace data-lazy-l10n-ids with actual ones
802     document
803       .getElementById("appMenu-notification-popup")
804       .querySelectorAll("[data-lazy-l10n-id]")
805       .forEach(el => {
806         el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
807         el.removeAttribute("data-lazy-l10n-id");
808       });
810     this.notificationPanel.openPopup(anchor, "bottomright topright");
811   },
813   _clearNotificationPanel() {
814     for (let popupnotification of this.notificationPanel.children) {
815       popupnotification.hidden = true;
816       popupnotification.notification = null;
817     }
818   },
820   _clearAllNotifications() {
821     this._clearNotificationPanel();
822     this._clearBadge();
823     this._clearBannerItem();
824   },
826   get notificationPanel() {
827     // Lazy load the panic-button-success-notification panel the first time we need to display it.
828     if (!this._notificationPanel) {
829       let template = document.getElementById("appMenuNotificationTemplate");
830       template.replaceWith(template.content);
831       this._notificationPanel = document.getElementById(
832         "appMenu-notification-popup"
833       );
834       for (let event of this.kEvents) {
835         this._notificationPanel.addEventListener(event, this);
836       }
837     }
838     return this._notificationPanel;
839   },
841   get mainView() {
842     if (!this._mainView) {
843       this._mainView = PanelMultiView.getViewNode(
844         document,
845         "appMenu-protonMainView"
846       );
847     }
848     return this._mainView;
849   },
851   get addonNotificationContainer() {
852     if (!this._addonNotificationContainer) {
853       this._addonNotificationContainer = PanelMultiView.getViewNode(
854         document,
855         "appMenu-proton-addon-banners"
856       );
857     }
859     return this._addonNotificationContainer;
860   },
862   _formatDescriptionMessage(n) {
863     let text = {};
864     let array = n.options.message.split("<>");
865     text.start = array[0] || "";
866     text.name = n.options.name || "";
867     text.end = array[1] || "";
868     return text;
869   },
871   _refreshNotificationPanel(notification) {
872     this._clearNotificationPanel();
874     let popupnotificationID = this._getPopupId(notification);
875     let popupnotification = document.getElementById(popupnotificationID);
877     popupnotification.setAttribute("id", popupnotificationID);
878     popupnotification.setAttribute(
879       "buttoncommand",
880       "PanelUI._onNotificationButtonEvent(event, 'buttoncommand');"
881     );
882     popupnotification.setAttribute(
883       "secondarybuttoncommand",
884       "PanelUI._onNotificationButtonEvent(event, 'secondarybuttoncommand');"
885     );
887     if (notification.options.message) {
888       let desc = this._formatDescriptionMessage(notification);
889       popupnotification.setAttribute("label", desc.start);
890       popupnotification.setAttribute("name", desc.name);
891       popupnotification.setAttribute("endlabel", desc.end);
892     }
893     if (notification.options.onRefresh) {
894       notification.options.onRefresh(window);
895     }
896     if (notification.options.popupIconURL) {
897       popupnotification.setAttribute("icon", notification.options.popupIconURL);
898       popupnotification.setAttribute("hasicon", true);
899     }
900     if (notification.options.learnMoreURL) {
901       popupnotification.setAttribute(
902         "learnmoreurl",
903         notification.options.learnMoreURL
904       );
905     }
907     popupnotification.notification = notification;
908     popupnotification.show();
909   },
911   _showBadge(notification) {
912     let badgeStatus = this._getBadgeStatus(notification);
913     this.menuButton.setAttribute("badge-status", badgeStatus);
914   },
916   // "Banner item" here refers to an item in the hamburger panel menu. They will
917   // typically show up as a colored row in the panel.
918   _showBannerItem(notification) {
919     const supportedIds = [
920       "update-downloading",
921       "update-available",
922       "update-manual",
923       "update-unsupported",
924       "update-restart",
925     ];
926     if (!supportedIds.includes(notification.id)) {
927       return;
928     }
930     if (!this._panelBannerItem) {
931       this._panelBannerItem = this.mainView.querySelector(".panel-banner-item");
932     }
934     let l10nId = "appmenuitem-banner-" + notification.id;
935     document.l10n.setAttributes(this._panelBannerItem, l10nId);
937     this._panelBannerItem.setAttribute("notificationid", notification.id);
938     this._panelBannerItem.hidden = false;
939     this._panelBannerItem.notification = notification;
940   },
942   _clearBadge() {
943     this.menuButton.removeAttribute("badge-status");
944   },
946   _clearBannerItem() {
947     if (this._panelBannerItem) {
948       this._panelBannerItem.notification = null;
949       this._panelBannerItem.hidden = true;
950     }
951   },
953   _onNotificationButtonEvent(event, type) {
954     let notificationEl = getNotificationFromElement(event.originalTarget);
956     if (!notificationEl) {
957       throw new Error(
958         "PanelUI._onNotificationButtonEvent: couldn't find notification element"
959       );
960     }
962     if (!notificationEl.notification) {
963       throw new Error(
964         "PanelUI._onNotificationButtonEvent: couldn't find notification"
965       );
966     }
968     let notification = notificationEl.notification;
970     if (type == "secondarybuttoncommand") {
971       AppMenuNotifications.callSecondaryAction(window, notification);
972     } else {
973       AppMenuNotifications.callMainAction(window, notification, true);
974     }
975   },
977   _onBannerItemSelected(event) {
978     let target = event.originalTarget;
979     if (!target.notification) {
980       throw new Error(
981         "menucommand target has no associated action/notification"
982       );
983     }
985     event.stopPropagation();
986     AppMenuNotifications.callMainAction(window, target.notification, false);
987   },
989   _getPopupId(notification) {
990     return "appMenu-" + notification.id + "-notification";
991   },
993   _getBadgeStatus(notification) {
994     return notification.id;
995   },
997   _getPanelAnchor(candidate) {
998     let iconAnchor = candidate.badgeStack || candidate.icon;
999     return iconAnchor || candidate;
1000   },
1002   _ensureShortcutsShown(view = this.mainView) {
1003     if (view.hasAttribute("added-shortcuts")) {
1004       return;
1005     }
1006     view.setAttribute("added-shortcuts", "true");
1007     for (let button of view.querySelectorAll("toolbarbutton[key]")) {
1008       let keyId = button.getAttribute("key");
1009       let key = document.getElementById(keyId);
1010       if (!key) {
1011         continue;
1012       }
1013       button.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(key));
1014     }
1015   },
1018 XPCOMUtils.defineConstant(this, "PanelUI", PanelUI);
1021  * Gets the currently selected locale for display.
1022  * @return  the selected locale
1023  */
1024 function getLocale() {
1025   return Services.locale.appLocaleAsBCP47;
1029  * Given a DOM node inside a <popupnotification>, return the parent <popupnotification>.
1030  */
1031 function getNotificationFromElement(aElement) {
1032   return aElement.closest("popupnotification");