feat: Add support for multi tabs dragging, b=no-bug, c=tabs

This commit is contained in:
mr. m
2025-12-25 02:17:43 +01:00
parent 40d75abef7
commit e81af6ff71
4 changed files with 78 additions and 109 deletions

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/drag-and-drop.js b/browser/components/tabbrowser/content/drag-and-drop.js
index 97b931c3c7385a52d20204369fcf6d6999053687..6c24d636459d1d83b6d8dcf8157b3f25b0158436 100644
index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da83292312996a73c 100644
--- a/browser/components/tabbrowser/content/drag-and-drop.js
+++ b/browser/components/tabbrowser/content/drag-and-drop.js
@@ -32,6 +32,9 @@
@@ -222,87 +222,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..6c24d636459d1d83b6d8dcf8157b3f25
let isPinned = tab.pinned;
let numPinned = gBrowser.pinnedTabCount;
let allTabs = this._tabbrowserTabs.ariaFocusableItems;
@@ -1608,8 +1641,9 @@
_animateExpandedPinnedTabMove(event) {
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
+ let zenFakeTab = this._invisibleTempTab || draggedTab;
let dragData = draggedTab._dragData;
- let movingTabs = dragData.movingTabs;
+ let movingTabs = this._invisibleTempTab ? [this._invisibleTempTab] : dragData.movingTabs;
dragData.animLastScreenX ??= dragData.screenX;
dragData.animLastScreenY ??= dragData.screenY;
@@ -1624,10 +1658,7 @@
return;
}
- let tabs = this._tabbrowserTabs.visibleTabs.slice(
- 0,
- gBrowser.pinnedTabCount
- );
+ let tabs = this._tabbrowserTabs.ariaFocusableItems.slice(0, gBrowser._numZenEssentials);
let directionX = screenX > dragData.animLastScreenX;
let directionY = screenY > dragData.animLastScreenY;
@@ -1635,7 +1666,9 @@
dragData.animLastScreenX = screenX;
let { width: tabWidth, height: tabHeight } =
- draggedTab.getBoundingClientRect();
+ zenFakeTab.getBoundingClientRect();
+ tabWidth += 4; // Add 4px to account for the gap
+ tabHeight += 4;
let shiftSizeX = tabWidth * movingTabs.length;
let shiftSizeY = tabHeight;
dragData.tabWidth = tabWidth;
@@ -1672,8 +1705,8 @@
let lastBoundX =
lastTabInRow.screenX +
lastTabInRow.getBoundingClientRect().width -
- (lastMovingTabScreenX + tabWidth);
- let lastBoundY = periphery.screenY - (lastMovingTabScreenY + tabHeight);
+ (lastMovingTabScreenX + tabWidth) + 4;
+ let lastBoundY = lastTab.screenY - lastMovingTabScreenY;
translateX = Math.min(Math.max(translateX, firstBoundX), lastBoundX);
translateY = Math.min(Math.max(translateY, firstBoundY), lastBoundY);
@@ -1704,7 +1737,6 @@
// * 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 firstTabCenterX = firstMovingTabScreenX + translateX + tabWidth / 2;
let lastTabCenterX = lastMovingTabScreenX + translateX + tabWidth / 2;
let tabCenterX = directionX ? lastTabCenterX : firstTabCenterX;
@@ -1716,7 +1748,7 @@
let getTabShift = (tab, dropIndex) => {
if (
- tab.elementIndex < draggedTab.elementIndex &&
+ tab.elementIndex < zenFakeTab.elementIndex &&
tab.elementIndex >= dropIndex
) {
// If tab is at the end of a row, shift back and down
@@ -1733,7 +1765,7 @@
return [RTL_UI ? -shiftSizeX : shiftSizeX, 0];
}
if (
- tab.elementIndex > draggedTab.elementIndex &&
+ tab.elementIndex > zenFakeTab.elementIndex &&
tab.elementIndex < dropIndex
) {
// If tab is not index 0 and at the start of a row, shift across and up
@@ -1759,7 +1791,7 @@
dragData.animDropElementIndex ?? movingTabs[0].elementIndex;
while (low <= high) {
let mid = Math.floor((low + high) / 2);
- if (tabs[mid] == draggedTab && ++mid > high) {
+ if (tabs[mid] == zenFakeTab && ++mid > high) {
break;
}
let [shiftX, shiftY] = getTabShift(tabs[mid], oldIndex);
@@ -2457,7 +2489,7 @@
@@ -2457,7 +2490,7 @@
tab.style.left = "";
tab.style.top = "";
tab.style.maxWidth = "";
@@ -311,7 +231,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..6c24d636459d1d83b6d8dcf8157b3f25
}
for (let label of draggedTabDocument.getElementsByClassName(
"tab-group-label-container"
@@ -2467,7 +2499,7 @@
@@ -2467,7 +2500,7 @@
label.style.left = "";
label.style.top = "";
label.style.maxWidth = "";

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tab.js b/browser/components/tabbrowser/content/tab.js
index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7600f564a 100644
index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..69af8c986add4f74f1c3105ccd6e5804fd33b80f 100644
--- a/browser/components/tabbrowser/content/tab.js
+++ b/browser/components/tabbrowser/content/tab.js
@@ -21,6 +21,7 @@
@@ -101,6 +101,15 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7
}
get splitview() {
@@ -446,7 +464,7 @@
// to detach tabs. Ensure that we do not show the drag image returning
// to its point of origin when this happens, as it makes the drag
// finishing feel very slow.
- event.dataTransfer.mozShowFailAnimation = false;
+ event.dataTransfer.mozShowFailAnimation = true;
if (event.eventPhase == Event.CAPTURING_PHASE) {
this.style.MozUserFocus = "";
} else if (
@@ -473,6 +491,8 @@
this.style.MozUserFocus = "ignore";
} else if (

View File

@@ -76,11 +76,55 @@
super.startTabDrag(event, tab, ...args);
const dt = event.dataTransfer;
const { offsetX, offsetY } = this.#getDragImageOffset(event, tab);
this.originalDragImageArgs = [tab, offsetX, offsetY];
const draggingTabs = tab.multiselected ? gBrowser.selectedTabs : [tab];
const { offsetX, offsetY } = this.#getDragImageOffset(event, tab, draggingTabs);
const dragImage = this.#createDragImageForTabs(draggingTabs);
this.originalDragImageArgs = [dragImage, offsetX, offsetY];
dt.setDragImage(...this.originalDragImageArgs);
}
#createDragImageForTabs(movingTabs) {
const periphery = gZenWorkspaces.activeWorkspaceElement.querySelector(
'#tabbrowser-arrowscrollbox-periphery'
);
const wrapper = document.createElement('div');
const tabRect = window.windowUtils.getBoundsWithoutFlushing(movingTabs[0]);
for (let i = 0; i < movingTabs.length; i++) {
const tab = movingTabs[i];
const tabClone = tab.cloneNode(true);
if (i > 0) {
tabClone.style.transform = `translate(${i * 4}px, -${i * (tabRect.height - 4)}px)`;
tabClone.style.opacity = '0.2';
tabClone.style.zIndex = `${-i}`;
}
wrapper.appendChild(tabClone);
}
if (movingTabs.length > 1) {
const dot = document.createElement('div');
dot.textContent = movingTabs.length;
dot.style.position = 'absolute';
dot.style.top = '-10px';
dot.style.left = '-16px';
dot.style.background = 'red';
dot.style.borderRadius = '50%';
dot.style.fontWeight = 'bold';
dot.style.fontSize = '10px';
dot.style.lineHeight = '16px';
dot.style.justifyContent = dot.style.alignItems = 'center';
dot.style.height = dot.style.minWidth = '16px';
dot.style.textAlign = 'center';
dot.style.color = 'white';
wrapper.appendChild(dot);
}
wrapper.style.width = tabRect.width + 'px';
wrapper.style.height = tabRect.height * movingTabs.length + 'px';
wrapper.style.position = 'fixed';
wrapper.style.top = '-9999px';
periphery.appendChild(wrapper);
this._tempDragImageParent = wrapper;
return wrapper;
}
_animateTabMove(event) {
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
let dragData = draggedTab._dragData;
@@ -541,6 +585,12 @@
}
}
handle_drop(event) {
const dt = event.dataTransfer;
dt.updateDragImage(...this.originalDragImageArgs);
super.handle_drop(event);
}
handle_drop_transition(dropElement, draggedTab, movingTabs, dropBefore) {
if (isTabGroupLabel(dropElement)) {
dropElement = dropElement.group;
@@ -623,11 +673,14 @@
this.originalDragImageArgs = [];
window.removeEventListener('dragover', this.handle_windowDragEnter, { capture: true });
this.#isOutOfWindow = false;
this.#clearInvisibleTempTab();
if (this._browserDragImageWrapper) {
this._browserDragImageWrapper.remove();
delete this._browserDragImageWrapper;
}
if (this._tempDragImageParent) {
this._tempDragImageParent.remove();
delete this._tempDragImageParent;
}
}
#applyDragOverBackground(element) {
@@ -658,14 +711,6 @@
gZenPinnedTabManager.removeTabContainersDragoverClass();
}
#clearInvisibleTempTab() {
if (this._invisibleTempTab) {
this._invisibleTempTab.remove();
delete this._invisibleTempTab;
this._tabbrowserTabs._invalidateCachedTabs();
}
}
#applyDragoverIndicator(event, tabs, movingTabs, draggedTab) {
const separation = 4;
const dropZoneSelector = ':is(.tabbrowser-tab, .zen-drop-target, .tab-group-label)';
@@ -687,19 +732,8 @@
}
dropElement = elementToMove(dropElement);
if (this._isContainerVerticalPinnedGrid(dropElement) && isTab(draggedTab)) {
if (!draggedTab.hasAttribute('zen-essential') && !this._invisibleTempTab) {
this._invisibleTempTab = draggedTab.cloneNode(true);
this._invisibleTempTab.setAttribute('zen-essential', 'true');
//this._invisibleTempTab.style.visibility = 'hidden';
this._tabbrowserTabs.ariaFocusableItems[gBrowser._numZenEssentials - 1].after(
this._invisibleTempTab
);
this._tabbrowserTabs._invalidateCachedTabs();
}
this._animateExpandedPinnedTabMove(event);
console.log('TODO: Handle essential tab dragover in vertical pinned grid');
return;
} else if (this._invisibleTempTab) {
this.#clearInvisibleTempTab();
}
if (this.#lastDropTarget !== dropElement) {
shouldPlayHapticFeedback = this.#lastDropTarget !== null;
@@ -761,7 +795,13 @@
return [dropBefore, dropElement];
}
#getDragImageOffset(event, tab) {
#getDragImageOffset(event, tab, draggingTabs) {
if (draggingTabs.length > 1) {
return {
offsetX: 18,
offsetY: 18,
};
}
const rect = tab.getBoundingClientRect();
return {
offsetX: event.clientX - rect.left,

View File

@@ -1166,7 +1166,7 @@
}
}
.zen-essentials-container > .tabbrowser-tab,
.tabbrowser-tab[zen-essential='true'],
#zen-welcome-initial-essentials-browser-sidebar-essentials .tabbrowser-tab {
--toolbarbutton-inner-padding: 0;
max-width: unset;