mirror of
https://github.com/zen-browser/desktop.git
synced 2026-08-13 02:05:18 +00:00
feat: Add support for drag-and-dropping tabs into groups, b=no-bug, c=common, folders, tabs, workspaces
This commit is contained in:
@@ -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..13e872a6c11061b5d7e669476072075d0e685eb9 100644
|
||||
index 97b931c3c7385a52d20204369fcf6d6999053687..b028c923d24adf0e9dbe12f80deb3ad4fda535eb 100644
|
||||
--- a/browser/components/tabbrowser/content/drag-and-drop.js
|
||||
+++ b/browser/components/tabbrowser/content/drag-and-drop.js
|
||||
@@ -32,6 +32,9 @@
|
||||
@@ -175,16 +175,23 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..13e872a6c11061b5d7e669476072075d
|
||||
let tabsPerRow = 0;
|
||||
let position = RTL_UI
|
||||
? window.windowUtils.getBoundsWithoutFlushing(
|
||||
@@ -1055,7 +1091,7 @@
|
||||
@@ -1055,7 +1091,6 @@
|
||||
// using updateDragImage. On Linux, we can use a panel.
|
||||
if (platform == "win" || platform == "macosx") {
|
||||
captureListener = function () {
|
||||
- dt.updateDragImage(canvas, dragImageOffset, dragImageOffset);
|
||||
+ dt.updateDragImage(tab, dragImageOffset, dragImageOffset);
|
||||
};
|
||||
} else {
|
||||
// Create a panel to use it in setDragImage
|
||||
@@ -1112,7 +1148,7 @@
|
||||
@@ -1093,7 +1128,6 @@
|
||||
);
|
||||
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
|
||||
@@ -1112,7 +1146,7 @@
|
||||
let dropEffect = this.getDropEffectForTabDrag(event);
|
||||
let isMovingInTabStrip = !fromTabList && dropEffect == "move";
|
||||
let collapseTabGroupDuringDrag =
|
||||
@@ -193,7 +200,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..13e872a6c11061b5d7e669476072075d
|
||||
|
||||
tab._dragData = {
|
||||
offsetX: this._tabbrowserTabs.verticalMode
|
||||
@@ -1122,7 +1158,7 @@
|
||||
@@ -1122,7 +1156,7 @@
|
||||
? event.screenY - window.screenY - tabOffset
|
||||
: event.screenY - window.screenY,
|
||||
scrollPos:
|
||||
@@ -202,7 +209,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..13e872a6c11061b5d7e669476072075d
|
||||
? this._tabbrowserTabs.pinnedTabsContainer.scrollPosition
|
||||
: this._tabbrowserTabs.arrowScrollbox.scrollPosition,
|
||||
screenX: event.screenX,
|
||||
@@ -1149,6 +1185,7 @@
|
||||
@@ -1149,6 +1183,7 @@
|
||||
|
||||
if (collapseTabGroupDuringDrag) {
|
||||
tab.group.collapsed = true;
|
||||
@@ -210,7 +217,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..13e872a6c11061b5d7e669476072075d
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1173,6 +1210,16 @@
|
||||
@@ -1173,6 +1208,16 @@
|
||||
if (tabStripItemElement.hasAttribute("dragtarget")) {
|
||||
return;
|
||||
}
|
||||
@@ -227,7 +234,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..13e872a6c11061b5d7e669476072075d
|
||||
let isPinned = tab.pinned;
|
||||
let numPinned = gBrowser.pinnedTabCount;
|
||||
let allTabs = this._tabbrowserTabs.ariaFocusableItems;
|
||||
@@ -1624,10 +1671,7 @@
|
||||
@@ -1624,10 +1669,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -239,7 +246,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..13e872a6c11061b5d7e669476072075d
|
||||
|
||||
let directionX = screenX > dragData.animLastScreenX;
|
||||
let directionY = screenY > dragData.animLastScreenY;
|
||||
@@ -1636,6 +1680,8 @@
|
||||
@@ -1636,6 +1678,8 @@
|
||||
|
||||
let { width: tabWidth, height: tabHeight } =
|
||||
draggedTab.getBoundingClientRect();
|
||||
@@ -248,7 +255,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..13e872a6c11061b5d7e669476072075d
|
||||
let shiftSizeX = tabWidth * movingTabs.length;
|
||||
let shiftSizeY = tabHeight;
|
||||
dragData.tabWidth = tabWidth;
|
||||
@@ -1672,8 +1718,8 @@
|
||||
@@ -1672,8 +1716,8 @@
|
||||
let lastBoundX =
|
||||
lastTabInRow.screenX +
|
||||
lastTabInRow.getBoundingClientRect().width -
|
||||
@@ -259,7 +266,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..13e872a6c11061b5d7e669476072075d
|
||||
translateX = Math.min(Math.max(translateX, firstBoundX), lastBoundX);
|
||||
translateY = Math.min(Math.max(translateY, firstBoundY), lastBoundY);
|
||||
|
||||
@@ -2417,6 +2463,7 @@
|
||||
@@ -2417,6 +2461,7 @@
|
||||
}
|
||||
|
||||
finishAnimateTabMove() {
|
||||
@@ -267,7 +274,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..13e872a6c11061b5d7e669476072075d
|
||||
if (!this.#isMovingTab()) {
|
||||
return;
|
||||
}
|
||||
@@ -2457,7 +2504,7 @@
|
||||
@@ -2457,7 +2502,7 @@
|
||||
tab.style.left = "";
|
||||
tab.style.top = "";
|
||||
tab.style.maxWidth = "";
|
||||
@@ -276,7 +283,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..13e872a6c11061b5d7e669476072075d
|
||||
}
|
||||
for (let label of draggedTabDocument.getElementsByClassName(
|
||||
"tab-group-label-container"
|
||||
@@ -2467,7 +2514,7 @@
|
||||
@@ -2467,7 +2512,7 @@
|
||||
label.style.left = "";
|
||||
label.style.top = "";
|
||||
label.style.maxWidth = "";
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
* @returns {MozTabbrowserTab|vbox}
|
||||
*/
|
||||
const elementToMove = (element) => {
|
||||
if (element.classList.contains('zen-current-workspace-indicator')) {
|
||||
return element;
|
||||
}
|
||||
if (element.group?.hasAttribute('split-view-group')) {
|
||||
return element.group;
|
||||
}
|
||||
@@ -45,6 +48,9 @@
|
||||
};
|
||||
|
||||
window.ZenDragAndDrop = class extends window.TabDragAndDrop {
|
||||
#dragOverBackground = null;
|
||||
#lastDropTarget = null;
|
||||
|
||||
constructor(tabbrowserTabs) {
|
||||
super(tabbrowserTabs);
|
||||
}
|
||||
@@ -54,7 +60,6 @@
|
||||
let dt = event.dataTransfer;
|
||||
|
||||
const { offsetX, offsetY } = this.#getDragImageOffset(tab);
|
||||
dt.updateDragImage(tab, offsetX, offsetY);
|
||||
}
|
||||
|
||||
_animateTabMove(event) {
|
||||
@@ -399,7 +404,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
this.#applyDragoverIndicator(translate, dropElement, draggedTab);
|
||||
this._tabbrowserTabs.removeAttribute('movingtab-group');
|
||||
this._resetGroupTarget(document.querySelector('[dragover-groupTarget]'));
|
||||
|
||||
delete dragData.shouldDropIntoCollapsedTabGroup;
|
||||
|
||||
// Default to dropping into `dropElement`'s tab group, if it exists.
|
||||
let dropElementGroup = dropElement?.group;
|
||||
let colorCode = dropElementGroup?.color;
|
||||
|
||||
let lastUnmovingTabInGroup = dropElementGroup?.tabs.findLast((t) => !movingTabsSet.has(t));
|
||||
if (
|
||||
isTab(dropElement) &&
|
||||
dropElementGroup &&
|
||||
dropElement == lastUnmovingTabInGroup &&
|
||||
!dropBefore
|
||||
) {
|
||||
// Dragging tab over the last tab of a tab group, but not enough
|
||||
// for it to drop into the tab group. Drop it after the tab group instead.
|
||||
dropElement = dropElementGroup;
|
||||
colorCode = undefined;
|
||||
} else if (isTabGroupLabel(dropElement)) {
|
||||
// Dropping right before the first tab in the tab group.
|
||||
dropElement = dropElementGroup.tabs[0];
|
||||
dropBefore = true;
|
||||
}
|
||||
this._setDragOverGroupColor(colorCode);
|
||||
this._tabbrowserTabs.toggleAttribute('movingtab-addToGroup', colorCode);
|
||||
this._tabbrowserTabs.toggleAttribute('movingtab-ungroup', !colorCode);
|
||||
|
||||
this.#applyDragoverIndicator(event, tabs, movingTabs, overlapPercent);
|
||||
|
||||
if (
|
||||
newDropElementIndex == oldDropElementIndex &&
|
||||
@@ -414,30 +448,88 @@
|
||||
dragData.animDropElementIndex = newDropElementIndex;
|
||||
}
|
||||
|
||||
#applyDragoverIndicator(translate, dropElement, draggedTab) {
|
||||
const separation = 8;
|
||||
handle_dragend(event) {
|
||||
super.handle_dragend(event);
|
||||
this.#removeDragOverBackground();
|
||||
gZenPinnedTabManager.removeTabContainersDragoverClass();
|
||||
}
|
||||
|
||||
#applyDragOverBackground(element) {
|
||||
if (this.#dragOverBackground && this.#lastDropTarget === element) {
|
||||
return false;
|
||||
}
|
||||
const margin = 2;
|
||||
const rect = window.windowUtils.getBoundsWithoutFlushing(element);
|
||||
this.#dragOverBackground = document.createElement('div');
|
||||
this.#dragOverBackground.id = 'zen-dragover-background';
|
||||
this.#dragOverBackground.style.height = `${rect.height - margin * 2}px`;
|
||||
this.#dragOverBackground.style.top = `${rect.top + margin}px`;
|
||||
gNavToolbox.appendChild(this.#dragOverBackground);
|
||||
this.#lastDropTarget = element;
|
||||
return true;
|
||||
}
|
||||
|
||||
#removeDragOverBackground() {
|
||||
if (this.#dragOverBackground) {
|
||||
this.#dragOverBackground.remove();
|
||||
this.#dragOverBackground = null;
|
||||
this.#lastDropTarget = null;
|
||||
}
|
||||
}
|
||||
|
||||
#applyDragoverIndicator(event, tabs, movingTabs, overlapPercent) {
|
||||
const separation = 4;
|
||||
const dropZoneSelector = ':is(.tabbrowser-tab, .zen-drop-target, .tab-group-label)';
|
||||
let shouldPlayHapticFeedback = false;
|
||||
let dropElement = event.target.closest(dropZoneSelector);
|
||||
if (!dropElement) {
|
||||
return;
|
||||
const numEssentials = gBrowser._numZenEssentials;
|
||||
const numPinned = gBrowser.pinnedTabCount - numEssentials;
|
||||
const tabToUse = event.target.closest(dropZoneSelector);
|
||||
if (!tabToUse) {
|
||||
this.#removeDragOverBackground();
|
||||
gZenPinnedTabManager.removeTabContainersDragoverClass();
|
||||
return;
|
||||
}
|
||||
const isPinned = tabToUse.pinned;
|
||||
const relativeTabs = tabs.slice(isPinned ? 0 : numPinned, isPinned ? numPinned : undefined);
|
||||
const draggedTabRect = elementToMove(tabToUse).getBoundingClientRect();
|
||||
dropElement = event.clientY > draggedTabRect.top ? relativeTabs.at(-1) : relativeTabs[0];
|
||||
}
|
||||
translate += draggedTab._dragData.screenY;
|
||||
let rect = elementToMove(dropElement).getBoundingClientRect();
|
||||
const indicator = gZenPinnedTabManager.dragIndicator;
|
||||
const halfSize = rect.height / 2;
|
||||
let top = 0;
|
||||
if (translate >= rect.top + halfSize) {
|
||||
top = Math.round(rect.top + rect.height) + 'px';
|
||||
} else {
|
||||
top = Math.round(rect.top) + 'px';
|
||||
dropElement = elementToMove(dropElement);
|
||||
if (this.#lastDropTarget !== dropElement) {
|
||||
shouldPlayHapticFeedback = this.#lastDropTarget !== null;
|
||||
this.#removeDragOverBackground();
|
||||
}
|
||||
if (indicator.style.top !== top) {
|
||||
shouldPlayHapticFeedback = true;
|
||||
let canHightlightGroup =
|
||||
gZenFolders.highlightGroupOnDragOver(dropElement.parentElement, movingTabs) ||
|
||||
!dropElement.parentElement?.isZenFolder;
|
||||
if (isTab(dropElement)) {
|
||||
const indicator = gZenPinnedTabManager.dragIndicator;
|
||||
let rect = dropElement.getBoundingClientRect();
|
||||
let top = 0;
|
||||
const threshold =
|
||||
Services.prefs.getIntPref('browser.tabs.dragDrop.moveOverThresholdPercent') / 100;
|
||||
if (overlapPercent > threshold) {
|
||||
top = Math.round(rect.top + rect.height) + 'px';
|
||||
} else {
|
||||
top = Math.round(rect.top) + 'px';
|
||||
}
|
||||
if (indicator.style.top !== top) {
|
||||
shouldPlayHapticFeedback = true;
|
||||
}
|
||||
indicator.setAttribute('orientation', 'horizontal');
|
||||
indicator.style.setProperty('--indicator-left', rect.left + separation / 2 + 'px');
|
||||
indicator.style.setProperty('--indicator-width', rect.width - separation + 'px');
|
||||
indicator.style.top = top;
|
||||
indicator.style.removeProperty('left');
|
||||
} else if (dropElement.classList.contains('zen-drop-target') && canHightlightGroup) {
|
||||
// removeTabContainersDragoverClass Already calls a new haptic feedback
|
||||
shouldPlayHapticFeedback =
|
||||
this.#applyDragOverBackground(dropElement) && !gZenPinnedTabManager._dragIndicator;
|
||||
gZenPinnedTabManager.removeTabContainersDragoverClass();
|
||||
}
|
||||
indicator.setAttribute('orientation', 'horizontal');
|
||||
indicator.style.setProperty('--indicator-left', rect.left + separation / 2 + 'px');
|
||||
indicator.style.setProperty('--indicator-width', rect.width - separation + 'px');
|
||||
indicator.style.top = top;
|
||||
indicator.style.removeProperty('left');
|
||||
|
||||
if (shouldPlayHapticFeedback) {
|
||||
Services.zen.playHapticFeedback();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ class ZenFolder extends MozTabbrowserTabGroup {
|
||||
#initialized = false;
|
||||
|
||||
static markup = `
|
||||
<hbox class="tab-group-label-container" pack="center">
|
||||
<hbox class="tab-group-label-container zen-drop-target" pack="center">
|
||||
<html:div class="tab-group-folder-icon"/>
|
||||
<label class="tab-group-label" role="button"/>
|
||||
<image class="tab-reset-button reset-icon" role="button" keyNav="false" data-l10n-id="zen-folders-unload-all-tooltip"/>
|
||||
|
||||
@@ -37,6 +37,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
|
||||
#popup = null;
|
||||
#popupTimer = null;
|
||||
#mouseTimer = null;
|
||||
#lastHighlightedGroup = null;
|
||||
|
||||
#lastFolderContextMenu = null;
|
||||
|
||||
@@ -97,7 +98,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
|
||||
.getElementById('context_zenChangeFolderSpace')
|
||||
.querySelector('menupopup');
|
||||
changeFolderSpace.innerHTML = '';
|
||||
for (const workspace of [...gZenWorkspaces._workspaceCache.workspaces].reverse()) {
|
||||
for (const workspace of [...gZenWorkspaces.getWorkspaces()].reverse()) {
|
||||
const item = gZenWorkspaces.generateMenuItemForWorkspace(workspace);
|
||||
item.addEventListener('command', (event) => {
|
||||
if (!this.#lastFolderContextMenu) return;
|
||||
@@ -1056,6 +1057,38 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
|
||||
this._sessionRestoring = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlights the given tab group and removes highlight from any previously highlighted group.
|
||||
* @param {MozTabbrowserTabGroup|undefined|null} folder The folder to highlight, or null to clear highlight.
|
||||
* @param {Array<MozTabbrowserTab>|null} movingTabs The tabs being moved.
|
||||
*/
|
||||
highlightGroupOnDragOver(folder, movingTabs) {
|
||||
if (folder === this.#lastHighlightedGroup) return true;
|
||||
const tab = movingTabs ? movingTabs[0] : null;
|
||||
if (this.#lastHighlightedGroup && this.#lastHighlightedGroup !== folder) {
|
||||
if (this.#lastHighlightedGroup.collapsed) {
|
||||
this.updateFolderIcon(this.#lastHighlightedGroup, 'close');
|
||||
}
|
||||
this.#lastHighlightedGroup = null;
|
||||
}
|
||||
if (
|
||||
folder?.isZenFolder &&
|
||||
(!folder.hasAttribute('split-view-group') || !folder.hasAttribute('selected')) &&
|
||||
folder !== tab?.group &&
|
||||
!(
|
||||
folder.level >= this.#ZEN_MAX_SUBFOLDERS &&
|
||||
movingTabs?.some((t) => gBrowser.isTabGroupLabel(t))
|
||||
)
|
||||
) {
|
||||
if (folder.collapsed) {
|
||||
this.updateFolderIcon(folder, 'open');
|
||||
}
|
||||
this.#lastHighlightedGroup = folder;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ungroup a tab from all the active groups it belongs to.
|
||||
* @param {MozTabbrowserTab[]} tabs The tab to ungroup.
|
||||
|
||||
@@ -503,15 +503,16 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
|
||||
}
|
||||
|
||||
moveToAnotherTabContainerIfNecessary(event, movingTabs) {
|
||||
movingTabs = [...movingTabs];
|
||||
if (!this.enabled) {
|
||||
return false;
|
||||
}
|
||||
movingTabs = [...movingTabs];
|
||||
try {
|
||||
const pinnedTabsTarget =
|
||||
event.target.closest('.zen-current-workspace-indicator') || this._isGoingToPinnedTabs;
|
||||
const pinnedTabsTarget = event.target.closest(
|
||||
':is(.zen-current-workspace-indicator, .zen-workspace-pinned-tabs-section)'
|
||||
);
|
||||
const essentialTabsTarget = event.target.closest('.zen-essentials-container');
|
||||
const tabsTarget = !this._isGoingToPinnedTabs;
|
||||
const tabsTarget = !pinnedTabsTarget;
|
||||
|
||||
// TODO: Solve the issue of adding a tab between two groups
|
||||
// Remove group labels from the moving tabs and replace it
|
||||
|
||||
@@ -1281,9 +1281,9 @@
|
||||
width: calc(var(--indicator-width) - 2 * var(--zen-drag-indicator-height) - 4px);
|
||||
height: var(--zen-drag-indicator-height);
|
||||
transition:
|
||||
top 0.1s ease-out,
|
||||
left 0.1s ease-out,
|
||||
width 0.1s ease-out;
|
||||
top 0.05s ease-out,
|
||||
left 0.05s ease-out,
|
||||
width 0.05s ease-out;
|
||||
|
||||
&::before {
|
||||
left: calc(-2 * var(--zen-drag-indicator-height));
|
||||
@@ -1370,3 +1370,13 @@
|
||||
.tab-group-label-container[zen-dragtarget] {
|
||||
z-index: 9 !important;
|
||||
}
|
||||
|
||||
#zen-dragover-background {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
/* Extra width to cover the sidebar splitter */
|
||||
width: calc(100% + var(--zen-toolbox-padding));
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
background: var(--zen-primary-color);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
class nsZenWorkspace extends MozXULElement {
|
||||
static get markup() {
|
||||
return `
|
||||
<vbox class="zen-workspace-tabs-section zen-current-workspace-indicator" flex="1" context="zenWorkspaceMoreActions">
|
||||
<vbox class="zen-workspace-tabs-section zen-current-workspace-indicator zen-drop-target" flex="1" context="zenWorkspaceMoreActions">
|
||||
<hbox class="zen-current-workspace-indicator-icon" />
|
||||
<label class="zen-current-workspace-indicator-name" flex="1" />
|
||||
<toolbarbutton class="toolbarbutton-1 chromeclass-toolbar-additional zen-workspaces-actions" context="zenWorkspaceMoreActions" />
|
||||
|
||||
Reference in New Issue
Block a user