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",
12 * Maintains the state and dispatches events for the main menu panel.
16 /** Panel events that we listen for. **/
18 return ["popupshowing", "popupshown", "popuphiding", "popuphidden"];
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.
26 multiView: "appMenu-multiView",
27 menuButton: "PanelUI-menu-button",
28 panel: "appMenu-popup",
29 overflowFixedList: "widget-overflow-fixed-list",
30 overflowPanel: "widget-overflow",
37 _notificationPanel: null,
39 init(shouldSuppress) {
40 this._shouldSuppress = shouldSuppress;
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(
52 "autoHideToolbarInFullScreen",
53 "browser.fullscreen.autohide",
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
62 window.removeEventListener("MozDOMFullscreen:Entered", this);
63 window.removeEventListener("MozDOMFullscreen:Exited", this);
64 window.addEventListener("fullscreen", this);
66 window.addEventListener("MozDOMFullscreen:Entered", this);
67 window.addEventListener("MozDOMFullscreen:Exited", this);
68 window.removeEventListener("fullscreen", this);
71 this.updateNotifications(false);
73 autoHidePref => autoHidePref && Services.appinfo.OS !== "Darwin"
76 if (this.autoHideToolbarInFullScreen) {
77 window.addEventListener("fullscreen", this);
79 window.addEventListener("MozDOMFullscreen:Entered", this);
80 window.addEventListener("MozDOMFullscreen:Exited", this);
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
95 this.updateOverflowStatus();
97 Services.obs.notifyObservers(
99 "appMenu-notifications-request",
103 this._initialized = true;
107 for (let [k, v] of Object.entries(this.kElements)) {
108 // Need to do fresh let-bindings per iteration
111 this.__defineGetter__(getKey, function () {
113 return (this[getKey] = document.getElementById(id));
118 _eventListenersAdded: false,
119 _ensureEventListenersAdded() {
120 if (this._eventListenersAdded) {
123 this._addEventListeners();
126 _addEventListeners() {
127 for (let event of this.kEvents) {
128 this.panel.addEventListener(event, this);
131 PanelMultiView.getViewNode(document, "PanelUI-helpView").addEventListener(
135 this._eventListenersAdded = true;
138 _removeEventListeners() {
139 for (let event of this.kEvents) {
140 this.panel.removeEventListener(event, this);
142 PanelMultiView.getViewNode(
145 ).removeEventListener("ViewShowing", this._onHelpViewShow);
146 this._eventListenersAdded = false;
150 this._removeEventListeners();
152 if (this._notificationPanel) {
153 for (let event of this.kEvents) {
154 this.notificationPanel.removeEventListener(event, this);
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);
172 * Opens the menu panel if it's closed, or closes it if it's
175 * @param aEvent the event that triggers the toggle.
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")) {
183 this._ensureEventListenersAdded();
184 if (this.panel.state == "open") {
186 } else if (this.panel.state == "closed") {
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.
196 * @param aEvent the event (if any) that triggers showing the menu.
199 this._ensureShortcutsShown();
201 await this.ensureReady();
204 this.panel.state == "open" ||
205 document.documentElement.hasAttribute("customizing")
211 if (aEvent && aEvent.type != "command") {
215 let anchor = this._getPanelAnchor(this.menuButton);
216 await PanelMultiView.openPopup(this.panel, anchor, {
217 triggerEvent: domEvent,
219 })().catch(console.error);
223 * If the menu panel is being shown, hide it.
226 if (document.documentElement.hasAttribute("customizing")) {
230 PanelMultiView.hidePopup(this.panel);
233 observe(subject, topic, status) {
235 case "fullscreen-nav-toolbox":
236 if (this._notifications) {
237 this.updateNotifications(false);
240 case "appMenu-notifications":
241 // Don't initialize twice.
242 if (status == "init" && this._notifications) {
245 this._notifications = AppMenuNotifications.notifications;
246 this.updateNotifications(true);
248 case "show-update-progress":
254 handleEvent(aEvent) {
255 // Ignore context menus and menu button menus showing and hiding:
256 if (aEvent.type.startsWith("popup") && aEvent.target != this.panel) {
259 switch (aEvent.type) {
261 updateEditUIVisibility();
264 if (aEvent.type == "popupshown") {
265 CustomizableUI.addPanelCloseListeners(this.panel);
269 if (aEvent.type == "popuphiding") {
270 updateEditUIVisibility();
274 this.updateNotifications();
275 this._updatePanelButton(aEvent.target);
276 if (aEvent.type == "popuphidden") {
277 CustomizableUI.removePanelCloseListeners(this.panel);
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.
284 aEvent.button == 0 &&
285 (AppConstants.platform != "macosx" || !aEvent.ctrlKey)
291 if (aEvent.key == " " || aEvent.key == "Enter") {
293 aEvent.stopPropagation();
296 case "MozDOMFullscreen:Entered":
297 case "MozDOMFullscreen:Exited":
300 this.updateNotifications();
306 return !!this._isReady;
309 get isNotificationPanelOpen() {
310 let panelState = this.notificationPanel.state;
312 return panelState == "showing" || panelState == "open";
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
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.
325 * @return a Promise that resolves once the panel is ready to roll.
327 async ensureReady() {
332 await window.delayedStartupPromise;
333 this._ensureEventListenersAdded();
334 this.panel.hidden = false;
335 this._isReady = true;
339 * Switch the panel to the help view if it's not already
342 showHelpView(aAnchor) {
343 this._ensureEventListenersAdded();
344 this.multiView.showSubView("PanelUI-helpView", aAnchor);
348 * Switch the panel to the "More Tools" view.
350 * @param moreTools The panel showing the "More Tools" view.
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");
364 * Shows a subview in the panel with a given ID.
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.
370 async showSubView(aViewId, aAnchor, 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.
375 aEvent.type == "mousedown" &&
376 (aEvent.button != 0 ||
377 (AppConstants.platform == "macosx" && aEvent.ctrlKey))
382 aEvent.type == "keypress" &&
384 aEvent.key != "Enter"
390 this._ensureEventListenersAdded();
392 let viewNode = PanelMultiView.getViewNode(document, aViewId);
394 console.error("Could not show panel subview with id: ", aViewId);
400 "Expected an anchor when opening subview with id: ",
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) {
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");
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);
426 if (aAnchor.getAttribute("locationspecific")) {
427 tempPanel.setAttribute("locationspecific", true);
429 if (this._disableAnimations) {
430 tempPanel.setAttribute("animate", "false");
432 tempPanel.setAttribute("context", "");
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) {
451 viewNode.classList.remove("cui-widget-panelview");
453 CustomizableUI.removePanelCloseListeners(tempPanel);
454 tempPanel.removeEventListener("popuphidden", panelRemover);
456 aAnchor.open = false;
458 PanelMultiView.removePopup(tempPanel);
461 if (aAnchor.parentNode.id == "PersonalToolbar") {
462 tempPanel.classList.add("bookmarks-toolbar");
465 let anchor = this._getPanelAnchor(aAnchor);
467 if (aAnchor != anchor && aAnchor.id) {
468 anchor.setAttribute("consumeanchor", aAnchor.id);
472 viewShown = await PanelMultiView.openPopup(tempPanel, anchor, {
473 position: "bottomright topright",
474 triggerEvent: aEvent,
481 CustomizableUI.addPanelCloseListeners(tempPanel);
482 tempPanel.addEventListener("popuphidden", panelRemover);
490 * Adds FTL before appending the panic view markup to the main DOM.
492 * @param {panelview} panelView The Panic View panelview.
494 ensurePanicViewInitialized(panelView) {
495 if (panelView.id != "PanelUI-panicView" || panelView._initialized) {
500 this.panic = panelView;
503 MozXULElement.insertFTLIfNeeded("browser/panicButton.ftl");
504 panelView._initialized = true;
508 * NB: The enable- and disableSingleSubviewPanelAnimations methods only
509 * affect the hiding/showing animations of single-subview panels (tempPanel
510 * in the showSubView method).
512 disableSingleSubviewPanelAnimations() {
513 this._disableAnimations = true;
516 enableSingleSubviewPanelAnimations() {
517 this._disableAnimations = false;
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");
532 onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
533 if (aContainer == this.overflowFixedList) {
534 this.updateOverflowStatus();
538 onAreaReset(aArea, aContainer) {
539 if (aContainer == this.overflowFixedList) {
540 this.updateOverflowStatus();
545 * Sets the anchor node into the open or closed state, depending
546 * on the state of the panel.
548 _updatePanelButton() {
549 let { state } = this.panel;
550 if (state == "open" || state == "showing") {
551 this.menuButton.open = true;
552 document.l10n.setAttributes(
554 "appmenu-menu-button-opened2"
557 this.menuButton.open = false;
558 document.l10n.setAttributes(
560 "appmenu-menu-button-closed2"
566 // Call global menu setup function
569 let helpMenu = document.getElementById("menu_HelpPopup");
570 let items = this.getElementsByTagName("vbox")[0];
581 // Remove all buttons from the view
582 while (items.firstChild) {
583 items.firstChild.remove();
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")
590 let fragment = document.createDocumentFragment();
591 for (let node of menuItems) {
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)) {
601 button.setAttribute(attrName, node.getAttribute(attrName));
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");
608 document.l10n.setAttributes(button, l10nId);
612 button.id = "appMenu_" + node.id;
615 button.classList.add("subviewbutton");
616 fragment.appendChild(button);
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"
624 if (helpPolicySupport) {
625 fragment.insertBefore(
627 fragment.querySelector("#appMenu_menu_HelpPopup_reportPhishingtoolmenu")
632 items.appendChild(fragment);
636 if (!this._notificationPanel) {
640 if (this.isNotificationPanelOpen) {
641 this.notificationPanel.hidePopup();
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.
651 async selectAndMarkItem(itemIds) {
652 // This shouldn't really occur, but return early just in case.
653 if (document.documentElement.hasAttribute("customizing")) {
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 });
666 if (this.panel.state != "open") {
667 await new Promise(resolve => {
668 this.panel.addEventListener("ViewShown", resolve, { once: true });
675 let viewShownCB = event => {
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);
690 let viewHidingCB = () => {
692 currentView.ignoreMouseMove = false;
697 let popupHiddenCB = () => {
699 this.panel.removeEventListener("ViewShown", viewShownCB);
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
714 currentView.ignoreMouseMove = true;
716 if (itemIds.length) {
717 this.panel.addEventListener("ViewShown", viewShownCB, { once: true });
719 this.panel.addEventListener("ViewHiding", viewHidingCB, { once: true });
722 this.panel.addEventListener("popuphidden", popupHiddenCB, { once: true });
723 markItem(this.mainView);
726 updateNotifications(notificationsChanged) {
727 let notifications = this._notifications;
728 if (!notifications || !notifications.length) {
729 if (notificationsChanged) {
730 this._clearAllNotifications();
737 (window.fullScreen && FullScreen.navToolboxHidden) ||
738 document.fullscreenElement ||
739 this._shouldSuppress()
745 let doorhangers = notifications.filter(
746 n => !n.dismissed && !n.options.badgeOnly
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
754 doorhangers.forEach(n => {
756 if (n.options.onDismissed) {
757 n.options.onDismissed(window);
761 if (!notifications[0].options.badgeOnly) {
762 this._showBannerItem(notifications[0]);
764 } else if (doorhangers.length) {
765 // Only show the doorhanger if the window is focused and not fullscreen
767 (window.fullScreen && this.autoHideToolbarInFullScreen) ||
768 Services.focus.activeWindow !== window
771 this._showBadge(doorhangers[0]);
772 this._showBannerItem(doorhangers[0]);
775 this._showNotificationPanel(doorhangers[0]);
779 this._showBadge(notifications[0]);
780 this._showBannerItem(notifications[0]);
784 _showNotificationPanel(notification) {
785 this._refreshNotificationPanel(notification);
787 if (this.isNotificationPanelOpen) {
791 if (notification.options.beforeShowDoorhanger) {
792 notification.options.beforeShowDoorhanger(document);
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
803 .getElementById("appMenu-notification-popup")
804 .querySelectorAll("[data-lazy-l10n-id]")
806 el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
807 el.removeAttribute("data-lazy-l10n-id");
810 this.notificationPanel.openPopup(anchor, "bottomright topright");
813 _clearNotificationPanel() {
814 for (let popupnotification of this.notificationPanel.children) {
815 popupnotification.hidden = true;
816 popupnotification.notification = null;
820 _clearAllNotifications() {
821 this._clearNotificationPanel();
823 this._clearBannerItem();
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"
834 for (let event of this.kEvents) {
835 this._notificationPanel.addEventListener(event, this);
838 return this._notificationPanel;
842 if (!this._mainView) {
843 this._mainView = PanelMultiView.getViewNode(
845 "appMenu-protonMainView"
848 return this._mainView;
851 get addonNotificationContainer() {
852 if (!this._addonNotificationContainer) {
853 this._addonNotificationContainer = PanelMultiView.getViewNode(
855 "appMenu-proton-addon-banners"
859 return this._addonNotificationContainer;
862 _formatDescriptionMessage(n) {
864 let array = n.options.message.split("<>");
865 text.start = array[0] || "";
866 text.name = n.options.name || "";
867 text.end = array[1] || "";
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(
880 "PanelUI._onNotificationButtonEvent(event, 'buttoncommand');"
882 popupnotification.setAttribute(
883 "secondarybuttoncommand",
884 "PanelUI._onNotificationButtonEvent(event, 'secondarybuttoncommand');"
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);
893 if (notification.options.onRefresh) {
894 notification.options.onRefresh(window);
896 if (notification.options.popupIconURL) {
897 popupnotification.setAttribute("icon", notification.options.popupIconURL);
898 popupnotification.setAttribute("hasicon", true);
900 if (notification.options.learnMoreURL) {
901 popupnotification.setAttribute(
903 notification.options.learnMoreURL
907 popupnotification.notification = notification;
908 popupnotification.show();
911 _showBadge(notification) {
912 let badgeStatus = this._getBadgeStatus(notification);
913 this.menuButton.setAttribute("badge-status", badgeStatus);
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",
923 "update-unsupported",
926 if (!supportedIds.includes(notification.id)) {
930 if (!this._panelBannerItem) {
931 this._panelBannerItem = this.mainView.querySelector(".panel-banner-item");
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;
943 this.menuButton.removeAttribute("badge-status");
947 if (this._panelBannerItem) {
948 this._panelBannerItem.notification = null;
949 this._panelBannerItem.hidden = true;
953 _onNotificationButtonEvent(event, type) {
954 let notificationEl = getNotificationFromElement(event.originalTarget);
956 if (!notificationEl) {
958 "PanelUI._onNotificationButtonEvent: couldn't find notification element"
962 if (!notificationEl.notification) {
964 "PanelUI._onNotificationButtonEvent: couldn't find notification"
968 let notification = notificationEl.notification;
970 if (type == "secondarybuttoncommand") {
971 AppMenuNotifications.callSecondaryAction(window, notification);
973 AppMenuNotifications.callMainAction(window, notification, true);
977 _onBannerItemSelected(event) {
978 let target = event.originalTarget;
979 if (!target.notification) {
981 "menucommand target has no associated action/notification"
985 event.stopPropagation();
986 AppMenuNotifications.callMainAction(window, target.notification, false);
989 _getPopupId(notification) {
990 return "appMenu-" + notification.id + "-notification";
993 _getBadgeStatus(notification) {
994 return notification.id;
997 _getPanelAnchor(candidate) {
998 let iconAnchor = candidate.badgeStack || candidate.icon;
999 return iconAnchor || candidate;
1002 _ensureShortcutsShown(view = this.mainView) {
1003 if (view.hasAttribute("added-shortcuts")) {
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);
1013 button.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(key));
1018 XPCOMUtils.defineConstant(this, "PanelUI", PanelUI);
1021 * Gets the currently selected locale for display.
1022 * @return the selected locale
1024 function getLocale() {
1025 return Services.locale.appLocaleAsBCP47;
1029 * Given a DOM node inside a <popupnotification>, return the parent <popupnotification>.
1031 function getNotificationFromElement(aElement) {
1032 return aElement.closest("popupnotification");