diff options
Diffstat (limited to 'browser/components/customizableui/content/panelUI.js')
-rw-r--r-- | browser/components/customizableui/content/panelUI.js | 1069 |
1 files changed, 1069 insertions, 0 deletions
diff --git a/browser/components/customizableui/content/panelUI.js b/browser/components/customizableui/content/panelUI.js new file mode 100644 index 0000000000..cf6461f9fb --- /dev/null +++ b/browser/components/customizableui/content/panelUI.js @@ -0,0 +1,1069 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +ChromeUtils.defineESModuleGetters(this, { + AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs", + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", +}); +ChromeUtils.defineModuleGetter( + this, + "PanelMultiView", + "resource:///modules/PanelMultiView.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "ToolbarPanelHub", + "resource://activity-stream/lib/ToolbarPanelHub.jsm" +); + +/** + * Maintains the state and dispatches events for the main menu panel. + */ + +const PanelUI = { + /** Panel events that we listen for. **/ + get kEvents() { + return ["popupshowing", "popupshown", "popuphiding", "popuphidden"]; + }, + /** + * Used for lazily getting and memoizing elements from the document. Lazy + * getters are set in init, and memoizing happens after the first retrieval. + */ + get kElements() { + return { + multiView: "appMenu-multiView", + menuButton: "PanelUI-menu-button", + panel: "appMenu-popup", + overflowFixedList: "widget-overflow-fixed-list", + overflowPanel: "widget-overflow", + navbar: "nav-bar", + }; + }, + + _initialized: false, + _notifications: null, + _notificationPanel: null, + + init(shouldSuppress) { + this._shouldSuppress = shouldSuppress; + this._initElements(); + + this.menuButton.addEventListener("mousedown", this); + this.menuButton.addEventListener("keypress", this); + + Services.obs.addObserver(this, "fullscreen-nav-toolbox"); + Services.obs.addObserver(this, "appMenu-notifications"); + Services.obs.addObserver(this, "show-update-progress"); + + XPCOMUtils.defineLazyPreferenceGetter( + this, + "autoHideToolbarInFullScreen", + "browser.fullscreen.autohide", + false, + (pref, previousValue, newValue) => { + // On OSX, or with autohide preffed off, MozDOMFullscreen is the only + // event we care about, since fullscreen should behave just like non + // fullscreen. Otherwise, we don't want to listen to these because + // we'd just be spamming ourselves with both of them whenever a user + // opened a video. + if (newValue) { + window.removeEventListener("MozDOMFullscreen:Entered", this); + window.removeEventListener("MozDOMFullscreen:Exited", this); + window.addEventListener("fullscreen", this); + } else { + window.addEventListener("MozDOMFullscreen:Entered", this); + window.addEventListener("MozDOMFullscreen:Exited", this); + window.removeEventListener("fullscreen", this); + } + + this.updateNotifications(false); + }, + autoHidePref => autoHidePref && Services.appinfo.OS !== "Darwin" + ); + + if (this.autoHideToolbarInFullScreen) { + window.addEventListener("fullscreen", this); + } else { + window.addEventListener("MozDOMFullscreen:Entered", this); + window.addEventListener("MozDOMFullscreen:Exited", this); + } + + window.addEventListener("activate", this); + CustomizableUI.addListener(this); + + // We do this sync on init because in order to have the overflow button show up + // we need to know whether anything is in the permanent panel area. + this.overflowFixedList.hidden = false; + // Also unhide the separator. We use CSS to hide/show it based on the panel's content. + this.overflowFixedList.previousElementSibling.hidden = false; + CustomizableUI.registerPanelNode( + this.overflowFixedList, + CustomizableUI.AREA_FIXED_OVERFLOW_PANEL + ); + this.updateOverflowStatus(); + + Services.obs.notifyObservers( + null, + "appMenu-notifications-request", + "refresh" + ); + + this._initialized = true; + }, + + _initElements() { + for (let [k, v] of Object.entries(this.kElements)) { + // Need to do fresh let-bindings per iteration + let getKey = k; + let id = v; + this.__defineGetter__(getKey, function() { + delete this[getKey]; + return (this[getKey] = document.getElementById(id)); + }); + } + }, + + _eventListenersAdded: false, + _ensureEventListenersAdded() { + if (this._eventListenersAdded) { + return; + } + this._addEventListeners(); + }, + + _addEventListeners() { + for (let event of this.kEvents) { + this.panel.addEventListener(event, this); + } + + PanelMultiView.getViewNode(document, "PanelUI-helpView").addEventListener( + "ViewShowing", + this._onHelpViewShow + ); + this._eventListenersAdded = true; + }, + + _removeEventListeners() { + for (let event of this.kEvents) { + this.panel.removeEventListener(event, this); + } + PanelMultiView.getViewNode( + document, + "PanelUI-helpView" + ).removeEventListener("ViewShowing", this._onHelpViewShow); + this._eventListenersAdded = false; + }, + + uninit() { + this._removeEventListeners(); + + if (this._notificationPanel) { + for (let event of this.kEvents) { + this.notificationPanel.removeEventListener(event, this); + } + } + + Services.obs.removeObserver(this, "fullscreen-nav-toolbox"); + Services.obs.removeObserver(this, "appMenu-notifications"); + Services.obs.removeObserver(this, "show-update-progress"); + + window.removeEventListener("MozDOMFullscreen:Entered", this); + window.removeEventListener("MozDOMFullscreen:Exited", this); + window.removeEventListener("fullscreen", this); + window.removeEventListener("activate", this); + this.menuButton.removeEventListener("mousedown", this); + this.menuButton.removeEventListener("keypress", this); + CustomizableUI.removeListener(this); + if (this.whatsNewPanel) { + this.whatsNewPanel.removeEventListener("ViewShowing", this); + } + }, + + /** + * Opens the menu panel if it's closed, or closes it if it's + * open. + * + * @param aEvent the event that triggers the toggle. + */ + toggle(aEvent) { + // Don't show the panel if the window is in customization mode, + // since this button doubles as an exit path for the user in this case. + if (document.documentElement.hasAttribute("customizing")) { + return; + } + this._ensureEventListenersAdded(); + if (this.panel.state == "open") { + this.hide(); + } else if (this.panel.state == "closed") { + this.show(aEvent); + } + }, + + /** + * Opens the menu panel. If the event target has a child with the + * toolbarbutton-icon attribute, the panel will be anchored on that child. + * Otherwise, the panel is anchored on the event target itself. + * + * @param aEvent the event (if any) that triggers showing the menu. + */ + show(aEvent) { + this._ensureShortcutsShown(); + (async () => { + await this.ensureReady(); + + if ( + this.panel.state == "open" || + document.documentElement.hasAttribute("customizing") + ) { + return; + } + + let domEvent = null; + if (aEvent && aEvent.type != "command") { + domEvent = aEvent; + } + + let anchor = this._getPanelAnchor(this.menuButton); + await PanelMultiView.openPopup(this.panel, anchor, { + triggerEvent: domEvent, + }); + })().catch(Cu.reportError); + }, + + /** + * If the menu panel is being shown, hide it. + */ + hide() { + if (document.documentElement.hasAttribute("customizing")) { + return; + } + + PanelMultiView.hidePopup(this.panel); + }, + + observe(subject, topic, status) { + switch (topic) { + case "fullscreen-nav-toolbox": + if (this._notifications) { + this.updateNotifications(false); + } + break; + case "appMenu-notifications": + // Don't initialize twice. + if (status == "init" && this._notifications) { + break; + } + this._notifications = AppMenuNotifications.notifications; + this.updateNotifications(true); + break; + case "show-update-progress": + openAboutDialog(); + break; + } + }, + + handleEvent(aEvent) { + // Ignore context menus and menu button menus showing and hiding: + if (aEvent.type.startsWith("popup") && aEvent.target != this.panel) { + return; + } + switch (aEvent.type) { + case "popupshowing": + updateEditUIVisibility(); + // Fall through + case "popupshown": + if (aEvent.type == "popupshown") { + CustomizableUI.addPanelCloseListeners(this.panel); + } + // Fall through + case "popuphiding": + if (aEvent.type == "popuphiding") { + updateEditUIVisibility(); + } + // Fall through + case "popuphidden": + this.updateNotifications(); + this._updatePanelButton(aEvent.target); + if (aEvent.type == "popuphidden") { + CustomizableUI.removePanelCloseListeners(this.panel); + } + break; + case "mousedown": + // On Mac, ctrl-click will send a context menu event from the widget, so + // we don't want to bring up the panel when ctrl key is pressed. + if ( + aEvent.button == 0 && + (AppConstants.platform != "macosx" || !aEvent.ctrlKey) + ) { + this.toggle(aEvent); + } + break; + case "keypress": + if (aEvent.key == " " || aEvent.key == "Enter") { + this.toggle(aEvent); + aEvent.stopPropagation(); + } + break; + case "MozDOMFullscreen:Entered": + case "MozDOMFullscreen:Exited": + case "fullscreen": + case "activate": + this.updateNotifications(); + break; + case "ViewShowing": + if (aEvent.target == this.whatsNewPanel) { + this.onWhatsNewPanelShowing(); + } + break; + } + }, + + get isReady() { + return !!this._isReady; + }, + + get isNotificationPanelOpen() { + let panelState = this.notificationPanel.state; + + return panelState == "showing" || panelState == "open"; + }, + + /** + * Registering the menu panel is done lazily for performance reasons. This + * method is exposed so that CustomizationMode can force panel-readyness in the + * event that customization mode is started before the panel has been opened + * by the user. + * + * @param aCustomizing (optional) set to true if this was called while entering + * customization mode. If that's the case, we trust that customization + * mode will handle calling beginBatchUpdate and endBatchUpdate. + * + * @return a Promise that resolves once the panel is ready to roll. + */ + async ensureReady() { + if (this._isReady) { + return; + } + + await window.delayedStartupPromise; + this._ensureEventListenersAdded(); + this.panel.hidden = false; + this._isReady = true; + }, + + /** + * Switch the panel to the help view if it's not already + * in that view. + */ + showHelpView(aAnchor) { + this._ensureEventListenersAdded(); + this.multiView.showSubView("PanelUI-helpView", aAnchor); + }, + + /** + * Switch the panel to the "More Tools" view. + * + * @param moreTools The panel showing the "More Tools" view. + */ + showMoreToolsPanel(moreTools) { + this.showSubView("appmenu-moreTools", moreTools); + + // Notify DevTools the panel view is showing and need it to populate the + // "Browser Tools" section of the panel. We notify the observer setup by + // DevTools because we want to ensure the same menuitem list is shared + // between both the AppMenu and toolbar button views. + let view = document.getElementById("appmenu-developer-tools-view"); + Services.obs.notifyObservers(view, "web-developer-tools-view-showing"); + }, + + /** + * Shows a subview in the panel with a given ID. + * + * @param aViewId the ID of the subview to show. + * @param aAnchor the element that spawned the subview. + * @param aEvent the event triggering the view showing. + */ + async showSubView(aViewId, aAnchor, aEvent) { + if (aEvent) { + // On Mac, ctrl-click will send a context menu event from the widget, so + // we don't want to bring up the panel when ctrl key is pressed. + if ( + aEvent.type == "mousedown" && + (aEvent.button != 0 || + (AppConstants.platform == "macosx" && aEvent.ctrlKey)) + ) { + return; + } + if ( + aEvent.type == "keypress" && + aEvent.key != " " && + aEvent.key != "Enter" + ) { + return; + } + } + + this._ensureEventListenersAdded(); + + let viewNode = PanelMultiView.getViewNode(document, aViewId); + if (!viewNode) { + Cu.reportError("Could not show panel subview with id: " + aViewId); + return; + } + + if (!aAnchor) { + Cu.reportError( + "Expected an anchor when opening subview with id: " + aViewId + ); + return; + } + + this.ensureWhatsNewInitialized(viewNode); + this.ensurePanicViewInitialized(viewNode); + + let container = aAnchor.closest("panelmultiview"); + if (container && !viewNode.hasAttribute("disallowSubView")) { + container.showSubView(aViewId, aAnchor); + } else if (!aAnchor.open) { + aAnchor.open = true; + + let tempPanel = document.createXULElement("panel"); + tempPanel.setAttribute("type", "arrow"); + tempPanel.setAttribute("id", "customizationui-widget-panel"); + if (viewNode.hasAttribute("neverhidden")) { + tempPanel.setAttribute("neverhidden", "true"); + } + + tempPanel.setAttribute("class", "cui-widget-panel panel-no-padding"); + tempPanel.setAttribute("viewId", aViewId); + if (aAnchor.getAttribute("tabspecific")) { + tempPanel.setAttribute("tabspecific", true); + } + if (aAnchor.getAttribute("locationspecific")) { + tempPanel.setAttribute("locationspecific", true); + } + if (this._disableAnimations) { + tempPanel.setAttribute("animate", "false"); + } + tempPanel.setAttribute("context", ""); + document + .getElementById(CustomizableUI.AREA_NAVBAR) + .appendChild(tempPanel); + + let multiView = document.createXULElement("panelmultiview"); + multiView.setAttribute("id", "customizationui-widget-multiview"); + multiView.setAttribute("viewCacheId", "appMenu-viewCache"); + multiView.setAttribute("mainViewId", viewNode.id); + multiView.appendChild(viewNode); + tempPanel.appendChild(multiView); + viewNode.classList.add("cui-widget-panelview", "PanelUI-subView"); + + let viewShown = false; + let panelRemover = () => { + viewNode.classList.remove("cui-widget-panelview"); + if (viewShown) { + CustomizableUI.removePanelCloseListeners(tempPanel); + tempPanel.removeEventListener("popuphidden", panelRemover); + } + aAnchor.open = false; + + PanelMultiView.removePopup(tempPanel); + }; + + if (aAnchor.parentNode.id == "PersonalToolbar") { + tempPanel.classList.add("bookmarks-toolbar"); + } + + let anchor = this._getPanelAnchor(aAnchor); + + if (aAnchor != anchor && aAnchor.id) { + anchor.setAttribute("consumeanchor", aAnchor.id); + } + + try { + viewShown = await PanelMultiView.openPopup(tempPanel, anchor, { + position: "bottomright topright", + triggerEvent: aEvent, + }); + } catch (ex) { + Cu.reportError(ex); + } + + if (viewShown) { + CustomizableUI.addPanelCloseListeners(tempPanel); + tempPanel.addEventListener("popuphidden", panelRemover); + } else { + panelRemover(); + } + } + }, + + /** + * Sets up the event listener for when the What's New panel is shown. + * + * @param {panelview} panelView The What's New panelview. + */ + ensureWhatsNewInitialized(panelView) { + if (panelView.id != "PanelUI-whatsNew" || panelView._initialized) { + return; + } + + if (!this.whatsNewPanel) { + this.whatsNewPanel = panelView; + } + + panelView._initialized = true; + panelView.addEventListener("ViewShowing", this); + }, + + /** + * Adds FTL before appending the panic view markup to the main DOM. + * + * @param {panelview} panelView The Panic View panelview. + */ + ensurePanicViewInitialized(panelView) { + if (panelView.id != "PanelUI-panicView" || panelView._initialized) { + return; + } + + if (!this.panic) { + this.panic = panelView; + } + + MozXULElement.insertFTLIfNeeded("browser/panicButton.ftl"); + panelView._initialized = true; + }, + + /** + * When the What's New panel is showing, we fetch the messages to show. + */ + onWhatsNewPanelShowing() { + ToolbarPanelHub.renderMessages( + window, + document, + "PanelUI-whatsNew-message-container" + ); + }, + + /** + * NB: The enable- and disableSingleSubviewPanelAnimations methods only + * affect the hiding/showing animations of single-subview panels (tempPanel + * in the showSubView method). + */ + disableSingleSubviewPanelAnimations() { + this._disableAnimations = true; + }, + + enableSingleSubviewPanelAnimations() { + this._disableAnimations = false; + }, + + updateOverflowStatus() { + let hasKids = this.overflowFixedList.hasChildNodes(); + if (hasKids && !this.navbar.hasAttribute("nonemptyoverflow")) { + this.navbar.setAttribute("nonemptyoverflow", "true"); + this.overflowPanel.setAttribute("hasfixeditems", "true"); + } else if (!hasKids && this.navbar.hasAttribute("nonemptyoverflow")) { + PanelMultiView.hidePopup(this.overflowPanel); + this.overflowPanel.removeAttribute("hasfixeditems"); + this.navbar.removeAttribute("nonemptyoverflow"); + } + }, + + onWidgetAfterDOMChange(aNode, aNextNode, aContainer, aWasRemoval) { + if (aContainer == this.overflowFixedList) { + this.updateOverflowStatus(); + } + }, + + onAreaReset(aArea, aContainer) { + if (aContainer == this.overflowFixedList) { + this.updateOverflowStatus(); + } + }, + + /** + * Sets the anchor node into the open or closed state, depending + * on the state of the panel. + */ + _updatePanelButton() { + let { state } = this.panel; + if (state == "open" || state == "showing") { + this.menuButton.open = true; + document.l10n.setAttributes( + this.menuButton, + "appmenu-menu-button-opened2" + ); + } else { + this.menuButton.open = false; + document.l10n.setAttributes( + this.menuButton, + "appmenu-menu-button-closed2" + ); + } + }, + + _onHelpViewShow(aEvent) { + // Call global menu setup function + buildHelpMenu(); + + let helpMenu = document.getElementById("menu_HelpPopup"); + let items = this.getElementsByTagName("vbox")[0]; + let attrs = [ + "command", + "oncommand", + "onclick", + "key", + "disabled", + "accesskey", + "label", + ]; + + // Remove all buttons from the view + while (items.firstChild) { + items.firstChild.remove(); + } + + // Add the current set of menuitems of the Help menu to this view + let menuItems = Array.prototype.slice.call( + helpMenu.getElementsByTagName("menuitem") + ); + let fragment = document.createDocumentFragment(); + for (let node of menuItems) { + if (node.hidden) { + continue; + } + let button = document.createXULElement("toolbarbutton"); + // Copy specific attributes from a menuitem of the Help menu + for (let attrName of attrs) { + if (!node.hasAttribute(attrName)) { + continue; + } + button.setAttribute(attrName, node.getAttribute(attrName)); + } + + // We have AppMenu-specific strings for the Help menu. By convention, + // their localization IDs are set on "appmenu-data-l10n-id" attributes. + let l10nId = node.getAttribute("appmenu-data-l10n-id"); + if (l10nId) { + button.setAttribute("data-l10n-id", l10nId); + } + + if (node.id) { + button.id = "appMenu_" + node.id; + } + + button.classList.add("subviewbutton"); + fragment.appendChild(button); + } + + // The Enterprise Support menu item has a different location than its + // placement in the menubar, so we need to specify it here. + let helpPolicySupport = fragment.querySelector( + "#appMenu_helpPolicySupport" + ); + if (helpPolicySupport) { + fragment.insertBefore( + helpPolicySupport, + fragment.querySelector("#appMenu_menu_HelpPopup_reportPhishingtoolmenu") + .nextSibling + ); + } + + items.appendChild(fragment); + }, + + _hidePopup() { + if (!this._notificationPanel) { + return; + } + + if (this.isNotificationPanelOpen) { + this.notificationPanel.hidePopup(); + } + }, + + /** + * Selects and marks an item by id from the main view. The ids are an array, + * the first in the main view and the later ids in subsequent subviews that + * become marked when the user opens the subview. The subview marking is + * cancelled if a different subview is opened. + */ + async selectAndMarkItem(itemIds) { + // This shouldn't really occur, but return early just in case. + if (document.documentElement.hasAttribute("customizing")) { + return; + } + + // This function was triggered from a button while the menu was + // already open, so the panel should be in the process of hiding. + // Wait for the panel to hide first, then reopen it. + if (this.panel.state == "hiding") { + await new Promise(resolve => { + this.panel.addEventListener("popuphidden", resolve, { once: true }); + }); + } + + if (this.panel.state != "open") { + await new Promise(resolve => { + this.panel.addEventListener("ViewShown", resolve, { once: true }); + this.show(); + }); + } + + let currentView; + + let viewShownCB = event => { + viewHidingCB(); + + if (itemIds.length) { + let subItem = window.document.getElementById(itemIds[0]); + if (event.target.id == subItem?.closest("panelview")?.id) { + Services.tm.dispatchToMainThread(() => { + markItem(event.target); + }); + } else { + itemIds = []; + } + } + }; + + let viewHidingCB = () => { + if (currentView) { + currentView.ignoreMouseMove = false; + } + currentView = null; + }; + + let popupHiddenCB = () => { + viewHidingCB(); + this.panel.removeEventListener("ViewShown", viewShownCB); + }; + + let markItem = viewNode => { + let id = itemIds.shift(); + let item = window.document.getElementById(id); + item.setAttribute("tabindex", "-1"); + + currentView = PanelView.forNode(viewNode); + currentView.selectedElement = item; + currentView.focusSelectedElement(true); + + // Prevent the mouse from changing the highlight temporarily. + // This flag gets removed when the view is hidden or a key + // is pressed. + currentView.ignoreMouseMove = true; + + if (itemIds.length) { + this.panel.addEventListener("ViewShown", viewShownCB, { once: true }); + } + this.panel.addEventListener("ViewHiding", viewHidingCB, { once: true }); + }; + + this.panel.addEventListener("popuphidden", popupHiddenCB, { once: true }); + markItem(this.mainView); + }, + + updateNotifications(notificationsChanged) { + let notifications = this._notifications; + if (!notifications || !notifications.length) { + if (notificationsChanged) { + this._clearAllNotifications(); + this._hidePopup(); + } + return; + } + + if ( + (window.fullScreen && FullScreen.navToolboxHidden) || + document.fullscreenElement || + this._shouldSuppress() + ) { + this._hidePopup(); + return; + } + + let doorhangers = notifications.filter( + n => !n.dismissed && !n.options.badgeOnly + ); + + if (this.panel.state == "showing" || this.panel.state == "open") { + // If the menu is already showing, then we need to dismiss all + // notifications since we don't want their doorhangers competing for + // attention. Don't hide the badge though; it isn't really in competition + // with anything. + doorhangers.forEach(n => { + n.dismissed = true; + if (n.options.onDismissed) { + n.options.onDismissed(window); + } + }); + this._hidePopup(); + if (!notifications[0].options.badgeOnly) { + this._showBannerItem(notifications[0]); + } + } else if (doorhangers.length) { + // Only show the doorhanger if the window is focused and not fullscreen + if ( + (window.fullScreen && this.autoHideToolbarInFullScreen) || + Services.focus.activeWindow !== window + ) { + this._hidePopup(); + this._showBadge(doorhangers[0]); + this._showBannerItem(doorhangers[0]); + } else { + this._clearBadge(); + this._showNotificationPanel(doorhangers[0]); + } + } else { + this._hidePopup(); + this._showBadge(notifications[0]); + this._showBannerItem(notifications[0]); + } + }, + + _showNotificationPanel(notification) { + this._refreshNotificationPanel(notification); + + if (this.isNotificationPanelOpen) { + return; + } + + if (notification.options.beforeShowDoorhanger) { + notification.options.beforeShowDoorhanger(document); + } + + let anchor = this._getPanelAnchor(this.menuButton); + + // Insert Fluent files when needed before notification is opened + MozXULElement.insertFTLIfNeeded("branding/brand.ftl"); + MozXULElement.insertFTLIfNeeded("browser/appMenuNotifications.ftl"); + + // After Fluent files are loaded into document replace data-lazy-l10n-ids with actual ones + document + .getElementById("appMenu-notification-popup") + .querySelectorAll("[data-lazy-l10n-id]") + .forEach(el => { + el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id")); + el.removeAttribute("data-lazy-l10n-id"); + }); + + this.notificationPanel.openPopup(anchor, "bottomright topright"); + }, + + _clearNotificationPanel() { + for (let popupnotification of this.notificationPanel.children) { + popupnotification.hidden = true; + popupnotification.notification = null; + } + }, + + _clearAllNotifications() { + this._clearNotificationPanel(); + this._clearBadge(); + this._clearBannerItem(); + }, + + get notificationPanel() { + // Lazy load the panic-button-success-notification panel the first time we need to display it. + if (!this._notificationPanel) { + let template = document.getElementById("appMenuNotificationTemplate"); + template.replaceWith(template.content); + this._notificationPanel = document.getElementById( + "appMenu-notification-popup" + ); + for (let event of this.kEvents) { + this._notificationPanel.addEventListener(event, this); + } + } + return this._notificationPanel; + }, + + get mainView() { + if (!this._mainView) { + this._mainView = PanelMultiView.getViewNode( + document, + "appMenu-protonMainView" + ); + } + return this._mainView; + }, + + get addonNotificationContainer() { + if (!this._addonNotificationContainer) { + this._addonNotificationContainer = PanelMultiView.getViewNode( + document, + "appMenu-proton-addon-banners" + ); + } + + return this._addonNotificationContainer; + }, + + _formatDescriptionMessage(n) { + let text = {}; + let array = n.options.message.split("<>"); + text.start = array[0] || ""; + text.name = n.options.name || ""; + text.end = array[1] || ""; + return text; + }, + + _refreshNotificationPanel(notification) { + this._clearNotificationPanel(); + + let popupnotificationID = this._getPopupId(notification); + let popupnotification = document.getElementById(popupnotificationID); + + popupnotification.setAttribute("id", popupnotificationID); + popupnotification.setAttribute( + "buttoncommand", + "PanelUI._onNotificationButtonEvent(event, 'buttoncommand');" + ); + popupnotification.setAttribute( + "secondarybuttoncommand", + "PanelUI._onNotificationButtonEvent(event, 'secondarybuttoncommand');" + ); + + if (notification.options.message) { + let desc = this._formatDescriptionMessage(notification); + popupnotification.setAttribute("label", desc.start); + popupnotification.setAttribute("name", desc.name); + popupnotification.setAttribute("endlabel", desc.end); + } + if (notification.options.onRefresh) { + notification.options.onRefresh(window); + } + if (notification.options.popupIconURL) { + popupnotification.setAttribute("icon", notification.options.popupIconURL); + popupnotification.setAttribute("hasicon", true); + } + + popupnotification.notification = notification; + popupnotification.show(); + }, + + _showBadge(notification) { + let badgeStatus = this._getBadgeStatus(notification); + this.menuButton.setAttribute("badge-status", badgeStatus); + }, + + // "Banner item" here refers to an item in the hamburger panel menu. They will + // typically show up as a colored row in the panel. + _showBannerItem(notification) { + const supportedIds = [ + "update-downloading", + "update-available", + "update-manual", + "update-unsupported", + "update-restart", + ]; + if (!supportedIds.includes(notification.id)) { + return; + } + + if (!this._panelBannerItem) { + this._panelBannerItem = this.mainView.querySelector(".panel-banner-item"); + } + + let l10nId = "appmenuitem-banner-" + notification.id; + document.l10n.setAttributes(this._panelBannerItem, l10nId); + + this._panelBannerItem.setAttribute("notificationid", notification.id); + this._panelBannerItem.hidden = false; + this._panelBannerItem.notification = notification; + }, + + _clearBadge() { + this.menuButton.removeAttribute("badge-status"); + }, + + _clearBannerItem() { + if (this._panelBannerItem) { + this._panelBannerItem.notification = null; + this._panelBannerItem.hidden = true; + } + }, + + _onNotificationButtonEvent(event, type) { + let notificationEl = getNotificationFromElement(event.originalTarget); + + if (!notificationEl) { + throw new Error( + "PanelUI._onNotificationButtonEvent: couldn't find notification element" + ); + } + + if (!notificationEl.notification) { + throw new Error( + "PanelUI._onNotificationButtonEvent: couldn't find notification" + ); + } + + let notification = notificationEl.notification; + + if (type == "secondarybuttoncommand") { + AppMenuNotifications.callSecondaryAction(window, notification); + } else { + AppMenuNotifications.callMainAction(window, notification, true); + } + }, + + _onBannerItemSelected(event) { + let target = event.originalTarget; + if (!target.notification) { + throw new Error( + "menucommand target has no associated action/notification" + ); + } + + event.stopPropagation(); + AppMenuNotifications.callMainAction(window, target.notification, false); + }, + + _getPopupId(notification) { + return "appMenu-" + notification.id + "-notification"; + }, + + _getBadgeStatus(notification) { + return notification.id; + }, + + _getPanelAnchor(candidate) { + let iconAnchor = candidate.badgeStack || candidate.icon; + return iconAnchor || candidate; + }, + + _ensureShortcutsShown(view = this.mainView) { + if (view.hasAttribute("added-shortcuts")) { + return; + } + view.setAttribute("added-shortcuts", "true"); + for (let button of view.querySelectorAll("toolbarbutton[key]")) { + let keyId = button.getAttribute("key"); + let key = document.getElementById(keyId); + if (!key) { + continue; + } + button.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(key)); + } + }, +}; + +XPCOMUtils.defineConstant(this, "PanelUI", PanelUI); + +/** + * Gets the currently selected locale for display. + * @return the selected locale + */ +function getLocale() { + return Services.locale.appLocaleAsBCP47; +} + +/** + * Given a DOM node inside a <popupnotification>, return the parent <popupnotification>. + */ +function getNotificationFromElement(aElement) { + return aElement.closest("popupnotification"); +} |