releases-comm-central/mailnews/base/content/folder-menupopup.js

1466 строки
51 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/. */
"use strict";
/* globals MozElements MozXULElement PanelUI */
// This file implements both `folder-menupopup` custom elements used in
// traditional menus and `folder-panelview` custom elements used in the appmenu.
// Wrap in a block to prevent leaking to window scope.
{
const { FeedUtils } = ChromeUtils.import("resource:///modules/FeedUtils.jsm");
const {
allAccountsSorted,
folderNameCompare,
getSpecialFolderString,
getMostRecentFolders,
} = ChromeUtils.import("resource:///modules/folderUtils.jsm");
const { fixIterator, toArray } = ChromeUtils.import(
"resource:///modules/iteratorUtils.jsm"
);
const { MailServices } = ChromeUtils.import(
"resource:///modules/MailServices.jsm"
);
const { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
const { Services } = ChromeUtils.import(
"resource://gre/modules/Services.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"}`
* or `{is: "folder-panelview"}`.
*/
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;
}
/**
* Each time this function is called it generates a unique ID attribute,
* e.g. for a `panelview` element. It keeps track of a counter (via a
* closure) that increments each time it is called, guaranteeing the IDs it
* returns are unique.
*
* @param {string} prefix Prefix that is combined with a number to make the ID.
* @return {string} The unique ID.
*/
const getUniquePanelViewId = (() => {
let counter = 0;
return prefix => {
counter += 1;
return prefix + counter;
};
})();
/**
* A "mixin" function to add shared code to the classes for the
* `folder-menupopup` and `folder-panelview` custom elements. Takes a "Base"
* class, and returns a class that extends the "Base" class.
*
* We use this mixin approach because the `folder-menupopup` class needs to
* extend `MozMenuPopup` while the `folder-panelview` class should just extend
* `MozXULElement`.
*
* The shared code in this mixin class works with both `menupopup` and
* `panelview` custom elements by using functions and properties from the
* "Base" `folder-menupopup` or `folder-panelview` class. Generally the
* mixin class determines *what* needs to be built and then defers to the
* "Base" custom element class to handle *how* to build it for that menu type.
*
* Note how this double duty raises some naming challenges. For example,
* variables that could be bound to either a `menupopup` or a `panelview`
* might be named "submenu". See also `menu`/`menuitem` vs `toolbarbutton`.
* (`panelview` and `toolbarbutton` are used in the appmenu.)
*
* @param {Class} Base A class to be extended with shared functionality.
* @return {Class} A class that extends the first class.
*/
let FolderMenuMixin = 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);
}
},
_itemAddedOrRemoved(item) {
if (!(item instanceof Ci.nsIMsgFolder)) {
return;
}
if (this._filterFunction && !this._filterFunction(item)) {
return;
}
// xxx we can optimize this later
this._clearMenu(this._menu);
},
OnItemAdded(ParentItem, item) {
this._itemAddedOrRemoved(item);
},
OnItemRemoved(ParentItem, item) {
this._itemAddedOrRemoved(item);
},
// 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.
OnItemPropertyChanged(item, property, old, newItem) {},
OnItemIntPropertyChanged(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);
},
OnItemBoolPropertyChanged(item, property, old, newItem) {
this._setCssSelectorsForItem(item);
},
OnItemUnicharPropertyChanged(item, property, old, newItem) {
this._setCssSelectorsForItem(item);
},
OnItemPropertyFlagChanged(item, property, old, newItem) {},
OnItemEvent(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;
}
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 = toArray(
fixIterator(parentFolder.subFolders, Ci.nsIMsgFolder)
);
} 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 = 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);
}
/**
* 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());
}
}
/**
* 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 or panelview.
*/
_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 = 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) => 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.
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.
* @return {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).
*
* @return {[Function, Object|null]} Array containing the shouldExpand
* function and 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.
* @return {Object} Contains the CSS selector attributes.
*/
_getCssSelectorAttributes(folder) {
let attributes = {};
// First the SpecialFolder attribute.
attributes.SpecialFolder = 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 = !!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.
* @return {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 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).
* @return {boolean} Is true if any usable folder was found, otherwise false.
* @note If inputFolder is not in this popup, but is instead a descendant of
* a member of the popup, that ancestor will be selected.
*/
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 ? getSpecialFolderString(folder) : "none"
);
menulist.setAttribute(
"IsFeedFolder",
Boolean(folder && 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 (needed for
* the appmenu where the `panelview` submenus are not children of the
* `toolbarbutton` menu items), resets all fields, and removes the listener.
* 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. It uses code that is also used by MozFolderPanelView
* via the FolderMenuMixin function.
*
* @extends {MozElements.MozMenuPopup}
*/
let MozFolderMenuPopup = FolderMenuMixin(
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 (node instanceof XULElement) {
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.
* @return {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.
* @return {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.
* @return {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",
});
/**
* Used as a panelview in the appmenu/hamburger menu. It 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. It uses code that is also used by MozFolderMenupopup via
* the FolderMenuMixin function.
*
* @extends {MozXULElement}
*/
let MozFolderPanelView = FolderMenuMixin(
class extends MozXULElement {
constructor() {
super();
// To improve performance, only build the menu when it is shown.
this.addEventListener(
"ViewShowing",
event => {
this._ensureInitialized();
},
true
);
}
connectedCallback() {
// In the appmenu the panelview elements may move around, so we only want
// connectedCallback to run once.
if (this.delayConnectedCallback() || this.hasConnected) {
return;
}
this.hasConnected = true;
this.setAttribute("is", "folder-panelview");
this._setUpPanelView(this);
}
/**
* Set up a `folder-panelview` or a plain `panelview` element. If the
* panelview was statically defined in a XUL file then it may already have
* a child <vbox> element, if it was dynamically generated it may not yet.
*
* @param {Element} panelview The panelview to set up.
*/
_setUpPanelView(panelview) {
let subviewBody = panelview.querySelector(".panel-subview-body");
if (!subviewBody) {
subviewBody = document.createXULElement("vbox");
subviewBody.classList.add("panel-subview-body");
panelview.appendChild(subviewBody);
}
// 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.
panelview.childWrapper = subviewBody;
panelview.classList.add("PanelUI-subView");
// Prevent the back button from firing the command that is set on the
// panelview by stopping propagation of the event. The back button does
// not exist until the panelview is shown for the first time (when the
// header is added to it).
panelview.addEventListener(
"ViewShown",
() => {
const backButton = panelview.querySelector(
".panel-header > .subviewbutton-back"
);
if (backButton) {
backButton.addEventListener("command", event =>
event.stopPropagation()
);
}
},
{ once: true }
);
}
/**
* Given a menu item, return the submenu that it opens.
*
* @param {Element} item The menu item, typically a `toolbarbutton`.
* @return {Element|null} The submenu (or null if none found), typically a
* `panelview` element.
*/
_getSubMenuForMenuItem(item) {
const panelviewId = item.getAttribute("panelviewId");
if (panelviewId) {
return document.getElementById(panelviewId);
}
return null;
}
/**
* Returns a `toolbarseparator` element for use in a `panelview`.
*/
_buildSeparator() {
return generateElement("toolbarseparator");
}
/**
* Builds a menu item (`toolbarbutton`) element that does not open a submenu.
*
* @param {Object} [attributes] Attributes to set on the element.
* @param {nsIMsgFolder} folder The folder associated with the menu item.
* @returns {Element} A `toolbarbutton`.
*/
_buildMenuItem(attributes, folder) {
const button = generateElement("toolbarbutton", attributes);
button._folder = folder;
button.classList.add(
"folderMenuItem",
"subviewbutton",
"subviewbutton-iconic"
);
return button;
}
/**
* Builds a menu item (`toolbarbutton`) element and an associated submenu
* (`panelview`) element.
*
* @param {Object} attributes Attributes to set on the
* `toolbarbutton` element.
* @param {boolean} folderSubmenu Whether the submenu is to be a
* `folder-panelview` element.
* @param {nsIMsgFolder} [folder] The folder associated with the menu item.
* @param {Object} submenuAttributes Attributes to set on the `panelview`
* element.
* @return {Element[]} Array containing the `toolbarbutton`
* and `panelview` elements.
*/
_buildMenuItemWithSubmenu(
attributes,
folderSubmenu,
folder,
submenuAttributes
) {
const button = generateElement("toolbarbutton", attributes);
button.classList.add(
"folderMenuItem",
"subviewbutton",
"subviewbutton-iconic",
"subviewbutton-nav"
);
const isObject = folderSubmenu ? { is: "folder-panelview" } : null;
const panelview = generateElement(
"panelview",
submenuAttributes,
isObject
);
if (!folderSubmenu) {
this._setUpPanelView(panelview);
}
if (folder) {
panelview._parentFolder = folder;
panelview._folder = folder;
}
const panelviewId = getUniquePanelViewId("folderPanelView");
panelview.setAttribute("id", panelviewId);
// Pass these attributes down from panelview to panelview.
["command", "oncommand"].forEach(attribute => {
if (this.hasAttribute(attribute)) {
panelview.setAttribute(attribute, this.getAttribute(attribute));
}
});
if (
!submenuAttributes ||
(submenuAttributes && !submenuAttributes.label)
) {
panelview.setAttribute("label", attributes.label);
}
button.addEventListener("command", event => {
// Stop event propagation so the command that is set on the panelview
// is not fired when we are just navigating to a submenu.
event.stopPropagation();
PanelUI.showSubView(panelviewId, panelview);
});
// Save the panelviewId on the menu item so we have a way to access the
// panelview from the menu item that opens it.
button.setAttribute("panelviewId", panelviewId);
button.setAttribute("closemenu", "none");
document.querySelector("#appMenu-multiView").appendChild(panelview);
return [button, panelview];
}
/**
* Build a special menu item (`toolbarbutton`) and an empty submenu
* (`panelview`) for it. The submenu is populated just before it is shown
* by `_populateSpecialSubmenu`.
*
* The submenu (`panelview`) is just a standard element, not a custom
* element (`folder-panelview`).
*
* @param {Object} [attributes] Attributes to set on the menu item element.
* @return {Element} The menu item (`toolbarbutton`) element.
*/
_buildSpecialMenu(attributes) {
const [button, panelview] = this._buildMenuItemWithSubmenu(attributes);
panelview.addEventListener(
"ViewShowing",
event => {
this._populateSpecialSubmenu(button, panelview);
},
{ once: true }
);
return button;
}
}
);
customElements.define("folder-panelview", MozFolderPanelView, {
extends: "panelview",
});
}