I ended up having to change some of the fundamentals of the popupnotification element because the code relied on being able to inherit inline event handler attributes such as buttoncommand as oncommand on the button element and so on. The primary user of the popupnotification element seems to be `PopupNotifications`. Following a pre-existing coupling with `PopupNotifications._onCheckboxCommand`, by default all commands/actions will simply call the corresponding `PopupNotifications` handler. However, I found at least one instance in `panelUI.js` that tries to override the command handlers. I solved this by dispatching a custom cancelable event on the popupnotification element. Differential Revision: https://phabricator.services.mozilla.com/D231927
1181 lines
36 KiB
JavaScript
1181 lines
36 KiB
JavaScript
/* 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",
|
|
ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
|
|
MenuMessage: "resource:///modules/asrouter/MenuMessage.sys.mjs",
|
|
NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
|
|
PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
|
|
});
|
|
|
|
/**
|
|
* 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"];
|
|
},
|
|
|
|
/// Notification events used for overwriting notification actions
|
|
get kNotificationEvents() {
|
|
return ["buttoncommand", "secondarybuttoncommand", "learnmoreclick"];
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
let helpView = PanelMultiView.getViewNode(document, "PanelUI-helpView");
|
|
helpView.addEventListener("ViewShowing", this._onHelpViewShow);
|
|
helpView.addEventListener("command", this._onHelpCommand);
|
|
this._onLibraryCommand = this._onLibraryCommand.bind(this);
|
|
PanelMultiView.getViewNode(
|
|
document,
|
|
"appMenu-libraryView"
|
|
).addEventListener("command", this._onLibraryCommand);
|
|
this.mainView.addEventListener("command", this);
|
|
this.mainView.addEventListener("ViewShowing", this._onMainViewShow);
|
|
this._eventListenersAdded = true;
|
|
},
|
|
|
|
_removeEventListeners() {
|
|
for (let event of this.kEvents) {
|
|
this.panel.removeEventListener(event, this);
|
|
}
|
|
let helpView = PanelMultiView.getViewNode(document, "PanelUI-helpView");
|
|
helpView.removeEventListener("ViewShowing", this._onHelpViewShow);
|
|
helpView.removeEventListener("command", this._onHelpCommand);
|
|
PanelMultiView.getViewNode(
|
|
document,
|
|
"appMenu-libraryView"
|
|
).removeEventListener("command", this._onLibraryCommand);
|
|
this.mainView.removeEventListener("command", this);
|
|
this._eventListenersAdded = false;
|
|
},
|
|
|
|
uninit() {
|
|
this._removeEventListeners();
|
|
|
|
if (this._notificationPanel) {
|
|
for (let event of this.kEvents) {
|
|
this.notificationPanel.removeEventListener(event, this);
|
|
}
|
|
for (let event of this.kNotificationEvents) {
|
|
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);
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
if (ASRouter.initialized) {
|
|
await ASRouter.sendTriggerMessage({
|
|
browser: gBrowser.selectedBrowser,
|
|
id: "menuOpened",
|
|
context: { source: MenuMessage.SOURCES.APP_MENU },
|
|
});
|
|
}
|
|
|
|
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(console.error);
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
MenuMessage.hideAppMenuMessage(gBrowser.selectedBrowser);
|
|
}
|
|
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 "command":
|
|
this.onCommand(aEvent);
|
|
break;
|
|
case "buttoncommand":
|
|
this._onNotificationButtonEvent(aEvent, "buttoncommand");
|
|
break;
|
|
case "secondarybuttoncommand":
|
|
this._onNotificationButtonEvent(aEvent, "secondarybuttoncommand");
|
|
break;
|
|
case "learnmoreclick":
|
|
// Don't fall back to PopupNotifications.
|
|
aEvent.preventDefault();
|
|
break;
|
|
}
|
|
},
|
|
|
|
// Note that we listen for bubbling command events. In the case where the
|
|
// button that the user clicks has a command attribute, those events are
|
|
// redirected to the relevant command element, and we never see them in
|
|
// here. Bear this in mind if you want to write code that applies to
|
|
// all commands, for which this wouldn't work well.
|
|
onCommand(aEvent) {
|
|
let { target } = aEvent;
|
|
switch (target.id) {
|
|
case "appMenu-update-banner":
|
|
this._onBannerItemSelected(aEvent);
|
|
break;
|
|
case "appMenu-fxa-label2":
|
|
gSync.toggleAccountPanel(target, aEvent);
|
|
break;
|
|
case "appMenu-bookmarks-button":
|
|
BookmarkingUI.showSubView(target);
|
|
break;
|
|
case "appMenu-history-button":
|
|
this.showSubView("PanelUI-history", target);
|
|
break;
|
|
case "appMenu-passwords-button":
|
|
LoginHelper.openPasswordManager(window, { entryPoint: "Mainmenu" });
|
|
break;
|
|
case "appMenu-fullscreen-button2":
|
|
// Note that we're custom-handling the hiding of the panel to make
|
|
// sure it disappears before entering fullscreen. Otherwise it can
|
|
// end up moving around on the screen during the fullscreen transition.
|
|
target.closest("panel").hidePopup();
|
|
setTimeout(() => BrowserCommands.fullScreen(), 0);
|
|
break;
|
|
case "appMenu-settings-button":
|
|
openPreferences();
|
|
break;
|
|
case "appMenu-more-button2":
|
|
this.showMoreToolsPanel(target);
|
|
break;
|
|
case "appMenu-help-button2":
|
|
this.showSubView("PanelUI-helpView", target);
|
|
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) {
|
|
console.error("Could not show panel subview with id: ", aViewId);
|
|
return;
|
|
}
|
|
|
|
if (!aAnchor) {
|
|
console.error(
|
|
"Expected an anchor when opening subview with id: ",
|
|
aViewId
|
|
);
|
|
return;
|
|
}
|
|
|
|
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 = event => {
|
|
// Avoid bubbled events triggering the panel closing.
|
|
if (event && event.target != tempPanel) {
|
|
return;
|
|
}
|
|
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) {
|
|
console.error(ex);
|
|
}
|
|
|
|
if (viewShown) {
|
|
CustomizableUI.addPanelCloseListeners(tempPanel);
|
|
tempPanel.addEventListener("popuphidden", panelRemover);
|
|
} else {
|
|
panelRemover();
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
},
|
|
|
|
/**
|
|
* 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) {
|
|
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"
|
|
);
|
|
}
|
|
},
|
|
|
|
_onMainViewShow(event) {
|
|
let panelview = event.target;
|
|
let messageId = panelview.getAttribute(
|
|
MenuMessage.SHOWING_FXA_MENU_MESSAGE_ATTR
|
|
);
|
|
if (messageId) {
|
|
MenuMessage.recordMenuMessageTelemetry(
|
|
"IMPRESSION",
|
|
MenuMessage.SOURCES.APP_MENU,
|
|
messageId
|
|
);
|
|
let message = ASRouter.getMessageById(messageId);
|
|
ASRouter.addImpression(message);
|
|
}
|
|
},
|
|
|
|
_onHelpViewShow() {
|
|
// Call global menu setup function
|
|
buildHelpMenu();
|
|
|
|
let helpMenu = document.getElementById("menu_HelpPopup");
|
|
let items = this.getElementsByTagName("vbox")[0];
|
|
let attrs = ["command", "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) {
|
|
document.l10n.setAttributes(button, 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);
|
|
},
|
|
|
|
_onHelpCommand(aEvent) {
|
|
switch (aEvent.target.id) {
|
|
case "appMenu_menu_openHelp":
|
|
openHelpLink("firefox-help");
|
|
break;
|
|
case "appMenu_menu_layout_debugger":
|
|
toOpenWindowByType(
|
|
"mozapp:layoutdebug",
|
|
"chrome://layoutdebug/content/layoutdebug.xhtml"
|
|
);
|
|
break;
|
|
case "appMenu_feedbackPage":
|
|
openFeedbackPage();
|
|
break;
|
|
case "appMenu_helpSafeMode":
|
|
safeModeRestart();
|
|
break;
|
|
case "appMenu_troubleShooting":
|
|
openTroubleshootingPage();
|
|
break;
|
|
case "appMenu_help_reportSiteIssue":
|
|
ReportSiteIssue();
|
|
break;
|
|
case "appMenu_menu_HelpPopup_reportPhishingtoolmenu":
|
|
openUILink(gSafeBrowsing.getReportURL("Phish"), aEvent, {
|
|
triggeringPrincipal:
|
|
Services.scriptSecurityManager.createNullPrincipal({}),
|
|
});
|
|
break;
|
|
case "appMenu_menu_HelpPopup_reportPhishingErrortoolmenu":
|
|
ReportFalseDeceptiveSite();
|
|
break;
|
|
case "appMenu_helpSwitchDevice":
|
|
openSwitchingDevicesPage();
|
|
break;
|
|
case "appMenu_aboutName":
|
|
openAboutDialog();
|
|
break;
|
|
case "appMenu_helpPolicySupport":
|
|
openTrustedLinkIn(Services.policies.getSupportMenu().URL.href, "tab");
|
|
break;
|
|
}
|
|
},
|
|
|
|
_onLibraryCommand(aEvent) {
|
|
let button = aEvent.target;
|
|
let { BookmarkingUI, DownloadsPanel } = button.ownerGlobal;
|
|
switch (button.id) {
|
|
case "appMenu-library-bookmarks-button":
|
|
BookmarkingUI.showSubView(button);
|
|
break;
|
|
case "appMenu-library-history-button":
|
|
this.showSubView("PanelUI-history", button);
|
|
break;
|
|
case "appMenu-library-downloads-button":
|
|
DownloadsPanel.showDownloadsHistory();
|
|
break;
|
|
}
|
|
},
|
|
|
|
_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);
|
|
}
|
|
for (let event of this.kNotificationEvents) {
|
|
this._notificationPanel.addEventListener(event, this);
|
|
}
|
|
}
|
|
return this._notificationPanel;
|
|
},
|
|
|
|
get mainView() {
|
|
if (!this._mainView) {
|
|
this._mainView = PanelMultiView.getViewNode(document, "appMenu-mainView");
|
|
}
|
|
return this._mainView;
|
|
},
|
|
|
|
get addonNotificationContainer() {
|
|
if (!this._addonNotificationContainer) {
|
|
this._addonNotificationContainer = PanelMultiView.getViewNode(
|
|
document,
|
|
"appMenu-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);
|
|
|
|
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);
|
|
}
|
|
if (notification.options.learnMoreURL) {
|
|
popupnotification.setAttribute(
|
|
"learnmoreurl",
|
|
notification.options.learnMoreURL
|
|
);
|
|
}
|
|
|
|
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) {
|
|
event.preventDefault();
|
|
|
|
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");
|
|
}
|