diff options
Diffstat (limited to 'comm/mail/base/content/tabmail.js')
-rw-r--r-- | comm/mail/base/content/tabmail.js | 2048 |
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 } +); |