/* 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" }); }