summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/base/content/folder-menupopup.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/base/content/folder-menupopup.js')
-rw-r--r--comm/mailnews/base/content/folder-menupopup.js1238
1 files changed, 1238 insertions, 0 deletions
diff --git a/comm/mailnews/base/content/folder-menupopup.js b/comm/mailnews/base/content/folder-menupopup.js
new file mode 100644
index 0000000000..61a912a4fd
--- /dev/null
+++ b/comm/mailnews/base/content/folder-menupopup.js
@@ -0,0 +1,1238 @@
+/* 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/. */
+
+"use strict";
+
+/* globals MozElements MozXULElement PanelUI */
+
+// This file implements `folder-menupopup` custom elements used in traditional
+// menus.
+
+// Wrap in a block to prevent leaking to window scope.
+{
+ const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+ );
+
+ const LazyModules = {};
+
+ ChromeUtils.defineModuleGetter(
+ LazyModules,
+ "FeedUtils",
+ "resource:///modules/FeedUtils.jsm"
+ );
+ ChromeUtils.defineModuleGetter(
+ LazyModules,
+ "FolderUtils",
+ "resource:///modules/FolderUtils.jsm"
+ );
+ ChromeUtils.defineModuleGetter(
+ LazyModules,
+ "MailUtils",
+ "resource:///modules/MailUtils.jsm"
+ );
+
+ /**
+ * Creates an element, sets attributes on it, including always setting the
+ * "generated" attribute to "true", and returns the element. The "generated"
+ * attribute is used to determine which elements to remove when clearing
+ * the menu.
+ *
+ * @param {string} tagName - The tag name of the element to generate.
+ * @param {object} [attributes] - Optional attributes to set on the element.
+ * @param {object} [isObject] - The optional "is" object to use when creating
+ * the element, typically `{is: "folder-menupopup"}`.
+ */
+ function generateElement(tagName, attributes, isObject) {
+ const element = document.createXULElement(tagName, isObject);
+ element.setAttribute("generated", "true");
+
+ if (attributes) {
+ Object.entries(attributes).forEach(([key, value]) => {
+ element.setAttribute(key, value);
+ });
+ }
+ return element;
+ }
+
+ /**
+ * A function to add shared code to the classes for the `folder-menupopup`
+ * custom element. Takes a "Base" class, and returns a class that extends
+ * the "Base" class.
+ *
+ * @param {Class} Base - A class to be extended with shared functionality.
+ * @returns {Class} A class that extends the first class.
+ */
+ let FolderMenu = Base =>
+ class extends Base {
+ constructor() {
+ super();
+
+ window.addEventListener(
+ "unload",
+ () => {
+ // Clean up when being destroyed.
+ this._removeListener();
+ this._teardown();
+ },
+ { once: true }
+ );
+
+ // If non-null, the subFolders of this nsIMsgFolder will be used to
+ // populate this menu. If this is null, the menu will be populated
+ // using the root-folders for all accounts.
+ this._parentFolder = null;
+
+ this._stringBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/folderWidgets.properties"
+ );
+
+ // Various filtering modes can be used with this menu-binding. To use
+ // one of them, append the mode="foo" attribute to the element. When
+ // building the menu, we will then use this._filters[mode] as a filter
+ // function to eliminate folders that should not be shown.
+ // note: extensions should feel free to plug in here.
+ this._filters = {
+ // Returns true if messages can be filed in the folder.
+ filing(folder) {
+ if (!folder.server.canFileMessagesOnServer) {
+ return false;
+ }
+
+ return folder.canFileMessages || folder.hasSubFolders;
+ },
+
+ // Returns true if we can get mail for this folder. (usually this just
+ // means the "root" fake folder).
+ getMail(folder) {
+ if (folder.isServer && folder.server.type != "none") {
+ return true;
+ }
+ if (folder.server.type == "nntp" || folder.server.type == "rss") {
+ return true;
+ }
+ return false;
+ },
+
+ // Returns true if we can add filters to this folder/account.
+ filters(folder) {
+ // We can always filter news.
+ if (folder.server.type == "nntp") {
+ return true;
+ }
+
+ return folder.server.canHaveFilters;
+ },
+
+ subscribe(folder) {
+ return folder.canSubscribe;
+ },
+
+ newFolder(folder) {
+ return (
+ folder.canCreateSubfolders &&
+ folder.server.canCreateFoldersOnServer
+ );
+ },
+
+ deferred(folder) {
+ return (
+ folder.server.canCreateFoldersOnServer && !folder.supportsOffline
+ );
+ },
+
+ // Folders that are not in a deferred account.
+ notDeferred(folder) {
+ let server = folder.server;
+ return !(
+ server instanceof Ci.nsIPop3IncomingServer &&
+ server.deferredToAccount
+ );
+ },
+
+ // Folders that can be searched.
+ search(folder) {
+ if (
+ !folder.server.canSearchMessages ||
+ folder.getFlag(Ci.nsMsgFolderFlags.Virtual)
+ ) {
+ return false;
+ }
+ return true;
+ },
+
+ // Folders that can subscribe feeds.
+ feeds(folder) {
+ if (
+ folder.server.type != "rss" ||
+ folder.getFlag(Ci.nsMsgFolderFlags.Trash) ||
+ folder.getFlag(Ci.nsMsgFolderFlags.Virtual)
+ ) {
+ return false;
+ }
+ return true;
+ },
+
+ junk(folder) {
+ // Don't show servers (nntp & any others) which do not allow search or filing
+ // I don't really understand why canSearchMessages is needed, but it was included in
+ // earlier code, so I include it as well.
+ if (
+ !folder.server.canFileMessagesOnServer ||
+ !folder.server.canSearchMessages
+ ) {
+ return false;
+ }
+ // Show parents that might have usable subfolders, or usable folders.
+ return folder.hasSubFolders || folder.canFileMessages;
+ },
+ };
+
+ // Is this list containing only servers (accounts) and no real folders?
+ this._serversOnly = true;
+
+ /**
+ * Our listener to let us know when folders change/appear/disappear so
+ * we can know to rebuild ourselves.
+ *
+ * @implements {nsIFolderListener}
+ */
+ this._listener = {
+ _menu: this,
+ _clearMenu(menu) {
+ // I'm not quite sure why this isn't always a function (bug 514445).
+ if (menu._teardown) {
+ menu._teardown();
+ }
+ },
+
+ _setCssSelectorsForItem(item) {
+ const child = this._getChildForItem(item);
+ if (child) {
+ this._menu._setCssSelectors(child._folder, child);
+ }
+ },
+
+ _folderAddedOrRemoved(folder) {
+ if (this._filterFunction && !this._filterFunction(folder)) {
+ return;
+ }
+ // xxx we can optimize this later
+ this._clearMenu(this._menu);
+ },
+
+ onFolderAdded(parentFolder, child) {
+ this._folderAddedOrRemoved(child);
+ },
+ onMessageAdded(parentFolder, msg) {},
+ onFolderRemoved(parentFolder, child) {
+ this._folderAddedOrRemoved(child);
+ },
+ onMessageRemoved(parentFolder, msg) {},
+
+ // xxx I stole this listener list from nsMsgFolderDatasource.cpp, but
+ // someone should really document what events are fired when, so that
+ // we make sure we're updating at the right times.
+ onFolderPropertyChanged(item, property, old, newItem) {},
+ onFolderIntPropertyChanged(item, property, old, aNew) {
+ if (item instanceof Ci.nsIMsgFolder) {
+ if (property == "FolderFlag") {
+ if (
+ this._menu.getAttribute("showFavorites") != "true" ||
+ !this._menu._initializedSpecials.has("favorites")
+ ) {
+ return;
+ }
+
+ if (
+ (old & Ci.nsMsgFolderFlags.Favorite) !=
+ (aNew & Ci.nsMsgFolderFlags.Favorite)
+ ) {
+ setTimeout(this._clearMenu, 0, this._menu);
+ }
+ }
+ }
+ this._setCssSelectorsForItem(item);
+ },
+ onFolderBoolPropertyChanged(item, property, old, newItem) {
+ this._setCssSelectorsForItem(item);
+ },
+ onFolderUnicharPropertyChanged(item, property, old, newItem) {
+ this._setCssSelectorsForItem(item);
+ },
+ onFolderPropertyFlagChanged(item, property, old, newItem) {},
+
+ onFolderEvent(folder, eventName) {
+ if (eventName == "MRMTimeChanged") {
+ if (
+ this._menu.getAttribute("showRecent") != "true" ||
+ !this._menu._initializedSpecials.has("recent") ||
+ !this._menu.childWrapper.firstElementChild
+ ) {
+ return;
+ }
+
+ const recentMenuItem = this._menu.childWrapper.firstElementChild;
+ const recentSubMenu =
+ this._menu._getSubMenuForMenuItem(recentMenuItem);
+
+ // If this folder is already in the recent menu, return.
+ if (
+ !recentSubMenu ||
+ this._getChildForItem(folder, recentSubMenu)
+ ) {
+ return;
+ }
+ } else if (eventName == "RenameCompleted") {
+ // Special casing folder renames here, since they require more work
+ // since sort-order may have changed.
+ if (!this._getChildForItem(folder)) {
+ return;
+ }
+ } else {
+ return;
+ }
+ // Folder renamed, or new recent folder, so rebuild.
+ setTimeout(this._clearMenu, 0, this._menu);
+ },
+
+ /**
+ * Helper function to check and see whether we have a menuitem for this
+ * particular nsIMsgFolder.
+ *
+ * @param {nsIMsgFolder} item - The folder to check.
+ * @param {Element} [menu] - Optional menu to look in, defaults to this._menu.
+ * @returns {Element|null} The menuitem for that folder, or null if no
+ * child for that folder exists.
+ */
+ _getChildForItem(item, menu = this._menu) {
+ if (
+ !menu ||
+ !menu.childWrapper.hasChildNodes() ||
+ !(item instanceof Ci.nsIMsgFolder)
+ ) {
+ return null;
+ }
+ for (let child of menu.childWrapper.children) {
+ if (child._folder && child._folder.URI == item.URI) {
+ return child;
+ }
+ }
+ return null;
+ },
+ };
+
+ // True if we have already built our menu items and are now just
+ // listening for changes.
+ this._initialized = false;
+
+ // A Set listing which of our special menus are already built.
+ // E.g. "recent", "favorites".
+ this._initializedSpecials = new Set();
+
+ // The format for displaying names of folders.
+ this._displayformat = null;
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+ // Call the connectedCallback of the "base" class this mixin class is extending.
+ super.connectedCallback();
+
+ // Get the displayformat if set.
+ if (this.parentNode && this.parentNode.localName == "menulist") {
+ this._displayformat = this.parentNode.getAttribute("displayformat");
+ }
+ }
+
+ set parentFolder(val) {
+ this._parentFolder = val;
+ this._teardown();
+ }
+
+ get parentFolder() {
+ return this._parentFolder;
+ }
+
+ /**
+ * Make sure we remove our listener when the window is being destroyed
+ * or the widget torn down.
+ */
+ _removeListener() {
+ if (!this._initialized) {
+ return;
+ }
+ MailServices.mailSession.RemoveFolderListener(this._listener);
+ }
+
+ /**
+ * Call this if you do not know whether the menu items have been built,
+ * but know that they need to be built now if they haven't been yet.
+ */
+ _ensureInitialized() {
+ if (this._initialized) {
+ return;
+ }
+
+ // The excludeServers attribute is a comma separated list of server keys.
+ const excludeServers = this.hasAttribute("excludeServers")
+ ? this.getAttribute("excludeServers").split(",")
+ : [];
+
+ // Extensions and other consumers can add to these modes too, see the
+ // note on the _filters field. (Note: empty strings ("") are falsy in JS.)
+ const mode = this.getAttribute("mode");
+
+ const filterFunction = mode ? this._filters[mode] : folder => true;
+
+ const folders = this._getFolders(
+ this._parentFolder,
+ excludeServers,
+ mode ? filterFunction : null
+ );
+
+ this._listener._filterFunction = filterFunction;
+
+ this._build(folders, mode);
+
+ // Lastly, we add a listener to get notified of changes in the folder
+ // structure.
+ MailServices.mailSession.AddFolderListener(
+ this._listener,
+ Ci.nsIFolderListener.all
+ );
+
+ this._initialized = true;
+ }
+
+ /**
+ * Get the folders that will appear in the menu.
+ *
+ * @param {Element} parentFolder - The parent menu popup/view element.
+ * @param {string[]} excludeServers - Server keys for the servers to exclude.
+ * @param {Function} [filterFunction] - Function for filtering the folders.
+ */
+ _getFolders(parentFolder, excludeServers, filterFunction) {
+ let folders;
+
+ // If we have a parent folder, just get the subFolders for that parent.
+ if (parentFolder) {
+ folders = parentFolder.subFolders;
+ } else {
+ // If we don't have a parent, then we assume we should build the
+ // top-level accounts. (Actually we build the fake root folders for
+ // those accounts.)
+ let accounts = LazyModules.FolderUtils.allAccountsSorted(true);
+
+ // Now generate our folder list. Note that we'll special case this
+ // situation elsewhere, to avoid destroying the sort order we just made.
+ folders = accounts.map(acct => acct.incomingServer.rootFolder);
+ }
+
+ if (filterFunction) {
+ folders = folders.filter(filterFunction);
+ }
+
+ if (excludeServers.length > 0) {
+ folders = folders.filter(
+ folder => !excludeServers.includes(folder.server.key)
+ );
+ }
+ return folders;
+ }
+
+ /**
+ * Actually constructs the menu items based on the folders given.
+ *
+ * @param {nsIMsgFolder[]} folders - An array of nsIMsgFolders to use for building.
+ * @param {string} [mode] - The filtering mode. See comment on _filters field.
+ */
+ _build(folders, mode) {
+ let globalInboxFolder = null;
+
+ // See if this is the toplevel menu (usually with accounts).
+ if (!this._parentFolder) {
+ this._addTopLevelMenuItems();
+
+ // If we are showing the accounts for deferring, move Local Folders to the top.
+ if (mode == "deferred") {
+ globalInboxFolder =
+ MailServices.accounts.localFoldersServer.rootFolder;
+ let localFoldersIndex = folders.indexOf(globalInboxFolder);
+ if (localFoldersIndex != -1) {
+ folders.splice(localFoldersIndex, 1);
+ folders.unshift(globalInboxFolder);
+ }
+ }
+ // If we're the root of the folder hierarchy, then we actually don't
+ // want to sort the folders, but rather the accounts to which the
+ // folders belong. Since that sorting was already done, we don't need
+ // to do anything for that case here.
+ } else {
+ this._maybeAddParentFolderMenuItem(mode);
+
+ // Sort the list of folders. We give first priority to the sortKey
+ // property if it is available, otherwise a case-insensitive
+ // comparison of names.
+ folders = folders.sort((a, b) => a.compareSortKeys(b));
+ }
+
+ this._addFoldersMenuItems(folders, mode, globalInboxFolder);
+ if (!this._parentFolder) {
+ this._addTopLevelBottomMenuItems();
+ }
+ }
+
+ /**
+ * Add menu items that only appear at top level, like "Recent".
+ */
+ _addTopLevelMenuItems() {
+ const showRecent = this.getAttribute("showRecent") == "true";
+ const showFavorites = this.getAttribute("showFavorites") == "true";
+
+ if (showRecent) {
+ this.childWrapper.appendChild(
+ this._buildSpecialMenu({
+ special: "recent",
+ label: this.getAttribute("recentLabel"),
+ accessKey: this.getAttribute("recentAccessKey"),
+ })
+ );
+ }
+ if (showFavorites) {
+ this.childWrapper.appendChild(
+ this._buildSpecialMenu({
+ special: "favorites",
+ label: this.getAttribute("favoritesLabel"),
+ accessKey: this.getAttribute("favoritesAccessKey"),
+ })
+ );
+ }
+ if (showRecent || showFavorites) {
+ this.childWrapper.appendChild(this._buildSeparator());
+ }
+ }
+
+ /**
+ * Add menu items that only appear at top level (but last), like "<last>".
+ */
+ _addTopLevelBottomMenuItems() {
+ if (this.getAttribute("showLast") != "true") {
+ return;
+ }
+ const folderURI = Services.prefs.getStringPref(
+ "mail.last_msg_movecopy_target_uri"
+ );
+ const folder =
+ folderURI && LazyModules.MailUtils.getExistingFolder(folderURI);
+ if (!folder) {
+ return;
+ }
+
+ this.childWrapper.appendChild(this._buildSeparator());
+ const attributes = {
+ label: `${folder.prettyName} - ${folder.server.prettyName}`,
+ ...this._getCssSelectorAttributes(folder),
+ };
+ this.childWrapper.appendChild(this._buildMenuItem(attributes, folder));
+ }
+
+ /**
+ * Populate a "recent" or "favorites" special submenu with either the
+ * recently used or favorite folders, to allow for easy access.
+ *
+ * @param {Element} menu - The menu or toolbarbutton element for which one
+ * wants to populate the special sub menu.
+ * @param {Element} submenu - The submenu element, typically a menupopup.
+ */
+ _populateSpecialSubmenu(menu, submenu) {
+ let specialType = menu.getAttribute("special");
+ if (this._initializedSpecials.has(specialType)) {
+ return;
+ }
+
+ // Iterate through all folders in all accounts matching the current filter.
+ let specialFolders = MailServices.accounts.allFolders;
+ if (this._listener._filterFunction) {
+ specialFolders = specialFolders.filter(
+ this._listener._filterFunction
+ );
+ }
+
+ switch (specialType) {
+ case "recent":
+ // Find the most recently modified ones.
+ specialFolders = LazyModules.FolderUtils.getMostRecentFolders(
+ specialFolders,
+ Services.prefs.getIntPref("mail.folder_widget.max_recent"),
+ "MRMTime"
+ );
+ break;
+ case "favorites":
+ specialFolders = specialFolders.filter(folder =>
+ folder.getFlag(Ci.nsMsgFolderFlags.Favorite)
+ );
+ break;
+ }
+
+ // Cache the pretty names so that they do not need to be fetched
+ // with quadratic complexity when sorting by name.
+ let specialFoldersMap = specialFolders.map(folder => {
+ return {
+ folder,
+ name: folder.prettyName,
+ };
+ });
+
+ // Because we're scanning across multiple accounts, we can end up with
+ // several folders with the same name. Find those dupes.
+ let dupeNames = new Set();
+ for (let i = 0; i < specialFoldersMap.length; i++) {
+ for (let j = i + 1; j < specialFoldersMap.length; j++) {
+ if (specialFoldersMap[i].name == specialFoldersMap[j].name) {
+ dupeNames.add(specialFoldersMap[i].name);
+ }
+ }
+ }
+
+ for (let folderItem of specialFoldersMap) {
+ // If this folder name appears multiple times in the recent list,
+ // append the server name to disambiguate.
+ // TODO:
+ // - maybe this could use verboseFolderFormat from messenger.properties
+ // instead of hardcoded " - ".
+ // - disambiguate folders with same name in same account
+ // (in different subtrees).
+ let label = folderItem.name;
+ if (dupeNames.has(label)) {
+ label += " - " + folderItem.folder.server.prettyName;
+ }
+
+ folderItem.label = label;
+ }
+
+ // Make sure the entries are sorted alphabetically.
+ specialFoldersMap.sort((a, b) =>
+ LazyModules.FolderUtils.folderNameCompare(a.label, b.label)
+ );
+
+ // Create entries for each of the recent folders.
+ for (let folderItem of specialFoldersMap) {
+ let attributes = {
+ label: folderItem.label,
+ ...this._getCssSelectorAttributes(folderItem.folder),
+ };
+
+ submenu.childWrapper.appendChild(
+ this._buildMenuItem(attributes, folderItem.folder)
+ );
+ }
+
+ if (specialFoldersMap.length == 0) {
+ menu.setAttribute("disabled", "true");
+ }
+
+ this._initializedSpecials.add(specialType);
+ }
+
+ /**
+ * Add a menu item that refers back to the parent folder when there is a
+ * showFileHereLabel attribute or no mode attribute. However don't
+ * add such a menu item if one of the following conditions is met:
+ * (-) There is no parent folder.
+ * (-) Folder is server and showAccountsFileHere is explicitly false.
+ * (-) Current folder has a mode, the parent folder can be selected,
+ * no messages can be filed into the parent folder (e.g. when the
+ * parent folder is a news group or news server) and the folder
+ * mode is not equal to newFolder.
+ * The menu item will have the value of the fileHereLabel attribute as
+ * label or if the attribute does not exist the name of the parent
+ * folder instead.
+ *
+ * @param {string} mode - The mode attribute.
+ */
+ _maybeAddParentFolderMenuItem(mode) {
+ let folder = this._parentFolder;
+ if (
+ folder &&
+ (this.getAttribute("showFileHereLabel") == "true" || !mode)
+ ) {
+ let showAccountsFileHere = this.getAttribute("showAccountsFileHere");
+ if (
+ (!folder.isServer || showAccountsFileHere != "false") &&
+ (!mode ||
+ mode == "newFolder" ||
+ folder.noSelect ||
+ folder.canFileMessages ||
+ showAccountsFileHere == "true")
+ ) {
+ let attributes = {};
+
+ if (this.hasAttribute("fileHereLabel")) {
+ attributes.label = this.getAttribute("fileHereLabel");
+ attributes.accesskey = this.getAttribute("fileHereAccessKey");
+ } else {
+ attributes.label = folder.prettyName;
+ Object.assign(attributes, this._getCssSelectorAttributes(folder));
+ }
+
+ if (folder.noSelect) {
+ attributes.disabled = "true";
+ }
+
+ this.childWrapper.appendChild(
+ this._buildMenuItem(attributes, folder)
+ );
+ this.childWrapper.appendChild(this._buildSeparator());
+ }
+ }
+ }
+
+ /**
+ * Add menu items, one for each folder.
+ *
+ * @param {nsIMsgFolder[]} folders - Array of folder objects.
+ * @param {string} mode - The mode attribute.
+ * @param {nsIMsgFolder} globalInboxFolder - The root/global inbox folder.
+ */
+ _addFoldersMenuItems(folders, mode, globalInboxFolder) {
+ // disableServers attribute is a comma separated list of server keys.
+ const disableServers = this.hasAttribute("disableServers")
+ ? this.getAttribute("disableServers").split(",")
+ : [];
+
+ // We need to call this, or hasSubFolders will always return false.
+ // Remove this workaround when Bug 502900 is fixed.
+ LazyModules.MailUtils.discoverFolders();
+ this._serversOnly = true;
+
+ let [shouldExpand, labels] = this._getShouldExpandAndLabels();
+
+ for (let folder of folders) {
+ if (!folder.isServer) {
+ this._serversOnly = false;
+ }
+
+ let attributes = {
+ label: this._getFolderLabel(mode, globalInboxFolder, folder),
+ ...this._getCssSelectorAttributes(folder),
+ };
+
+ if (disableServers.includes(folder.server.key)) {
+ attributes.disabled = "true";
+ }
+
+ if (!folder.hasSubFolders || !shouldExpand(folder.server.type)) {
+ // There are no subfolders, create a simple menu item.
+ this.childWrapper.appendChild(
+ this._buildMenuItem(attributes, folder)
+ );
+ } else {
+ // There are subfolders, create a menu item with a submenu.
+ // xxx this is slightly problematic in that we haven't confirmed
+ // whether any of the subfolders will pass the filter.
+
+ this._serversOnly = false;
+
+ let submenuAttributes = {};
+
+ [
+ "class",
+ "type",
+ "fileHereLabel",
+ "showFileHereLabel",
+ "oncommand",
+ "showAccountsFileHere",
+ "mode",
+ "disableServers",
+ "position",
+ ].forEach(attribute => {
+ if (this.hasAttribute(attribute)) {
+ submenuAttributes[attribute] = this.getAttribute(attribute);
+ }
+ });
+
+ const [menuItem, submenu] = this._buildMenuItemWithSubmenu(
+ attributes,
+ true,
+ folder,
+ submenuAttributes
+ );
+
+ // If there are labels, we add an item and separator to the submenu.
+ if (labels) {
+ const serverAttributes = { label: labels[folder.server.type] };
+
+ submenu.childWrapper.appendChild(
+ this._buildMenuItem(serverAttributes, folder, this)
+ );
+
+ submenu.childWrapper.appendChild(this._buildSeparator());
+ }
+
+ this.childWrapper.appendChild(menuItem);
+ }
+ }
+ }
+
+ /**
+ * Return the label to use for a folder.
+ *
+ * @param {string} mode - The mode, e.g. "deferred".
+ * @param {nsIMsgFolder} globalInboxFolder - The root/global inbox folder.
+ * @param {nsIMsgFolder} folder - The folder for which we are getting a label.
+ * @returns {string} The label to use for the folder.
+ */
+ _getFolderLabel(mode, globalInboxFolder, folder) {
+ if (
+ mode == "deferred" &&
+ folder.isServer &&
+ folder.server.rootFolder == globalInboxFolder
+ ) {
+ return this._stringBundle.formatStringFromName("globalInbox", [
+ folder.prettyName,
+ ]);
+ }
+ return folder.prettyName;
+ }
+
+ /**
+ * Let the user have a list of subfolders for all account types, none of
+ * them, or only some of them. Returns an array containing a function that
+ * determines whether to show subfolders for a given account type, and an
+ * object mapping account types to label names (may be null).
+ *
+ * @returns {any[]} - An array; [0] is the shouldExpand function, [1] is
+ * the labels object.
+ */
+ _getShouldExpandAndLabels() {
+ let shouldExpand;
+ let labels = null;
+ if (
+ this.getAttribute("expandFolders") == "true" ||
+ !this.hasAttribute("expandFolders")
+ ) {
+ shouldExpand = () => true;
+ } else if (this.getAttribute("expandFolders") == "false") {
+ shouldExpand = () => false;
+ } else {
+ // We want a subfolder list for only some servers. We also may need
+ // to create headers to select the servers. If so, then headlabels
+ // is a comma-delimited list of labels corresponding to the server
+ // types specified in expandFolders.
+ let types = this.getAttribute("expandFolders").split(/ *, */);
+ // Set the labels. labels[type] = label
+ if (this.hasAttribute("headlabels")) {
+ let labelNames = this.getAttribute("headlabels").split(/ *, */);
+ labels = {};
+ // If the length isn't equal, don't give them any of the labels,
+ // since any combination will probably be wrong.
+ if (labelNames.length == types.length) {
+ for (let index in types) {
+ labels[types[index]] = labelNames[index];
+ }
+ }
+ }
+ shouldExpand = e => types.includes(e);
+ }
+ return [shouldExpand, labels];
+ }
+
+ /**
+ * Set attributes on a menu, menuitem, or toolbarbutton element to allow
+ * for CSS styling.
+ *
+ * @param {nsIMsgFolder} folder - The folder that corresponds to the menu/menuitem.
+ * @param {Element} menuNode - The actual DOM node to set attributes on.
+ */
+ _setCssSelectors(folder, menuNode) {
+ const cssAttributes = this._getCssSelectorAttributes(folder);
+
+ Object.entries(cssAttributes).forEach(([key, value]) =>
+ menuNode.setAttribute(key, value)
+ );
+ }
+
+ /**
+ * Returns attributes to be set on a menu, menuitem, or toolbarbutton
+ * element to allow for CSS styling.
+ *
+ * @param {nsIMsgFolder} folder - The folder that corresponds to the menu item.
+ * @returns {object} Contains the CSS selector attributes.
+ */
+ _getCssSelectorAttributes(folder) {
+ let attributes = {};
+
+ // First the SpecialFolder attribute.
+ attributes.SpecialFolder =
+ LazyModules.FolderUtils.getSpecialFolderString(folder);
+
+ // Now the biffState.
+ let biffStates = ["NewMail", "NoMail", "UnknownMail"];
+ for (let state of biffStates) {
+ if (folder.biffState == Ci.nsIMsgFolder["nsMsgBiffState_" + state]) {
+ attributes.BiffState = state;
+ break;
+ }
+ }
+
+ attributes.IsServer = folder.isServer;
+ attributes.IsSecure = folder.server.isSecure;
+ attributes.ServerType = folder.server.type;
+ attributes.IsFeedFolder =
+ !!LazyModules.FeedUtils.getFeedUrlsInFolder(folder);
+
+ return attributes;
+ }
+
+ /**
+ * This function returns a formatted display name for a menulist
+ * selected folder. The desired format is set as the 'displayformat'
+ * attribute of the folderpicker's <menulist>, one of:
+ * 'name' (default) - Folder
+ * 'verbose' - Folder on Account
+ * 'path' - Account/Folder/Subfolder
+ *
+ * @param {nsIMsgFolder} folder - The folder that corresponds to the menu/menuitem.
+ * @returns {string} The display name.
+ */
+ getDisplayName(folder) {
+ if (folder.isServer) {
+ return folder.prettyName;
+ }
+
+ if (this._displayformat == "verbose") {
+ return this._stringBundle.formatStringFromName(
+ "verboseFolderFormat",
+ [folder.prettyName, folder.server.prettyName]
+ );
+ }
+
+ if (this._displayformat == "path") {
+ return (
+ LazyModules.FeedUtils.getFolderPrettyPath(folder) || folder.name
+ );
+ }
+
+ return folder.name;
+ }
+
+ /**
+ * Makes a given folder selected.
+ * TODO: This function does not work yet for the appmenu. However, as of
+ * June 2019, this functionality is not used in the appmenu.
+ *
+ * @param {nsIMsgFolder} inputFolder - The folder to select (if none,
+ * then Choose Folder). If inputFolder is not in this popup, but is
+ * instead a descendant of a member of the popup, that ancestor will be
+ * selected.
+ * @returns {boolean} Is true if any usable folder was found, otherwise false.
+ */
+ selectFolder(inputFolder) {
+ // Set the label of the menulist element as if folder had been selected.
+ function setupParent(folder, menulist, noFolders) {
+ let menupopup = menulist.menupopup;
+ if (folder) {
+ menulist.setAttribute("label", menupopup.getDisplayName(folder));
+ } else if (noFolders) {
+ menulist.setAttribute(
+ "label",
+ menupopup._stringBundle.GetStringFromName("noFolders")
+ );
+ } else if (menupopup._serversOnly) {
+ menulist.setAttribute(
+ "label",
+ menupopup._stringBundle.GetStringFromName("chooseAccount")
+ );
+ } else {
+ menulist.setAttribute(
+ "label",
+ menupopup._stringBundle.GetStringFromName("chooseFolder")
+ );
+ }
+ menulist.setAttribute("value", folder ? folder.URI : "");
+ menulist.setAttribute("IsServer", folder ? folder.isServer : false);
+ menulist.setAttribute(
+ "IsSecure",
+ folder ? folder.server.isSecure : false
+ );
+ menulist.setAttribute(
+ "ServerType",
+ folder ? folder.server.type : "none"
+ );
+ menulist.setAttribute(
+ "SpecialFolder",
+ folder
+ ? LazyModules.FolderUtils.getSpecialFolderString(folder)
+ : "none"
+ );
+ menulist.setAttribute(
+ "IsFeedFolder",
+ Boolean(folder && LazyModules.FeedUtils.getFeedUrlsInFolder(folder))
+ );
+ }
+
+ let folder;
+ if (inputFolder) {
+ for (let child of this.children) {
+ if (
+ child &&
+ child._folder &&
+ !child.disabled &&
+ (child._folder.URI == inputFolder.URI ||
+ (child.tagName == "menu" &&
+ child._folder.isAncestorOf(inputFolder)))
+ ) {
+ if (child._folder.URI == inputFolder.URI) {
+ this.parentNode.selectedItem = child;
+ }
+ folder = inputFolder;
+ break;
+ }
+ }
+ }
+
+ // If the caller specified a folder to select and it was not
+ // found, or if the caller didn't pass a folder (meaning a logical
+ // and valid folder wasn't determined), don't blow up but reset
+ // attributes and set a nice Choose Folder label so the user may
+ // select a valid folder per the filter for this picker. If there are
+ // no children, then no folder passed the filter; disable the menulist
+ // as there's nothing to choose from.
+ let noFolders;
+ if (!this.childElementCount) {
+ this.parentNode.setAttribute("disabled", true);
+ noFolders = true;
+ } else {
+ this.parentNode.removeAttribute("disabled");
+ noFolders = false;
+ }
+
+ setupParent(folder, this.parentNode, noFolders);
+ return !!folder;
+ }
+
+ /**
+ * Removes all menu items from this menu, removes their submenus. This
+ * function is called when a change that affects this menu is detected
+ * by the listener.
+ */
+ _teardown() {
+ if (!this._initialized) {
+ return;
+ }
+ const children = this.childWrapper.children;
+ // We iterate in reverse order because children is live so it changes
+ // as we remove child nodes.
+ for (let i = children.length - 1; i >= 0; i--) {
+ const item = children[i];
+ if (item.getAttribute("generated") != "true") {
+ continue;
+ }
+ const submenu = this._getSubMenuForMenuItem(item);
+
+ if (submenu && "_teardown" in submenu) {
+ submenu._teardown();
+ submenu.remove();
+ }
+ item.remove();
+ }
+
+ this._removeListener();
+
+ this._initialized = false;
+ this._initializedSpecials.clear();
+ }
+ };
+
+ /**
+ * The MozFolderMenupopup widget is used as a menupopup that contains menu
+ * items and submenus for all folders from every account (or some subset of
+ * folders and accounts). It is also used to provide a menu with a menuitem
+ * for each account. Each menu item gets displayed with the folder or
+ * account name and icon.
+ *
+ * @augments {MozElements.MozMenuPopup}
+ */
+ let MozFolderMenuPopup = FolderMenu(
+ class extends MozElements.MozMenuPopup {
+ constructor() {
+ super();
+
+ // To improve performance, only build the menu when it is shown.
+ this.addEventListener(
+ "popupshowing",
+ event => {
+ this._ensureInitialized();
+ },
+ true
+ );
+
+ // Because the menu items in a panelview go inside a child vbox but are
+ // direct children of a menupopup, we set up a consistent way to append
+ // and access menu items for both cases.
+ this.childWrapper = this;
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+
+ this.setAttribute("is", "folder-menupopup");
+
+ // Find out if we are in a wrapper (customize toolbars mode is active).
+ let inWrapper = false;
+ let node = this;
+ while (XULElement.isInstance(node)) {
+ if (node.id.startsWith("wrapper-")) {
+ inWrapper = true;
+ break;
+ }
+ node = node.parentNode;
+ }
+
+ if (!inWrapper) {
+ if (this.hasAttribute("original-width")) {
+ // If we were in a wrapper before and have a width stored, restore it now.
+ if (this.getAttribute("original-width") == "none") {
+ this.removeAttribute("width");
+ } else {
+ this.setAttribute("width", this.getAttribute("original-width"));
+ }
+
+ this.removeAttribute("original-width");
+ }
+
+ // If we are a child of a menulist, and we aren't in a wrapper, we
+ // need to build our content right away, otherwise the menulist
+ // won't have proper sizing.
+ if (this.parentNode && this.parentNode.localName == "menulist") {
+ this._ensureInitialized();
+ }
+ } else {
+ // But if we're in a wrapper, remove our children, because we're
+ // getting re-created when the toolbar customization closes.
+ this._teardown();
+
+ // Store our current width and set a safe small width when we show
+ // in a wrapper.
+ if (!this.hasAttribute("original-width")) {
+ this.setAttribute(
+ "original-width",
+ this.hasAttribute("width") ? this.getAttribute("width") : "none"
+ );
+ this.setAttribute("width", "100");
+ }
+ }
+ }
+
+ /**
+ * Given a menu item, return the menupopup that it opens.
+ *
+ * @param {Element} menu - The menu item, typically a `menu` element.
+ * @returns {Element|null} The `menupopup` element or null if none found.
+ */
+ _getSubMenuForMenuItem(menu) {
+ return menu.querySelector("menupopup");
+ }
+
+ /**
+ * Returns a `menuseparator` element for use in a `menupopup`.
+ */
+ _buildSeparator() {
+ return generateElement("menuseparator");
+ }
+
+ /**
+ * Builds a menu item (`menuitem`) element that does not open a submenu
+ * (i.e. not a `menu` element).
+ *
+ * @param {object} [attributes] - Attributes to set on the element.
+ * @param {nsIMsgFolder} folder - The folder associated with the menu item.
+ * @returns {Element} A `menuitem`.
+ */
+ _buildMenuItem(attributes, folder) {
+ const menuitem = generateElement("menuitem", attributes);
+ menuitem.classList.add("folderMenuItem", "menuitem-iconic");
+ menuitem._folder = folder;
+ return menuitem;
+ }
+
+ /**
+ * Builds a menu item (`menu`) element and an associated submenu
+ * (`menupopup`) element.
+ *
+ * @param {object} attributes - Attributes to set on the `menu` element.
+ * @param {boolean} folderSubmenu - Whether the submenu is to be a
+ * `folder-menupopup` element.
+ * @param {nsIMsgFolder} [folder] - The folder associated with the menu item.
+ * @param {object} submenuAttributes - Attributes to set on the `menupopup` element.
+ * @returns {Element[]} Array containing the `menu` and
+ * `menupopup` elements.
+ */
+ _buildMenuItemWithSubmenu(
+ attributes,
+ folderSubmenu,
+ folder,
+ submenuAttributes
+ ) {
+ const menu = generateElement("menu", attributes);
+ menu.classList.add("folderMenuItem", "menu-iconic");
+
+ const isObject = folderSubmenu ? { is: "folder-menupopup" } : null;
+
+ const menupopup = generateElement(
+ "menupopup",
+ submenuAttributes,
+ isObject
+ );
+
+ if (folder) {
+ menu._folder = folder;
+ menupopup._parentFolder = folder;
+ }
+
+ if (!menupopup.childWrapper) {
+ menupopup.childWrapper = menupopup;
+ }
+
+ menu.appendChild(menupopup);
+
+ return [menu, menupopup];
+ }
+
+ /**
+ * Build a special menu item (`menu`) and an empty submenu (`menupopup`)
+ * for it. The submenu is populated just before it is shown by
+ * `_populateSpecialSubmenu`.
+ *
+ * The submenu (`menupopup`) is just a standard element, not a custom
+ * element (`folder-menupopup`).
+ *
+ * @param {object} [attributes] - Attributes to set on the menu item element.
+ * @returns {Element} The menu item (`menu`) element.
+ */
+ _buildSpecialMenu(attributes) {
+ const [menu, menupopup] = this._buildMenuItemWithSubmenu(attributes);
+
+ menupopup.addEventListener(
+ "popupshowing",
+ event => {
+ this._populateSpecialSubmenu(menu, menupopup);
+ },
+ { once: true }
+ );
+
+ return menu;
+ }
+ }
+ );
+
+ customElements.define("folder-menupopup", MozFolderMenuPopup, {
+ extends: "menupopup",
+ });
+}