Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
/* eslint-env mozilla/browser-window */
"use strict";
// This is loaded into all browser windows. Wrap in a block to prevent
// leaking to window scope.
{
const TAB_PREVIEW_PREF = "browser.tabs.cardPreview.enabled";
class MozTabbrowserTabs extends MozElements.TabsBase {
constructor() {
super();
this.addEventListener("TabSelect", this);
this.addEventListener("TabClose", this);
this.addEventListener("TabAttrModified", this);
this.addEventListener("TabHide", this);
this.addEventListener("TabShow", this);
this.addEventListener("TabPinned", this);
this.addEventListener("TabUnpinned", this);
this.addEventListener("TabHoverStart", this);
this.addEventListener("TabHoverEnd", this);
this.addEventListener("transitionend", this);
this.addEventListener("dblclick", this);
this.addEventListener("click", this);
this.addEventListener("click", this, true);
this.addEventListener("keydown", this, { mozSystemGroup: true });
this.addEventListener("dragstart", this);
this.addEventListener("dragover", this);
this.addEventListener("drop", this);
this.addEventListener("dragend", this);
this.addEventListener("dragleave", this);
this.addEventListener("mouseleave", this);
}
init() {
this.arrowScrollbox = this.querySelector("arrowscrollbox");
this.arrowScrollbox.addEventListener("wheel", this, true);
this.baseConnect();
this._blockDblClick = false;
this._tabDropIndicator = this.querySelector(".tab-drop-indicator");
this._dragOverDelay = 350;
this._dragTime = 0;
this._closeButtonsUpdatePending = false;
this._closingTabsSpacer = this.querySelector(".closing-tabs-spacer");
this._tabDefaultMaxWidth = NaN;
this._lastTabClosedByMouse = false;
this._hasTabTempMaxWidth = false;
this._scrollButtonWidth = 0;
this._lastNumPinned = 0;
this._pinnedTabsLayoutCache = null;
this._animateElement = this.arrowScrollbox;
this._tabClipWidth = Services.prefs.getIntPref(
"browser.tabs.tabClipWidth"
);
this._hiddenSoundPlayingTabs = new Set();
this._allTabs = null;
this._visibleTabs = null;
this._previewPanel = null;
var tab = this.allTabs[0];
tab.label = this.emptyTabTitle;
// Hide the secondary text for locales where it is unsupported due to size constraints.
const language = Services.locale.appLocaleAsBCP47;
const unsupportedLocales = Services.prefs.getCharPref(
"browser.tabs.secondaryTextUnsupportedLocales"
);
this.toggleAttribute(
"secondarytext-unsupported",
unsupportedLocales.split(",").includes(language.split("-")[0])
);
this.newTabButton.setAttribute(
"aria-label",
GetDynamicShortcutTooltipText("tabs-newtab-button")
);
let handleResize = () => {
this._updateCloseButtons();
this._handleTabSelect(true);
};
window.addEventListener("resize", handleResize);
this._fullscreenMutationObserver = new MutationObserver(handleResize);
this._fullscreenMutationObserver.observe(document.documentElement, {
attributeFilter: ["inFullscreen", "inDOMFullscreen"],
});
this.boundObserve = (...args) => this.observe(...args);
Services.prefs.addObserver("privacy.userContext", this.boundObserve);
this.observe(null, "nsPref:changed", "privacy.userContext.enabled");
XPCOMUtils.defineLazyPreferenceGetter(
this,
"_tabMinWidthPref",
"browser.tabs.tabMinWidth",
null,
(pref, prevValue, newValue) => (this._tabMinWidth = newValue),
newValue => {
const LIMIT = 50;
return Math.max(newValue, LIMIT);
}
);
this._tabMinWidth = this._tabMinWidthPref;
CustomizableUI.addListener(this);
this._updateNewTabVisibility();
this._initializeArrowScrollbox();
XPCOMUtils.defineLazyPreferenceGetter(
this,
"_closeTabByDblclick",
"browser.tabs.closeTabByDblclick",
false
);
if (gMultiProcessBrowser) {
this.tabbox.tabpanels.setAttribute("async", "true");
}
this.configureTooltip = () => {
// fall back to original tooltip behavior if pref is not set
if (this._showCardPreviews) {
this.tooltip = null;
} else {
this.tooltip = "tabbrowser-tab-tooltip";
this._previewPanel = null;
}
};
XPCOMUtils.defineLazyPreferenceGetter(
this,
"_showCardPreviews",
TAB_PREVIEW_PREF,
false,
() => this.configureTooltip()
);
this.configureTooltip();
}
on_TabSelect() {
this._handleTabSelect();
}
on_TabClose(event) {
this._hiddenSoundPlayingStatusChanged(event.target, { closed: true });
}
on_TabAttrModified(event) {
if (
["soundplaying", "muted", "activemedia-blocked", "sharing"].some(attr =>
event.detail.changed.includes(attr)
)
) {
this.updateTabIndicatorAttr(event.target);
}
if (
event.detail.changed.includes("soundplaying") &&
event.target.hidden
) {
this._hiddenSoundPlayingStatusChanged(event.target);
}
}
on_TabHide(event) {
if (event.target.soundPlaying) {
this._hiddenSoundPlayingStatusChanged(event.target);
}
}
on_TabShow(event) {
if (event.target.soundPlaying) {
this._hiddenSoundPlayingStatusChanged(event.target);
}
}
on_TabPinned(event) {
this.updateTabIndicatorAttr(event.target);
}
on_TabUnpinned(event) {
this.updateTabIndicatorAttr(event.target);
}
on_TabHoverStart(event) {
if (!this._showCardPreviews) {
return;
}
if (!this._previewPanel) {
// load the tab preview component
const TabPreviewPanel = ChromeUtils.importESModule(
).default;
this._previewPanel = new TabPreviewPanel(
document.getElementById("tab-preview-panel")
);
}
this._previewPanel.activate(event.target);
}
on_TabHoverEnd(event) {
this._previewPanel?.deactivate(event.target);
}
on_transitionend(event) {
if (event.propertyName != "max-width") {
return;
}
let tab = event.target ? event.target.closest("tab") : null;
if (tab.hasAttribute("fadein")) {
if (tab._fullyOpen) {
this._updateCloseButtons();
} else {
this._handleNewTab(tab);
}
} else if (tab.closing) {
gBrowser._endRemoveTab(tab);
}
let evt = new CustomEvent("TabAnimationEnd", { bubbles: true });
tab.dispatchEvent(evt);
}
on_dblclick(event) {
// When the tabbar has an unified appearance with the titlebar
// and menubar, a double-click in it should have the same behavior
// as double-clicking the titlebar
if (TabsInTitlebar.enabled) {
return;
}
if (event.button != 0 || event.originalTarget.localName != "scrollbox") {
return;
}
if (!this._blockDblClick) {
BrowserCommands.openTab();
}
event.preventDefault();
}
on_click(event) {
if (event.eventPhase == Event.CAPTURING_PHASE && event.button == 0) {
/* Catches extra clicks meant for the in-tab close button.
* Placed here to avoid leaking (a temporary handler added from the
* in-tab close button binding would close over the tab and leak it
* until the handler itself was removed). (bug 897751)
*
* The only sequence in which a second click event (i.e. dblclik)
* can be dispatched on an in-tab close button is when it is shown
* after the first click (i.e. the first click event was dispatched
* on the tab). This happens when we show the close button only on
* the active tab. (bug 352021)
* The only sequence in which a third click event can be dispatched
* on an in-tab close button is when the tab was opened with a
* double click on the tabbar. (bug 378344)
* In both cases, it is most likely that the close button area has
* been accidentally clicked, therefore we do not close the tab.
*
* We don't want to ignore processing of more than one click event,
* though, since the user might actually be repeatedly clicking to
* close many tabs at once.
*/
let target = event.originalTarget;
if (target.classList.contains("tab-close-button")) {
// We preemptively set this to allow the closing-multiple-tabs-
// in-a-row case.
if (this._blockDblClick) {
target._ignoredCloseButtonClicks = true;
} else if (event.detail > 1 && !target._ignoredCloseButtonClicks) {
target._ignoredCloseButtonClicks = true;
event.stopPropagation();
return;
} else {
// Reset the "ignored click" flag
target._ignoredCloseButtonClicks = false;
}
}
/* Protects from close-tab-button errant doubleclick:
* Since we're removing the event target, if the user
* double-clicks the button, the dblclick event will be dispatched
* with the tabbar as its event target (and explicit/originalTarget),
* which treats that as a mouse gesture for opening a new tab.
* In this context, we're manually blocking the dblclick event.
*/
if (this._blockDblClick) {
if (!("_clickedTabBarOnce" in this)) {
this._clickedTabBarOnce = true;
return;
}
delete this._clickedTabBarOnce;
this._blockDblClick = false;
}
} else if (
event.eventPhase == Event.BUBBLING_PHASE &&
event.button == 1
) {
let tab = event.target ? event.target.closest("tab") : null;
if (tab) {
if (tab.multiselected) {
gBrowser.removeMultiSelectedTabs();
} else {
gBrowser.removeTab(tab, {
animate: true,
triggeringEvent: event,
});
}
} else if (
event.originalTarget.closest("scrollbox") &&
!Services.prefs.getBoolPref(
"widget.gtk.titlebar-action-middle-click-enabled"
)
) {
// Check whether the click
// was dispatched on the open space of it.
let visibleTabs = this._getVisibleTabs();
let lastTab = visibleTabs[visibleTabs.length - 1];
let winUtils = window.windowUtils;
let endOfTab =
winUtils.getBoundsWithoutFlushing(lastTab)[
RTL_UI ? "left" : "right"
];
if (
(!RTL_UI && event.clientX > endOfTab) ||
(RTL_UI && event.clientX < endOfTab)
) {
BrowserCommands.openTab();
}
} else {
return;
}
event.preventDefault();
event.stopPropagation();
}
}
on_keydown(event) {
let { altKey, shiftKey } = event;
let [accel, nonAccel] =
AppConstants.platform == "macosx"
? [event.metaKey, event.ctrlKey]
: [event.ctrlKey, event.metaKey];
let keyComboForMove = accel && shiftKey && !altKey && !nonAccel;
let keyComboForFocus = accel && !shiftKey && !altKey && !nonAccel;
if (!keyComboForMove && !keyComboForFocus) {
return;
}
// Don't check if the event was already consumed because tab navigation
// should work always for better user experience.
let { visibleTabs, selectedTab } = gBrowser;
let { arrowKeysShouldWrap } = this;
let focusedTabIndex = this.ariaFocusedIndex;
if (focusedTabIndex == -1) {
focusedTabIndex = visibleTabs.indexOf(selectedTab);
}
let lastFocusedTabIndex = focusedTabIndex;
switch (event.keyCode) {
case KeyEvent.DOM_VK_UP:
if (keyComboForMove) {
gBrowser.moveTabBackward();
} else {
focusedTabIndex--;
}
break;
case KeyEvent.DOM_VK_DOWN:
if (keyComboForMove) {
gBrowser.moveTabForward();
} else {
focusedTabIndex++;
}
break;
case KeyEvent.DOM_VK_RIGHT:
case KeyEvent.DOM_VK_LEFT:
if (keyComboForMove) {
gBrowser.moveTabOver(event);
} else if (
(!RTL_UI && event.keyCode == KeyEvent.DOM_VK_RIGHT) ||
(RTL_UI && event.keyCode == KeyEvent.DOM_VK_LEFT)
) {
focusedTabIndex++;
} else {
focusedTabIndex--;
}
break;
case KeyEvent.DOM_VK_HOME:
if (keyComboForMove) {
gBrowser.moveTabToStart();
} else {
focusedTabIndex = 0;
}
break;
case KeyEvent.DOM_VK_END:
if (keyComboForMove) {
gBrowser.moveTabToEnd();
} else {
focusedTabIndex = visibleTabs.length - 1;
}
break;
case KeyEvent.DOM_VK_SPACE:
if (visibleTabs[lastFocusedTabIndex].multiselected) {
gBrowser.removeFromMultiSelectedTabs(
visibleTabs[lastFocusedTabIndex]
);
} else {
gBrowser.addToMultiSelectedTabs(visibleTabs[lastFocusedTabIndex]);
}
break;
default:
// Consume the keydown event for the above keyboard
// shortcuts only.
return;
}
if (arrowKeysShouldWrap) {
if (focusedTabIndex >= visibleTabs.length) {
focusedTabIndex = 0;
} else if (focusedTabIndex < 0) {
focusedTabIndex = visibleTabs.length - 1;
}
} else {
focusedTabIndex = Math.min(
visibleTabs.length - 1,
Math.max(0, focusedTabIndex)
);
}
if (keyComboForFocus && focusedTabIndex != lastFocusedTabIndex) {
this.ariaFocusedItem = visibleTabs[focusedTabIndex];
}
event.preventDefault();
}
on_dragstart(event) {
var tab = this._getDragTargetTab(event);
if (!tab || this._isCustomizing) {
return;
}
this._previewPanel?.deactivate();
this.startTabDrag(event, tab);
}
startTabDrag(event, tab, { fromTabList = false } = {}) {
let selectedTabs = gBrowser.selectedTabs;
let otherSelectedTabs = selectedTabs.filter(
selectedTab => selectedTab != tab
);
let dataTransferOrderedTabs = [tab].concat(otherSelectedTabs);
let dt = event.dataTransfer;
for (let i = 0; i < dataTransferOrderedTabs.length; i++) {
let dtTab = dataTransferOrderedTabs[i];
dt.mozSetDataAt(TAB_DROP_TYPE, dtTab, i);
let dtBrowser = dtTab.linkedBrowser;
// We must not set text/x-moz-url or text/plain data here,
// otherwise trying to detach the tab by dropping it on the desktop
// may result in an "internet shortcut"
dt.mozSetDataAt(
"text/x-moz-text-internal",
dtBrowser.currentURI.spec,
i
);
}
// Set the cursor to an arrow during tab drags.
dt.mozCursor = "default";
// Set the tab as the source of the drag, which ensures we have a stable
// node to deliver the `dragend` event. See bug 1345473.
dt.addElement(tab);
if (tab.multiselected) {
this._groupSelectedTabs(tab);
}
// Create a canvas to which we capture the current tab.
// Until canvas is HiDPI-aware (bug 780362), we need to scale the desired
// canvas size (in CSS pixels) to the window's backing resolution in order
// to get a full-resolution drag image for use on HiDPI displays.
let scale = window.devicePixelRatio;
let canvas = this._dndCanvas;
if (!canvas) {
this._dndCanvas = canvas = document.createElementNS(
"canvas"
);
canvas.style.width = "100%";
canvas.style.height = "100%";
canvas.mozOpaque = true;
}
canvas.width = 160 * scale;
canvas.height = 90 * scale;
let toDrag = canvas;
let dragImageOffset = -16;
let browser = tab.linkedBrowser;
if (gMultiProcessBrowser) {
var context = canvas.getContext("2d");
context.fillStyle = "white";
context.fillRect(0, 0, canvas.width, canvas.height);
let captureListener;
let platform = AppConstants.platform;
// On Windows and Mac we can update the drag image during a drag
// using updateDragImage. On Linux, we can use a panel.
if (platform == "win" || platform == "macosx") {
captureListener = function () {
dt.updateDragImage(canvas, dragImageOffset, dragImageOffset);
};
} else {
// Create a panel to use it in setDragImage
// which will tell xul to render a panel that follows
// the pointer while a dnd session is on.
if (!this._dndPanel) {
this._dndCanvas = canvas;
this._dndPanel = document.createXULElement("panel");
this._dndPanel.className = "dragfeedback-tab";
this._dndPanel.setAttribute("type", "drag");
let wrapper = document.createElementNS(
"div"
);
wrapper.style.width = "160px";
wrapper.style.height = "90px";
wrapper.appendChild(canvas);
this._dndPanel.appendChild(wrapper);
document.documentElement.appendChild(this._dndPanel);
}
toDrag = this._dndPanel;
}
// PageThumb is async with e10s but that's fine
// since we can update the image during the dnd.
PageThumbs.captureToCanvas(browser, canvas)
.then(captureListener)
.catch(e => console.error(e));
} else {
// For the non e10s case we can just use PageThumbs
// sync, so let's use the canvas for setDragImage.
PageThumbs.captureToCanvas(browser, canvas).catch(e =>
console.error(e)
);
dragImageOffset = dragImageOffset * scale;
}
dt.setDragImage(toDrag, dragImageOffset, dragImageOffset);
// _dragData.offsetX/Y give the coordinates that the mouse should be
// positioned relative to the corner of the new window created upon
// dragend such that the mouse appears to have the same position
// relative to the corner of the dragged tab.
function clientX(ele) {
return ele.getBoundingClientRect().left;
}
let tabOffsetX = clientX(tab) - clientX(this);
tab._dragData = {
offsetX: event.screenX - window.screenX - tabOffsetX,
offsetY: event.screenY - window.screenY,
scrollX: this.arrowScrollbox.scrollbox.scrollLeft,
screenX: event.screenX,
movingTabs: (tab.multiselected ? gBrowser.selectedTabs : [tab]).filter(
t => t.pinned == tab.pinned
),
fromTabList,
};
event.stopPropagation();
if (fromTabList) {
Services.telemetry.scalarAdd(
"browser.ui.interaction.all_tabs_panel_dragstart_tab_event_count",
1
);
}
}
on_dragover(event) {
var effects = this.getDropEffectForTabDrag(event);
var ind = this._tabDropIndicator;
if (effects == "" || effects == "none") {
ind.hidden = true;
return;
}
event.preventDefault();
event.stopPropagation();
var arrowScrollbox = this.arrowScrollbox;
// autoscroll the tab strip if we drag over the scroll
// buttons, even if we aren't dragging a tab, but then
// return to avoid drawing the drop indicator
var pixelsToScroll = 0;
if (this.hasAttribute("overflow")) {
switch (event.originalTarget) {
case arrowScrollbox._scrollButtonUp:
pixelsToScroll = arrowScrollbox.scrollIncrement * -1;
break;
case arrowScrollbox._scrollButtonDown:
pixelsToScroll = arrowScrollbox.scrollIncrement;
break;
}
if (pixelsToScroll) {
arrowScrollbox.scrollByPixels(
(RTL_UI ? -1 : 1) * pixelsToScroll,
true
);
}
}
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
if (
(effects == "move" || effects == "copy") &&
this == draggedTab.container &&
!draggedTab._dragData.fromTabList
) {
ind.hidden = true;
if (!this._isGroupTabsAnimationOver()) {
// Wait for grouping tabs animation to finish
return;
}
this._finishGroupSelectedTabs(draggedTab);
if (effects == "move") {
this._animateTabMove(event);
return;
}
}
this._finishAnimateTabMove();
if (effects == "link") {
let tab = this._getDragTargetTab(event, { ignoreTabSides: true });
if (tab) {
if (!this._dragTime) {
this._dragTime = Date.now();
}
if (Date.now() >= this._dragTime + this._dragOverDelay) {
this.selectedItem = tab;
}
ind.hidden = true;
return;
}
}
var rect = arrowScrollbox.getBoundingClientRect();
var newMargin;
if (pixelsToScroll) {
// if we are scrolling, put the drop indicator at the edge
// so that it doesn't jump while scrolling
let scrollRect = arrowScrollbox.scrollClientRect;
let minMargin = scrollRect.left - rect.left;
let maxMargin = Math.min(
minMargin + scrollRect.width,
scrollRect.right
);
if (RTL_UI) {
[minMargin, maxMargin] = [
this.clientWidth - maxMargin,
this.clientWidth - minMargin,
];
}
newMargin = pixelsToScroll > 0 ? maxMargin : minMargin;
} else {
let newIndex = this._getDropIndex(event);
let children = this.allTabs;
if (newIndex == children.length) {
let tabRect = this._getVisibleTabs().at(-1).getBoundingClientRect();
if (RTL_UI) {
newMargin = rect.right - tabRect.left;
} else {
newMargin = tabRect.right - rect.left;
}
} else {
let tabRect = children[newIndex].getBoundingClientRect();
if (RTL_UI) {
newMargin = rect.right - tabRect.right;
} else {
newMargin = tabRect.left - rect.left;
}
}
}
ind.hidden = false;
newMargin += ind.clientWidth / 2;
if (RTL_UI) {
newMargin *= -1;
}
ind.style.transform = "translate(" + Math.round(newMargin) + "px)";
}
on_drop(event) {
var dt = event.dataTransfer;
var dropEffect = dt.dropEffect;
var draggedTab;
let movingTabs;
if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) {
// tab copy or move
draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
// not our drop then
if (!draggedTab) {
return;
}
movingTabs = draggedTab._dragData.movingTabs;
draggedTab.container._finishGroupSelectedTabs(draggedTab);
}
this._tabDropIndicator.hidden = true;
event.stopPropagation();
if (draggedTab && dropEffect == "copy") {
// copy the dropped tab (wherever it's from)
let newIndex = this._getDropIndex(event);
let draggedTabCopy;
for (let tab of movingTabs) {
let newTab = gBrowser.duplicateTab(tab);
gBrowser.moveTabTo(newTab, newIndex++);
if (tab == draggedTab) {
draggedTabCopy = newTab;
}
}
if (draggedTab.container != this || event.shiftKey) {
this.selectedItem = draggedTabCopy;
}
} else if (draggedTab && draggedTab.container == this) {
let oldTranslateX = Math.round(draggedTab._dragData.translateX);
let tabWidth = Math.round(draggedTab._dragData.tabWidth);
let translateOffset = oldTranslateX % tabWidth;
let newTranslateX = oldTranslateX - translateOffset;
if (oldTranslateX > 0 && translateOffset > tabWidth / 2) {
newTranslateX += tabWidth;
} else if (oldTranslateX < 0 && -translateOffset > tabWidth / 2) {
newTranslateX -= tabWidth;
}
let dropIndex;
if (draggedTab._dragData.fromTabList) {
dropIndex = this._getDropIndex(event);
} else {
dropIndex =
"animDropIndex" in draggedTab._dragData &&
draggedTab._dragData.animDropIndex;
}
let incrementDropIndex = true;
if (dropIndex && dropIndex > movingTabs[0]._tPos) {
dropIndex--;
incrementDropIndex = false;
}
if (oldTranslateX && oldTranslateX != newTranslateX && !gReduceMotion) {
for (let tab of movingTabs) {
tab.toggleAttribute("tabdrop-samewindow", true);
tab.style.transform = "translateX(" + newTranslateX + "px)";
let postTransitionCleanup = () => {
tab.removeAttribute("tabdrop-samewindow");
this._finishAnimateTabMove();
if (dropIndex !== false) {
gBrowser.moveTabTo(tab, dropIndex);
if (incrementDropIndex) {
dropIndex++;
}
}
gBrowser.syncThrobberAnimations(tab);
};
if (gReduceMotion) {
postTransitionCleanup();
} else {
let onTransitionEnd = transitionendEvent => {
if (
transitionendEvent.propertyName != "transform" ||
transitionendEvent.originalTarget != tab
) {
return;
}
tab.removeEventListener("transitionend", onTransitionEnd);
postTransitionCleanup();
};
tab.addEventListener("transitionend", onTransitionEnd);
}
}
} else {
this._finishAnimateTabMove();
if (dropIndex !== false) {
for (let tab of movingTabs) {
gBrowser.moveTabTo(tab, dropIndex);
if (incrementDropIndex) {
dropIndex++;
}
}
}
}
} else if (draggedTab) {
// Move the tabs. To avoid multiple tab-switches in the original window,
// the selected tab should be adopted last.
const dropIndex = this._getDropIndex(event);
let newIndex = dropIndex;
let selectedTab;
let indexForSelectedTab;
for (let i = 0; i < movingTabs.length; ++i) {
const tab = movingTabs[i];
if (tab.selected) {
selectedTab = tab;
indexForSelectedTab = newIndex;
} else {
const newTab = gBrowser.adoptTab(tab, newIndex, tab == draggedTab);
if (newTab) {
++newIndex;
}
}
}
if (selectedTab) {
const newTab = gBrowser.adoptTab(
selectedTab,
indexForSelectedTab,
selectedTab == draggedTab
);
if (newTab) {
++newIndex;
}
}
// Restore tab selection
gBrowser.addRangeToMultiSelectedTabs(
gBrowser.tabs[dropIndex],
gBrowser.tabs[newIndex - 1]
);
} else {
// Pass true to disallow dropping javascript: or data: urls
let links;
try {
links = browserDragAndDrop.dropLinks(event, true);
} catch (ex) {}
if (!links || links.length === 0) {
return;
}
let inBackground = Services.prefs.getBoolPref(
"browser.tabs.loadInBackground"
);
if (event.shiftKey) {
inBackground = !inBackground;
}
let targetTab = this._getDragTargetTab(event, { ignoreTabSides: true });
let userContextId = this.selectedItem.getAttribute("usercontextid");
let replace = !!targetTab;
let newIndex = this._getDropIndex(event);
let urls = links.map(link => link.url);
let csp = browserDragAndDrop.getCsp(event);
let triggeringPrincipal =
browserDragAndDrop.getTriggeringPrincipal(event);
(async () => {
if (
urls.length >=
Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")
) {
// Sync dialog cannot be used inside drop event handler.
let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs(
urls.length,
window
);
if (!answer) {
return;
}
}
gBrowser.loadTabs(urls, {
inBackground,
replace,
allowThirdPartyFixup: true,
targetTab,
newIndex,
userContextId,
triggeringPrincipal,
csp,
});
})();
}
if (draggedTab) {
delete draggedTab._dragData;
}
}
on_dragend(event) {
var dt = event.dataTransfer;
var draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
// Prevent this code from running if a tabdrop animation is
// running since calling _finishAnimateTabMove would clear
// any CSS transition that is running.
if (draggedTab.hasAttribute("tabdrop-samewindow")) {
return;
}
this._finishGroupSelectedTabs(draggedTab);
this._finishAnimateTabMove();
if (
dt.mozUserCancelled ||
dt.dropEffect != "none" ||
this._isCustomizing
) {
delete draggedTab._dragData;
return;
}
// Check if tab detaching is enabled
if (!Services.prefs.getBoolPref("browser.tabs.allowTabDetach")) {
return;
}
// Disable detach within the browser toolbox
var eX = event.screenX;
var eY = event.screenY;
var wX = window.screenX;
// check if the drop point is horizontally within the window
if (eX > wX && eX < wX + window.outerWidth) {
// also avoid detaching if the the tab was dropped too close to
// the tabbar (half a tab)
let rect = window.windowUtils.getBoundsWithoutFlushing(
this.arrowScrollbox
);
let detachTabThresholdY = window.screenY + rect.top + 1.5 * rect.height;
if (eY < detachTabThresholdY && eY > window.screenY) {
return;
}
}
// screen.availLeft et. al. only check the screen that this window is on,
// but we want to look at the screen the tab is being dropped onto.
var screen = event.screen;
var availX = {},
availY = {},
availWidth = {},
availHeight = {};
// Get available rect in desktop pixels.
screen.GetAvailRectDisplayPix(availX, availY, availWidth, availHeight);
availX = availX.value;
availY = availY.value;
availWidth = availWidth.value;
availHeight = availHeight.value;
// Compute the final window size in desktop pixels ensuring that the new
// window entirely fits within `screen`.
let ourCssToDesktopScale =
window.devicePixelRatio / window.desktopToDeviceScale;
let screenCssToDesktopScale =
screen.defaultCSSScaleFactor / screen.contentsScaleFactor;
// NOTE(emilio): Multiplying the sizes here for screenCssToDesktopScale
// means that we'll try to create a window that has the same amount of CSS
// pixels than our current window, not the same amount of device pixels.
// There are pros and cons of both conversions, though this matches the
// pre-existing intended behavior.
var winWidth = Math.min(
window.outerWidth * screenCssToDesktopScale,
availWidth
);
var winHeight = Math.min(
window.outerHeight * screenCssToDesktopScale,
availHeight
);
// This is slightly tricky: _dragData.offsetX/Y is an offset in CSS
// pixels. Since we're doing the sizing above based on those, we also need
// to apply the offset with pixels relative to the screen's scale rather
// than our scale.
var left = Math.min(
Math.max(
eX * ourCssToDesktopScale -
draggedTab._dragData.offsetX * screenCssToDesktopScale,
availX
),
availX + availWidth - winWidth
);
var top = Math.min(
Math.max(
eY * ourCssToDesktopScale -
draggedTab._dragData.offsetY * screenCssToDesktopScale,
availY
),
availY + availHeight - winHeight
);
// Convert back left and top to our CSS pixel space.
left /= ourCssToDesktopScale;
top /= ourCssToDesktopScale;
delete draggedTab._dragData;
if (gBrowser.tabs.length == 1) {
// resize _before_ move to ensure the window fits the new screen. if
// the window is too large for its screen, the window manager may do
// automatic repositioning.
//
// Since we're resizing before moving to our new screen, we need to use
// sizes relative to the current screen. If we moved, then resized, then
// we could avoid this special-case and share this with the else branch
// below...
winWidth /= ourCssToDesktopScale;
winHeight /= ourCssToDesktopScale;
window.resizeTo(winWidth, winHeight);
window.moveTo(left, top);
window.focus();
} else {
// We're opening a new window in a new screen, so make sure to use sizes
// relative to the new screen.
winWidth /= screenCssToDesktopScale;
winHeight /= screenCssToDesktopScale;
let props = { screenX: left, screenY: top, suppressanimation: 1 };
if (AppConstants.platform != "win") {
props.outerWidth = winWidth;
props.outerHeight = winHeight;
}
gBrowser.replaceTabsWithWindow(draggedTab, props);
}
event.stopPropagation();
}
on_dragleave(event) {
this._dragTime = 0;
// This does not work at all (see bug 458613)
var target = event.relatedTarget;
while (target && target != this) {
target = target.parentNode;
}
if (target) {
return;
}
this._tabDropIndicator.hidden = true;
event.stopPropagation();
}
on_wheel(event) {
if (
Services.prefs.getBoolPref("toolkit.tabbox.switchByScrolling", false)
) {
event.stopImmediatePropagation();
}
}
get emptyTabTitle() {
// Normal tab title is used also in the permanent private browsing mode.
const l10nId =
PrivateBrowsingUtils.isWindowPrivate(window) &&
!Services.prefs.getBoolPref("browser.privatebrowsing.autostart")
? "tabbrowser-empty-private-tab-title"
: "tabbrowser-empty-tab-title";
return gBrowser.tabLocalization.formatValueSync(l10nId);
}
get tabbox() {
return document.getElementById("tabbrowser-tabbox");
}
get newTabButton() {
return this.querySelector("#tabs-newtab-button");
}
// Accessor for tabs. arrowScrollbox has a container for non-tab elements
// at the end, everything else is <tab>s.
get allTabs() {
if (this._allTabs) {
return this._allTabs;
}
let children = Array.from(this.arrowScrollbox.children);
children.pop();
this._allTabs = children;
return children;
}
get previewPanel() {
return this._previewPanel;
}
_getVisibleTabs() {
if (!this._visibleTabs) {
this._visibleTabs = Array.prototype.filter.call(
this.allTabs,
tab => !tab.hidden && !tab.closing
);
}
return this._visibleTabs;
}
_invalidateCachedTabs() {
this._allTabs = null;
this._visibleTabs = null;
}
_invalidateCachedVisibleTabs() {
this._visibleTabs = null;
}
appendChild(tab) {
return this.insertBefore(tab, null);
}
insertBefore(tab, node) {
if (!this.arrowScrollbox) {
throw new Error("Shouldn't call this without arrowscrollbox");
}
let { arrowScrollbox } = this;
if (node == null) {
// We have a container for non-tab elements at the end of the scrollbox.
node = arrowScrollbox.lastChild;
}
return arrowScrollbox.insertBefore(tab, node);
}
set _tabMinWidth(val) {
this.style.setProperty("--tab-min-width", val + "px");
}
get _isCustomizing() {
return document.documentElement.getAttribute("customizing") == "true";
}
// This overrides the TabsBase _selectNewTab method so that we can
// potentially interrupt keyboard tab switching when sharing the
// window or screen.
_selectNewTab(aNewTab, aFallbackDir, aWrap) {
if (!gSharedTabWarning.willShowSharedTabWarning(aNewTab)) {
super._selectNewTab(aNewTab, aFallbackDir, aWrap);
}
}
_initializeArrowScrollbox() {
let arrowScrollbox = this.arrowScrollbox;
arrowScrollbox.shadowRoot.addEventListener(
"underflow",
event => {
// Ignore underflow events:
// - from nested scrollable elements
// - for vertical orientation
// - corresponding to an overflow event that we ignored
if (
event.originalTarget != arrowScrollbox.scrollbox ||
event.detail == 0 ||
!this.hasAttribute("overflow")
) {
return;
}
this.removeAttribute("overflow");
if (this._lastTabClosedByMouse) {
this._expandSpacerBy(this._scrollButtonWidth);
}
for (let tab of gBrowser._removingTabs) {
gBrowser.removeTab(tab);
}
this._positionPinnedTabs();
this._updateCloseButtons();
},
true
);
arrowScrollbox.shadowRoot.addEventListener("overflow", event => {
// Ignore overflow events:
// - from nested scrollable elements
// - for vertical orientation
if (
event.originalTarget != arrowScrollbox.scrollbox ||
event.detail == 0
) {
return;
}
this.toggleAttribute("overflow", true);
this._positionPinnedTabs();
this._updateCloseButtons();
this._handleTabSelect(true);
});
// Override arrowscrollbox.js method, since our scrollbox's children are
// inherited from the scrollbox binding parent (this).
arrowScrollbox._getScrollableElements = () => {
return this.allTabs.filter(arrowScrollbox._canScrollToElement);
};
arrowScrollbox._canScrollToElement = tab => {
return !tab._pinnedUnscrollable && !tab.hidden;
};
}
observe(aSubject, aTopic) {
switch (aTopic) {
case "nsPref:changed":
// This is has to deal with changes in
// privacy.userContext.enabled and
// privacy.userContext.newTabContainerOnLeftClick.enabled.
let containersEnabled =
Services.prefs.getBoolPref("privacy.userContext.enabled") &&
!PrivateBrowsingUtils.isWindowPrivate(window);
// This pref won't change so often, so just recreate the menu.
const newTabLeftClickOpensContainersMenu = Services.prefs.getBoolPref(
"privacy.userContext.newTabContainerOnLeftClick.enabled"
);
// There are separate "new tab" buttons for when the tab strip
// is overflowed and when it is not. Attach the long click
// popup to both of them.
const newTab = document.getElementById("new-tab-button");
const newTab2 = this.newTabButton;
for (let parent of [newTab, newTab2]) {
if (!parent) {
continue;
}
parent.removeAttribute("type");
if (parent.menupopup) {
parent.menupopup.remove();
}
if (containersEnabled) {
parent.setAttribute("context", "new-tab-button-popup");
let popup = document
.getElementById("new-tab-button-popup")
.cloneNode(true);
popup.removeAttribute("id");
popup.className = "new-tab-popup";
popup.setAttribute("position", "after_end");
parent.prepend(popup);
parent.setAttribute("type", "menu");
// Update tooltip text
nodeToTooltipMap[parent.id] = newTabLeftClickOpensContainersMenu
? "newTabAlwaysContainer.tooltip"
: "newTabContainer.tooltip";
} else {
nodeToTooltipMap[parent.id] = "newTabButton.tooltip";
parent.removeAttribute("context", "new-tab-button-popup");
}
// evict from tooltip cache
gDynamicTooltipCache.delete(parent.id);
// If containers and press-hold container menu are both used,
// add to gClickAndHoldListenersOnElement; otherwise, remove.
if (containersEnabled && !newTabLeftClickOpensContainersMenu) {
gClickAndHoldListenersOnElement.add(parent);
} else {
gClickAndHoldListenersOnElement.remove(parent);
}
}
break;
}
}
_updateCloseButtons() {
// If we're overflowing, tabs are at their minimum widths.
if (this.hasAttribute("overflow")) {
this.setAttribute("closebuttons", "activetab");
return;
}
if (this._closeButtonsUpdatePending) {
return;
}
this._closeButtonsUpdatePending = true;
// Wait until after the next paint to get current layout data from
// getBoundsWithoutFlushing.
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
this._closeButtonsUpdatePending = false;
// The scrollbox may have started overflowing since we checked
// overflow earlier, so check again.
if (this.hasAttribute("overflow")) {
this.setAttribute("closebuttons", "activetab");
return;
}
// Check if tab widths are below the threshold where we want to
// remove close buttons from background tabs so that people don't
// accidentally close tabs by selecting them.
let rect = ele => {
return window.windowUtils.getBoundsWithoutFlushing(ele);
};
let tab = this._getVisibleTabs()[gBrowser._numPinnedTabs];
if (tab && rect(tab).width <= this._tabClipWidth) {
this.setAttribute("closebuttons", "activetab");
} else {
this.removeAttribute("closebuttons");
}
});
});
}
_updateHiddenTabsStatus() {
this.toggleAttribute(
"hashiddentabs",
gBrowser.visibleTabs.length < gBrowser.tabs.length
);
}
_handleTabSelect(aInstant) {
let selectedTab = this.selectedItem;
if (this.hasAttribute("overflow")) {
this.arrowScrollbox.ensureElementIsVisible(selectedTab, aInstant);
}
selectedTab._notselectedsinceload = false;
}
/**
* Try to keep the active tab's close button under the mouse cursor
*/
_lockTabSizing(aTab, aTabWidth) {
let tabs = this._getVisibleTabs();
if (!tabs.length) {
return;
}
var isEndTab = aTab._tPos > tabs[tabs.length - 1]._tPos;
if (!this._tabDefaultMaxWidth) {
this._tabDefaultMaxWidth = parseFloat(
window.getComputedStyle(aTab).maxWidth
);
}
this._lastTabClosedByMouse = true;
this._scrollButtonWidth = window.windowUtils.getBoundsWithoutFlushing(
this.arrowScrollbox._scrollButtonDown
).width;
if (this.hasAttribute("overflow")) {
// Don't need to do anything if we're in overflow mode and aren't scrolled
// all the way to the right, or if we're closing the last tab.
if (isEndTab || !this.arrowScrollbox._scrollButtonDown.disabled) {
return;
}
// If the tab has an owner that will become the active tab, the owner will
// be to the left of it, so we actually want the left tab to slide over.
// This can't be done as easily in non-overflow mode, so we don't bother.
if (aTab.owner) {
return;
}
this._expandSpacerBy(aTabWidth);
} else {
// non-overflow mode
// Locking is neither in effect nor needed, so let tabs expand normally.
if (isEndTab && !this._hasTabTempMaxWidth) {
return;
}
let numPinned = gBrowser._numPinnedTabs;
// Force tabs to stay the same width, unless we're closing the last tab,
// which case we need to let them expand just enough so that the overall
// tabbar width is the same.
if (isEndTab) {
let numNormalTabs = tabs.length - numPinned;
aTabWidth = (aTabWidth * (numNormalTabs + 1)) / numNormalTabs;
if (aTabWidth > this._tabDefaultMaxWidth) {
aTabWidth = this._tabDefaultMaxWidth;
}
}
aTabWidth += "px";
let tabsToReset = [];
for (let i = numPinned; i < tabs.length; i++) {
let tab = tabs[i];
tab.style.setProperty("max-width", aTabWidth, "important");
if (!isEndTab) {
// keep tabs the same width
tab.style.transition = "none";
tabsToReset.push(tab);
}
}
if (tabsToReset.length) {
window
.promiseDocumentFlushed(() => {})
.then(() => {
window.requestAnimationFrame(() => {
for (let tab of tabsToReset) {
tab.style.transition = "";
}
});
});
}
this._hasTabTempMaxWidth = true;
gBrowser.addEventListener("mousemove", this);
window.addEventListener("mouseout", this);
}
}
_expandSpacerBy(pixels) {
let spacer = this._closingTabsSpacer;
spacer.style.width = parseFloat(spacer.style.width) + pixels + "px";
this.toggleAttribute("using-closing-tabs-spacer", true);
gBrowser.addEventListener("mousemove", this);
window.addEventListener("mouseout", this);
}
_unlockTabSizing() {
gBrowser.removeEventListener("mousemove", this);
window.removeEventListener("mouseout", this);
if (this._hasTabTempMaxWidth) {
this._hasTabTempMaxWidth = false;
let tabs = this._getVisibleTabs();
for (let i = 0; i < tabs.length; i++) {
tabs[i].style.maxWidth = "";
}
}
if (this.hasAttribute("using-closing-tabs-spacer")) {
this.removeAttribute("using-closing-tabs-spacer");
this._closingTabsSpacer.style.width = 0;
}
}
uiDensityChanged() {
this._positionPinnedTabs();
this._updateCloseButtons();
this._handleTabSelect(true);
}
_positionPinnedTabs() {
let tabs = this._getVisibleTabs();
let numPinned = gBrowser._numPinnedTabs;
let doPosition =
this.hasAttribute("overflow") &&
tabs.length > numPinned &&
numPinned > 0;
this.toggleAttribute("haspinnedtabs", !!numPinned);
this.toggleAttribute("positionpinnedtabs", doPosition);
if (doPosition) {
let layoutData = this._pinnedTabsLayoutCache;
let uiDensity = document.documentElement.getAttribute("uidensity");
if (!layoutData || layoutData.uiDensity != uiDensity) {
let arrowScrollbox = this.arrowScrollbox;
layoutData = this._pinnedTabsLayoutCache = {
uiDensity,
pinnedTabWidth: tabs[0].getBoundingClientRect().width,
scrollStartOffset:
arrowScrollbox.scrollbox.getBoundingClientRect().left -
arrowScrollbox.getBoundingClientRect().left +
parseFloat(
getComputedStyle(arrowScrollbox.scrollbox).paddingInlineStart
),
};
}
let width = 0;
for (let i = numPinned - 1; i >= 0; i--) {
let tab = tabs[i];
width += layoutData.pinnedTabWidth;
tab.style.setProperty(
"margin-inline-start",
-(width + layoutData.scrollStartOffset) + "px",
"important"
);
tab._pinnedUnscrollable = true;
}
this.style.setProperty(
"--tab-overflow-pinned-tabs-width",
width + "px"
);
} else {
for (let i = 0; i < numPinned; i++) {
let tab = tabs[i];
tab.style.marginInlineStart = "";
tab._pinnedUnscrollable = false;
}
this.style.removeProperty("--tab-overflow-pinned-tabs-width");
}
if (this._lastNumPinned != numPinned) {
this._lastNumPinned = numPinned;
this._handleTabSelect(true);
}
}
_animateTabMove(event) {
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
let movingTabs = draggedTab._dragData.movingTabs;
if (!this.hasAttribute("movingtab")) {
this.toggleAttribute("movingtab", true);
gNavToolbox.toggleAttribute("movingtab", true);
if (!draggedTab.multiselected) {
this.selectedItem = draggedTab;
}
}
if (!("animLastScreenX" in draggedTab._dragData)) {
draggedTab._dragData.animLastScreenX = draggedTab._dragData.screenX;
}
let screenX = event.screenX;
if (screenX == draggedTab._dragData.animLastScreenX) {
return;
}
// Direction of the mouse movement.
let ltrMove = screenX > draggedTab._dragData.animLastScreenX;
draggedTab._dragData.animLastScreenX = screenX;
let pinned = draggedTab.pinned;
let numPinned = gBrowser._numPinnedTabs;
let tabs = this._getVisibleTabs().slice(
pinned ? 0 : numPinned,
pinned ? numPinned : undefined
);
if (RTL_UI) {
tabs.reverse();
// Copy moving tabs array to avoid infinite reversing.
movingTabs = [...movingTabs].reverse();
}
let tabWidth = draggedTab.getBoundingClientRect().width;
let shiftWidth = tabWidth * movingTabs.length;
draggedTab._dragData.tabWidth = tabWidth;
// Move the dragged tab based on the mouse position.
let leftTab = tabs[0];
let rightTab = tabs[tabs.length - 1];
let rightMovingTabScreenX = movingTabs[movingTabs.length - 1].screenX;
let leftMovingTabScreenX = movingTabs[0].screenX;
let translateX = screenX - draggedTab._dragData.screenX;
if (!pinned) {
translateX +=
this.arrowScrollbox.scrollbox.scrollLeft -
draggedTab._dragData.scrollX;
}
let leftBound = leftTab.screenX - leftMovingTabScreenX;
let rightBound =
rightTab.screenX +
rightTab.getBoundingClientRect().width -
(rightMovingTabScreenX + tabWidth);
translateX = Math.min(Math.max(translateX, leftBound), rightBound);
for (let tab of movingTabs) {
tab.style.transform = "translateX(" + translateX + "px)";
}
draggedTab._dragData.translateX = translateX;
// Determine what tab we're dragging over.
// * Single tab dragging: Point of reference is the center of the dragged tab. If that
// point touches a background tab, the dragged tab would take that
// tab's position when dropped.
// * Multiple tabs dragging: All dragged tabs are one "giant" tab with two
// points of reference (center of tabs on the extremities). When
// mouse is moving from left to right, the right reference gets activated,
// otherwise the left reference will be used. Everything else works the same
// as single tab dragging.
// * We're doing a binary search in order to reduce the amount of
// tabs we need to check.
tabs = tabs.filter(t => !movingTabs.includes(t) || t == draggedTab);
let leftTabCenter = leftMovingTabScreenX + translateX + tabWidth / 2;
let rightTabCenter = rightMovingTabScreenX + translateX + tabWidth / 2;
let tabCenter = ltrMove ? rightTabCenter : leftTabCenter;
let newIndex = -1;
let oldIndex =
"animDropIndex" in draggedTab._dragData
? draggedTab._dragData.animDropIndex
: movingTabs[0]._tPos;
let low = 0;
let high = tabs.length - 1;
while (low <= high) {
let mid = Math.floor((low + high) / 2);
if (tabs[mid] == draggedTab && ++mid > high) {
break;
}
screenX = tabs[mid].screenX + getTabShift(tabs[mid], oldIndex);
if (screenX > tabCenter) {
high = mid - 1;
} else if (
screenX + tabs[mid].getBoundingClientRect().width <
tabCenter
) {
low = mid + 1;
} else {
newIndex = tabs[mid]._tPos;
break;
}
}
if (newIndex >= oldIndex) {
newIndex++;
}
if (newIndex < 0 || newIndex == oldIndex) {
return;
}
draggedTab._dragData.animDropIndex = newIndex;
// Shift background tabs to leave a gap where the dragged tab
// would currently be dropped.
for (let tab of tabs) {
if (tab != draggedTab) {
let shift = getTabShift(tab, newIndex);
tab.style.transform = shift ? "translateX(" + shift + "px)" : "";
}
}
function getTabShift(tab, dropIndex) {
if (tab._tPos < draggedTab._tPos && tab._tPos >= dropIndex) {
return RTL_UI ? -shiftWidth : shiftWidth;
}
if (tab._tPos > draggedTab._tPos && tab._tPos < dropIndex) {
return RTL_UI ? shiftWidth : -shiftWidth;
}
return 0;
}
}
_finishAnimateTabMove() {
if (!this.hasAttribute("movingtab")) {
return;
}
for (let tab of this._getVisibleTabs()) {
tab.style.transform = "";
}
this.removeAttribute("movingtab");
gNavToolbox.removeAttribute("movingtab");
this._handleTabSelect();
}
/**
* Regroup all selected tabs around the
* tab in param
*/
_groupSelectedTabs(tab) {
let draggedTabPos = tab._tPos;
let selectedTabs = gBrowser.selectedTabs;
let animate = !gReduceMotion;
tab.groupingTabsData = {
finished: !animate,
};
// Animate left selected tabs
let insertAtPos = draggedTabPos - 1;
for (let i = selectedTabs.indexOf(tab) - 1; i > -1; i--) {
let movingTab = selectedTabs[i];
insertAtPos = newIndex(movingTab, insertAtPos);
if (animate) {
movingTab.groupingTabsData = {};
addAnimationData(movingTab, insertAtPos, "left");
} else {
gBrowser.moveTabTo(movingTab, insertAtPos);
}
insertAtPos--;
}
// Animate right selected tabs
insertAtPos = draggedTabPos + 1;
for (
let i = selectedTabs.indexOf(tab) + 1;
i < selectedTabs.length;
i++
) {
let movingTab = selectedTabs[i];
insertAtPos = newIndex(movingTab, insertAtPos);
if (animate) {
movingTab.groupingTabsData = {};
addAnimationData(movingTab, insertAtPos, "right");
} else {
gBrowser.moveTabTo(movingTab, insertAtPos);
}
insertAtPos++;
}
// Slide the relevant tabs to their new position.
for (let t of this._getVisibleTabs()) {
if (t.groupingTabsData && t.groupingTabsData.translateX) {
let translateX = (RTL_UI ? -1 : 1) * t.groupingTabsData.translateX;
t.style.transform = "translateX(" + translateX + "px)";
}
}
function newIndex(aTab, index) {
// Don't allow mixing pinned and unpinned tabs.
if (aTab.pinned) {
return Math.min(index, gBrowser._numPinnedTabs - 1);
}
return Math.max(index, gBrowser._numPinnedTabs);
}
function addAnimationData(movingTab, movingTabNewIndex, side) {
let movingTabOldIndex = movingTab._tPos;
if (movingTabOldIndex == movingTabNewIndex) {
// movingTab is already at the right position
// and thus don't need to be animated.
return;
}
let movingTabWidth = movingTab.getBoundingClientRect().width;
let shift = (movingTabNewIndex - movingTabOldIndex) * movingTabWidth;
movingTab.groupingTabsData.animate = true;
movingTab.toggleAttribute("tab-grouping", true);
movingTab.groupingTabsData.translateX = shift;
let postTransitionCleanup = () => {
movingTab.groupingTabsData.newIndex = movingTabNewIndex;
movingTab.groupingTabsData.animate = false;
};
if (gReduceMotion) {
postTransitionCleanup();
} else {
let onTransitionEnd = transitionendEvent => {
if (
transitionendEvent.propertyName != "transform" ||
transitionendEvent.originalTarget != movingTab
) {
return;
}
movingTab.removeEventListener("transitionend", onTransitionEnd);
postTransitionCleanup();
};
movingTab.addEventListener("transitionend", onTransitionEnd);
}
// Add animation data for tabs between movingTab (selected
// tab moving towards the dragged tab) and draggedTab.
// Those tabs in the middle should move in
// the opposite direction of movingTab.
let lowerIndex = Math.min(movingTabOldIndex, draggedTabPos);
let higherIndex = Math.max(movingTabOldIndex, draggedTabPos);
for (let i = lowerIndex + 1; i < higherIndex; i++) {
let middleTab = gBrowser.visibleTabs[i];
if (middleTab.pinned != movingTab.pinned) {
// Don't mix pinned and unpinned tabs
break;
}
if (middleTab.multiselected) {
// Skip because this selected tab should
// be shifted towards the dragged Tab.
continue;
}
if (
!middleTab.groupingTabsData ||
!middleTab.groupingTabsData.translateX
) {
middleTab.groupingTabsData = { translateX: 0 };
}
if (side == "left") {
middleTab.groupingTabsData.translateX -= movingTabWidth;
} else {
middleTab.groupingTabsData.translateX += movingTabWidth;
}
middleTab.toggleAttribute("tab-grouping", true);
}
}
}
_finishGroupSelectedTabs(tab) {
if (!tab.groupingTabsData || tab.groupingTabsData.finished) {
return;
}
tab.groupingTabsData.finished = true;
let selectedTabs = gBrowser.selectedTabs;
let tabIndex = selectedTabs.indexOf(tab);
// Moving left tabs
for (let i = tabIndex - 1; i > -1; i--) {
let movingTab = selectedTabs[i];
if (movingTab.groupingTabsData.newIndex) {
gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
}
}
// Moving right tabs
for (let i = tabIndex + 1; i < selectedTabs.length; i++) {
let movingTab = selectedTabs[i];
if (movingTab.groupingTabsData.newIndex) {
gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
}
}
for (let t of this._getVisibleTabs()) {
t.style.transform = "";
t.removeAttribute("tab-grouping");
delete t.groupingTabsData;
}
}
_isGroupTabsAnimationOver() {
for (let tab of gBrowser.selectedTabs) {
if (tab.groupingTabsData && tab.groupingTabsData.animate) {
return false;
}
}
return true;
}
handleEvent(aEvent) {
switch (aEvent.type) {
case "mouseout":
// If the "related target" (the node to which the pointer went) is not
// a child of the current document, the mouse just left the window.
let relatedTarget = aEvent.relatedTarget;
if (relatedTarget && relatedTarget.ownerDocument == document) {
break;
}
// fall through
case "mousemove":
if (document.getElementById("tabContextMenu").state != "open") {
this._unlockTabSizing();
}
break;
case "mouseleave":
this._previewPanel?.deactivate();
break;
default:
let methodName = `on_${aEvent.type}`;
if (methodName in this) {
this[methodName](aEvent);
} else {
throw new Error(`Unexpected event ${aEvent.type}`);
}
}
}
_notifyBackgroundTab(aTab) {
if (aTab.pinned || aTab.hidden || !this.hasAttribute("overflow")) {
return;
}
this._lastTabToScrollIntoView = aTab;
if (!this._backgroundTabScrollPromise) {
this._backgroundTabScrollPromise = window
.promiseDocumentFlushed(() => {
let lastTabRect =
this._lastTabToScrollIntoView.getBoundingClientRect();
let selectedTab = this.selectedItem;
if (selectedTab.pinned) {
selectedTab = null;
} else {
selectedTab = selectedTab.getBoundingClientRect();
selectedTab = {
left: selectedTab.left,
right: selectedTab.right,
};
}
return [
this._lastTabToScrollIntoView,
this.arrowScrollbox.scrollClientRect,
{ left: lastTabRect.left, right: lastTabRect.right },
selectedTab,
];
})
.then(([tabToScrollIntoView, scrollRect, tabRect, selectedRect]) => {
// First off, remove the promise so we can re-enter if necessary.
delete this._backgroundTabScrollPromise;
// Then, if the layout info isn't for the last-scrolled-to-tab, re-run
// the code above to get layout info for *that* tab, and don't do
// anything here, as we really just want to run this for the last-opened tab.
if (this._lastTabToScrollIntoView != tabToScrollIntoView) {
this._notifyBackgroundTab(this._lastTabToScrollIntoView);
return;
}
delete this._lastTabToScrollIntoView;
// Is the new tab already completely visible?
if (
scrollRect.left <= tabRect.left &&
tabRect.right <= scrollRect.right
) {
return;
}
if (this.arrowScrollbox.smoothScroll) {
// Can we make both the new tab and the selected tab completely visible?
if (
!selectedRect ||
Math.max(
tabRect.right - selectedRect.left,
selectedRect.right - tabRect.left
) <= scrollRect.width
) {
this.arrowScrollbox.ensureElementIsVisible(tabToScrollIntoView);
return;
}
this.arrowScrollbox.scrollByPixels(
RTL_UI
? selectedRect.right - scrollRect.right
: selectedRect.left - scrollRect.left
);
}
if (!this._animateElement.hasAttribute("highlight")) {
this._animateElement.toggleAttribute("highlight", true);
setTimeout(
function (ele) {
ele.removeAttribute("highlight");
},
150,
this._animateElement
);
}
});
}
}
/**
* Returns the tab where an event happened, or null if it didn't occur on a tab.
*
* @param {Event} event
* The event for which we want to know on which tab it happened.
* @param {object} options
* @param {boolean} options.ignoreTabSides
* If set to true: events will only be associated with a tab if they happened
* on its central part (from 25% to 75%); if they happened on the left or right
* sides of the tab, the method will return null.
*/
_getDragTargetTab(event, { ignoreTabSides = false } = {}) {
let { target } = event;
if (target.nodeType != Node.ELEMENT_NODE) {
target = target.parentElement;
}
let tab = target?.closest("tab");
if (tab && ignoreTabSides) {
let { width } = tab.getBoundingClientRect();
if (
event.screenX < tab.screenX + width * 0.25 ||
event.screenX > tab.screenX + width * 0.75
) {
return null;
}
}
return tab;
}
_getDropIndex(event) {
let tab = this._getDragTargetTab(event);
if (!tab) {
return this.allTabs.length;
}
let middle = tab.screenX + tab.getBoundingClientRect().width / 2;
let isBeforeMiddle = RTL_UI
? event.screenX > middle
: event.screenX < middle;
return tab._tPos + (isBeforeMiddle ? 0 : 1);
}
getDropEffectForTabDrag(event) {
var dt = event.dataTransfer;
let isMovingTabs = dt.mozItemCount > 0;
for (let i = 0; i < dt.mozItemCount; i++) {
// tabs are always added as the first type
let types = dt.mozTypesAt(0);
if (types[0] != TAB_DROP_TYPE) {
isMovingTabs = false;
break;
}
}
if (isMovingTabs) {
let sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
if (
XULElement.isInstance(sourceNode) &&
sourceNode.localName == "tab" &&
sourceNode.ownerGlobal.isChromeWindow &&
sourceNode.ownerDocument.documentElement.getAttribute("windowtype") ==
"navigator:browser" &&
sourceNode.ownerGlobal.gBrowser.tabContainer == sourceNode.container
) {
// Do not allow transfering a private tab to a non-private window
// and vice versa.
if (
PrivateBrowsingUtils.isWindowPrivate(window) !=
PrivateBrowsingUtils.isWindowPrivate(sourceNode.ownerGlobal)
) {
return "none";
}
if (
window.gMultiProcessBrowser !=
sourceNode.ownerGlobal.gMultiProcessBrowser
) {
return "none";
}
if (
window.gFissionBrowser != sourceNode.ownerGlobal.gFissionBrowser
) {
return "none";
}
return dt.dropEffect == "copy" ? "copy" : "move";
}
}
if (browserDragAndDrop.canDropLink(event)) {
return "link";
}
return "none";
}
_handleNewTab(tab) {
if (tab.container != this) {
return;
}
tab._fullyOpen = true;
gBrowser.tabAnimationsInProgress--;
this._updateCloseButtons();
if (tab.hasAttribute("selected")) {
this._handleTabSelect();
} else if (!tab.hasAttribute("skipbackgroundnotify")) {
this._notifyBackgroundTab(tab);
}
// XXXmano: this is a temporary workaround for bug 345399
// We need to manually update the scroll buttons disabled state
// if a tab was inserted to the overflow area or removed from it
// without any scrolling and when the tabbar has already
// overflowed.
this.arrowScrollbox._updateScrollButtonsDisabledState();
// If this browser isn't lazy (indicating it's probably created by
// session restore), preload the next about:newtab if we don't
// already have a preloaded browser.
if (tab.linkedPanel) {
NewTabPagePreloading.maybeCreatePreloadedBrowser(window);
}
if (UserInteraction.running("browser.tabs.opening", window)) {
UserInteraction.finish("browser.tabs.opening", window);
}
}
_canAdvanceToTab(aTab) {
return !aTab.closing;
}
/**
* Returns the panel associated with a tab if it has a connected browser
* and/or it is the selected tab.
* For background lazy browsers, this will return null.
*/
getRelatedElement(aTab) {
if (!aTab) {
return null;
}
// Cannot access gBrowser before it's initialized.
if (!gBrowser._initialized) {
return this.tabbox.tabpanels.firstElementChild;
}
// If the tab's browser is lazy, we need to `_insertBrowser` in order
// to have a linkedPanel. This will also serve to bind the browser
// and make it ready to use. We only do this if the tab is selected
// because otherwise, callers might end up unintentionally binding the
// browser for lazy background tabs.
if (!aTab.linkedPanel) {
if (!aTab.selected) {
return null;
}
gBrowser._insertBrowser(aTab);
}
return document.getElementById(aTab.linkedPanel);
}
_updateNewTabVisibility() {
// Helper functions to help deal with customize mode wrapping some items
let wrap = n =>
n.parentNode.localName == "toolbarpaletteitem" ? n.parentNode : n;
let unwrap = n =>
n && n.localName == "toolbarpaletteitem" ? n.firstElementChild : n;
// Starting from the tabs element, find the next sibling that:
// - isn't hidden; and
// - isn't the all-tabs button.
// If it's the new tab button, consider the new tab button adjacent to the tabs.
// If the new tab button is marked as adjacent and the tabstrip doesn't
// overflow, we'll display the 'new tab' button inline in the tabstrip.
// In all other cases, the separate new tab button is displayed in its
// customized location.
let sib = this;
do {
sib = unwrap(wrap(sib).nextElementSibling);
} while (sib && (sib.hidden || sib.id == "alltabs-button"));
this.toggleAttribute(
"hasadjacentnewtabbutton",
sib && sib.id == "new-tab-button"
);
}
onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
if (
aContainer.ownerDocument == document &&
aContainer.id == "TabsToolbar-customization-target"
) {
this._updateNewTabVisibility();
}
}
onAreaNodeRegistered(aArea, aContainer) {
if (aContainer.ownerDocument == document && aArea == "TabsToolbar") {
this._updateNewTabVisibility();
}
}
onAreaReset(aArea, aContainer) {
this.onAreaNodeRegistered(aArea, aContainer);
}
_hiddenSoundPlayingStatusChanged(tab, opts) {
let closed = opts && opts.closed;
if (!closed && tab.soundPlaying && tab.hidden) {
this._hiddenSoundPlayingTabs.add(tab);
this.toggleAttribute("hiddensoundplaying", true);
} else {
this._hiddenSoundPlayingTabs.delete(tab);
if (this._hiddenSoundPlayingTabs.size == 0) {
this.removeAttribute("hiddensoundplaying");
}
}
}
destroy() {
if (this.boundObserve) {
Services.prefs.removeObserver("privacy.userContext", this.boundObserve);
}
CustomizableUI.removeListener(this);
}
updateTabIndicatorAttr(tab) {
const theseAttributes = ["soundplaying", "muted", "activemedia-blocked"];
const notTheseAttributes = ["pinned", "sharing", "crashed"];
if (notTheseAttributes.some(attr => tab.hasAttribute(attr))) {
tab.removeAttribute("indicator-replaces-favicon");
return;
}
tab.toggleAttribute(
"indicator-replaces-favicon",
theseAttributes.some(attr => tab.hasAttribute(attr))
);
}
}
customElements.define("tabbrowser-tabs", MozTabbrowserTabs, {
extends: "tabs",
});
}