summaryrefslogtreecommitdiffstats
path: root/comm/mail/base/content/widgets/tabmail-tabs.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/base/content/widgets/tabmail-tabs.js')
-rw-r--r--comm/mail/base/content/widgets/tabmail-tabs.js723
1 files changed, 723 insertions, 0 deletions
diff --git a/comm/mail/base/content/widgets/tabmail-tabs.js b/comm/mail/base/content/widgets/tabmail-tabs.js
new file mode 100644
index 0000000000..004a60122d
--- /dev/null
+++ b/comm/mail/base/content/widgets/tabmail-tabs.js
@@ -0,0 +1,723 @@
+/* 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 */
+
+// Wrap in a block to prevent leaking to window scope.
+{
+ const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+ );
+
+ /**
+ * The MozTabs widget holds all the tabs for the main tab UI.
+ *
+ * @augments {MozTabs}
+ */
+ class MozTabmailTabs extends customElements.get("tabs") {
+ constructor() {
+ super();
+
+ this.addEventListener("dragstart", event => {
+ let draggedTab = this._getDragTargetTab(event);
+
+ if (!draggedTab) {
+ return;
+ }
+
+ let tab = this.tabmail.selectedTab;
+
+ if (!tab || !tab.canClose) {
+ return;
+ }
+
+ let dt = event.dataTransfer;
+
+ // If we drag within the same window, we use the tab directly
+ dt.mozSetDataAt("application/x-moz-tabmail-tab", draggedTab, 0);
+
+ // Otherwise we use session restore & JSON to migrate the tab.
+ let uri = this.tabmail.persistTab(tab);
+
+ // In case the tab implements session restore, we use JSON to convert
+ // it into a string.
+ //
+ // If a tab does not support session restore it returns null. We can't
+ // moved such tabs to a new window. However moving them within the same
+ // window works perfectly fine.
+ if (uri) {
+ uri = JSON.stringify(uri);
+ }
+
+ dt.mozSetDataAt("application/x-moz-tabmail-json", uri, 0);
+
+ dt.mozCursor = "default";
+
+ // Create Drag Image.
+ let panel = document.getElementById("tabpanelcontainer");
+
+ let thumbnail = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ thumbnail.width = Math.ceil(screen.availWidth / 5.75);
+ thumbnail.height = Math.round(thumbnail.width * 0.5625);
+
+ let snippetWidth = panel.getBoundingClientRect().width * 0.6;
+ let scale = thumbnail.width / snippetWidth;
+
+ let ctx = thumbnail.getContext("2d");
+
+ ctx.scale(scale, scale);
+
+ ctx.drawWindow(
+ window,
+ panel.screenX - window.mozInnerScreenX,
+ panel.screenY - window.mozInnerScreenY,
+ snippetWidth,
+ snippetWidth * 0.5625,
+ "rgb(255,255,255)"
+ );
+
+ dt = event.dataTransfer;
+ dt.setDragImage(thumbnail, 0, 0);
+
+ event.stopPropagation();
+ });
+
+ this.addEventListener("dragover", event => {
+ let dt = event.dataTransfer;
+
+ if (dt.mozItemCount == 0) {
+ return;
+ }
+
+ // Bug 516247:
+ // in case the user is dragging something else than a tab, and
+ // keeps hovering over a tab, we assume he wants to switch to this tab.
+ if (
+ dt.mozTypesAt(0)[0] != "application/x-moz-tabmail-tab" &&
+ dt.mozTypesAt(0)[1] != "application/x-moz-tabmail-json"
+ ) {
+ let tab = this._getDragTargetTab(event);
+
+ if (!tab) {
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (!this._dragTime) {
+ this._dragTime = Date.now();
+ return;
+ }
+
+ if (Date.now() <= this._dragTime + this._dragOverDelay) {
+ return;
+ }
+
+ if (this.tabmail.tabContainer.selectedItem == tab) {
+ return;
+ }
+
+ this.tabmail.tabContainer.selectedItem = tab;
+
+ return;
+ }
+
+ // As some tabs do not support session restore they can't be
+ // moved to a different or new window. We should not show
+ // a dropmarker in such a case.
+ if (!dt.mozGetDataAt("application/x-moz-tabmail-json", 0)) {
+ let draggedTab = dt.mozGetDataAt("application/x-moz-tabmail-tab", 0);
+
+ if (!draggedTab) {
+ return;
+ }
+
+ if (this.tabmail.tabContainer.getIndexOfItem(draggedTab) == -1) {
+ return;
+ }
+ }
+
+ dt.effectAllowed = "copyMove";
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ let ltr = window.getComputedStyle(this).direction == "ltr";
+ let ind = this._tabDropIndicator;
+ let arrowScrollbox = this.arrowScrollbox;
+
+ // Let's scroll
+ let pixelsToScroll = 0;
+ if (arrowScrollbox.getAttribute("overflow") == "true") {
+ switch (event.target) {
+ case arrowScrollbox._scrollButtonDown:
+ pixelsToScroll = arrowScrollbox.scrollIncrement * -1;
+ break;
+ case arrowScrollbox._scrollButtonUp:
+ pixelsToScroll = arrowScrollbox.scrollIncrement;
+ break;
+ }
+
+ if (ltr) {
+ pixelsToScroll = pixelsToScroll * -1;
+ }
+
+ if (pixelsToScroll) {
+ // Hide Indicator while Scrolling
+ ind.hidden = true;
+ arrowScrollbox.scrollByPixels(pixelsToScroll);
+ return;
+ }
+ }
+
+ let newIndex = this._getDropIndex(event);
+
+ // Fix the DropIndex in case it points to tab that can't be closed.
+ let tabInfo = this.tabmail.tabInfo;
+
+ while (newIndex < tabInfo.length && !tabInfo[newIndex].canClose) {
+ newIndex++;
+ }
+
+ let scrollRect = this.arrowScrollbox.scrollClientRect;
+ let rect = this.getBoundingClientRect();
+ let minMargin = scrollRect.left - rect.left;
+ let maxMargin = Math.min(
+ minMargin + scrollRect.width,
+ scrollRect.right
+ );
+
+ if (!ltr) {
+ [minMargin, maxMargin] = [
+ this.clientWidth - maxMargin,
+ this.clientWidth - minMargin,
+ ];
+ }
+
+ let newMargin;
+ let tabs = this.allTabs;
+
+ if (newIndex == tabs.length) {
+ let tabRect = tabs[newIndex - 1].getBoundingClientRect();
+
+ if (ltr) {
+ newMargin = tabRect.right - rect.left;
+ } else {
+ newMargin = rect.right - tabRect.left;
+ }
+ } else {
+ let tabRect = tabs[newIndex].getBoundingClientRect();
+
+ if (ltr) {
+ newMargin = tabRect.left - rect.left;
+ } else {
+ newMargin = rect.right - tabRect.right;
+ }
+ }
+
+ ind.hidden = false;
+
+ newMargin -= ind.clientWidth / 2;
+
+ ind.style.insetInlineStart = `${Math.round(newMargin)}px`;
+ });
+
+ this.addEventListener("drop", event => {
+ let dt = event.dataTransfer;
+
+ if (dt.mozItemCount != 1) {
+ return;
+ }
+
+ let draggedTab = dt.mozGetDataAt("application/x-moz-tabmail-tab", 0);
+
+ if (!draggedTab) {
+ return;
+ }
+
+ event.stopPropagation();
+ this._tabDropIndicator.hidden = true;
+
+ // Is the tab one of our children?
+ if (this.tabmail.tabContainer.getIndexOfItem(draggedTab) == -1) {
+ // It's a tab from an other window, so we have to trigger session
+ // restore to get our tab
+
+ let tabmail2 = draggedTab.ownerDocument.getElementById("tabmail");
+ if (!tabmail2) {
+ return;
+ }
+
+ let draggedJson = dt.mozGetDataAt(
+ "application/x-moz-tabmail-json",
+ 0
+ );
+ if (!draggedJson) {
+ return;
+ }
+
+ draggedJson = JSON.parse(draggedJson);
+
+ // Some tab exist only once, so we have to gamble a bit. We close
+ // the tab and try to reopen it. If something fails the tab is gone.
+
+ tabmail2.closeTab(draggedTab, true);
+
+ if (!this.tabmail.restoreTab(draggedJson)) {
+ return;
+ }
+
+ draggedTab =
+ this.tabmail.tabContainer.allTabs[
+ this.tabmail.tabContainer.allTabs.length - 1
+ ];
+ }
+
+ let idx = this._getDropIndex(event);
+
+ // Fix the DropIndex in case it points to tab that can't be closed
+ let tabInfo = this.tabmail.tabInfo;
+ while (idx < tabInfo.length && !tabInfo[idx].canClose) {
+ idx++;
+ }
+
+ this.tabmail.moveTabTo(draggedTab, idx);
+
+ this.tabmail.switchToTab(draggedTab);
+ this.tabmail.updateCurrentTab();
+ });
+
+ this.addEventListener("dragend", event => {
+ // Note: while this case is correctly handled here, this event
+ // isn't dispatched when the tab is moved within the tabstrip,
+ // see bug 460801.
+
+ // The user pressed ESC to cancel the drag, or the drag succeeded.
+ let dt = event.dataTransfer;
+ if (dt.mozUserCancelled || dt.dropEffect != "none") {
+ return;
+ }
+
+ // Disable detach within the browser toolbox.
+ let eX = event.screenX;
+ let wX = window.screenX;
+
+ // Check if the drop point is horizontally within the window.
+ if (eX > wX && eX < wX + window.outerWidth) {
+ let bo = this.arrowScrollbox;
+ // Also avoid detaching if the the tab was dropped too close to
+ // the tabbar (half a tab).
+ let endScreenY = bo.screenY + 1.5 * bo.getBoundingClientRect().height;
+ let eY = event.screenY;
+
+ if (eY < endScreenY && eY > window.screenY) {
+ return;
+ }
+ }
+
+ // User wants to deatach tab from window...
+ if (dt.mozItemCount != 1) {
+ return;
+ }
+
+ let draggedTab = dt.mozGetDataAt("application/x-moz-tabmail-tab", 0);
+
+ if (!draggedTab) {
+ return;
+ }
+
+ this.tabmail.replaceTabWithWindow(draggedTab);
+ });
+
+ this.addEventListener("dragleave", event => {
+ this._dragTime = 0;
+
+ this._tabDropIndicator.hidden = true;
+ event.stopPropagation();
+ });
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+ super.connectedCallback();
+
+ this.tabmail = document.getElementById("tabmail");
+
+ this.arrowScrollboxWidth = 0;
+
+ this.arrowScrollbox = this.querySelector("arrowscrollbox");
+
+ this.mCollapseToolbar = document.getElementById(
+ this.getAttribute("collapsetoolbar")
+ );
+
+ // @implements {nsIObserver}
+ this._prefObserver = (subject, topic, data) => {
+ if (topic == "nsPref:changed") {
+ subject.QueryInterface(Ci.nsIPrefBranch);
+ if (data == "mail.tabs.autoHide") {
+ this.mAutoHide = subject.getBoolPref("mail.tabs.autoHide");
+ }
+ }
+ };
+
+ this._tabDropIndicator = this.querySelector(".tab-drop-indicator");
+
+ this._dragOverDelay = 350;
+
+ this._dragTime = 0;
+
+ this._mAutoHide = false;
+
+ this.mAllTabsButton = document.getElementById(
+ this.getAttribute("alltabsbutton")
+ );
+ this.mAllTabsPopup = this.mAllTabsButton.menu;
+
+ this.mDownBoxAnimate = this.arrowScrollbox;
+
+ this._animateTimer = null;
+
+ this._animateStep = -1;
+
+ this._animateDelay = 25;
+
+ this._animatePercents = [
+ 1.0, 0.85, 0.8, 0.75, 0.71, 0.68, 0.65, 0.62, 0.59, 0.57, 0.54, 0.52,
+ 0.5, 0.47, 0.45, 0.44, 0.42, 0.4, 0.38, 0.37, 0.35, 0.34, 0.32, 0.31,
+ 0.3, 0.29, 0.28, 0.27, 0.26, 0.25, 0.24, 0.23, 0.23, 0.22, 0.22, 0.21,
+ 0.21, 0.21, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.19, 0.19, 0.19,
+ 0.18, 0.18, 0.17, 0.17, 0.16, 0.15, 0.14, 0.13, 0.11, 0.09, 0.06,
+ ];
+
+ this.mTabMinWidth = Services.prefs.getIntPref("mail.tabs.tabMinWidth");
+ this.mTabMaxWidth = Services.prefs.getIntPref("mail.tabs.tabMaxWidth");
+ this.mTabClipWidth = Services.prefs.getIntPref("mail.tabs.tabClipWidth");
+ this.mAutoHide = Services.prefs.getBoolPref("mail.tabs.autoHide");
+
+ if (this.mAutoHide) {
+ this.mCollapseToolbar.collapsed = true;
+ document.documentElement.setAttribute("tabbarhidden", "true");
+ }
+
+ this._updateCloseButtons();
+
+ Services.prefs.addObserver("mail.tabs.", this._prefObserver);
+
+ window.addEventListener("resize", this);
+
+ // Listen to overflow/underflow events on the tabstrip,
+ // we cannot put these as xbl handlers on the entire binding because
+ // they would also get called for the all-tabs popup scrollbox.
+ // Also, we can't rely on event.target because these are all
+ // anonymous nodes.
+ this.arrowScrollbox.shadowRoot.addEventListener("overflow", this);
+ this.arrowScrollbox.shadowRoot.addEventListener("underflow", this);
+
+ this.addEventListener("select", event => {
+ this._handleTabSelect();
+
+ if (
+ !("updateCurrentTab" in this.tabmail) ||
+ event.target.localName != "tabs"
+ ) {
+ return;
+ }
+
+ this.tabmail.updateCurrentTab();
+ });
+
+ this.addEventListener("TabSelect", event => {
+ this._handleTabSelect();
+ });
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_tabMinWidthPref",
+ "mail.tabs.tabMinWidth",
+ null,
+ (pref, prevValue, newValue) => (this._tabMinWidth = newValue),
+ newValue => {
+ const LIMIT = 50;
+ return Math.max(newValue, LIMIT);
+ }
+ );
+ this._tabMinWidth = this._tabMinWidthPref;
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_tabMaxWidthPref",
+ "mail.tabs.tabMaxWidth",
+ null,
+ (pref, prevValue, newValue) => (this._tabMaxWidth = newValue)
+ );
+ this._tabMaxWidth = this._tabMaxWidthPref;
+ }
+
+ get tabbox() {
+ return document.getElementById("tabmail-tabbox");
+ }
+
+ // Accessor for tabs.
+ get allTabs() {
+ if (!this.arrowScrollbox) {
+ return [];
+ }
+
+ return Array.from(this.arrowScrollbox.children);
+ }
+
+ appendChild(tab) {
+ return this.insertBefore(tab, null);
+ }
+
+ insertBefore(tab, node) {
+ if (!this.arrowScrollbox) {
+ return;
+ }
+
+ if (node == null) {
+ this.arrowScrollbox.appendChild(tab);
+ return;
+ }
+
+ this.arrowScrollbox.insertBefore(tab, node);
+ }
+
+ set mAutoHide(val) {
+ if (val != this._mAutoHide) {
+ if (this.allTabs.length == 1) {
+ this.mCollapseToolbar.collapsed = val;
+ }
+ this._mAutoHide = val;
+ }
+ }
+
+ get mAutoHide() {
+ return this._mAutoHide;
+ }
+
+ set selectedIndex(val) {
+ let tab = this.getItemAtIndex(val);
+ let alreadySelected = tab && tab.selected;
+
+ this.__proto__.__proto__
+ .__lookupSetter__("selectedIndex")
+ .call(this, val);
+
+ if (!alreadySelected) {
+ // Fire an onselect event for the tabs element.
+ let event = document.createEvent("Events");
+ event.initEvent("select", true, true);
+ this.dispatchEvent(event);
+ }
+ }
+
+ get selectedIndex() {
+ return this.__proto__.__proto__
+ .__lookupGetter__("selectedIndex")
+ .call(this);
+ }
+
+ _updateCloseButtons() {
+ let width =
+ this.arrowScrollbox.firstElementChild.getBoundingClientRect().width;
+ // 0 width is an invalid value and indicates
+ // an item without display, so ignore.
+ if (width > this.mTabClipWidth || width == 0) {
+ this.setAttribute("closebuttons", "alltabs");
+ } else {
+ this.setAttribute("closebuttons", "activetab");
+ }
+ }
+
+ _handleTabSelect() {
+ this.arrowScrollbox.ensureElementIsVisible(this.selectedItem);
+ }
+
+ handleEvent(aEvent) {
+ let alltabsButton = document.getElementById("alltabs-button");
+
+ switch (aEvent.type) {
+ case "overflow":
+ this.arrowScrollbox.ensureElementIsVisible(this.selectedItem);
+
+ // filter overflow events which were dispatched on nested scrollboxes
+ // and ignore vertical events.
+ if (
+ aEvent.target != this.arrowScrollbox.scrollbox ||
+ aEvent.detail == 0
+ ) {
+ return;
+ }
+
+ this.arrowScrollbox.setAttribute("overflow", "true");
+ alltabsButton.removeAttribute("hidden");
+ break;
+ case "underflow":
+ // filter underflow events which were dispatched on nested scrollboxes
+ // and ignore vertical events.
+ if (
+ aEvent.target != this.arrowScrollbox.scrollbox ||
+ aEvent.detail == 0
+ ) {
+ return;
+ }
+
+ this.arrowScrollbox.removeAttribute("overflow");
+ alltabsButton.setAttribute("hidden", "true");
+ break;
+ case "resize":
+ let width = this.arrowScrollbox.getBoundingClientRect().width;
+ if (width != this.arrowScrollboxWidth) {
+ this._updateCloseButtons();
+ // XXX without this line the tab bar won't budge
+ this.arrowScrollbox.scrollByPixels(1);
+ this._handleTabSelect();
+ this.arrowScrollboxWidth = width;
+ }
+ break;
+ }
+ }
+
+ _stopAnimation() {
+ if (this._animateStep != -1) {
+ if (this._animateTimer) {
+ this._animateTimer.cancel();
+ }
+
+ this._animateStep = -1;
+ this.mAllTabsBoxAnimate.style.opacity = 0.0;
+ this.mDownBoxAnimate.style.opacity = 0.0;
+ }
+ }
+
+ _notifyBackgroundTab(aTab) {
+ let tsbo = this.arrowScrollbox;
+ let tsboStart = tsbo.screenX;
+ let tsboEnd = tsboStart + tsbo.getBoundingClientRect().width;
+
+ let ctboStart = aTab.screenX;
+ let ctboEnd = ctboStart + aTab.getBoundingClientRect().width;
+
+ // only start the flash timer if the new tab (which was loaded in
+ // the background) is not completely visible
+ if (tsboStart > ctboStart || ctboEnd > tsboEnd) {
+ this._animateStep = 0;
+
+ if (!this._animateTimer) {
+ this._animateTimer = Cc["@mozilla.org/timer;1"].createInstance(
+ Ci.nsITimer
+ );
+ } else {
+ this._animateTimer.cancel();
+ }
+
+ this._animateTimer.initWithCallback(
+ this,
+ this._animateDelay,
+ Ci.nsITimer.TYPE_REPEATING_SLACK
+ );
+ }
+ }
+
+ notify(aTimer) {
+ if (!document) {
+ aTimer.cancel();
+ }
+
+ let percent = this._animatePercents[this._animateStep];
+ this.mAllTabsBoxAnimate.style.opacity = percent;
+ this.mDownBoxAnimate.style.opacity = percent;
+
+ if (this._animateStep < this._animatePercents.length - 1) {
+ this._animateStep++;
+ } else {
+ this._stopAnimation();
+ }
+ }
+
+ _getDragTargetTab(event) {
+ let tab = event.target;
+ while (tab && tab.localName != "tab") {
+ tab = tab.parentNode;
+ }
+
+ if (!tab) {
+ return null;
+ }
+
+ if (event.type != "drop" && event.type != "dragover") {
+ return tab;
+ }
+
+ let tabRect = tab.getBoundingClientRect();
+ if (event.screenX < tab.screenX + tabRect.width * 0.25) {
+ return null;
+ }
+
+ if (event.screenX > tab.screenX + tabRect.width * 0.75) {
+ return null;
+ }
+
+ return tab;
+ }
+
+ _getDropIndex(event) {
+ let tabs = this.allTabs;
+
+ if (window.getComputedStyle(this).direction == "ltr") {
+ for (let i = 0; i < tabs.length; i++) {
+ if (
+ event.screenX <
+ tabs[i].screenX + tabs[i].getBoundingClientRect().width / 2
+ ) {
+ return i;
+ }
+ }
+ } else {
+ for (let i = 0; i < tabs.length; i++) {
+ if (
+ event.screenX >
+ tabs[i].screenX + tabs[i].getBoundingClientRect().width / 2
+ ) {
+ return i;
+ }
+ }
+ }
+
+ return tabs.length;
+ }
+
+ set _tabMinWidth(val) {
+ this.arrowScrollbox.style.setProperty("--tab-min-width", `${val}px`);
+ }
+ set _tabMaxWidth(val) {
+ this.arrowScrollbox.style.setProperty("--tab-max-width", `${val}px`);
+ }
+
+ disconnectedCallback() {
+ Services.prefs.removeObserver("mail.tabs.", this._prefObserver);
+
+ // Release timer to avoid reference cycles.
+ if (this._animateTimer) {
+ this._animateTimer.cancel();
+ this._animateTimer = null;
+ }
+
+ this.arrowScrollbox.shadowRoot.removeEventListener("overflow", this);
+ this.arrowScrollbox.shadowRoot.removeEventListener("underflow", this);
+ }
+ }
+
+ MozXULElement.implementCustomInterface(MozTabmailTabs, [Ci.nsITimerCallback]);
+ customElements.define("tabmail-tabs", MozTabmailTabs, { extends: "tabs" });
+}