summaryrefslogtreecommitdiffstats
path: root/comm/mail/base/content/tabmail.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/base/content/tabmail.js')
-rw-r--r--comm/mail/base/content/tabmail.js2048
1 files changed, 2048 insertions, 0 deletions
diff --git a/comm/mail/base/content/tabmail.js b/comm/mail/base/content/tabmail.js
new file mode 100644
index 0000000000..c2ab652ef9
--- /dev/null
+++ b/comm/mail/base/content/tabmail.js
@@ -0,0 +1,2048 @@
+/* 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"; // from mailWindow.js
+
+/* global MozElements, MozXULElement */
+
+/* import-globals-from mailCore.js */
+/* globals contentProgress, statusFeedback */
+
+var { UIFontSize } = ChromeUtils.import("resource:///modules/UIFontSize.jsm");
+
+// Wrap in a block to prevent leaking to window scope.
+{
+ /**
+ * The MozTabmailAlltabsMenuPopup widget is used as a menupopup to list all the
+ * currently opened tabs.
+ *
+ * @augments {MozElements.MozMenuPopup}
+ * @implements {EventListener}
+ */
+ class MozTabmailAlltabsMenuPopup extends MozElements.MozMenuPopup {
+ connectedCallback() {
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+
+ this.tabmail = document.getElementById("tabmail");
+
+ this._mutationObserver = new MutationObserver((records, observer) => {
+ records.forEach(mutation => {
+ let menuItem = mutation.target.mCorrespondingMenuitem;
+ if (menuItem) {
+ this._setMenuitemAttributes(menuItem, mutation.target);
+ }
+ });
+ });
+
+ this.addEventListener("popupshowing", event => {
+ // Set up the menu popup.
+ let tabcontainer = this.tabmail.tabContainer;
+ let tabs = tabcontainer.allTabs;
+
+ // Listen for changes in the tab bar.
+ this._mutationObserver.observe(tabcontainer, {
+ attributes: true,
+ subtree: true,
+ attributeFilter: ["label", "crop", "busy", "image", "selected"],
+ });
+
+ this.tabmail.addEventListener("TabOpen", this);
+ tabcontainer.arrowScrollbox.addEventListener("scroll", this);
+
+ // If an animation is in progress and the user
+ // clicks on the "all tabs" button, stop the animation.
+ tabcontainer._stopAnimation();
+
+ for (let i = 0; i < tabs.length; i++) {
+ this._createTabMenuItem(tabs[i]);
+ }
+ this._updateTabsVisibilityStatus();
+ });
+
+ this.addEventListener("popuphiding", event => {
+ // Clear out the menu popup and remove the listeners.
+ while (this.hasChildNodes()) {
+ let menuItem = this.lastElementChild;
+ menuItem.removeEventListener("command", this);
+ menuItem.tab.removeEventListener("TabClose", this);
+ menuItem.tab.mCorrespondingMenuitem = null;
+ menuItem.remove();
+ }
+ this._mutationObserver.disconnect();
+
+ this.tabmail.tabContainer.arrowScrollbox.removeEventListener(
+ "scroll",
+ this
+ );
+ this.tabmail.removeEventListener("TabOpen", this);
+ });
+ }
+
+ _menuItemOnCommand(aEvent) {
+ this.tabmail.tabContainer.selectedItem = aEvent.target.tab;
+ }
+
+ _tabOnTabClose(aEvent) {
+ let menuItem = aEvent.target.mCorrespondingMenuitem;
+ if (menuItem) {
+ menuItem.remove();
+ }
+ }
+
+ handleEvent(aEvent) {
+ if (!aEvent.isTrusted) {
+ return;
+ }
+
+ switch (aEvent.type) {
+ case "command":
+ this._menuItemOnCommand(aEvent);
+ break;
+ case "TabClose":
+ this._tabOnTabClose(aEvent);
+ break;
+ case "TabOpen":
+ this._createTabMenuItem(aEvent.target);
+ break;
+ case "scroll":
+ this._updateTabsVisibilityStatus();
+ break;
+ }
+ }
+
+ _updateTabsVisibilityStatus() {
+ let tabStrip = this.tabmail.tabContainer.arrowScrollbox;
+ // We don't want menu item decoration unless there is overflow.
+ if (tabStrip.getAttribute("overflow") != "true") {
+ return;
+ }
+
+ let tabStripBox = tabStrip.getBoundingClientRect();
+
+ for (let i = 0; i < this.children.length; i++) {
+ let currentTabBox = this.children[i].tab.getBoundingClientRect();
+
+ if (
+ currentTabBox.left >= tabStripBox.left &&
+ currentTabBox.right <= tabStripBox.right
+ ) {
+ this.children[i].setAttribute("tabIsVisible", "true");
+ } else {
+ this.children[i].removeAttribute("tabIsVisible");
+ }
+ }
+ }
+
+ _createTabMenuItem(aTab) {
+ let menuItem = document.createXULElement("menuitem");
+
+ menuItem.setAttribute(
+ "class",
+ "menuitem-iconic alltabs-item menuitem-with-favicon"
+ );
+
+ this._setMenuitemAttributes(menuItem, aTab);
+
+ // Keep some attributes of the menuitem in sync with its
+ // corresponding tab (e.g. the tab label).
+ aTab.mCorrespondingMenuitem = menuItem;
+ aTab.addEventListener("TabClose", this);
+ menuItem.tab = aTab;
+ menuItem.addEventListener("command", this);
+
+ this.appendChild(menuItem);
+ return menuItem;
+ }
+
+ _setMenuitemAttributes(aMenuitem, aTab) {
+ aMenuitem.setAttribute("label", aTab.label);
+ aMenuitem.setAttribute("crop", "end");
+
+ if (aTab.hasAttribute("busy")) {
+ aMenuitem.setAttribute("busy", aTab.getAttribute("busy"));
+ aMenuitem.removeAttribute("image");
+ } else {
+ aMenuitem.setAttribute("image", aTab.getAttribute("image"));
+ aMenuitem.removeAttribute("busy");
+ }
+
+ // Change the tab icon accordingly.
+ let style = window.getComputedStyle(aTab);
+ aMenuitem.style.listStyleImage = style.listStyleImage;
+ aMenuitem.style.MozImageRegion = style.MozImageRegion;
+
+ if (aTab.hasAttribute("pending")) {
+ aMenuitem.setAttribute("pending", aTab.getAttribute("pending"));
+ } else {
+ aMenuitem.removeAttribute("pending");
+ }
+
+ if (aTab.selected) {
+ aMenuitem.setAttribute("selected", "true");
+ } else {
+ aMenuitem.removeAttribute("selected");
+ }
+ }
+ }
+
+ customElements.define(
+ "tabmail-alltabs-menupopup",
+ MozTabmailAlltabsMenuPopup,
+ { extends: "menupopup" }
+ );
+
+ /**
+ * Thunderbird's tab UI mechanism.
+ *
+ * We expect to be instantiated with the following children:
+ * One "tabpanels" child element whose id must be placed in the
+ * "panelcontainer" attribute on the element we are being bound to. We do
+ * this because it is important to allow overlays to contribute panels.
+ * When we attempted to have the immediate children of the bound element
+ * be propagated through use of the "children" tag, we found that children
+ * contributed by overlays did not propagate.
+ * Any children you want added to the right side of the tab bar. This is
+ * primarily intended to allow for "open a BLANK tab" buttons, namely
+ * calendar and tasks. For reasons similar to the tabpanels case, we
+ * expect the instantiating element to provide a child hbox for overlays
+ * to contribute buttons to.
+ *
+ * From a javascript perspective, there are three types of code that we
+ * expect to interact with:
+ * 1) Code that wants to open new tabs.
+ * 2) Code that wants to contribute one or more varieties of tabs.
+ * 3) Code that wants to monitor to know when the active tab changes.
+ *
+ * Consumer code should use the following methods:
+ * openTab(aTabModeName, aArgs)
+ * Open a tab of the given "mode", passing the provided arguments as an
+ * object. The tab type author should tell you the modes they implement
+ * and the required/optional arguments.
+ *
+ * Each tab type can define the set of arguments that it expects, but
+ * there are also a few common ones that all should obey, including:
+ *
+ * "background": if this is true, the tab will be loaded in the
+ * background.
+ * "disregardOpener": if this is true, then the tab opener will not
+ * be switched to automatically by tabmail if the new tab is immediately
+ * closed.
+ *
+ * closeTab(aOptionalTabIndexInfoOrTabNode, aNoUndo):
+ * If no argument is provided, the current tab is closed. The first
+ * argument specifies a specific tab to be closed. It can be a tab index,
+ * a tab info object, or a tab's DOM element. In case the second
+ * argument is true, the closed tab can't be restored by calling
+ * undoCloseTab().
+ * Please note, some tabs cannot be closed. Trying to close such tab,
+ * will fail silently.
+ * undoCloseTab():
+ * Restores the most recent tab closed by the user.
+ * switchToTab(aTabIndexInfoOrTabNode):
+ * Switch to the tab by providing a tab index, tab info object, or tab
+ * node (tabmail-tab bound element.) Instead of calling this method,
+ * you can also just poke at tabmail.tabContainer and its selectedIndex
+ * and selectedItem properties.
+ * replaceTabWithWindow(aTab):
+ * Detaches a tab from this tabbar to new window. The argument "aTab" is
+ * required and can be a tab index, a tab info object or a tabs's
+ * DOM element. Calling this method works only for tabs implementing
+ * session restore.
+ * moveTabTo(aTab, aIndex):
+ * moves the given tab to the given Index. The first argument can be
+ * a tab index, a tab info object or a tab's DOM element. The second
+ * argument specifies the tabs new absolute position within the tabbar.
+ *
+ * Less-friendly consumer methods:
+ * * persistTab(tab):
+ * serializes a tab into an object, by passing a tab info object as
+ * argument. It is used for session restore and moving tabs between
+ * windows. Returns null in case persist fails.
+ * * removeCurrentTab():
+ * Close the current tab.
+ * * removeTabByNode(aTabElement):
+ * Close the tab whose tabmail-tab bound element is passed in.
+ * Changing the currently displayed tab is accomplished by changing
+ * tabmail.tabContainer's selectedIndex or selectedItem property.
+ *
+ * Code that lives in a tab should use the following methods:
+ * * setTabTitle([aOptionalTabInfo]): Tells us that the title of the current
+ * tab (if no argument is provided) or provided tab needs to be updated.
+ * This will result in a call to the tab mode's logic to update the title.
+ * In the event this is not for the current tab, the caller is responsible
+ * for ensuring that the underlying tab mode is capable of providing a tab
+ * title when it is in the background. (The is currently not the case for
+ * "folder" and "mail" modes because of their implementation.)
+ * * setTabBusy(aTabNode, aBusyState): Tells us that the tab in question
+ * is now busy or not busy. "Busy" means that it is occupied and
+ * will not be able to respond to you until it is no longer busy.
+ * This impacts the cursor display, as well as potentially
+ * providing tab display hints.
+ * * setTabThinking(aTabNode, aThinkingState): Tells us that the
+ * tab in question is now thinking or not thinking. "Thinking" means
+ * that the tab is involved in some ongoing process but you can still
+ * interact with the tab while it is thinking. A search would be an
+ * example of thinking. This impacts spinny-thing feedback as well as
+ * potential providing tab display hints. aThinkingState may be a
+ * boolean or a localized string explaining what you are thinking about.
+ *
+ * Tab contributing code should define a tab type object and register it
+ * with us by calling registerTabType. You can remove a registered tab
+ * type (eg when unloading a restartless addon) by calling unregisterTabType.
+ * Each tab type can provide multiple tab modes. The rationale behind this
+ * organization is that Thunderbird historically/currently uses a single
+ * 3-pane view to display both three-pane folder browsing and single message
+ * browsing across multiple tabs. Each tab type has the ability to use a
+ * single tab panel for all of its display needs. So Thunderbird's "mail"
+ * tab type covers both the "folder" (3-pane folder-based browsing) and
+ * "message" (just a single message) tab modes. Likewise, calendar/lightning
+ * currently displays both its calendar and tasks in the same panel. A tab
+ * type can also create a new tabpanel for each tab as it is created. In
+ * that case, the tab type should probably only have a single mode unless
+ * there are a number of similar modes that can gain from code sharing.
+ *
+ * If you're adding a new tab type, please update TabmailTab.type in
+ * mail/components/extensions/parent/ext-mail.js.
+ *
+ * The tab type definition should include the following attributes:
+ * * name: The name of the tab-type, mainly to aid in debugging.
+ * * panelId or perTabPanel: If using a single tab panel, the id of the
+ * panel must be provided in panelId. If using one tab panel per tab,
+ * perTabPanel should be either the XUL element name that should be
+ * created for each tab, or a helper function to create and return the
+ * element.
+ * * modes: An object whose attributes are mode names (which are
+ * automatically propagated to a 'name' attribute for debugging) and
+ * values are objects with the following attributes...
+ * * any of the openTab/closeTab/saveTabState/showTab/onTitleChanged
+ * functions as described on the mode definitions. These will only be
+ * called if the mode does not provide the functions. Note that because
+ * the 'this' variable passed to the functions will always reference the
+ * tab type definition (rather than the mode definition), the mode
+ * functions can defer to the tab type functions by calling
+ * this.functionName(). (This should prove convenient.)
+ * Mode definition attributes:
+ * * type: The "type" attribute to set on the displayed tab for CSS purposes.
+ * Generally, this would be the same as the mode name, but you can do as
+ * you please.
+ * * isDefault: This should only be present and should be true for the tab
+ * mode that is the tab displayed automatically on startup.
+ * * maxTabs: The maximum number of this mode that can be opened at a time.
+ * If this limit is reached, any additional calls to openTab for this
+ * mode will simply result in the first existing tab of this mode being
+ * displayed.
+ * * shouldSwitchTo(aArgs): Optional function. Called when openTab is called
+ * on the top-level tabmail binding. It is used to decide if the openTab
+ * function should switch to an existing tab or actually open a new tab.
+ * If the openTab function should switch to an existing tab, return the
+ * index of that tab; otherwise return -1.
+ * aArgs is a set of named parameters (the ones that are later passed to
+ * openTab).
+ * * openTab(aTab, aArgs): Called when a tab of the given mode is in the
+ * process of being opened. aTab will have its "mode" attribute
+ * set to the mode definition of the tab mode being opened. You should
+ * set the "title" attribute on it, and may set any other attributes
+ * you wish for your own use in subsequent functions. Note that 'this'
+ * points to the tab type definition, not the mode definition as you
+ * might expect. This allows you to place common logic code on the
+ * tab type for use by multiple modes and to defer to it. Any arguments
+ * provided to the caller of tabmail.openTab will be passed to your
+ * function as well, including background.
+ * * closeTab(aTab): Called when aTab is being closed. The tab need not be
+ * currently displayed. You are responsible for properly cleaning up
+ * any state you preserved in aTab.
+ * * saveTabState(aTab): Called when aTab is being switched away from so that
+ * you can preserve its state on aTab. This is primarily for single
+ * tab panel implementations; you may not have much state to save if your
+ * tab has its own tab panel.
+ * * showTab(aTab): Called when aTab is being displayed and you should
+ * restore its state (if required).
+ * * persistTab(aTab): Called when we want to persist the tab because we are
+ * saving the session state. You should return an object suitable for
+ * JSON serialization. The object will be provided to your restoreTab
+ * method when we attempt to restore the session. If your code is
+ * unable or unwilling to persist the tab (some of the time), you should
+ * return null in that case. If your code never wants to persist the tab
+ * you should not implement this method. You must implement restoreTab
+ * if you implement this method.
+ * * restoreTab(aTabmail, aPersistedState): Called when we are restoring a
+ * tab session and a tab with your mode was previously persisted via a
+ * call to your persistTab implementation. You are provided with a
+ * reference to this tabmail instance and the (deserialized) state object
+ * you returned from your persistTab implementation. It is your
+ * function's job to determine if you can restore the tab, and if so,
+ * you should invoke aTabmail.openTab to actually cause your tab to be
+ * opened. This may seem odd, but it should help keep your code simple
+ * while letting you do whatever you want. Since openTab is synchronous
+ * and returns the tabInfo structure built for the tab, you can perform
+ * any additional work you need after the call to openTab.
+ * * onTitleChanged(aTab): Called when someone calls tabmail.setTabTitle() to
+ * hint that the tab's title needs to be updated. This function should
+ * update aTab.title if it can.
+ * Mode definition functions to do with menu/toolbar commands:
+ * * supportsCommand(aCommand, aTab): Called when a menu or toolbar needs to
+ * be updated. Return true if you support that command in
+ * isCommandEnabled and doCommand, return false otherwise.
+ * * isCommandEnabled(aCommand, aTab): Called when a menu or toolbar needs
+ * to be updated. Return true if the command can be executed at the
+ * current time, false otherwise.
+ * * doCommand(aCommand, aTab): Called when a menu or toolbar command is to
+ * be executed. Perform the action appropriate to the command.
+ * * onEvent(aEvent, aTab): This can be used to handle different events on
+ * the window.
+ * * getBrowser(aTab): This function should return the browser element for
+ * your tab if there is one (return null or don't define this function
+ * otherwise). It is used for some toolkit functions that require a
+ * global "getBrowser" function, e.g. ZoomManager.
+ *
+ * Tab monitoring code is expected to be used for widgets on the screen
+ * outside of the tab box that need to update themselves as the active tab
+ * changes.
+ * Tab monitoring code (un)registers itself via (un)registerTabMonitor.
+ * The following attributes should be provided on the monitor object:
+ * * monitorName: A string value naming the tab monitor/extension. This is
+ * the canonical name for the tab monitor for all persistence purposes.
+ * If the tab monitor wants to store data in the tab info object and its
+ * name is FOO it should store it in 'tabInfo._ext.FOO'. This is the
+ * only place the tab monitor should store information on the tab info
+ * object. The FOO attribute will not be automatically created; it is
+ * up to the code. The _ext attribute will be there, reliably, however.
+ * The name is also used when persisting state, but the tab monitor
+ * does not need to do anything in that case; the name is automatically
+ * used in the course of wrapping the object.
+ * The following functions should be provided on the monitor object:
+ * * onTabTitleChanged(aTab): Called when the tab's title changes.
+ * * onTabSwitched(aTab, aOldTab): Called when a new tab is made active.
+ * Also called when the monitor is registered if one or more tabs exist.
+ * If this is the first call, aOldTab will be null, otherwise aOldTab
+ * will be the previously active tab.
+ * * onTabOpened(aTab, aIsFirstTab, aWasCurrentTab): Called when a new tab is
+ * opened. This method is invoked after the tab mode's openTab method
+ * is invoked. This method is invoked before the tab monitor
+ * onTabSwitched method in the case where it will be invoked. (It is
+ * not invoked if the tab is opened in the background.)
+ * * onTabClosing(aTab): Called when a tab is being closed. This method is
+ * is invoked before the call to the tab mode's closeTab function.
+ * * onTabPersist(aTab): Return a JSON-representable object to persist for
+ * the tab. Return null if you do not have anything to persist.
+ * * onTabRestored(aTab, aState, aIsFirstTab): Called when a tab is being
+ * restored and there is data previously persisted by the tab monitor.
+ * This method is called instead of invoking onTabOpened. This is done
+ * because the restoreTab method (potentially) uses the tabmail openTab
+ * API to effect restoration. (Note: the first opened tab is special;
+ * it will produce an onTabOpened notification potentially followed by
+ * an onTabRestored notification.)
+ * Tab monitor code is also allowed to hook into the command processing
+ * logic. We support the standard supportsCommand/isCommandEnabled/
+ * doCommand functions but with a twist to indicate when other tab monitors
+ * and the actual tab itself should get a chance to process: supportsCommand
+ * and isCommandEnabled should return null when they are not handling the
+ * case. doCommand should return true if it handled the case, null
+ * otherwise.
+ */
+
+ /**
+ * The MozTabmail widget handles the Tab UI mechanism.
+ *
+ * @augments {MozXULElement}
+ */
+ class MozTabmail extends MozXULElement {
+ /**
+ * Flag indicating that the UI is currently covered by an overlay.
+ *
+ * @type {boolean}
+ */
+ globalOverlay = false;
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+
+ this.tabbox = this.getElementsByTagName("tabbox").item(0);
+ this.currentTabInfo = null;
+
+ /**
+ * Temporary field that only has a non-null value during a call to
+ * openTab, and whose value is the currentTabInfo of the tab that was
+ * open when we received the call to openTab.
+ */
+ this._mostRecentTabInfo = null;
+ /**
+ * Tab id, incremented on each openTab() and set on the browser.
+ */
+ this.tabId = 0;
+ this.tabTypes = {};
+ this.tabModes = {};
+ this.defaultTabMode = null;
+ this.tabInfo = [];
+ this.tabContainer = document.getElementById(
+ this.getAttribute("tabcontainer")
+ );
+ this.panelContainer = document.getElementById(
+ this.getAttribute("panelcontainer")
+ );
+ this.tabMonitors = [];
+ this.recentlyClosedTabs = [];
+ this.mLastTabOpener = null;
+ this.unrestoredTabs = [];
+
+ // @implements {nsIController}
+ this.tabController = {
+ supportsCommand: aCommand => {
+ let tab = this.currentTabInfo;
+ // This can happen if we're starting up and haven't got a tab
+ // loaded yet.
+ if (!tab) {
+ return false;
+ }
+
+ for (let tabMonitor of this.tabMonitors) {
+ try {
+ if ("supportsCommand" in tabMonitor) {
+ let result = tabMonitor.supportsCommand(aCommand, tab);
+ if (result !== null) {
+ return result;
+ }
+ }
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+
+ let supportsCommandFunc =
+ tab.mode.supportsCommand || tab.mode.tabType.supportsCommand;
+ if (supportsCommandFunc) {
+ return supportsCommandFunc.call(tab.mode.tabType, aCommand, tab);
+ }
+
+ return false;
+ },
+
+ isCommandEnabled: aCommand => {
+ let tab = this.currentTabInfo;
+ // This can happen if we're starting up and haven't got a tab
+ // loaded yet.
+ if (!tab || this.globalOverlay) {
+ return false;
+ }
+
+ for (let tabMonitor of this.tabMonitors) {
+ try {
+ if ("isCommandEnabled" in tabMonitor) {
+ let result = tabMonitor.isCommandEnabled(aCommand, tab);
+ if (result !== null) {
+ return result;
+ }
+ }
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+
+ let isCommandEnabledFunc =
+ tab.mode.isCommandEnabled || tab.mode.tabType.isCommandEnabled;
+ if (isCommandEnabledFunc) {
+ return isCommandEnabledFunc.call(tab.mode.tabType, aCommand, tab);
+ }
+
+ return false;
+ },
+
+ doCommand: (aCommand, ...args) => {
+ let tab = this.currentTabInfo;
+ // This can happen if we're starting up and haven't got a tab
+ // loaded yet.
+ if (!tab) {
+ return;
+ }
+
+ for (let tabMonitor of this.tabMonitors) {
+ try {
+ if ("doCommand" in tabMonitor) {
+ let result = tabMonitor.doCommand(aCommand, tab);
+ if (result === true) {
+ return;
+ }
+ }
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+
+ let doCommandFunc = tab.mode.doCommand || tab.mode.tabType.doCommand;
+ if (doCommandFunc) {
+ doCommandFunc.call(tab.mode.tabType, aCommand, tab, ...args);
+ }
+ },
+
+ onEvent: aEvent => {
+ let tab = this.currentTabInfo;
+ // This can happen if we're starting up and haven't got a tab
+ // loaded yet.
+ if (!tab) {
+ return null;
+ }
+
+ let onEventFunc = tab.mode.onEvent || tab.mode.tabType.onEvent;
+ if (onEventFunc) {
+ return onEventFunc.call(tab.mode.tabType, aEvent, tab);
+ }
+
+ return false;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIController"]),
+ };
+
+ // This is the second-highest priority controller. It's preceded by
+ // DefaultController and followed by calendarController, then whatever
+ // Gecko adds.
+ window.controllers.insertControllerAt(1, this.tabController);
+ this._restoringTabState = null;
+ }
+
+ set selectedTab(val) {
+ this.switchToTab(val);
+ }
+
+ get selectedTab() {
+ if (!this.currentTabInfo) {
+ this.currentTabInfo = this.tabInfo[0];
+ }
+
+ return this.currentTabInfo;
+ }
+
+ get tabs() {
+ return this.tabContainer.allTabs;
+ }
+
+ get selectedBrowser() {
+ return this.getBrowserForSelectedTab();
+ }
+
+ registerTabType(aTabType) {
+ if (aTabType.name in this.tabTypes) {
+ return;
+ }
+
+ this.tabTypes[aTabType.name] = aTabType;
+ for (let [modeName, modeDetails] of Object.entries(aTabType.modes)) {
+ modeDetails.name = modeName;
+ modeDetails.tabType = aTabType;
+ modeDetails.tabs = [];
+ this.tabModes[modeName] = modeDetails;
+ if (modeDetails.isDefault) {
+ this.defaultTabMode = modeDetails;
+ }
+ }
+
+ if (aTabType.panelId) {
+ aTabType.panel = document.getElementById(aTabType.panelId);
+ } else if (!aTabType.perTabPanel) {
+ throw new Error(
+ "Trying to register a tab type with neither panelId " +
+ "nor perTabPanel attributes."
+ );
+ }
+
+ setTimeout(() => {
+ for (let modeName of Object.keys(aTabType.modes)) {
+ let i = 0;
+ while (i < this.unrestoredTabs.length) {
+ let state = this.unrestoredTabs[i];
+ if (state.mode == modeName) {
+ this.restoreTab(state);
+ this.unrestoredTabs.splice(i, 1);
+ } else {
+ i++;
+ }
+ }
+ }
+ }, 0);
+ }
+
+ unregisterTabType(aTabType) {
+ // we can skip if the tab type was never registered...
+ if (!(aTabType.name in this.tabTypes)) {
+ return;
+ }
+
+ // ... if the tab type is still in use, we can not remove it without
+ // breaking the UI. So we throw an exception.
+ for (let modeName of Object.keys(aTabType.modes)) {
+ if (this.tabModes[modeName].tabs.length) {
+ throw new Error("Tab mode " + modeName + " still in use. Close tabs");
+ }
+ }
+ // ... finally get rid of the tab type
+ for (let modeName of Object.keys(aTabType.modes)) {
+ delete this.tabModes[modeName];
+ }
+
+ delete this.tabTypes[aTabType.name];
+ }
+
+ registerTabMonitor(aTabMonitor) {
+ if (!this.tabMonitors.includes(aTabMonitor)) {
+ this.tabMonitors.push(aTabMonitor);
+ if (this.tabInfo.length) {
+ aTabMonitor.onTabSwitched(this.currentTabInfo, null);
+ }
+ }
+ }
+
+ unregisterTabMonitor(aTabMonitor) {
+ if (this.tabMonitors.includes(aTabMonitor)) {
+ this.tabMonitors.splice(this.tabMonitors.indexOf(aTabMonitor), 1);
+ }
+ }
+
+ /**
+ * Given an index, tab node or tab info object, return a tuple of
+ * [iTab, tab info dictionary, tab DOM node]. If
+ * aTabIndexNodeOrInfo is not specified and aDefaultToCurrent is
+ * true, the current tab will be returned. Otherwise, an
+ * exception will be thrown.
+ */
+ _getTabContextForTabbyThing(aTabIndexNodeOrInfo, aDefaultToCurrent) {
+ let iTab;
+ let tab;
+ let tabNode;
+ if (aTabIndexNodeOrInfo == null) {
+ if (!aDefaultToCurrent) {
+ throw new Error("You need to specify a tab!");
+ }
+ iTab = this.tabContainer.selectedIndex;
+ return [iTab, this.tabInfo[iTab], this.tabContainer.allTabs[iTab]];
+ }
+ if (typeof aTabIndexNodeOrInfo == "number") {
+ iTab = aTabIndexNodeOrInfo;
+ tabNode = this.tabContainer.allTabs[iTab];
+ tab = this.tabInfo[iTab];
+ } else if (
+ aTabIndexNodeOrInfo.tagName &&
+ aTabIndexNodeOrInfo.tagName == "tab"
+ ) {
+ tabNode = aTabIndexNodeOrInfo;
+ iTab = this.tabContainer.getIndexOfItem(tabNode);
+ tab = this.tabInfo[iTab];
+ } else {
+ tab = aTabIndexNodeOrInfo;
+ iTab = this.tabInfo.indexOf(tab);
+ tabNode = iTab >= 0 ? this.tabContainer.allTabs[iTab] : null;
+ }
+ return [iTab, tab, tabNode];
+ }
+
+ openFirstTab() {
+ // From the moment of creation, our customElement already has a visible
+ // tab. We need to create a tab information structure for this tab.
+ // In the process we also generate a synthetic tab title changed
+ // event to ensure we have an accurate title. We assume the tab
+ // contents will set themselves up correctly.
+ if (this.tabInfo.length == 0) {
+ let tab = this.openTab("mail3PaneTab", { first: true });
+ this.tabs[0].linkedPanel = tab.panel.id;
+ }
+ }
+
+ // eslint-disable-next-line complexity
+ openTab(aTabModeName, aArgs = {}) {
+ try {
+ if (!(aTabModeName in this.tabModes)) {
+ throw new Error("No such tab mode: " + aTabModeName);
+ }
+
+ let tabMode = this.tabModes[aTabModeName];
+ // if we are already at our limit for this mode, show an existing one
+ if (tabMode.tabs.length == tabMode.maxTabs) {
+ let desiredTab = tabMode.tabs[0];
+ this.tabContainer.selectedIndex = this.tabInfo.indexOf(desiredTab);
+ return null;
+ }
+
+ // Do this so that we don't generate strict warnings
+ let background = aArgs.background;
+ // If the mode wants us to, we should switch to an existing tab
+ // rather than open a new one. We shouldn't switch to the tab if
+ // we're opening it in the background, though.
+ let shouldSwitchToFunc =
+ tabMode.shouldSwitchTo || tabMode.tabType.shouldSwitchTo;
+ if (shouldSwitchToFunc) {
+ let tabIndex = shouldSwitchToFunc.apply(tabMode.tabType, [aArgs]);
+ if (tabIndex >= 0) {
+ if (!background) {
+ this.selectTabByIndex(null, tabIndex);
+ }
+ return this.tabInfo[tabIndex];
+ }
+ }
+
+ if (!aArgs.first && !background) {
+ // we need to save the state before it gets corrupted
+ this.saveCurrentTabState();
+ }
+
+ let tab = {
+ first: !!aArgs.first,
+ mode: tabMode,
+ busy: false,
+ canClose: true,
+ thinking: false,
+ beforeTabOpen: true,
+ favIconUrl: null,
+ _ext: {},
+ };
+
+ tab.tabId = this.tabId++;
+ tabMode.tabs.push(tab);
+
+ let t;
+ if (aArgs.first) {
+ t = this.tabContainer.querySelector(`tab[is="tabmail-tab"]`);
+ } else {
+ t = document.createXULElement("tab", { is: "tabmail-tab" });
+ t.className = "tabmail-tab";
+ t.setAttribute("validate", "never");
+ this.tabContainer.appendChild(t);
+ }
+ tab.tabNode = t;
+
+ if (
+ this.tabContainer.mCollapseToolbar.collapsed &&
+ (!this.tabContainer.mAutoHide || this.tabContainer.allTabs.length > 1)
+ ) {
+ this.tabContainer.mCollapseToolbar.collapsed = false;
+ this.tabContainer._updateCloseButtons();
+ document.documentElement.removeAttribute("tabbarhidden");
+ }
+
+ let oldTab = (this._mostRecentTabInfo = this.currentTabInfo);
+ // If we're not disregarding the opening, hold a reference to opener
+ // so that if the new tab is closed without switching, we can switch
+ // back to the opener tab.
+ if (aArgs.disregardOpener) {
+ this.mLastTabOpener = null;
+ } else {
+ this.mLastTabOpener = oldTab;
+ }
+
+ // the order of the following statements is important
+ this.tabInfo[this.tabContainer.allTabs.length - 1] = tab;
+ if (!background) {
+ this.currentTabInfo = tab;
+ // this has a side effect of calling updateCurrentTab, but our
+ // setting currentTabInfo above will cause it to take no action.
+ this.tabContainer.selectedIndex =
+ this.tabContainer.allTabs.length - 1;
+ }
+
+ // make sure we are on the right panel
+ if (tab.mode.tabType.perTabPanel) {
+ // should we create the element for them, or will they do it?
+ if (typeof tab.mode.tabType.perTabPanel == "string") {
+ tab.panel = document.createXULElement(tab.mode.tabType.perTabPanel);
+ } else {
+ tab.panel = tab.mode.tabType.perTabPanel(tab);
+ }
+
+ this.panelContainer.appendChild(tab.panel);
+
+ if (!background) {
+ this.panelContainer.selectedPanel = tab.panel;
+ }
+ } else {
+ if (!background) {
+ this.panelContainer.selectedPanel = tab.mode.tabType.panel;
+ }
+ t.linkedPanel = tab.mode.tabType.panelId;
+ }
+
+ // Make sure the new panel is marked selected.
+ let oldPanel = [...this.panelContainer.children].find(p =>
+ p.hasAttribute("selected")
+ );
+ // Blur the currently focused element only if we're actually switching
+ // to the newly opened tab.
+ if (oldPanel && !background) {
+ this.rememberLastActiveElement(oldTab);
+ oldPanel.removeAttribute("selected");
+ if (oldTab.chromeBrowser) {
+ oldTab.chromeBrowser.docShellIsActive = false;
+ }
+ }
+
+ this.panelContainer.selectedPanel.setAttribute("selected", "true");
+ let tabOpenFunc = tab.mode.openTab || tab.mode.tabType.openTab;
+ tabOpenFunc.apply(tab.mode.tabType, [tab, aArgs]);
+ if (tab.chromeBrowser) {
+ tab.chromeBrowser.docShellIsActive = !background;
+ }
+
+ if (!t.linkedPanel) {
+ if (!tab.panel.id) {
+ // No id set. Create our own.
+ tab.panel.id = "unnamedTab" + Math.random().toString().substring(2);
+ console.warn(`Tab mode ${aTabModeName} should set an id
+ on the first argument of openTab.`);
+ }
+ t.linkedPanel = tab.panel.id;
+ }
+
+ // Set the tabId after defining a <browser> and before notifications.
+ let browser = this.getBrowserForTab(tab);
+ if (browser && !tab.browser) {
+ tab.browser = browser;
+ if (!tab.linkedBrowser) {
+ tab.linkedBrowser = browser;
+ }
+ }
+
+ let restoreState = this._restoringTabState;
+ for (let tabMonitor of this.tabMonitors) {
+ try {
+ if (
+ "onTabRestored" in tabMonitor &&
+ restoreState &&
+ tabMonitor.monitorName in restoreState.ext
+ ) {
+ tabMonitor.onTabRestored(
+ tab,
+ restoreState.ext[tabMonitor.monitorName],
+ false
+ );
+ } else if ("onTabOpened" in tabMonitor) {
+ tabMonitor.onTabOpened(tab, false, oldTab);
+ }
+ if (!background) {
+ tabMonitor.onTabSwitched(tab, oldTab);
+ }
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+
+ // clear _mostRecentTabInfo; we only needed it during the call to
+ // openTab.
+ this._mostRecentTabInfo = null;
+ t.setAttribute("label", tab.title);
+ // For styling purposes, apply the type to the tab.
+ t.setAttribute("type", tab.mode.type);
+
+ if (!background) {
+ this.setDocumentTitle(tab);
+ // Move the focus on the newly selected tab.
+ this.panelContainer.selectedPanel.focus();
+ }
+
+ let moving = restoreState ? restoreState.moving : null;
+ // Dispatch tab opening event
+ let evt = new CustomEvent("TabOpen", {
+ bubbles: true,
+ detail: { tabInfo: tab, moving },
+ });
+ t.dispatchEvent(evt);
+ delete tab.beforeTabOpen;
+
+ contentProgress.addProgressListenerToBrowser(browser);
+
+ return tab;
+ } catch (e) {
+ console.error(e);
+ return null;
+ }
+ }
+
+ selectTabByMode(aTabModeName) {
+ let tabMode = this.tabModes[aTabModeName];
+ if (tabMode.tabs.length) {
+ let desiredTab = tabMode.tabs[0];
+ this.tabContainer.selectedIndex = this.tabInfo.indexOf(desiredTab);
+ }
+ }
+
+ selectTabByIndex(aEvent, aIndex) {
+ // count backwards for aIndex < 0
+ if (aIndex < 0) {
+ aIndex += this.tabInfo.length;
+ }
+
+ if (
+ aIndex >= 0 &&
+ aIndex < this.tabInfo.length &&
+ aIndex != this.tabContainer.selectedIndex
+ ) {
+ this.tabContainer.selectedIndex = aIndex;
+ }
+
+ if (aEvent) {
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ }
+ }
+
+ /**
+ * If the current/most recent tab is of mode aTabModeName, return its
+ * tab info, otherwise return the tab info for the first tab of the
+ * given mode.
+ * You would want to use this method when you would like to mimic the
+ * settings of an existing instance of your mode. In such a case,
+ * it is reasonable to assume that if the 'current' tab was of the
+ * same mode that its settings should be used. Otherwise, we must
+ * fall back to another tab. We currently choose the first tab of
+ * the instance, because for the "folder" tab, it is the canonical tab.
+ * In other cases, having an MRU order and choosing the MRU tab might
+ * be more appropriate.
+ *
+ * @returns the tab info object for the tab meeting the above criteria,
+ * or null if no such tab exists.
+ */
+ getTabInfoForCurrentOrFirstModeInstance(aTabMode) {
+ // If we're in the middle of opening a new tab
+ // (this._mostRecentTabInfo is non-null), we shouldn't consider the
+ // current tab
+ let tabToConsider = this._mostRecentTabInfo || this.currentTabInfo;
+ if (tabToConsider && tabToConsider.mode == aTabMode) {
+ return tabToConsider;
+ } else if (aTabMode.tabs.length) {
+ return aTabMode.tabs[0];
+ }
+
+ return null;
+ }
+
+ undoCloseTab(aIdx) {
+ if (!this.recentlyClosedTabs.length) {
+ return;
+ }
+ if (aIdx >= this.recentlyClosedTabs.length) {
+ aIdx = this.recentlyClosedTabs.length - 1;
+ }
+ // splice always returns an array
+ let history = this.recentlyClosedTabs.splice(aIdx, 1)[0];
+ if (!history.tab) {
+ return;
+ }
+
+ if (!this.restoreTab(JSON.parse(history.tab))) {
+ return;
+ }
+
+ let idx = Math.min(history.idx, this.tabInfo.length);
+ let tab = this.tabContainer.allTabs[this.tabInfo.length - 1];
+ this.moveTabTo(tab, idx);
+ this.switchToTab(tab);
+ }
+
+ closeTab(aOptTabIndexNodeOrInfo, aNoUndo) {
+ let [iTab, tab, tabNode] = this._getTabContextForTabbyThing(
+ aOptTabIndexNodeOrInfo,
+ true
+ );
+ if (!tab.canClose) {
+ return;
+ }
+
+ // Give the tab type a chance to make its own decisions about
+ // whether its tabs can be closed or not. For instance, contentTabs
+ // and chromeTabs run onbeforeunload event handlers that may
+ // exercise their right to prompt the user for confirmation before
+ // closing.
+ let tryCloseFunc = tab.mode.tryCloseTab || tab.mode.tabType.tryCloseTab;
+ if (tryCloseFunc && !tryCloseFunc.call(tab.mode.tabType, tab)) {
+ return;
+ }
+
+ let evt = new CustomEvent("TabClose", {
+ bubbles: true,
+ detail: { tabInfo: tab, moving: tab.moving },
+ });
+
+ tabNode.dispatchEvent(evt);
+ for (let tabMonitor of this.tabMonitors) {
+ try {
+ if ("onTabClosing" in tabMonitor) {
+ tabMonitor.onTabClosing(tab);
+ }
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+
+ if (!aNoUndo) {
+ // Allow user to undo accidentally closed tabs
+ let session = this.persistTab(tab);
+ if (session) {
+ this.recentlyClosedTabs.unshift({
+ tab: JSON.stringify(session),
+ idx: iTab,
+ title: tab.title,
+ });
+ if (this.recentlyClosedTabs.length > 10) {
+ this.recentlyClosedTabs.pop();
+ }
+ }
+ }
+
+ tab.closed = true;
+ let closeFunc = tab.mode.closeTab || tab.mode.tabType.closeTab;
+ closeFunc.call(tab.mode.tabType, tab);
+ this.tabInfo.splice(iTab, 1);
+ tab.mode.tabs.splice(tab.mode.tabs.indexOf(tab), 1);
+ tabNode.remove();
+
+ if (this.tabContainer.selectedIndex == -1) {
+ if (this.mLastTabOpener && this.tabInfo.includes(this.mLastTabOpener)) {
+ this.tabContainer.selectedIndex = this.tabInfo.indexOf(
+ this.mLastTabOpener
+ );
+ } else {
+ this.tabContainer.selectedIndex =
+ iTab == this.tabContainer.allTabs.length ? iTab - 1 : iTab;
+ }
+ }
+
+ // Clear the last tab opener - we don't need this anymore.
+ this.mLastTabOpener = null;
+ if (this.currentTabInfo == tab) {
+ this.updateCurrentTab();
+ }
+
+ if (tab.panel) {
+ tab.panel.remove();
+ delete tab.panel;
+ // Ensure current tab is still selected and displayed in the
+ // panelContainer.
+ this.panelContainer.selectedPanel =
+ this.currentTabInfo.panel || this.currentTabInfo.mode.tabType.panel;
+ }
+
+ if (
+ this.tabContainer.allTabs.length == 1 &&
+ this.tabContainer.mAutoHide
+ ) {
+ this.tabContainer.mCollapseToolbar.collapsed = true;
+ document.documentElement.setAttribute("tabbarhidden", "true");
+ }
+ }
+
+ removeTabByNode(aTabNode) {
+ this.closeTab(aTabNode);
+ }
+
+ /**
+ * Given a tabNode (or tabby thing), close all of the other tabs
+ * that are closeable.
+ */
+ closeOtherTabs(aTabNode, aNoUndo) {
+ let [, thisTab] = this._getTabContextForTabbyThing(aTabNode, false);
+ // closeTab mutates the tabInfo array, so start from the end.
+ for (let i = this.tabInfo.length - 1; i >= 0; i--) {
+ let tab = this.tabInfo[i];
+ if (tab != thisTab && tab.canClose) {
+ this.closeTab(tab, aNoUndo);
+ }
+ }
+ }
+
+ replaceTabWithWindow(aTab, aTargetWindow, aTargetPosition) {
+ if (this.tabInfo.length <= 1) {
+ return null;
+ }
+
+ let tab = this._getTabContextForTabbyThing(aTab, false)[1];
+ if (!tab.canClose) {
+ return null;
+ }
+
+ // We use JSON and session restore transfer the tab to the new window.
+ tab = this.persistTab(tab);
+ if (!tab) {
+ return null;
+ }
+
+ // Converting to JSON and back again creates clean javascript
+ // object with absolutely no references to our current window.
+ tab = JSON.parse(JSON.stringify(tab));
+ // Set up an identifier for the move, consumers may want to correlate TabClose and
+ // TabOpen events.
+ let moveSession = Services.uuid.generateUUID().toString();
+ tab.moving = moveSession;
+ aTab.moving = moveSession;
+ this.closeTab(aTab, true);
+
+ if (aTargetWindow && aTargetWindow !== "popup") {
+ let targetTabmail = aTargetWindow.document.getElementById("tabmail");
+ targetTabmail.restoreTab(tab);
+ if (aTargetPosition) {
+ let droppedTab =
+ targetTabmail.tabInfo[targetTabmail.tabInfo.length - 1];
+ targetTabmail.moveTabTo(droppedTab, aTargetPosition);
+ }
+ return aTargetWindow;
+ }
+
+ let features = ["chrome"];
+ if (aTargetWindow === "popup") {
+ features.push(
+ "dialog",
+ "resizable",
+ "minimizable",
+ "centerscreen",
+ "titlebar",
+ "close"
+ );
+ } else {
+ features.push("dialog=no", "all", "status", "toolbar");
+ }
+
+ return window
+ .openDialog(
+ "chrome://messenger/content/messenger.xhtml",
+ "_blank",
+ features.join(","),
+ null,
+ {
+ action: "restore",
+ tabs: [tab],
+ }
+ )
+ .focus();
+ }
+
+ moveTabTo(aTabIndexNodeOrInfo, aIndex) {
+ let [oldIdx, tab, tabNode] = this._getTabContextForTabbyThing(
+ aTabIndexNodeOrInfo,
+ false
+ );
+ if (
+ !tab ||
+ !tabNode ||
+ tabNode.tagName != "tab" ||
+ oldIdx < 0 ||
+ oldIdx == aIndex
+ ) {
+ return -1;
+ }
+
+ // remove the entries from tabInfo, tabMode and the tabContainer
+ this.tabInfo.splice(oldIdx, 1);
+ tab.mode.tabs.splice(tab.mode.tabs.indexOf(tab), 1);
+ tabNode.remove();
+ // as we removed items, we might need to update indices
+ if (oldIdx < aIndex) {
+ aIndex--;
+ }
+
+ // Read it into tabInfo and the tabContainer
+ this.tabInfo.splice(aIndex, 0, tab);
+ this.tabContainer.insertBefore(
+ tabNode,
+ this.tabContainer.allTabs[aIndex]
+ );
+ // Now it's getting a bit ugly, as tabModes stores redundant
+ // information we need to get it in sync with tabInfo.
+ //
+ // As tabModes.tabs is a subset of tabInfo, every tab can be mapped
+ // to a tabInfo index. So we check for each tab in tabModes if it is
+ // directly in front of our moved tab. We do this by looking up the
+ // index in tabInfo and compare it with the moved tab's index. If we
+ // found our tab, we insert the moved tab directly behind into tabModes
+ // In case find no tab we simply append it
+ let modeIdx = tab.mode.tabs.length + 1;
+ for (let i = 0; i < tab.mode.tabs.length; i++) {
+ if (this.tabInfo.indexOf(tab.mode.tabs[i]) < aIndex) {
+ continue;
+ }
+ modeIdx = i;
+ break;
+ }
+
+ tab.mode.tabs.splice(modeIdx, 0, tab);
+ let evt = new CustomEvent("TabMove", {
+ bubbles: true,
+ view: window,
+ detail: { idx: oldIdx, tabInfo: tab },
+ });
+ tabNode.dispatchEvent(evt);
+
+ return aIndex;
+ }
+
+ // Returns null in case persist fails.
+ persistTab(tab) {
+ let persistFunc = tab.mode.persistTab || tab.mode.tabType.persistTab;
+ // if we can't restore the tab we can't move it
+ if (!persistFunc) {
+ return null;
+ }
+
+ // If there is a non-null tab-state, then persisting succeeded and
+ // we should store it. We store the tab's persisted state in its
+ // own distinct object rather than mixing things up in a dictionary
+ // to avoid bugs and because we may eventually let extensions store
+ // per-tab information in the persisted state.
+ let tabState;
+ // Wrap this in an exception handler so that if the persistence
+ // logic fails, things like tab closure still run to completion.
+ try {
+ tabState = persistFunc.call(tab.mode.tabType, tab);
+ } catch (ex) {
+ // Report this so that our unit testing framework sees this
+ // error and (extension) developers likewise can see when their
+ // extensions are ill-behaved.
+ console.error(ex);
+ }
+
+ if (!tabState) {
+ return null;
+ }
+
+ let ext = {};
+ for (let tabMonitor of this.tabMonitors) {
+ try {
+ if ("onTabPersist" in tabMonitor) {
+ let monState = tabMonitor.onTabPersist(tab);
+ if (monState !== null) {
+ ext[tabMonitor.monitorName] = monState;
+ }
+ }
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+
+ return { mode: tab.mode.name, state: tabState, ext };
+ }
+
+ /**
+ * Persist the state of all tab modes implementing persistTab methods
+ * to a JSON-serializable object representation and return it. Call
+ * restoreTabs with the result to restore the tab state.
+ * Calling this method should have no side effects; tabs will not be
+ * closed, displays will not change, etc. This means the method is
+ * safe to use in an auto-save style so that if we crash we can
+ * restore the (approximate) state at the time of the crash.
+ *
+ * @returns {object} The persisted tab states.
+ */
+ persistTabs() {
+ let state = {
+ // Explicitly specify a revision so we don't wish we had later.
+ rev: 0,
+ // If our currently selected tab gets persisted, we will update this
+ selectedIndex: null,
+ };
+
+ let tabs = (state.tabs = []);
+ for (let [iTab, tab] of this.tabInfo.entries()) {
+ let persistTab = this.persistTab(tab);
+ if (!persistTab) {
+ continue;
+ }
+ tabs.push(persistTab);
+ // Mark this persisted tab as selected
+ if (iTab == this.tabContainer.selectedIndex) {
+ state.selectedIndex = tabs.length - 1;
+ }
+ }
+
+ return state;
+ }
+
+ restoreTab(aState) {
+ // Migrate old mail tabs to new mail tabs. This can be removed after ESR 115.
+ if (aState.mode == "folder") {
+ aState.mode = "mail3PaneTab";
+ } else if (aState.mode == "message") {
+ aState.mode = "mailMessageTab";
+ }
+
+ // if we no longer know about the mode, we can't restore the tab
+ let mode = this.tabModes[aState.mode];
+ if (!mode) {
+ this.unrestoredTabs.push(aState);
+ return false;
+ }
+
+ let restoreFunc = mode.restoreTab || mode.tabType.restoreTab;
+ if (!restoreFunc) {
+ return false;
+ }
+
+ // normalize the state to have an ext attribute if it does not.
+ if (!("ext" in aState)) {
+ aState.ext = {};
+ }
+
+ this._restoringTabState = aState;
+ restoreFunc.call(mode.tabType, this, aState.state);
+ this._restoringTabState = null;
+
+ return true;
+ }
+
+ /**
+ * Attempts to restore tabs persisted from a prior call to
+ * |persistTabs|. This is currently a synchronous operation, but in
+ * the future this may kick off an asynchronous mechanism to restore
+ * the tabs one-by-one.
+ */
+ restoreTabs(aPersistedState, aDontRestoreFirstTab) {
+ let tabs = aPersistedState.tabs;
+ let indexToSelect = null;
+
+ for (let [iTab, tabState] of tabs.entries()) {
+ if (tabState.state.firstTab && aDontRestoreFirstTab) {
+ tabState.state.dontRestoreFirstTab = aDontRestoreFirstTab;
+ }
+
+ if (!this.restoreTab(tabState)) {
+ continue;
+ }
+
+ // If this persisted tab was the selected one, then mark the newest
+ // tab as the guy to select.
+ if (iTab == aPersistedState.selectedIndex) {
+ indexToSelect = this.tabInfo.length - 1;
+ }
+ }
+
+ if (indexToSelect != null && !aDontRestoreFirstTab) {
+ this.tabContainer.selectedIndex = indexToSelect;
+ } else {
+ this.tabContainer.selectedIndex = 0;
+ }
+
+ if (
+ this.tabContainer.allTabs.length == 1 &&
+ this.tabContainer.mAutoHide
+ ) {
+ this.tabContainer.mCollapseToolbar.collapsed = true;
+ document.documentElement.setAttribute("tabbarhidden", "true");
+ }
+ }
+
+ clearRecentlyClosedTabs() {
+ this.recentlyClosedTabs.length = 0;
+ }
+ /**
+ * Called when the window is being unloaded, this calls the close
+ * function for every tab.
+ */
+ _teardown() {
+ for (var i = 0; i < this.tabInfo.length; i++) {
+ let tab = this.tabInfo[i];
+ let tabCloseFunc = tab.mode.closeTab || tab.mode.tabType.closeTab;
+ tabCloseFunc.call(tab.mode.tabType, tab);
+ }
+ }
+
+ /**
+ * The content window of the current tab, if it is a 3-pane tab.
+ *
+ * @type {?Window}
+ */
+ get currentAbout3Pane() {
+ if (this.currentTabInfo.mode.name == "mail3PaneTab") {
+ return this.currentTabInfo.chromeBrowser.contentWindow;
+ }
+ return null;
+ }
+
+ /**
+ * The content window of the current tab, if it is a message tab, OR if it
+ * is a 3-pane tab, the content window of the message browser within.
+ *
+ * @type {?Window}
+ */
+ get currentAboutMessage() {
+ switch (this.currentTabInfo.mode.name) {
+ case "mail3PaneTab": {
+ let messageBrowser = this.currentAbout3Pane.messageBrowser;
+ return messageBrowser && !messageBrowser.hidden
+ ? messageBrowser.contentWindow
+ : null;
+ }
+ case "mailMessageTab":
+ return this.currentTabInfo.chromeBrowser.contentWindow;
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * getBrowserForSelectedTab is required as some toolkit functions
+ * require a getBrowser() function.
+ */
+ getBrowserForSelectedTab() {
+ if (!this.tabInfo) {
+ return null;
+ }
+
+ if (!this.currentTabInfo) {
+ this.currentTabInfo = this.tabInfo[0];
+ }
+
+ if (this.currentTabInfo) {
+ return this.getBrowserForTab(this.currentTabInfo);
+ }
+
+ return null;
+ }
+
+ getBrowserForTab(aTab) {
+ let browserFunc = aTab
+ ? aTab.mode.getBrowser || aTab.mode.tabType.getBrowser
+ : null;
+ return browserFunc ? browserFunc.call(aTab.mode.tabType, aTab) : null;
+ }
+
+ /**
+ * getBrowserForDocument is used to find the browser for a specific
+ * document that's been loaded
+ */
+ getBrowserForDocument(aDocument) {
+ for (let i = 0; i < this.tabInfo.length; ++i) {
+ let browserFunc =
+ this.tabInfo[i].mode.getBrowser ||
+ this.tabInfo[i].mode.tabType.getBrowser;
+
+ if (browserFunc) {
+ let possBrowser = browserFunc.call(
+ this.tabInfo[i].mode.tabType,
+ this.tabInfo[i]
+ );
+ if (possBrowser && possBrowser.contentWindow == aDocument) {
+ return this.tabInfo[i];
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * getBrowserForDocumentId is used to find the browser for a specific
+ * document via its id attribute.
+ */
+ getBrowserForDocumentId(aDocumentId) {
+ for (let i = 0; i < this.tabInfo.length; ++i) {
+ let browserFunc =
+ this.tabInfo[i].mode.getBrowser ||
+ this.tabInfo[i].mode.tabType.getBrowser;
+ if (browserFunc) {
+ let possBrowser = browserFunc.call(
+ this.tabInfo[i].mode.tabType,
+ this.tabInfo[i]
+ );
+ if (
+ possBrowser &&
+ possBrowser.contentDocument.documentElement.id == aDocumentId
+ ) {
+ return this.tabInfo[i];
+ }
+ }
+ }
+
+ return null;
+ }
+
+ getTabForBrowser(aBrowser) {
+ // Check the selected browser first, since that's the most likely.
+ if (this.getBrowserForSelectedTab() == aBrowser) {
+ return this.currentTabInfo;
+ }
+ for (let tabInfo of this.tabInfo) {
+ if (this.getBrowserForTab(tabInfo) == aBrowser) {
+ return tabInfo;
+ }
+ }
+ return null;
+ }
+
+ removeCurrentTab() {
+ this.removeTabByNode(
+ this.tabContainer.allTabs[this.tabContainer.selectedIndex]
+ );
+ }
+
+ switchToTab(aTabIndexNodeOrInfo) {
+ let [iTab] = this._getTabContextForTabbyThing(aTabIndexNodeOrInfo, false);
+ this.tabContainer.selectedIndex = iTab;
+ }
+
+ /**
+ * Finds the active element and stores it on `tabInfo` for restoring focus
+ * when this tab next becomes active.
+ *
+ * @param {object} tabInfo
+ */
+ rememberLastActiveElement(tabInfo) {
+ // Check for anything inside tabmail-container rather than the panel
+ // because focus could be in the Today Pane.
+ let activeElement = document.activeElement;
+ let container = document.getElementById("tabmail-container");
+ if (container.contains(activeElement)) {
+ while (activeElement.localName == "browser") {
+ let next = activeElement.contentDocument?.activeElement;
+ if (!next || next.localName == "body") {
+ break;
+ }
+ activeElement = next;
+ }
+ // If the active element is inside a container, store the container
+ // instead of the element, so that `.focus()` returns focus to the
+ // right place.
+ tabInfo.lastActiveElement =
+ activeElement.closest("[aria-activedescendant]") ?? activeElement;
+ Services.focus.clearFocus(window);
+ } else {
+ delete tabInfo.lastActiveElement;
+ }
+ }
+
+ /**
+ * UpdateCurrentTab - called in response to changing the current tab.
+ */
+ updateCurrentTab() {
+ if (
+ this.currentTabInfo != this.tabInfo[this.tabContainer.selectedIndex]
+ ) {
+ if (this.currentTabInfo) {
+ this.saveCurrentTabState();
+ }
+
+ let oldTab = this.currentTabInfo;
+ let oldPanel = [...this.panelContainer.children].find(p =>
+ p.hasAttribute("selected")
+ );
+ let tab = (this.currentTabInfo =
+ this.tabInfo[this.tabContainer.selectedIndex]);
+ // Update the selected attribute on the current and old tab panel.
+ if (oldPanel) {
+ this.rememberLastActiveElement(oldTab);
+ oldPanel.removeAttribute("selected");
+ if (oldTab.chromeBrowser) {
+ oldTab.chromeBrowser.docShellIsActive = false;
+ }
+ }
+
+ this.panelContainer.selectedPanel.setAttribute("selected", "true");
+ let showTabFunc = tab.mode.showTab || tab.mode.tabType.showTab;
+ showTabFunc.call(tab.mode.tabType, tab);
+ if (tab.chromeBrowser) {
+ tab.chromeBrowser.docShellIsActive = true;
+ }
+
+ let browser = this.getBrowserForTab(tab);
+ if (browser && !tab.browser) {
+ tab.browser = browser;
+ if (!tab.linkedBrowser) {
+ tab.linkedBrowser = browser;
+ }
+ }
+
+ for (let tabMonitor of this.tabMonitors) {
+ try {
+ tabMonitor.onTabSwitched(tab, oldTab);
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+
+ // always update the cursor status when we switch tabs
+ SetBusyCursor(window, tab.busy);
+ // active tabs should not have the wasBusy attribute
+ this.tabContainer.selectedItem.removeAttribute("wasBusy");
+ // update the thinking status when we switch tabs
+ this._setActiveThinkingState(tab.thinking);
+ // active tabs should not have the wasThinking attribute
+ this.tabContainer.selectedItem.removeAttribute("wasThinking");
+ this.setDocumentTitle(tab);
+
+ // We switched tabs, so we don't need to know the last tab
+ // opener anymore.
+ this.mLastTabOpener = null;
+
+ // Try to set focus where it was when the tab was last selected.
+ this.panelContainer.selectedPanel.focus();
+ if (tab.lastActiveElement) {
+ tab.lastActiveElement.focus();
+ delete tab.lastActiveElement;
+ }
+
+ let evt = new CustomEvent("TabSelect", {
+ bubbles: true,
+ detail: {
+ tabInfo: tab,
+ previousTabInfo: oldTab,
+ },
+ });
+ this.tabContainer.selectedItem.dispatchEvent(evt);
+ }
+ }
+
+ saveCurrentTabState() {
+ if (!this.currentTabInfo) {
+ this.currentTabInfo = this.tabInfo[0];
+ }
+
+ let tab = this.currentTabInfo;
+ // save the old tab state before we change the current tab
+ let saveTabFunc = tab.mode.saveTabState || tab.mode.tabType.saveTabState;
+ saveTabFunc.call(tab.mode.tabType, tab);
+ }
+
+ setTabTitle(aTabNodeOrInfo) {
+ let [iTab, tab] = this._getTabContextForTabbyThing(aTabNodeOrInfo, true);
+ if (tab) {
+ let tabNode = this.tabContainer.allTabs[iTab];
+ let titleChangeFunc =
+ tab.mode.onTitleChanged || tab.mode.tabType.onTitleChanged;
+ if (titleChangeFunc) {
+ titleChangeFunc.call(tab.mode.tabType, tab, tabNode);
+ }
+
+ let defaultTabTitle =
+ document.documentElement.getAttribute("defaultTabTitle");
+ let oldLabel = tabNode.getAttribute("label");
+ let newLabel = aTabNodeOrInfo ? tab.title : defaultTabTitle;
+ if (oldLabel == newLabel) {
+ return;
+ }
+
+ for (let tabMonitor of this.tabMonitors) {
+ try {
+ tabMonitor.onTabTitleChanged(tab);
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+
+ // If the displayed tab is the one at the moment of creation
+ // (aTabNodeOrInfo is null), set the default title as its title.
+ tabNode.setAttribute("label", newLabel);
+ // Update the window title if we're the displayed tab.
+ if (iTab == this.tabContainer.selectedIndex) {
+ this.setDocumentTitle(tab);
+ }
+
+ // Notify tab title change
+ if (!tab.beforeTabOpen) {
+ let evt = new CustomEvent("TabAttrModified", {
+ bubbles: true,
+ cancelable: false,
+ detail: { changed: ["label"], tabInfo: tab },
+ });
+ tabNode.dispatchEvent(evt);
+ }
+ }
+ }
+
+ /**
+ * Set the favIconUrl for the given tab and display it as the tab's icon.
+ * If the given favicon is missing or loads with an error, a fallback icon
+ * will be displayed instead.
+ *
+ * Note that the new favIconUrl is reported to the extension API's
+ * tabs.onUpdated.
+ *
+ * @param {object} tabInfo - The tabInfo object for the tab.
+ * @param {string|null} favIconUrl - The favIconUrl to set for the given
+ * tab.
+ * @param {string} fallbackSrc - The fallback icon src to display in case
+ * of missing or broken favicons.
+ */
+ setTabFavIcon(tabInfo, favIconUrl, fallbackSrc) {
+ let prevUrl = tabInfo.favIconUrl;
+ // The favIconUrl value is used by the TabmailTab _favIconUrl getter,
+ // which is used by the tab wrapper in the TabAttrModified callback.
+ tabInfo.favIconUrl = favIconUrl;
+ // NOTE: we always report the given favIconUrl, rather than the icon that
+ // is used in the tab. In particular, if the favIconUrl is null, we pass
+ // null rather than the fallbackIcon that is displayed.
+ if (favIconUrl != prevUrl && !tabInfo.beforeTabOpen) {
+ let evt = new CustomEvent("TabAttrModified", {
+ bubbles: true,
+ cancelable: false,
+ detail: { changed: ["favIconUrl"], tabInfo },
+ });
+ tabInfo.tabNode.dispatchEvent(evt);
+ }
+
+ tabInfo.tabNode.setIcon(favIconUrl, fallbackSrc);
+ }
+
+ /**
+ * Updates the global state to reflect the active tab's thinking
+ * state (which the caller provides).
+ */
+ _setActiveThinkingState(aThinkingState) {
+ if (aThinkingState) {
+ statusFeedback.showProgress(0);
+ if (typeof aThinkingState == "string") {
+ statusFeedback.showStatusString(aThinkingState);
+ }
+ } else {
+ statusFeedback.showProgress(0);
+ }
+ }
+
+ setTabThinking(aTabNodeOrInfo, aThinking) {
+ let [iTab, tab, tabNode] = this._getTabContextForTabbyThing(
+ aTabNodeOrInfo,
+ false
+ );
+ let isSelected = iTab == this.tabContainer.selectedIndex;
+ // if we are the current tab, update the cursor
+ if (isSelected) {
+ this._setActiveThinkingState(aThinking);
+ }
+
+ // if we are busy, hint our tab
+ if (aThinking) {
+ tabNode.setAttribute("thinking", "true");
+ } else {
+ // if we were thinking and are not selected, set the
+ // "wasThinking" attribute.
+ if (tab.thinking && !isSelected) {
+ tabNode.setAttribute("wasThinking", "true");
+ }
+ tabNode.removeAttribute("thinking");
+ }
+
+ // update the tab info to store the busy state.
+ tab.thinking = aThinking;
+ }
+
+ setTabBusy(aTabNodeOrInfo, aBusy) {
+ let [iTab, tab, tabNode] = this._getTabContextForTabbyThing(
+ aTabNodeOrInfo,
+ false
+ );
+ let isSelected = iTab == this.tabContainer.selectedIndex;
+
+ // if we are the current tab, update the cursor
+ if (isSelected) {
+ SetBusyCursor(window, aBusy);
+ }
+
+ // if we are busy, hint our tab
+ if (aBusy) {
+ tabNode.setAttribute("busy", "true");
+ } else {
+ // if we were busy and are not selected, set the
+ // "wasBusy" attribute.
+ if (tab.busy && !isSelected) {
+ tabNode.setAttribute("wasBusy", "true");
+ }
+ tabNode.removeAttribute("busy");
+ }
+
+ // update the tab info to store the busy state.
+ tab.busy = aBusy;
+ }
+
+ /**
+ * Set the document title based on the tab title
+ */
+ setDocumentTitle(aTab = this.selectedTab) {
+ let docTitle = aTab.title ? aTab.title.trim() : "";
+ let docElement = document.documentElement;
+ // If the document title is blank, add the default title.
+ if (!docTitle) {
+ docTitle = docElement.getAttribute("defaultTabTitle");
+ }
+
+ if (docElement.hasAttribute("titlepreface")) {
+ docTitle = docElement.getAttribute("titlepreface") + docTitle;
+ }
+
+ // If we're on Mac, don't display the separator and the modifier.
+ if (AppConstants.platform != "macosx") {
+ docTitle +=
+ docElement.getAttribute("titlemenuseparator") +
+ docElement.getAttribute("titlemodifier");
+ }
+
+ document.title = docTitle;
+ }
+
+ // Called by <browser>, unused by tabmail.
+ finishBrowserRemotenessChange(browser, loadSwitchId) {}
+
+ /**
+ * Returns the find bar for a tab.
+ */
+ getCachedFindBar(tab = this.selectedTab) {
+ return tab.findbar ?? null;
+ }
+
+ /**
+ * Implementation of gBrowser's lazy-loaded find bar. We don't lazily load
+ * the find bar, and some of our tabs don't have a find bar.
+ */
+ async getFindBar(tab = this.selectedTab) {
+ return tab.findbar ?? null;
+ }
+
+ disconnectedCallback() {
+ window.controllers.removeController(this.tabController);
+ }
+ }
+
+ customElements.define("tabmail", MozTabmail);
+}
+
+/**
+ * Refresh the contents of the recently closed tags popup menu/panel.
+ * Used for example for appmenu/Go/Recently_Closed_Tabs panel.
+ *
+ * @param {Element} parent - Parent element that will contain the menu items.
+ * @param {string} [elementName] - Type of menu item, e.g. "menuitem", "toolbarbutton".
+ * @param {string} [classes] - Classes to set on the menu items.
+ * @param {string} [separatorName] - Type of separator, e.g. "menuseparator", "toolbarseparator".
+ */
+function InitRecentlyClosedTabsPopup(
+ parent,
+ elementName = "menuitem",
+ classes,
+ separatorName = "menuseparator"
+) {
+ const tabs = document.getElementById("tabmail").recentlyClosedTabs;
+
+ // Show Popup only when there are restorable tabs.
+ if (!tabs.length) {
+ return false;
+ }
+
+ // Clear the list.
+ while (parent.hasChildNodes()) {
+ parent.lastChild.remove();
+ }
+
+ // Insert menu items to rebuild the recently closed tab list.
+ tabs.forEach((tab, index) => {
+ const item = document.createXULElement(elementName);
+ item.setAttribute("label", tab.title);
+ item.setAttribute(
+ "oncommand",
+ `document.getElementById("tabmail").undoCloseTab(${index});`
+ );
+ if (classes) {
+ item.setAttribute("class", classes);
+ }
+
+ if (index == 0) {
+ item.setAttribute("key", "key_undoCloseTab");
+ }
+ parent.appendChild(item);
+ });
+
+ // Only show "Restore All Tabs" if there is more than one tab to restore.
+ if (tabs.length > 1) {
+ parent.appendChild(document.createXULElement(separatorName));
+
+ const item = document.createXULElement(elementName);
+ item.setAttribute(
+ "label",
+ document.getElementById("bundle_messenger").getString("restoreAllTabs")
+ );
+
+ item.addEventListener("command", () => {
+ let tabmail = document.getElementById("tabmail");
+ let len = tabmail.recentlyClosedTabs.length;
+ while (len--) {
+ document.getElementById("tabmail").undoCloseTab();
+ }
+ });
+
+ if (classes) {
+ item.setAttribute("class", classes);
+ }
+ parent.appendChild(item);
+ }
+
+ return true;
+}
+
+// Set up the tabContextMenu, which is used as the context menu for all tabmail
+// tabs.
+window.addEventListener(
+ "DOMContentLoaded",
+ () => {
+ let tabmail = document.getElementById("tabmail");
+ let tabMenu = document.getElementById("tabContextMenu");
+
+ let openInWindowItem = document.getElementById(
+ "tabContextMenuOpenInWindow"
+ );
+ let closeOtherTabsItem = document.getElementById(
+ "tabContextMenuCloseOtherTabs"
+ );
+ let recentlyClosedMenu = document.getElementById(
+ "tabContextMenuRecentlyClosed"
+ );
+ let closeItem = document.getElementById("tabContextMenuClose");
+
+ // Shared variable: the tabNode that was activated to open the context menu.
+ let currentTabInfo = null;
+
+ tabMenu.addEventListener("popupshowing", () => {
+ let tabNode = tabMenu.triggerNode?.closest("tab");
+
+ // this happens when the user did not actually-click on a tab but
+ // instead on the strip behind it.
+ if (!tabNode) {
+ currentTabInfo = null;
+ return false;
+ }
+
+ currentTabInfo = tabmail.tabInfo.find(info => info.tabNode == tabNode);
+ openInWindowItem.setAttribute(
+ "disabled",
+ currentTabInfo.canClose && tabmail.persistTab(currentTabInfo)
+ );
+ closeOtherTabsItem.setAttribute(
+ "disabled",
+ tabmail.tabInfo.every(info => info == currentTabInfo || !info.canClose)
+ );
+ recentlyClosedMenu.setAttribute(
+ "disabled",
+ !tabmail.recentlyClosedTabs.length
+ );
+ closeItem.setAttribute("disabled", !currentTabInfo.canClose);
+ return true;
+ });
+
+ // Tidy up.
+ tabMenu.addEventListener("popuphidden", () => {
+ currentTabInfo = null;
+ });
+
+ openInWindowItem.addEventListener("command", () => {
+ tabmail.replaceTabWithWindow(currentTabInfo);
+ });
+ closeOtherTabsItem.addEventListener("command", () => {
+ tabmail.closeOtherTabs(currentTabInfo);
+ });
+ closeItem.addEventListener("command", () => {
+ tabmail.closeTab(currentTabInfo);
+ });
+
+ let recentlyClosedPopup = recentlyClosedMenu.querySelector("menupopup");
+ recentlyClosedPopup.addEventListener("popupshowing", () =>
+ InitRecentlyClosedTabsPopup(recentlyClosedPopup)
+ );
+
+ // Register the tabmail window font size only after everything else loaded.
+ UIFontSize.registerWindow(window);
+ },
+ { once: true }
+);