Bug 1908439 - Drag and drop for moving a group within the window. r=dwalker,tabbrowser-reviewers,sessionstore-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D239494
This commit is contained in:
Dão Gottwald
2025-03-06 22:20:21 +00:00
parent c998fff42f
commit 0fb65f4185
6 changed files with 194 additions and 91 deletions

View File

@@ -51,6 +51,7 @@
orient="horizontal" orient="horizontal"
stopwatchid="FX_TAB_CLICK_MS"> stopwatchid="FX_TAB_CLICK_MS">
<hbox class="tab-drop-indicator" hidden="true"/> <hbox class="tab-drop-indicator" hidden="true"/>
<html:span id="tab-drag-empty-feedback"/>
# If the name (tabbrowser-arrowscrollbox) or structure of this changes # If the name (tabbrowser-arrowscrollbox) or structure of this changes
# significantly, there is an optimization in # significantly, there is an optimization in
# DisplayPortUtils::MaybeCreateDisplayPortInFirstScrollFrameEncountered based # DisplayPortUtils::MaybeCreateDisplayPortInFirstScrollFrameEncountered based

View File

@@ -5578,7 +5578,7 @@ var SessionStoreInternal = {
for (let i = 0; i < initialTabs.length; i++) { for (let i = 0; i < initialTabs.length; i++) {
tabbrowser.unpinTab(initialTabs[i]); tabbrowser.unpinTab(initialTabs[i]);
tabbrowser.moveTabTo(initialTabs[i], endPosition, { tabbrowser.moveTabTo(initialTabs[i], endPosition, {
forceStandaloneTab: true, forceUngrouped: true,
}); });
} }
} }

View File

@@ -33,6 +33,21 @@
const DIRECTION_FORWARD = 1; const DIRECTION_FORWARD = 1;
const DIRECTION_BACKWARD = -1; const DIRECTION_BACKWARD = -1;
/**
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} element
* @returns {boolean}
* `true` if element is a `<tab>`
*/
const isTab = element => !!(element?.tagName == "tab");
/**
* @param {MozTabbrowserTab|MozTextLabel} element
* @returns {boolean}
* `true` if element is the `<label>` in a `<tab-group>`
*/
const isTabGroupLabel = element =>
!!element?.classList?.contains("tab-group-label");
/** /**
* Updates the User Context UI indicators if the browser is in a non-default context * Updates the User Context UI indicators if the browser is in a non-default context
*/ */
@@ -819,7 +834,7 @@
this.verticalPinnedTabsContainer.appendChild(aTab) this.verticalPinnedTabsContainer.appendChild(aTab)
); );
} else { } else {
this.moveTabTo(aTab, this.pinnedTabCount, { forceStandaloneTab: true }); this.moveTabTo(aTab, this.pinnedTabCount, { forceUngrouped: true });
} }
aTab.setAttribute("pinned", "true"); aTab.setAttribute("pinned", "true");
this._updateTabBarForPinnedTabs(); this._updateTabBarForPinnedTabs();
@@ -841,7 +856,7 @@
}); });
} else { } else {
this.moveTabTo(aTab, this.pinnedTabCount - 1, { this.moveTabTo(aTab, this.pinnedTabCount - 1, {
forceStandaloneTab: true, forceUngrouped: true,
}); });
aTab.removeAttribute("pinned"); aTab.removeAttribute("pinned");
} }
@@ -5800,30 +5815,38 @@
} }
/** /**
* @param {MozTabbrowserTab} aTab * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aTab
* The tab or tab group to move. Also accepts a tab group label as a
* stand-in for its group.
* @param {number} aIndex * @param {number} aIndex
* @param {object} [options] * @param {object} [options]
* @param {boolean} [options.forceStandaloneTab=false] * @param {boolean} [options.forceUngrouped=false]
* Force `aTab` to move into position as a standalone tab, overriding * Force `aTab` to move into position as a standalone tab, overriding
* any possibility of entering a tab group. For example, setting `true` * any possibility of entering a tab group. For example, setting `true`
* ensures that a pinned tab will not accidentally be placed inside of * ensures that a pinned tab will not accidentally be placed inside of
* a tab group, since pinned tabs are presently not allowed in tab groups. * a tab group, since pinned tabs are presently not allowed in tab groups.
* @returns {void}
*/ */
moveTabTo(aTab, aIndex, { forceStandaloneTab = false } = {}) { moveTabTo(aTab, aIndex, { forceUngrouped = false } = {}) {
// Don't allow mixing pinned and unpinned tabs. if (isTab(aTab)) {
if (aTab.pinned) { // Don't allow mixing pinned and unpinned tabs.
aIndex = Math.min(aIndex, this.pinnedTabCount - 1); if (aTab.pinned) {
aIndex = Math.min(aIndex, this.pinnedTabCount - 1);
} else {
aIndex = Math.max(aIndex, this.pinnedTabCount);
}
if (aTab._tPos == aIndex && !(aTab.group && forceUngrouped)) {
return;
}
} else { } else {
aIndex = Math.max(aIndex, this.pinnedTabCount); forceUngrouped = true;
} if (isTabGroupLabel(aTab)) {
if (aTab._tPos == aIndex && !(aTab.group && forceStandaloneTab)) { aTab = aTab.group;
return; }
} }
this.#handleTabMove(aTab, () => { this.#handleTabMove(aTab, () => {
let neighbor = this.tabs[aIndex]; let neighbor = this.tabs[aIndex];
if (forceStandaloneTab && neighbor.group) { if (forceUngrouped && neighbor.group) {
neighbor = neighbor.group; neighbor = neighbor.group;
} }
if (neighbor && aIndex > aTab._tPos) { if (neighbor && aIndex > aTab._tPos) {
@@ -5835,7 +5858,7 @@
} }
/** /**
* @param {MozTabbrowserTab} tab * @param {MozTabbrowserTab|MozTabbrowserTabGroup} tab
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement * @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement
*/ */
moveTabBefore(tab, targetElement) { moveTabBefore(tab, targetElement) {
@@ -5843,7 +5866,7 @@
} }
/** /**
* @param {MozTabbrowserTab[]} tabs * @param {MozTabbrowserTab|MozTabbrowserTabGroup[]} tabs
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement * @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement
*/ */
moveTabsBefore(tabs, targetElement) { moveTabsBefore(tabs, targetElement) {
@@ -5851,7 +5874,7 @@
} }
/** /**
* @param {MozTabbrowserTab} tab * @param {MozTabbrowserTab|MozTabbrowserTabGroup} tab
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement * @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement
*/ */
moveTabAfter(tab, targetElement) { moveTabAfter(tab, targetElement) {
@@ -5859,7 +5882,7 @@
} }
/** /**
* @param {MozTabbrowserTab[]} tabs * @param {MozTabbrowserTab|MozTabbrowserTabGroup[]} tabs
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement * @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement
*/ */
moveTabsAfter(tabs, targetElement) { moveTabsAfter(tabs, targetElement) {
@@ -5867,17 +5890,27 @@
} }
/** /**
* @param {MozTabbrowserTab} tab * @param {MozTabbrowserTab|MozTabbrowserTabGroup} tab
* The tab or tab group to move. Also accepts a tab group label as a
* stand-in for its group.
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement * @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement
* @param {boolean} moveBefore * @param {boolean} moveBefore
*/ */
#moveTabNextTo(tab, targetElement, moveBefore = false) { #moveTabNextTo(tab, targetElement, moveBefore = false) {
if (isTabGroupLabel(tab)) {
tab = tab.group;
if (targetElement?.group) {
targetElement = targetElement.group;
}
}
let getContainer = () => { let getContainer = () => {
if (tab.pinned && this.tabContainer.verticalMode) { if (tab.pinned && this.tabContainer.verticalMode) {
return this.tabContainer.verticalPinnedTabsContainer; return this.tabContainer.verticalPinnedTabsContainer;
} }
return this.tabContainer; return this.tabContainer;
}; };
this.#handleTabMove(tab, () => { this.#handleTabMove(tab, () => {
if (moveBefore) { if (moveBefore) {
getContainer().insertBefore(tab, targetElement); getContainer().insertBefore(tab, targetElement);
@@ -5922,7 +5955,7 @@
*/ */
#handleTabMove(aTab, moveActionCallback) { #handleTabMove(aTab, moveActionCallback) {
let wasFocused = document.activeElement == this.selectedTab; let wasFocused = document.activeElement == this.selectedTab;
let oldPosition = aTab._tPos; let oldPosition = isTab(aTab) && aTab.elementIndex;
moveActionCallback(); moveActionCallback();
@@ -5945,12 +5978,11 @@
} }
// Pinning/unpinning vertical tabs, and moving tabs into tab groups, both bypass moveTabTo. // Pinning/unpinning vertical tabs, and moving tabs into tab groups, both bypass moveTabTo.
// We still want to check whether its worth dispatching an event. // We still want to check whether its worth dispatching an event.
if (oldPosition == aTab._tPos) { if (isTab(aTab) && oldPosition != aTab.elementIndex) {
return; let evt = document.createEvent("UIEvents");
evt.initUIEvent("TabMove", true, false, window, oldPosition);
aTab.dispatchEvent(evt);
} }
var evt = document.createEvent("UIEvents");
evt.initUIEvent("TabMove", true, false, window, oldPosition);
aTab.dispatchEvent(evt);
} }
/** /**
@@ -6075,11 +6107,11 @@
} }
moveTabToStart(aTab = this.selectedTab) { moveTabToStart(aTab = this.selectedTab) {
this.moveTabTo(aTab, 0, { forceStandaloneTab: true }); this.moveTabTo(aTab, 0, { forceUngrouped: true });
} }
moveTabToEnd(aTab = this.selectedTab) { moveTabToEnd(aTab = this.selectedTab) {
this.moveTabTo(aTab, this.tabs.length - 1, { forceStandaloneTab: true }); this.moveTabTo(aTab, this.tabs.length - 1, { forceUngrouped: true });
} }
/** /**

View File

@@ -57,18 +57,21 @@
this.initializeAttributeInheritance(); this.initializeAttributeInheritance();
this.#labelElement = this.querySelector(".tab-group-label"); this.#labelElement = this.querySelector(".tab-group-label");
// Mirroring MozTabbrowserTab
this.#labelElement.container = gBrowser.tabContainer;
this.#labelElement.group = this;
this.#labelElement.addEventListener("click", this); this.#labelElement.addEventListener("click", this);
this.#updateLabelAriaAttributes();
this.#updateCollapsedAriaAttributes();
this.addEventListener("TabSelect", this);
this.#labelElement.addEventListener("contextmenu", e => { this.#labelElement.addEventListener("contextmenu", e => {
e.preventDefault(); e.preventDefault();
gBrowser.tabGroupMenu.openEditModal(this); gBrowser.tabGroupMenu.openEditModal(this);
return false; return false;
}); });
this.#updateLabelAriaAttributes();
this.#updateCollapsedAriaAttributes();
this.addEventListener("TabSelect", this);
} }
disconnectedCallback() { disconnectedCallback() {
@@ -174,6 +177,12 @@
if (!!val == this.collapsed) { if (!!val == this.collapsed) {
return; return;
} }
if (val) {
for (let tab of this.tabs) {
// Unlock tab sizes.
tab.style.maxWidth = "";
}
}
this.toggleAttribute("collapsed", val); this.toggleAttribute("collapsed", val);
this.#updateCollapsedAriaAttributes(); this.#updateCollapsedAriaAttributes();
const eventName = val ? "TabGroupCollapse" : "TabGroupExpand"; const eventName = val ? "TabGroupCollapse" : "TabGroupExpand";
@@ -203,7 +212,6 @@
} }
); );
this.#labelElement?.setAttribute("aria-label", tabGroupName); this.#labelElement?.setAttribute("aria-label", tabGroupName);
this.#labelElement.group = this;
this.#labelElement?.setAttribute("aria-description", tabGroupDescription); this.#labelElement?.setAttribute("aria-description", tabGroupDescription);
} }

View File

@@ -682,8 +682,12 @@
} }
on_dragstart(event) { on_dragstart(event) {
if (this._isCustomizing) {
return;
}
var tab = this._getDragTargetTab(event); var tab = this._getDragTargetTab(event);
if (!tab || this._isCustomizing) { if (!tab) {
return; return;
} }
@@ -711,34 +715,36 @@
} }
let dataTransferOrderedTabs; let dataTransferOrderedTabs;
if (!fromTabList) { if (fromTabList || isTabGroupLabel(tab)) {
// Dragging a group label or an item in the all tabs menu doesn't
// change the currently selected tabs, and it's not possible to select
// multiple tabs from the list, thus handle only the dragged tab in
// this case.
dataTransferOrderedTabs = [tab];
} else {
let selectedTabs = gBrowser.selectedTabs; let selectedTabs = gBrowser.selectedTabs;
let otherSelectedTabs = selectedTabs.filter( let otherSelectedTabs = selectedTabs.filter(
selectedTab => selectedTab != tab selectedTab => selectedTab != tab
); );
dataTransferOrderedTabs = [tab].concat(otherSelectedTabs); dataTransferOrderedTabs = [tab].concat(otherSelectedTabs);
} else {
// Dragging an item in the tabs list doesn't change the currently
// selected tabs, and it's not possible to select multiple tabs from
// the list, thus handle only the dragged tab in this case.
dataTransferOrderedTabs = [tab];
} }
let dt = event.dataTransfer; let dt = event.dataTransfer;
for (let i = 0; i < dataTransferOrderedTabs.length; i++) { for (let i = 0; i < dataTransferOrderedTabs.length; i++) {
let dtTab = dataTransferOrderedTabs[i]; let dtTab = dataTransferOrderedTabs[i];
dt.mozSetDataAt(TAB_DROP_TYPE, dtTab, i); dt.mozSetDataAt(TAB_DROP_TYPE, dtTab, i);
let dtBrowser = dtTab.linkedBrowser; if (isTab(dtTab)) {
let dtBrowser = dtTab.linkedBrowser;
// We must not set text/x-moz-url or text/plain data here, // 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 // otherwise trying to detach the tab by dropping it on the desktop
// may result in an "internet shortcut" // may result in an "internet shortcut"
dt.mozSetDataAt( dt.mozSetDataAt(
"text/x-moz-text-internal", "text/x-moz-text-internal",
dtBrowser.currentURI.spec, dtBrowser.currentURI.spec,
i i
); );
}
} }
// Set the cursor to an arrow during tab drags. // Set the cursor to an arrow during tab drags.
@@ -748,8 +754,20 @@
// node to deliver the `dragend` event. See bug 1345473. // node to deliver the `dragend` event. See bug 1345473.
dt.addElement(tab); dt.addElement(tab);
let expandedTabGroups;
if (tab.multiselected) { if (tab.multiselected) {
this.#moveTogetherSelectedTabs(tab); this.#moveTogetherSelectedTabs(tab);
} else if (isTabGroupLabel(tab)) {
expandedTabGroups = gBrowser.tabGroups.filter(
group => !group.collapsed
);
if (expandedTabGroups.length) {
this._lockTabSizing();
this.#keepTabSizeLocked = true;
}
for (let group of expandedTabGroups) {
group.collapsed = true;
}
} }
// Create a canvas to which we capture the current tab. // Create a canvas to which we capture the current tab.
@@ -772,8 +790,10 @@
canvas.height = 90 * scale; canvas.height = 90 * scale;
let toDrag = canvas; let toDrag = canvas;
let dragImageOffset = -16; let dragImageOffset = -16;
let browser = tab.linkedBrowser; let browser = isTab(tab) && tab.linkedBrowser;
if (gMultiProcessBrowser) { if (isTabGroupLabel(tab)) {
toDrag = document.getElementById("tab-drag-empty-feedback");
} else if (gMultiProcessBrowser) {
var context = canvas.getContext("2d"); var context = canvas.getContext("2d");
context.fillStyle = "white"; context.fillStyle = "white";
context.fillRect(0, 0, canvas.width, canvas.height); context.fillRect(0, 0, canvas.width, canvas.height);
@@ -851,6 +871,7 @@
), ),
fromTabList, fromTabList,
tabGroupCreationColor: gBrowser.tabGroupMenu.nextUnusedColor, tabGroupCreationColor: gBrowser.tabGroupMenu.nextUnusedColor,
expandedTabGroups,
}; };
event.stopPropagation(); event.stopPropagation();
@@ -897,7 +918,7 @@
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0); let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
if ( if (
(effects == "move" || effects == "copy") && (effects == "move" || effects == "copy") &&
this == draggedTab.container && document == draggedTab.ownerDocument &&
!draggedTab._dragData.fromTabList !draggedTab._dragData.fromTabList
) { ) {
ind.hidden = true; ind.hidden = true;
@@ -1117,12 +1138,16 @@
if (shouldTranslate) { if (shouldTranslate) {
let translationPromises = []; let translationPromises = [];
for (let tab of movingTabs) { for (let item of movingTabs) {
if (isTabGroupLabel(item)) {
// Shift the `.tab-group-label-container` to shift the label element.
item = item.parentElement;
}
let translationPromise = new Promise(resolve => { let translationPromise = new Promise(resolve => {
tab.toggleAttribute("tabdrop-samewindow", true); item.toggleAttribute("tabdrop-samewindow", true);
tab.style.transform = `translate(${newTranslateX}px, ${newTranslateY}px)`; item.style.transform = `translate(${newTranslateX}px, ${newTranslateY}px)`;
let postTransitionCleanup = () => { let postTransitionCleanup = () => {
tab.removeAttribute("tabdrop-samewindow"); item.removeAttribute("tabdrop-samewindow");
resolve(); resolve();
}; };
if (gReduceMotion) { if (gReduceMotion) {
@@ -1131,15 +1156,15 @@
let onTransitionEnd = transitionendEvent => { let onTransitionEnd = transitionendEvent => {
if ( if (
transitionendEvent.propertyName != "transform" || transitionendEvent.propertyName != "transform" ||
transitionendEvent.originalTarget != tab transitionendEvent.originalTarget != item
) { ) {
return; return;
} }
tab.removeEventListener("transitionend", onTransitionEnd); item.removeEventListener("transitionend", onTransitionEnd);
postTransitionCleanup(); postTransitionCleanup();
}; };
tab.addEventListener("transitionend", onTransitionEnd); item.addEventListener("transitionend", onTransitionEnd);
} }
}); });
translationPromises.push(translationPromise); translationPromises.push(translationPromise);
@@ -1257,6 +1282,13 @@
} }
if (draggedTab) { if (draggedTab) {
if (draggedTab._dragData.expandedTabGroups?.length) {
for (let group of draggedTab._dragData.expandedTabGroups) {
group.collapsed = false;
}
this.#keepTabSizeLocked = true;
this._unlockTabSizing();
}
delete draggedTab._dragData; delete draggedTab._dragData;
} }
} }
@@ -1861,10 +1893,12 @@
selectedTab._notselectedsinceload = false; selectedTab._notselectedsinceload = false;
} }
#keepTabSizeLocked;
/** /**
* Try to keep the active tab's close button under the mouse cursor * Try to keep the active tab's close button under the mouse cursor
*/ */
_lockTabSizing(aTab, aTabWidth) { _lockTabSizing(aClosingTab, aTabWidth) {
if (this.verticalMode) { if (this.verticalMode) {
return; return;
} }
@@ -1874,17 +1908,23 @@
return; return;
} }
var isEndTab = aTab._tPos > tabs.at(-1)._tPos; let numPinned = gBrowser.pinnedTabCount;
let isEndTab = aClosingTab && aClosingTab._tPos > tabs.at(-1)._tPos;
if (!this._tabDefaultMaxWidth) { if (!this._tabDefaultMaxWidth) {
this._tabDefaultMaxWidth = parseFloat( this._tabDefaultMaxWidth = parseFloat(
window.getComputedStyle(aTab).maxWidth window.getComputedStyle(tabs[numPinned]).maxWidth
); );
} }
this._lastTabClosedByMouse = true; this._lastTabClosedByMouse = true;
this._scrollButtonWidth = window.windowUtils.getBoundsWithoutFlushing( this._scrollButtonWidth = window.windowUtils.getBoundsWithoutFlushing(
this.arrowScrollbox._scrollButtonDown this.arrowScrollbox._scrollButtonDown
).width; ).width;
if (aTabWidth === undefined) {
aTabWidth = window.windowUtils.getBoundsWithoutFlushing(
tabs[numPinned]
).width;
}
if (this.overflowing) { if (this.overflowing) {
// Don't need to do anything if we're in overflow mode and aren't scrolled // Don't need to do anything if we're in overflow mode and aren't scrolled
@@ -1895,17 +1935,15 @@
// If the tab has an owner that will become the active tab, the owner will // 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. // 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. // This can't be done as easily in non-overflow mode, so we don't bother.
if (aTab.owner) { if (aClosingTab?.owner) {
return; return;
} }
this._expandSpacerBy(aTabWidth); this._expandSpacerBy(aTabWidth);
} else { } /* non-overflow mode */ else {
// non-overflow mode
// Locking is neither in effect nor needed, so let tabs expand normally.
if (isEndTab && !this._hasTabTempMaxWidth) { if (isEndTab && !this._hasTabTempMaxWidth) {
// Locking is neither in effect nor needed, so let tabs expand normally.
return; return;
} }
let numPinned = gBrowser.pinnedTabCount;
// Force tabs to stay the same width, unless we're closing the last tab, // 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 // which case we need to let them expand just enough so that the overall
// tabbar width is the same. // tabbar width is the same.
@@ -1955,6 +1993,10 @@
} }
_unlockTabSizing() { _unlockTabSizing() {
if (this.#keepTabSizeLocked) {
return;
}
gBrowser.removeEventListener("mousemove", this); gBrowser.removeEventListener("mousemove", this);
window.removeEventListener("mouseout", this); window.removeEventListener("mouseout", this);
@@ -2343,8 +2385,12 @@
let lastBound = endEdge(lastTab) - lastMovingTabScreen; let lastBound = endEdge(lastTab) - lastMovingTabScreen;
translate = Math.min(Math.max(translate, firstBound), lastBound); translate = Math.min(Math.max(translate, firstBound), lastBound);
for (let tab of movingTabs) { for (let item of movingTabs) {
tab.style.transform = `${translateAxis}(${translate}px)`; if (isTabGroupLabel(item)) {
// Shift the `.tab-group-label-container` to shift the label element.
item = item.parentElement;
}
item.style.transform = `${translateAxis}(${translate}px)`;
} }
dragData.translatePos = translate; dragData.translatePos = translate;
@@ -2562,7 +2608,7 @@
} }
} }
if (gBrowser._tabGroupsEnabled && !isPinned) { if (gBrowser._tabGroupsEnabled && isTab(draggedTab) && !isPinned) {
let dragOverGroupingThreshold = 1 - moveOverThreshold; let dragOverGroupingThreshold = 1 - moveOverThreshold;
// When dragging tab(s) over an ungrouped tab, signal to the user // When dragging tab(s) over an ungrouped tab, signal to the user
@@ -2902,7 +2948,10 @@
} }
// fall through // fall through
case "mousemove": case "mousemove":
if (document.getElementById("tabContextMenu").state != "open") { if (
document.getElementById("tabContextMenu").state != "open" &&
!this.hasAttribute("movingtab")
) {
this._unlockTabSizing(); this._unlockTabSizing();
} }
break; break;
@@ -3027,28 +3076,30 @@
*/ */
_getDragTargetTab(event, { ignoreTabSides = false } = {}) { _getDragTargetTab(event, { ignoreTabSides = false } = {}) {
let { target } = event; let { target } = event;
if (target.nodeType != Node.ELEMENT_NODE) { while (target) {
target = target.parentElement; if (isTab(target) || isTabGroupLabel(target)) {
break;
}
target = target.parentNode;
} }
let tab = target?.closest("tab"); if (target && ignoreTabSides) {
if (tab && ignoreTabSides) { let { width, height } = target.getBoundingClientRect();
let { width, height } = tab.getBoundingClientRect();
if ( if (
event.screenX < tab.screenX + width * 0.25 || event.screenX < target.screenX + width * 0.25 ||
event.screenX > tab.screenX + width * 0.75 || event.screenX > target.screenX + width * 0.75 ||
((event.screenY < tab.screenY + height * 0.25 || ((event.screenY < target.screenY + height * 0.25 ||
event.screenY > tab.screenY + height * 0.75) && event.screenY > target.screenY + height * 0.75) &&
this.verticalMode) this.verticalMode)
) { ) {
return null; return null;
} }
} }
return tab; return target;
} }
_getDropIndex(event) { _getDropIndex(event) {
let tab = this._getDragTargetTab(event); let tab = this._getDragTargetTab(event);
if (!tab) { if (!isTab(tab)) {
return this.allTabs.length; return this.allTabs.length;
} }
let isBeforeMiddle; let isBeforeMiddle;
@@ -3080,12 +3131,10 @@
if (isMovingTabs) { if (isMovingTabs) {
let sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0); let sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
if ( if (
XULElement.isInstance(sourceNode) && (isTab(sourceNode) || isTabGroupLabel(sourceNode)) &&
sourceNode.localName == "tab" &&
sourceNode.ownerGlobal.isChromeWindow && sourceNode.ownerGlobal.isChromeWindow &&
sourceNode.ownerDocument.documentElement.getAttribute("windowtype") == sourceNode.ownerDocument.documentElement.getAttribute("windowtype") ==
"navigator:browser" && "navigator:browser"
sourceNode.ownerGlobal.gBrowser.tabContainer == sourceNode.container
) { ) {
// Do not allow transfering a private tab to a non-private window // Do not allow transfering a private tab to a non-private window
// and vice versa. // and vice versa.

View File

@@ -218,7 +218,7 @@
display: block; display: block;
} }
#tabbrowser-tabs[movingtab] &:is([selected], [multiselected]) { #tabbrowser-tabs[movingtab] &:is(:active, [multiselected]) {
position: relative; position: relative;
z-index: 2; z-index: 2;
pointer-events: none; /* avoid blocking dragover events on scroll buttons */ pointer-events: none; /* avoid blocking dragover events on scroll buttons */
@@ -838,7 +838,7 @@
display: flex; display: flex;
} }
#tabbrowser-tabs[movingtab] &:is([selected],[multiselected]) { #tabbrowser-tabs[movingtab] &:is(:active,[multiselected]) {
display: flex; display: flex;
background-color: light-dark(var(--dragover-tab-group-color), var(--dragover-tab-group-color-invert)); background-color: light-dark(var(--dragover-tab-group-color), var(--dragover-tab-group-color-invert));
border-radius: 1px; border-radius: 1px;
@@ -901,6 +901,11 @@
} }
.tab-group-label-container { .tab-group-label-container {
#tabbrowser-tabs[movingtab] &:active {
position: relative;
z-index: 2;
pointer-events: none; /* avoid blocking dragover events on scroll buttons */
}
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
#tabbrowser-tabs[movingtab] &, #tabbrowser-tabs[movingtab] &,
&[multiselected-move-together], &[multiselected-move-together],
@@ -997,6 +1002,14 @@
} }
} }
#tab-drag-empty-feedback {
background: rgba(0,0,0,0.01);
width: 1px;
height: 1px;
position: absolute;
pointer-events: none;
}
.tab-group-editor-panel { .tab-group-editor-panel {
--panel-width: 20em; --panel-width: 20em;
--panel-padding: var(--space-large); --panel-padding: var(--space-large);