releases-comm-central/mail/base/content/tabmail.js

2082 строки
72 KiB
JavaScript
Исходник Обычный вид История

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* global MozElements, MozXULElement */
/* import-globals-from commandglue.js */
/* import-globals-from mailCore.js */
/* import-globals-from mailWindow.js */
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()) {
Bug 1570954 - Port bug 1479125 to Thunderbird: Rewrite callers firstChild/lastChild/childNodes/previousSibling/nextSibling with firstElementChild/lastElementChild/children/previousElementSibling/nextElementSibling. r=pmorris This allows the JS to work in HTML documents, where whitespace is preserved. In XUL documents, whitespace is ignored when parsing so text nodes are generally not returned. The following changes were made, with manual cleanups as necessary (i.e. when firstChild actually refers to a text node, or when firstChild is used in a loop to empty out an element): firstChild->firstElementChild lastChild->lastElementChild nextSibling->nextElementSibling previousSibling->previousElementSibling childNodes->children # firstChild -> firstElementChild grep -rl '\bfirstChild\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\bfirstChild\b/firstElementChild/g' # lastChild->lastElementChild grep -rl '\lastChild\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\blastChild\b/lastElementChild/g' # nextSibling->nextElementSibling grep -rl '\nextSibling\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\bnextSibling\b/nextElementSibling/g' # previousSibling->previousElementSibling grep -rl '\previousSibling\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\bpreviousSibling\b/previousElementSibling/g' # childNodes->children grep -rl '\childNodes\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\bchildNodes\b/children/g' # revert for clearning out. lastElementChild.remove() -> lastChild.remove() grep -rl '\lastElementChild.remove\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\lastElementChild.remove\b/lastChild.remove/g' # skip matches in cpp and other wrong changes hg revert mailnews/base/src/nsSubscribableServer.cpp hg revert mailnews/base/src/nsSubscribableServer.h hg revert mailnews/mime/src/mimemsig.cpp hg revert mail/test/mozmill/composition/test-signature-updating.js hg revert mail/test/mozmill/shared-modules/DOMHelpers.jsm hg revert mailnews/addrbook/jsaddrbook/AddrBookMailingList.jsm hg revert mailnews/addrbook/public/nsIAbDirectory.idl hg revert mailnews/addrbook/test/unit/test_basic_nsIAbDirectory.js hg revert mailnews/addrbook/test/unit/test_jsaddrbook.js hg revert mailnews/addrbook/test/unit/test_mailList1.js hg revert mailnews/addrbook/test/unit/test_uid.js hg revert mailnews/compose/src/nsMsgCompose.cpp hg revert mail/test/mozmill/cloudfile/test-cloudfile-attachment-urls.js hg revert mail/test/mozmill/composition/test-forward-headers.js hg revert calendar/import-export/calHtmlExport.js hg revert mail/components/compose/content/cloudAttachmentLinkManager.js hg revert mail/components/compose/content/dialogs/EdConvertToTable.js hg revert mail/components/compose/content/dialogs/EdDialogCommon.js hg revert mail/components/compose/content/dialogs/EdInsertTOC.js hg revert mail/components/customizableui/CustomizeMode.jsm hg revert mail/components/customizableui/PanelMultiView.jsm hg revert mail/test/mozmill/shared-modules/ComposeHelpers.jsm hg revert mailnews/addrbook/jsaddrbook/AddrBookDirectory.jsm hg revert mailnews/base/util/JXON.js hg revert mailnews/base/util/errUtils.js hg revert mailnews/extensions/newsblog/content/feed-parser.js hg revert mail/components/compose/texzilla/TeXZilla.js ../mach eslint --fix
2019-10-13 21:42:57 +03:00
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();
Bug 1570954 - Port bug 1479125 to Thunderbird: Rewrite callers firstChild/lastChild/childNodes/previousSibling/nextSibling with firstElementChild/lastElementChild/children/previousElementSibling/nextElementSibling. r=pmorris This allows the JS to work in HTML documents, where whitespace is preserved. In XUL documents, whitespace is ignored when parsing so text nodes are generally not returned. The following changes were made, with manual cleanups as necessary (i.e. when firstChild actually refers to a text node, or when firstChild is used in a loop to empty out an element): firstChild->firstElementChild lastChild->lastElementChild nextSibling->nextElementSibling previousSibling->previousElementSibling childNodes->children # firstChild -> firstElementChild grep -rl '\bfirstChild\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\bfirstChild\b/firstElementChild/g' # lastChild->lastElementChild grep -rl '\lastChild\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\blastChild\b/lastElementChild/g' # nextSibling->nextElementSibling grep -rl '\nextSibling\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\bnextSibling\b/nextElementSibling/g' # previousSibling->previousElementSibling grep -rl '\previousSibling\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\bpreviousSibling\b/previousElementSibling/g' # childNodes->children grep -rl '\childNodes\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\bchildNodes\b/children/g' # revert for clearning out. lastElementChild.remove() -> lastChild.remove() grep -rl '\lastElementChild.remove\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\lastElementChild.remove\b/lastChild.remove/g' # skip matches in cpp and other wrong changes hg revert mailnews/base/src/nsSubscribableServer.cpp hg revert mailnews/base/src/nsSubscribableServer.h hg revert mailnews/mime/src/mimemsig.cpp hg revert mail/test/mozmill/composition/test-signature-updating.js hg revert mail/test/mozmill/shared-modules/DOMHelpers.jsm hg revert mailnews/addrbook/jsaddrbook/AddrBookMailingList.jsm hg revert mailnews/addrbook/public/nsIAbDirectory.idl hg revert mailnews/addrbook/test/unit/test_basic_nsIAbDirectory.js hg revert mailnews/addrbook/test/unit/test_jsaddrbook.js hg revert mailnews/addrbook/test/unit/test_mailList1.js hg revert mailnews/addrbook/test/unit/test_uid.js hg revert mailnews/compose/src/nsMsgCompose.cpp hg revert mail/test/mozmill/cloudfile/test-cloudfile-attachment-urls.js hg revert mail/test/mozmill/composition/test-forward-headers.js hg revert calendar/import-export/calHtmlExport.js hg revert mail/components/compose/content/cloudAttachmentLinkManager.js hg revert mail/components/compose/content/dialogs/EdConvertToTable.js hg revert mail/components/compose/content/dialogs/EdDialogCommon.js hg revert mail/components/compose/content/dialogs/EdInsertTOC.js hg revert mail/components/customizableui/CustomizeMode.jsm hg revert mail/components/customizableui/PanelMultiView.jsm hg revert mail/test/mozmill/shared-modules/ComposeHelpers.jsm hg revert mailnews/addrbook/jsaddrbook/AddrBookDirectory.jsm hg revert mailnews/base/util/JXON.js hg revert mailnews/base/util/errUtils.js hg revert mailnews/extensions/newsblog/content/feed-parser.js hg revert mail/components/compose/texzilla/TeXZilla.js ../mach eslint --fix
2019-10-13 21:42:57 +03:00
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
) {
Bug 1570954 - Port bug 1479125 to Thunderbird: Rewrite callers firstChild/lastChild/childNodes/previousSibling/nextSibling with firstElementChild/lastElementChild/children/previousElementSibling/nextElementSibling. r=pmorris This allows the JS to work in HTML documents, where whitespace is preserved. In XUL documents, whitespace is ignored when parsing so text nodes are generally not returned. The following changes were made, with manual cleanups as necessary (i.e. when firstChild actually refers to a text node, or when firstChild is used in a loop to empty out an element): firstChild->firstElementChild lastChild->lastElementChild nextSibling->nextElementSibling previousSibling->previousElementSibling childNodes->children # firstChild -> firstElementChild grep -rl '\bfirstChild\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\bfirstChild\b/firstElementChild/g' # lastChild->lastElementChild grep -rl '\lastChild\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\blastChild\b/lastElementChild/g' # nextSibling->nextElementSibling grep -rl '\nextSibling\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\bnextSibling\b/nextElementSibling/g' # previousSibling->previousElementSibling grep -rl '\previousSibling\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\bpreviousSibling\b/previousElementSibling/g' # childNodes->children grep -rl '\childNodes\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\bchildNodes\b/children/g' # revert for clearning out. lastElementChild.remove() -> lastChild.remove() grep -rl '\lastElementChild.remove\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\lastElementChild.remove\b/lastChild.remove/g' # skip matches in cpp and other wrong changes hg revert mailnews/base/src/nsSubscribableServer.cpp hg revert mailnews/base/src/nsSubscribableServer.h hg revert mailnews/mime/src/mimemsig.cpp hg revert mail/test/mozmill/composition/test-signature-updating.js hg revert mail/test/mozmill/shared-modules/DOMHelpers.jsm hg revert mailnews/addrbook/jsaddrbook/AddrBookMailingList.jsm hg revert mailnews/addrbook/public/nsIAbDirectory.idl hg revert mailnews/addrbook/test/unit/test_basic_nsIAbDirectory.js hg revert mailnews/addrbook/test/unit/test_jsaddrbook.js hg revert mailnews/addrbook/test/unit/test_mailList1.js hg revert mailnews/addrbook/test/unit/test_uid.js hg revert mailnews/compose/src/nsMsgCompose.cpp hg revert mail/test/mozmill/cloudfile/test-cloudfile-attachment-urls.js hg revert mail/test/mozmill/composition/test-forward-headers.js hg revert calendar/import-export/calHtmlExport.js hg revert mail/components/compose/content/cloudAttachmentLinkManager.js hg revert mail/components/compose/content/dialogs/EdConvertToTable.js hg revert mail/components/compose/content/dialogs/EdDialogCommon.js hg revert mail/components/compose/content/dialogs/EdInsertTOC.js hg revert mail/components/customizableui/CustomizeMode.jsm hg revert mail/components/customizableui/PanelMultiView.jsm hg revert mail/test/mozmill/shared-modules/ComposeHelpers.jsm hg revert mailnews/addrbook/jsaddrbook/AddrBookDirectory.jsm hg revert mailnews/base/util/JXON.js hg revert mailnews/base/util/errUtils.js hg revert mailnews/extensions/newsblog/content/feed-parser.js hg revert mail/components/compose/texzilla/TeXZilla.js ../mach eslint --fix
2019-10-13 21:42:57 +03:00
this.children[i].setAttribute("tabIsVisible", "true");
} else {
Bug 1570954 - Port bug 1479125 to Thunderbird: Rewrite callers firstChild/lastChild/childNodes/previousSibling/nextSibling with firstElementChild/lastElementChild/children/previousElementSibling/nextElementSibling. r=pmorris This allows the JS to work in HTML documents, where whitespace is preserved. In XUL documents, whitespace is ignored when parsing so text nodes are generally not returned. The following changes were made, with manual cleanups as necessary (i.e. when firstChild actually refers to a text node, or when firstChild is used in a loop to empty out an element): firstChild->firstElementChild lastChild->lastElementChild nextSibling->nextElementSibling previousSibling->previousElementSibling childNodes->children # firstChild -> firstElementChild grep -rl '\bfirstChild\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\bfirstChild\b/firstElementChild/g' # lastChild->lastElementChild grep -rl '\lastChild\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\blastChild\b/lastElementChild/g' # nextSibling->nextElementSibling grep -rl '\nextSibling\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\bnextSibling\b/nextElementSibling/g' # previousSibling->previousElementSibling grep -rl '\previousSibling\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\bpreviousSibling\b/previousElementSibling/g' # childNodes->children grep -rl '\childNodes\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\bchildNodes\b/children/g' # revert for clearning out. lastElementChild.remove() -> lastChild.remove() grep -rl '\lastElementChild.remove\b' --exclude-dir=.hg --exclude-dir=suite --exclude-dir=editor . | xargs sed -i 's/\lastElementChild.remove\b/lastChild.remove/g' # skip matches in cpp and other wrong changes hg revert mailnews/base/src/nsSubscribableServer.cpp hg revert mailnews/base/src/nsSubscribableServer.h hg revert mailnews/mime/src/mimemsig.cpp hg revert mail/test/mozmill/composition/test-signature-updating.js hg revert mail/test/mozmill/shared-modules/DOMHelpers.jsm hg revert mailnews/addrbook/jsaddrbook/AddrBookMailingList.jsm hg revert mailnews/addrbook/public/nsIAbDirectory.idl hg revert mailnews/addrbook/test/unit/test_basic_nsIAbDirectory.js hg revert mailnews/addrbook/test/unit/test_jsaddrbook.js hg revert mailnews/addrbook/test/unit/test_mailList1.js hg revert mailnews/addrbook/test/unit/test_uid.js hg revert mailnews/compose/src/nsMsgCompose.cpp hg revert mail/test/mozmill/cloudfile/test-cloudfile-attachment-urls.js hg revert mail/test/mozmill/composition/test-forward-headers.js hg revert calendar/import-export/calHtmlExport.js hg revert mail/components/compose/content/cloudAttachmentLinkManager.js hg revert mail/components/compose/content/dialogs/EdConvertToTable.js hg revert mail/components/compose/content/dialogs/EdDialogCommon.js hg revert mail/components/compose/content/dialogs/EdInsertTOC.js hg revert mail/components/customizableui/CustomizeMode.jsm hg revert mail/components/customizableui/PanelMultiView.jsm hg revert mail/test/mozmill/shared-modules/ComposeHelpers.jsm hg revert mailnews/addrbook/jsaddrbook/AddrBookDirectory.jsm hg revert mailnews/base/util/JXON.js hg revert mailnews/base/util/errUtils.js hg revert mailnews/extensions/newsblog/content/feed-parser.js hg revert mail/components/compose/texzilla/TeXZilla.js ../mach eslint --fix
2019-10-13 21:42:57 +03:00
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 {
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.mTabsProgressListeners = new Set();
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) {
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 => {
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);
}
},
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"]),
};
window.controllers.insertControllerAt(0, 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) {
if (Services.prefs.getBoolPref("mail.useNewMailTabs")) {
let tab = this.openTab("mail3PaneTab", { first: true });
this.tabs[0].linkedPanel = tab.panel.id;
return;
}
let firstTab = {
mode: this.defaultTabMode,
busy: false,
canClose: false,
thinking: false,
_ext: {},
get linkedBrowser() {
// This is a hack to make Marionette work. It needs a linkedBrowser
// from the first tab before it will start. Because linkedBrowser is
// implemented as a getter, it's ignored by anything that
// JSON-serializes this tab.
let browserFunc =
this.mode.getBrowser || this.mode.tabType.getBrowser;
let browser = browserFunc
? browserFunc.call(this.mode.tabType, this)
: null;
if (browser && !("permanentKey" in browser)) {
// The permanentKey property is a unique Object, thus allowing this
// browser to be stored in a WeakMap.
// Use the JSM global to create the permanentKey, so that if the
// permanentKey is held by something after this window closes, it
// doesn't keep the window alive.
browser.permanentKey = new (Cu.getGlobalForObject(
Services
).Object)();
}
return browser;
},
};
firstTab.tabNode = this.tabContainer.arrowScrollbox.firstElementChild;
firstTab.tabId = this.tabId++;
firstTab.mode.tabs.push(firstTab);
this.tabs[0].linkedPanel = "mailContent";
this.tabInfo[0] = this.currentTabInfo = firstTab;
let tabOpenFirstFunc =
firstTab.mode.openFirstTab || firstTab.mode.tabType.openFirstTab;
tabOpenFirstFunc.call(firstTab.mode.tabType, firstTab);
this.setTabTitle(null);
// Set the tabId after defining a <browser> and before notifications.
firstTab.browser = this.getBrowserForTab(firstTab);
firstTab.browser._activeTabId = firstTab.tabId;
// Register browser progress listeners. For firstTab, it is the shared
// #messagepane so only do it once.
for (let tabMonitor of this.tabMonitors) {
try {
if ("onTabOpened" in tabMonitor) {
tabMonitor.onTabOpened(firstTab, true);
}
tabMonitor.onTabSwitched(firstTab, null);
} catch (ex) {
console.error(ex);
}
}
let panel = document.getElementById(firstTab.tabNode.linkedPanel);
panel.setAttribute("selected", "true");
// Dispatch tab opening event
let evt = new CustomEvent("TabOpen", {
bubbles: true,
detail: { tabInfo: firstTab, moving: false },
});
firstTab.tabNode.dispatchEvent(evt);
firstTab.browser._progressListener = new TabProgressListener(
firstTab.browser,
this
);
firstTab.browser.webProgress.addProgressListener(
firstTab.browser._progressListener,
Ci.nsIWebProgress.NOTIFY_ALL
);
}
}
// 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.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) {
// Remember what has focus for when we return to this tab.
if (
oldPanel.compareDocumentPosition(document.activeElement) &
Node.DOCUMENT_POSITION_CONTAINED_BY
) {
oldTab.lastActiveElement = document.activeElement;
document.activeElement.blur();
} else {
delete oldTab.lastActiveElement;
}
oldPanel.removeAttribute("selected");
}
this.panelContainer.selectedPanel.setAttribute("selected", "true");
let tabOpenFunc = tab.mode.openTab || tab.mode.tabType.openTab;
tabOpenFunc.apply(tab.mode.tabType, [tab, aArgs]);
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;
}
}
if (tab.browser && !background) {
tab.browser._activeTabId = tab.tabId;
}
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);
// Update the toolbar status - we don't need to do menus as they
// do themselves when we open them.
UpdateMailToolbar("tabmail");
// 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;
// Register browser progress listeners
if (browser && browser.webProgress && !browser._progressListener) {
// It would probably be better to have the tabs register this listener, since the
// browser can change. This wasn't trivial to do while implementing basic WebExtension
// support, so let's assume one browser only for now.
browser._progressListener = new TabProgressListener(browser, this);
browser.webProgress.addProgressListener(
browser._progressListener,
Ci.nsIWebProgress.NOTIFY_ALL
);
}
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();
}
}
}
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) {
// 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;
}
}
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);
}
}
/**
* 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) {
// Tabs from the "mail" type share the same browser. Return the active
// one, if possible.
if (
aBrowser &&
aBrowser.id == "messagepane" &&
this.selectedTab.mode.tabType.name == "mail"
) {
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;
}
/**
* 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) {
// Remember what has focus for when we return to this tab. Check for
// anything inside tabmail-container rather than the panel because
// focus could be in the Today Pane.
let container = document.getElementById("tabmail-container");
if (
container.compareDocumentPosition(document.activeElement) &
Node.DOCUMENT_POSITION_CONTAINED_BY
) {
oldTab.lastActiveElement = document.activeElement;
document.activeElement.blur();
} else {
delete oldTab.lastActiveElement;
}
oldPanel.removeAttribute("selected");
}
this.panelContainer.selectedPanel.setAttribute("selected", "true");
let showTabFunc = tab.mode.showTab || tab.mode.tabType.showTab;
showTabFunc.call(tab.mode.tabType, tab);
let browser = this.getBrowserForTab(tab);
if (browser && !tab.browser) {
tab.browser = browser;
if (!tab.linkedBrowser) {
tab.linkedBrowser = browser;
}
}
if (tab.browser) {
tab.browser._activeTabId = tab.tabId;
}
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);
// Update the toolbar status - we don't need to do menus as they
// do themselves when we open them.
UpdateMailToolbar("tabmail");
// 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 },
});
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;
}
addTabsProgressListener(aListener) {
this.mTabsProgressListeners.add(aListener);
}
removeTabsProgressListener(aListener) {
this.mTabsProgressListeners.delete(aListener);
}
_callTabListeners(aMethod, aArgs) {
let rv = true;
for (let listener of this.mTabsProgressListeners.values()) {
if (aMethod in listener) {
try {
if (!listener[aMethod](...aArgs)) {
rv = false;
}
} catch (e) {
console.error(e);
}
}
}
return rv;
}
// 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);
// @implements {nsIWebProgressListener}
class TabProgressListener {
constructor(browser, tabmail) {
this.browser = browser;
this.tabmail = tabmail;
}
_callTabListeners(method, args) {
args.unshift(this.browser);
this.tabmail._callTabListeners(method, args);
}
onProgressChange(...args) {
this._callTabListeners("onProgressChange", args);
}
onProgressChange64(...args) {
this._callTabListeners("onProgressChange64", args);
}
onLocationChange(...args) {
this._callTabListeners("onLocationChange", args);
}
onStateChange(...args) {
this._callTabListeners("onStateChange", args);
}
onStatusChange(...args) {
this._callTabListeners("onStatusChange", args);
}
onSecurityChange(...args) {
this._callTabListeners("onSecurityChange", args);
}
onContentBlockingEvent(...args) {
this._callTabListeners("onContentBlockingEvent", args);
}
onRefreshAttempted(...args) {
return this._callTabListeners("onRefreshAttempted", args);
}
}
TabProgressListener.prototype.QueryInterface = ChromeUtils.generateQI([
"nsIWebProgressListener",
"nsIWebProgressListener2",
"nsISupportsWeakReference",
]);
}
// 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 }
);