1234 lines
48 KiB
JavaScript
1234 lines
48 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
'use strict';
|
|
|
|
// Wrap in a block to prevent leaking to window scope.
|
|
{
|
|
const isTab = (element) => gBrowser.isTab(element);
|
|
const isTabGroupLabel = (element) => gBrowser.isTabGroupLabel(element);
|
|
|
|
/**
|
|
* The elements in the tab strip from `this.ariaFocusableItems` that contain
|
|
* logical information are:
|
|
*
|
|
* - <tab> (.tabbrowser-tab)
|
|
* - <tab-group> label element (.tab-group-label)
|
|
*
|
|
* The elements in the tab strip that contain the space inside of the <tabs>
|
|
* element are:
|
|
*
|
|
* - <tab> (.tabbrowser-tab)
|
|
* - <tab-group> label element wrapper (.tab-group-label-container)
|
|
*
|
|
* When working with tab strip items, if you need logical information, you
|
|
* can get it directly, e.g. `element.elementIndex` or `element._tPos`. If
|
|
* you need spatial information like position or dimensions, then you should
|
|
* call this function. For example, `elementToMove(element).getBoundingClientRect()`
|
|
* or `elementToMove(element).style.top`.
|
|
*
|
|
* @param {MozTabbrowserTab|typeof MozTabbrowserTabGroup.labelElement} element
|
|
* @returns {MozTabbrowserTab|vbox}
|
|
*/
|
|
const elementToMove = (element) => {
|
|
if (
|
|
element.closest('.zen-current-workspace-indicator') ||
|
|
element.hasAttribute('split-view-group')
|
|
) {
|
|
return element;
|
|
}
|
|
if (element.group?.hasAttribute('split-view-group')) {
|
|
return element.group;
|
|
}
|
|
if (isTab(element)) {
|
|
return element;
|
|
}
|
|
if (isTabGroupLabel(element)) {
|
|
return element.closest('.tab-group-label-container');
|
|
}
|
|
throw new Error(`Element "${element.tagName}" is not expected to move`);
|
|
};
|
|
|
|
window.ZenDragAndDrop = class extends window.TabDragAndDrop {
|
|
#dragOverBackground = null;
|
|
#lastDropTarget = null;
|
|
originalDragImageArgs = [];
|
|
#isOutOfWindow = false;
|
|
#maxTabsPerRow = 0;
|
|
#changeSpaceTimer = null;
|
|
#isAnimatingTabMove = false;
|
|
|
|
constructor(tabbrowserTabs) {
|
|
super(tabbrowserTabs);
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
this,
|
|
'ZenDragAndDropService',
|
|
'@mozilla.org/zen/drag-and-drop;1',
|
|
Ci.nsIZenDragAndDrop
|
|
);
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
this,
|
|
'_dndSwitchSpaceDelay',
|
|
'zen.tabs.dnd-switch-space-delay',
|
|
1000
|
|
);
|
|
}
|
|
|
|
init() {
|
|
super.init();
|
|
this.handle_windowDragEnter = this.handle_windowDragEnter.bind(this);
|
|
window.addEventListener('dragleave', this.handle_windowDragLeave.bind(this), {
|
|
capture: true,
|
|
});
|
|
}
|
|
|
|
startTabDrag(event, tab, ...args) {
|
|
this.ZenDragAndDropService.onDragStart(1);
|
|
|
|
super.startTabDrag(event, tab, ...args);
|
|
const dt = event.dataTransfer;
|
|
if (isTabGroupLabel(tab)) {
|
|
tab = tab.group;
|
|
}
|
|
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];
|
|
setTimeout(() => {
|
|
dt.setDragImage(...this.originalDragImageArgs);
|
|
}, 0);
|
|
}
|
|
|
|
#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 (tabClone.hasAttribute('zen-essential')) {
|
|
const rect = tab.getBoundingClientRect();
|
|
tabClone.style.minWidth = tabClone.style.maxWidth = `${rect.width}px`;
|
|
tabClone.style.minHeight = tabClone.style.maxHeight = `${rect.height}px`;
|
|
}
|
|
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);
|
|
}
|
|
this.#maybeCreateDragImageDot(movingTabs, wrapper);
|
|
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;
|
|
}
|
|
|
|
#maybeCreateDragImageDot(movingTabs, wrapper) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
_animateTabMove(event) {
|
|
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
|
|
if (event.target.closest('#zen-essentials')) {
|
|
if (!isTab(draggedTab)) {
|
|
this.clearDragOverVisuals();
|
|
return;
|
|
}
|
|
return this.#animateVerticalPinnedGridDragOver(event);
|
|
} else if (this._fakeEssentialTab) {
|
|
this.#makeDragImageNonEssential(event);
|
|
}
|
|
let dragData = draggedTab._dragData;
|
|
let movingTabs = dragData.movingTabs;
|
|
let movingTabsSet = dragData.movingTabsSet;
|
|
|
|
dragData.animLastScreenPos ??= this._tabbrowserTabs.verticalMode
|
|
? dragData.screenY
|
|
: dragData.screenX;
|
|
let allTabs = this._tabbrowserTabs.ariaFocusableItems;
|
|
let numEssentials = gBrowser._numZenEssentials;
|
|
let isEssential = draggedTab.hasAttribute('zen-essential');
|
|
let tabs = allTabs.slice(
|
|
isEssential ? 0 : numEssentials,
|
|
isEssential ? numEssentials : undefined
|
|
);
|
|
|
|
let screen = this._tabbrowserTabs.verticalMode ? event.screenY : event.screenX;
|
|
if (screen == dragData.animLastScreenPos) {
|
|
return;
|
|
}
|
|
let screenForward = screen > dragData.animLastScreenPos;
|
|
dragData.animLastScreenPos = screen;
|
|
|
|
this._clearDragOverGroupingTimer();
|
|
|
|
if (this._rtlMode) {
|
|
tabs.reverse();
|
|
}
|
|
|
|
let bounds = (ele) => window.windowUtils.getBoundsWithoutFlushing(ele);
|
|
let logicalForward = screenForward != this._rtlMode;
|
|
let screenAxis = this._tabbrowserTabs.verticalMode ? 'screenY' : 'screenX';
|
|
let size = this._tabbrowserTabs.verticalMode ? 'height' : 'width';
|
|
let { width: tabWidth, height: tabHeight } = bounds(draggedTab);
|
|
let tabSize = this._tabbrowserTabs.verticalMode ? tabHeight : tabWidth;
|
|
let translateX = event.screenX - dragData.screenX;
|
|
let translateY = event.screenY - dragData.screenY;
|
|
|
|
dragData.tabWidth = tabWidth;
|
|
dragData.tabHeight = tabHeight;
|
|
dragData.translateX = translateX;
|
|
dragData.translateY = translateY;
|
|
|
|
// Move the dragged tab based on the mouse position.
|
|
let periphery = document.getElementById('tabbrowser-arrowscrollbox-periphery');
|
|
let lastMovingTab = movingTabs.at(-1);
|
|
let firstMovingTab = movingTabs[0];
|
|
let endEdge = (ele) => ele[screenAxis] + bounds(ele)[size];
|
|
let lastMovingTabScreen = endEdge(lastMovingTab);
|
|
let firstMovingTabScreen = firstMovingTab[screenAxis];
|
|
let shiftSize = lastMovingTabScreen - firstMovingTabScreen;
|
|
let translate = screen - dragData[screenAxis];
|
|
|
|
// Constrain the range over which the moving tabs can move between the edge of the tabstrip and periphery.
|
|
// Add 1 to periphery so we don't overlap it.
|
|
let startBound = this._rtlMode
|
|
? endEdge(periphery) + 1 - firstMovingTabScreen
|
|
: this._tabbrowserTabs[screenAxis] - firstMovingTabScreen;
|
|
let endBound = this._rtlMode
|
|
? endEdge(this._tabbrowserTabs) - lastMovingTabScreen
|
|
: periphery[screenAxis] - 1 - lastMovingTabScreen;
|
|
let firstTab = tabs.at(this._rtlMode ? -1 : 0);
|
|
let lastTab = tabs.at(this._rtlMode ? 0 : -1);
|
|
startBound = firstTab[screenAxis] - firstMovingTabScreen;
|
|
endBound = endEdge(lastTab) - lastMovingTabScreen;
|
|
translate = Math.min(Math.max(translate, startBound), endBound);
|
|
|
|
// Center the tab under the cursor if the tab is not under the cursor while dragging
|
|
let draggedTabScreenAxis = draggedTab[screenAxis] + translate;
|
|
if (
|
|
(screen < draggedTabScreenAxis || screen > draggedTabScreenAxis + tabSize) &&
|
|
draggedTabScreenAxis + tabSize < endBound &&
|
|
draggedTabScreenAxis > startBound
|
|
) {
|
|
translate = screen - draggedTab[screenAxis] - tabSize / 2;
|
|
// Ensure, after the above calculation, we are still within bounds
|
|
translate = Math.min(Math.max(translate, startBound), endBound);
|
|
}
|
|
|
|
if (!gBrowser.pinnedTabCount && !this._dragToPinPromoCard.shouldRender) {
|
|
let pinnedDropIndicatorMargin = parseFloat(
|
|
window.getComputedStyle(this._pinnedDropIndicator).marginInline
|
|
);
|
|
this._checkWithinPinnedContainerBounds({
|
|
firstMovingTabScreen,
|
|
lastMovingTabScreen,
|
|
pinnedTabsStartEdge: this._rtlMode
|
|
? endEdge(this._tabbrowserTabs.arrowScrollbox) + pinnedDropIndicatorMargin
|
|
: this[screenAxis],
|
|
pinnedTabsEndEdge: this._rtlMode
|
|
? endEdge(this._tabbrowserTabs)
|
|
: this._tabbrowserTabs.arrowScrollbox[screenAxis] - pinnedDropIndicatorMargin,
|
|
translate,
|
|
draggedTab,
|
|
});
|
|
}
|
|
|
|
dragData.translatePos = translate;
|
|
|
|
tabs = tabs.filter((t) => !movingTabsSet.has(t) || t == draggedTab);
|
|
|
|
/**
|
|
* When the `draggedTab` is just starting to move, the `draggedTab` is in
|
|
* its original location and the `dropElementIndex == draggedTab.elementIndex`.
|
|
* Any tabs or tab group labels passed in as `item` will result in a 0 shift
|
|
* because all of those items should also continue to appear in their original
|
|
* locations.
|
|
*
|
|
* Once the `draggedTab` is more "backward" in the tab strip than its original
|
|
* position, any tabs or tab group labels between the `draggedTab`'s original
|
|
* `elementIndex` and the current `dropElementIndex` should shift "forward"
|
|
* out of the way of the dragging tabs.
|
|
*
|
|
* When the `draggedTab` is more "forward" in the tab strip than its original
|
|
* position, any tabs or tab group labels between the `draggedTab`'s original
|
|
* `elementIndex` and the current `dropElementIndex` should shift "backward"
|
|
* out of the way of the dragging tabs.
|
|
*
|
|
* @param {MozTabbrowserTab|MozTabbrowserTabGroup.label} item
|
|
* @param {number} dropElementIndex
|
|
* @returns {number}
|
|
*/
|
|
let getTabShift = (item, dropElementIndex) => {
|
|
if (item.elementIndex < draggedTab.elementIndex && item.elementIndex >= dropElementIndex) {
|
|
return this._rtlMode ? -shiftSize : shiftSize;
|
|
}
|
|
if (item.elementIndex > draggedTab.elementIndex && item.elementIndex < dropElementIndex) {
|
|
return this._rtlMode ? shiftSize : -shiftSize;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
let oldDropElementIndex = dragData.animDropElementIndex ?? movingTabs[0].elementIndex;
|
|
|
|
/**
|
|
* Returns the higher % by which one element overlaps another
|
|
* in the tab strip.
|
|
*
|
|
* When element 1 is further forward in the tab strip:
|
|
*
|
|
* p1 p2 p1+s1 p2+s2
|
|
* | | | |
|
|
* ---------------------------------
|
|
* ========================
|
|
* s1
|
|
* ===================
|
|
* s2
|
|
* ==========
|
|
* overlap
|
|
*
|
|
* When element 2 is further forward in the tab strip:
|
|
*
|
|
* p2 p1 p2+s2 p1+s1
|
|
* | | | |
|
|
* ---------------------------------
|
|
* ========================
|
|
* s2
|
|
* ===================
|
|
* s1
|
|
* ==========
|
|
* overlap
|
|
*
|
|
* @param {number} p1
|
|
* Position (x or y value in screen coordinates) of element 1.
|
|
* @param {number} s1
|
|
* Size (width or height) of element 1.
|
|
* @param {number} p2
|
|
* Position (x or y value in screen coordinates) of element 2.
|
|
* @param {number} s2
|
|
* Size (width or height) of element 1.
|
|
* @returns {number}
|
|
* Percent between 0.0 and 1.0 (inclusive) of element 1 or element 2
|
|
* that is overlapped by the other element. If the elements have
|
|
* different sizes, then this returns the larger overlap percentage.
|
|
*/
|
|
function greatestOverlap(p1, s1, p2, s2) {
|
|
let overlapSize;
|
|
if (p1 < p2) {
|
|
// element 1 starts first
|
|
overlapSize = p1 + s1 - p2;
|
|
} else {
|
|
// element 2 starts first
|
|
overlapSize = p2 + s2 - p1;
|
|
}
|
|
|
|
// No overlap if size is <= 0
|
|
if (overlapSize <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
// Calculate the overlap fraction from each element's perspective.
|
|
let overlapPercent = Math.max(overlapSize / s1, overlapSize / s2);
|
|
|
|
return Math.min(overlapPercent, 1);
|
|
}
|
|
|
|
/**
|
|
* Determine what tab/tab group label we're dragging over.
|
|
*
|
|
* When dragging right or downwards, the reference point for overlap is
|
|
* the right or bottom edge of the most forward moving tab.
|
|
*
|
|
* When dragging left or upwards, the reference point for overlap is the
|
|
* left or top edge of the most backward moving tab.
|
|
*
|
|
* @returns {Element|null}
|
|
* The tab or tab group label that should be used to visually shift tab
|
|
* strip elements out of the way of the dragged tab(s) during a drag
|
|
* operation. Note: this is not used to determine where the dragged
|
|
* tab(s) will be dropped, it is only used for visual animation at this
|
|
* time.
|
|
*/
|
|
let getOverlappedElement = () => {
|
|
let point = (screenForward ? lastMovingTabScreen : firstMovingTabScreen) + translate;
|
|
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;
|
|
}
|
|
let element = tabs[mid];
|
|
let elementForSize = elementToMove(element);
|
|
screen = elementForSize[screenAxis] + getTabShift(element, oldDropElementIndex);
|
|
|
|
if (screen > point) {
|
|
high = mid - 1;
|
|
} else if (screen + bounds(elementForSize)[size] < point) {
|
|
low = mid + 1;
|
|
} else {
|
|
return element;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
let dropElement = getOverlappedElement();
|
|
|
|
let newDropElementIndex;
|
|
if (dropElement) {
|
|
newDropElementIndex = dropElement.elementIndex;
|
|
} else {
|
|
// When the dragged element(s) moves past a tab strip item, the dragged
|
|
// element's leading edge starts dragging over empty space, resulting in
|
|
// no overlapping `dropElement`. In these cases, try to fall back to the
|
|
// previous animation drop element index to avoid unstable animations
|
|
// (tab strip items snapping back and forth to shift out of the way of
|
|
// the dragged element(s)).
|
|
newDropElementIndex = oldDropElementIndex;
|
|
|
|
// We always want to have a `dropElement` so that we can determine where to
|
|
// logically drop the dragged element(s).
|
|
//
|
|
// It's tempting to set `dropElement` to
|
|
// `this.ariaFocusableItems.at(oldDropElementIndex)`, and that is correct
|
|
// for most cases, but there are edge cases:
|
|
//
|
|
// 1) the drop element index range needs to be one larger than the number of
|
|
// items that can move in the tab strip. The simplest example is when all
|
|
// tabs are ungrouped and unpinned: for 5 tabs, the drop element index needs
|
|
// to be able to go from 0 (become the first tab) to 5 (become the last tab).
|
|
// `this.ariaFocusableItems.at(5)` would be `undefined` when dragging to the
|
|
// end of the tab strip. In this specific case, it works to fall back to
|
|
// setting the drop element to the last tab.
|
|
//
|
|
// 2) the `elementIndex` values of the tab strip items do not change during
|
|
// the drag operation. When dragging the last tab or multiple tabs at the end
|
|
// of the tab strip, having `dropElement` fall back to the last tab makes the
|
|
// drop element one of the moving tabs. This can have some unexpected behavior
|
|
// if not careful. Falling back to the last tab that's not moving (instead of
|
|
// just the last tab) helps ensure that `dropElement` is always a stable target
|
|
// to drop next to.
|
|
//
|
|
// 3) all of the elements in the tab strip are moving, in which case there can't
|
|
// be a drop element and it should stay `undefined`.
|
|
//
|
|
// 4) we just started dragging and the `oldDropElementIndex` has its default
|
|
// valuë of `movingTabs[0].elementIndex`. In this case, the drop element
|
|
// shouldn't be a moving tab, so keep it `undefined`.
|
|
let lastPossibleDropElement = this._rtlMode
|
|
? tabs.find((t) => t != draggedTab)
|
|
: tabs.findLast((t) => t != draggedTab);
|
|
let maxElementIndexForDropElement = lastPossibleDropElement?.elementIndex;
|
|
if (Number.isInteger(maxElementIndexForDropElement)) {
|
|
let index = Math.min(oldDropElementIndex, maxElementIndexForDropElement);
|
|
let oldDropElementCandidate = this._tabbrowserTabs.ariaFocusableItems.at(index);
|
|
if (!movingTabsSet.has(oldDropElementCandidate)) {
|
|
dropElement = oldDropElementCandidate;
|
|
}
|
|
}
|
|
}
|
|
|
|
let moveOverThreshold;
|
|
let overlapPercent;
|
|
let dropBefore;
|
|
if (dropElement) {
|
|
let dropElementForOverlap = elementToMove(dropElement);
|
|
|
|
let dropElementScreen = dropElementForOverlap[screenAxis];
|
|
let dropElementPos = dropElementScreen + getTabShift(dropElement, oldDropElementIndex);
|
|
let dropElementSize = bounds(dropElementForOverlap)[size];
|
|
let firstMovingTabPos = firstMovingTabScreen + translate;
|
|
overlapPercent = greatestOverlap(
|
|
firstMovingTabPos,
|
|
shiftSize,
|
|
dropElementPos,
|
|
dropElementSize
|
|
);
|
|
|
|
moveOverThreshold = gBrowser._tabGroupsEnabled
|
|
? Services.prefs.getIntPref('browser.tabs.dragDrop.moveOverThresholdPercent') / 100
|
|
: 0.5;
|
|
moveOverThreshold = Math.min(1, Math.max(0, moveOverThreshold));
|
|
let shouldMoveOver = overlapPercent > moveOverThreshold;
|
|
if (logicalForward && shouldMoveOver) {
|
|
newDropElementIndex++;
|
|
} else if (!logicalForward && !shouldMoveOver) {
|
|
newDropElementIndex++;
|
|
if (newDropElementIndex > oldDropElementIndex) {
|
|
// FIXME: Not quite sure what's going on here, but this check
|
|
// prevents jittery back-and-forth movement of background tabs
|
|
// in certain cases.
|
|
newDropElementIndex = oldDropElementIndex;
|
|
}
|
|
}
|
|
|
|
// Recalculate the overlap with the updated drop index for when the
|
|
// drop element moves over.
|
|
dropElementPos = dropElementScreen + getTabShift(dropElement, newDropElementIndex);
|
|
overlapPercent = greatestOverlap(
|
|
firstMovingTabPos,
|
|
shiftSize,
|
|
dropElementPos,
|
|
dropElementSize
|
|
);
|
|
dropBefore = firstMovingTabPos < dropElementPos;
|
|
if (this._rtlMode) {
|
|
dropBefore = !dropBefore;
|
|
}
|
|
}
|
|
|
|
this._tabbrowserTabs.removeAttribute('movingtab-group');
|
|
this._resetGroupTarget(document.querySelector('[dragover-groupTarget]'));
|
|
|
|
delete dragData.shouldDropIntoCollapsedTabGroup;
|
|
|
|
[dropBefore, dropElement] = this.#applyDragoverIndicator(
|
|
event,
|
|
tabs,
|
|
movingTabs,
|
|
draggedTab
|
|
) ?? [dropBefore, dropElement];
|
|
|
|
// 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);
|
|
|
|
if (
|
|
newDropElementIndex == oldDropElementIndex &&
|
|
dropBefore == dragData.dropBefore &&
|
|
dropElement == dragData.dropElement
|
|
) {
|
|
return;
|
|
}
|
|
|
|
dragData.dropElement = dropElement;
|
|
dragData.dropBefore = dropBefore;
|
|
dragData.animDropElementIndex = newDropElementIndex;
|
|
}
|
|
|
|
#isMovingTab() {
|
|
return this._tabbrowserTabs.hasAttribute('movingtab');
|
|
}
|
|
|
|
get #dragShiftableItems() {
|
|
const separator = gZenWorkspaces.pinnedTabsContainer.querySelector(
|
|
'.pinned-tabs-container-separator'
|
|
);
|
|
// Make sure to always return the separator at the start of the array
|
|
return Services.prefs.getBoolPref('zen.view.show-newtab-button-top')
|
|
? [separator, gZenWorkspaces.activeWorkspaceElement.newTabButton]
|
|
: [separator];
|
|
}
|
|
|
|
handle_dragover(event) {
|
|
super.handle_dragover(event);
|
|
if (!gZenVerticalTabsManager._prefsSidebarExpanded) {
|
|
return;
|
|
}
|
|
this.#handle_sidebarDragOver(event);
|
|
}
|
|
|
|
#shouldSwitchSpace(event) {
|
|
const padding = 10;
|
|
// If we are hovering over the edges of the gNavToolbox or the splitter, we
|
|
// can change the workspace after a short delay.
|
|
const splitter = document.getElementById('zen-sidebar-splitter');
|
|
let rect = window.windowUtils.getBoundsWithoutFlushing(gNavToolbox);
|
|
if (!(gZenCompactModeManager.preference && gZenCompactModeManager.canHideSidebar)) {
|
|
rect.width += window.windowUtils.getBoundsWithoutFlushing(splitter).width;
|
|
}
|
|
const { clientX } = event;
|
|
const isNearLeftEdge = clientX >= rect.left - padding && clientX <= rect.left + padding;
|
|
const isNearRightEdge = clientX >= rect.right - padding && clientX <= rect.right + padding;
|
|
return { isNearLeftEdge, isNearRightEdge };
|
|
}
|
|
|
|
clearSpaceSwitchTimer() {
|
|
if (this.#changeSpaceTimer) {
|
|
clearTimeout(this.#changeSpaceTimer);
|
|
this.#changeSpaceTimer = null;
|
|
}
|
|
}
|
|
|
|
#handle_sidebarDragOver(event) {
|
|
const dt = event.dataTransfer;
|
|
const draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
|
|
// TODO: Add support for switching spaces when dragging folders and split-view groups.
|
|
if (!isTab(draggedTab) || draggedTab.hasAttribute('zen-essential')) {
|
|
this.clearSpaceSwitchTimer();
|
|
return;
|
|
}
|
|
const { isNearLeftEdge, isNearRightEdge } = this.#shouldSwitchSpace(event);
|
|
if (isNearLeftEdge || isNearRightEdge) {
|
|
if (!this.#changeSpaceTimer) {
|
|
this.#changeSpaceTimer = setTimeout(() => {
|
|
this.clearDragOverVisuals();
|
|
dt.updateDragImage(...this.originalDragImageArgs);
|
|
gZenWorkspaces.changeWorkspaceShortcut(
|
|
isNearLeftEdge ? -1 : 1,
|
|
false,
|
|
/* Disable wrapping */ true
|
|
);
|
|
this.#changeSpaceTimer = null;
|
|
}, this._dndSwitchSpaceDelay);
|
|
}
|
|
} else if (this.#changeSpaceTimer) {
|
|
this.clearSpaceSwitchTimer();
|
|
}
|
|
}
|
|
|
|
handle_windowDragEnter(event) {
|
|
if (!this.#isMovingTab() || !this.#isOutOfWindow) {
|
|
return;
|
|
}
|
|
this.#isOutOfWindow = false;
|
|
const dt = event.dataTransfer;
|
|
dt.updateDragImage(...this.originalDragImageArgs);
|
|
}
|
|
|
|
handle_windowDragLeave(event) {
|
|
const canvas = this._tabbrowserTabs._dndCanvas;
|
|
if (!this.#isMovingTab() || !canvas) {
|
|
return;
|
|
}
|
|
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
|
|
if (!isTab(draggedTab)) {
|
|
return;
|
|
}
|
|
const { clientX, clientY } = event;
|
|
const { innerWidth, innerHeight } = window;
|
|
const isOutOfWindow =
|
|
clientX < 0 || clientX > innerWidth || clientY < 0 || clientY > innerHeight;
|
|
if (isOutOfWindow && !this.#isOutOfWindow) {
|
|
this.#isOutOfWindow = true;
|
|
this.#maybeClearVerticalPinnedGridDragOver();
|
|
this.clearSpaceSwitchTimer();
|
|
this.clearDragOverVisuals();
|
|
const dt = event.dataTransfer;
|
|
let dragData = draggedTab._dragData;
|
|
let movingTabs = dragData.movingTabs;
|
|
if (!this._browserDragImageWrapper) {
|
|
const wrappingDiv = document.createXULElement('vbox');
|
|
canvas.style.borderRadius = '8px';
|
|
canvas.style.border = '2px solid white';
|
|
wrappingDiv.style.width = 200 + 'px';
|
|
wrappingDiv.style.height = 130 + 'px';
|
|
wrappingDiv.style.position = 'relative';
|
|
this.#maybeCreateDragImageDot(movingTabs, wrappingDiv);
|
|
wrappingDiv.appendChild(canvas);
|
|
this._browserDragImageWrapper = wrappingDiv;
|
|
document.documentElement.appendChild(wrappingDiv);
|
|
}
|
|
dt.updateDragImage(
|
|
this._browserDragImageWrapper,
|
|
this.originalDragImageArgs[1],
|
|
this.originalDragImageArgs[2]
|
|
);
|
|
window.addEventListener('dragover', this.handle_windowDragEnter, {
|
|
once: true,
|
|
capture: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
handle_drop(event) {
|
|
this.clearSpaceSwitchTimer();
|
|
super.handle_drop(event);
|
|
const dt = event.dataTransfer;
|
|
let draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
|
|
if (
|
|
isTab(draggedTab) &&
|
|
!draggedTab.hasAttribute('zen-essential') &&
|
|
draggedTab.getAttribute('zen-workspace-id') != gZenWorkspaces.activeWorkspace
|
|
) {
|
|
const movingTabs = draggedTab._dragData?.movingTabs || [draggedTab];
|
|
for (let tab of movingTabs) {
|
|
tab.setAttribute('zen-workspace-id', gZenWorkspaces.activeWorkspace);
|
|
}
|
|
gBrowser.selectedTab = draggedTab;
|
|
}
|
|
gZenWorkspaces.updateTabsContainers();
|
|
}
|
|
|
|
handle_drop_transition(dropElement, draggedTab, movingTabs, dropBefore) {
|
|
if (isTabGroupLabel(dropElement)) {
|
|
dropElement = dropElement.group;
|
|
}
|
|
if (isTabGroupLabel(draggedTab)) {
|
|
draggedTab = draggedTab.group;
|
|
}
|
|
let animations = [];
|
|
try {
|
|
if (
|
|
this.#isAnimatingTabMove ||
|
|
!gZenStartup.isReady ||
|
|
gReduceMotion ||
|
|
!dropElement ||
|
|
dropElement.group !== draggedTab.group ||
|
|
dropElement.hasAttribute('zen-essential') ||
|
|
draggedTab.hasAttribute('zen-essential') ||
|
|
draggedTab.getAttribute('zen-workspace-id') != gZenWorkspaces.activeWorkspace
|
|
) {
|
|
return;
|
|
}
|
|
this.#isAnimatingTabMove = true;
|
|
for (let item of this._tabbrowserTabs.ariaFocusableItems) {
|
|
item = elementToMove(item);
|
|
item.style.transform = '';
|
|
}
|
|
const animateElement = (ele, translateY) => {
|
|
ele.style.transform = `translateY(${translateY}px)`;
|
|
setTimeout(() => {
|
|
setTimeout(() => {
|
|
animations.push(
|
|
gZenUIManager.motion
|
|
.animate(
|
|
ele,
|
|
{
|
|
y: [translateY, 0],
|
|
},
|
|
{
|
|
duration: 0.1,
|
|
bounce: 0,
|
|
}
|
|
)
|
|
.then(() => {
|
|
ele.style.transform = '';
|
|
})
|
|
);
|
|
});
|
|
});
|
|
};
|
|
const items = this._tabbrowserTabs.ariaFocusableItems;
|
|
let rect = window.windowUtils.getBoundsWithoutFlushing(draggedTab);
|
|
let tabsInBetween = [];
|
|
let startIndex = Math.min(draggedTab.elementIndex, dropElement.elementIndex + !dropBefore);
|
|
let endIndex = Math.max(draggedTab.elementIndex, dropElement.elementIndex - dropBefore);
|
|
for (let i = startIndex; i <= endIndex; i++) {
|
|
let tab = items[i];
|
|
if (!movingTabs.includes(tab) && isTab(tab)) {
|
|
tabsInBetween.push(tab);
|
|
}
|
|
}
|
|
let extraTranslate = 0;
|
|
let translateY =
|
|
draggedTab.elementIndex > dropElement.elementIndex ? -rect.height : rect.height;
|
|
translateY *= movingTabs.length;
|
|
if (draggedTab.pinned != dropElement.pinned) {
|
|
const shiftableItems = this.#dragShiftableItems;
|
|
for (let item of shiftableItems) {
|
|
// We also need to animate these shiftable items and add it to the extraTranslate
|
|
// so the dragged tab ends up in the right position.
|
|
let itemRect = window.windowUtils.getBoundsWithoutFlushing(item);
|
|
extraTranslate += itemRect.height;
|
|
animateElement(item, translateY);
|
|
}
|
|
}
|
|
// Animate tabs in between moving out of the way
|
|
for (let tab of tabsInBetween) {
|
|
animateElement(tab, translateY);
|
|
}
|
|
let draggedTabTranslateY =
|
|
draggedTab.elementIndex > dropElement.elementIndex
|
|
? rect.height * tabsInBetween.length
|
|
: -rect.height * tabsInBetween.length;
|
|
draggedTabTranslateY +=
|
|
extraTranslate * (draggedTab.elementIndex > dropElement.elementIndex ? 1 : -1);
|
|
animateElement(draggedTab, draggedTabTranslateY);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
Promise.all(animations).finally(() => {
|
|
this.#isAnimatingTabMove = false;
|
|
});
|
|
}
|
|
|
|
handle_dragend(event) {
|
|
this.ZenDragAndDropService.onDragEnd();
|
|
super.handle_dragend(event);
|
|
this.#removeDragOverBackground();
|
|
gZenPinnedTabManager.removeTabContainersDragoverClass();
|
|
this.#maybeClearVerticalPinnedGridDragOver();
|
|
this.originalDragImageArgs = [];
|
|
window.removeEventListener('dragover', this.handle_windowDragEnter, { capture: true });
|
|
this.#isOutOfWindow = false;
|
|
if (this._browserDragImageWrapper) {
|
|
this._browserDragImageWrapper.remove();
|
|
delete this._browserDragImageWrapper;
|
|
}
|
|
if (this._tempDragImageParent) {
|
|
this._tempDragImageParent.remove();
|
|
delete this._tempDragImageParent;
|
|
}
|
|
}
|
|
|
|
#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;
|
|
}
|
|
}
|
|
|
|
clearDragOverVisuals() {
|
|
this.#removeDragOverBackground();
|
|
gZenPinnedTabManager.removeTabContainersDragoverClass();
|
|
}
|
|
|
|
#applyDragoverIndicator(event, tabs, movingTabs, draggedTab) {
|
|
const separation = 4;
|
|
const dropZoneSelector =
|
|
':is(.tabbrowser-tab, .zen-drop-target, .tab-group-label, tab-group[split-view-group])';
|
|
let shouldPlayHapticFeedback = false;
|
|
let showIndicatorUnderNewTabButton = false;
|
|
let dropElement = event.target.closest(dropZoneSelector);
|
|
let dropBefore;
|
|
if (!dropElement) {
|
|
if (event.target.classList.contains('zen-workspace-empty-space')) {
|
|
dropElement = this._tabbrowserTabs.ariaFocusableItems.at(-1);
|
|
// Only if there are no normal tabs to drop after
|
|
showIndicatorUnderNewTabButton = !tabs.some((tab) => !(tab.group || tab).pinned);
|
|
} else {
|
|
const numEssentials = gBrowser._numZenEssentials;
|
|
const numPinned = gBrowser.pinnedTabCount - numEssentials;
|
|
const tabToUse = event.target.closest(dropZoneSelector);
|
|
if (!tabToUse) {
|
|
this.clearDragOverVisuals();
|
|
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];
|
|
}
|
|
}
|
|
dropElement = elementToMove(dropElement);
|
|
this.#maybeClearVerticalPinnedGridDragOver();
|
|
if (this.#lastDropTarget !== dropElement) {
|
|
shouldPlayHapticFeedback = this.#lastDropTarget !== null;
|
|
this.#removeDragOverBackground();
|
|
}
|
|
let isZenFolder = dropElement.parentElement?.isZenFolder;
|
|
let canHightlightGroup =
|
|
gZenFolders.highlightGroupOnDragOver(dropElement.parentElement, movingTabs) || !isZenFolder;
|
|
let rect = window.windowUtils.getBoundsWithoutFlushing(dropElement);
|
|
const overlapPercent = (event.clientY - rect.top) / rect.height;
|
|
// We wan't to leave a small threshold (20% for example) so we can drag tabs below and above
|
|
// a folder label without dragging into the folder.
|
|
let threshold = Services.prefs.getIntPref('zen.tabs.folder-dragover-threshold-percent') / 100;
|
|
let dropIntoFolder =
|
|
isZenFolder && (overlapPercent < threshold || overlapPercent > 1 - threshold);
|
|
if (
|
|
isTabGroupLabel(draggedTab) &&
|
|
draggedTab.group?.isZenFolder &&
|
|
(isTab(dropElement) || dropElement.hasAttribute('split-view-group')) &&
|
|
(!dropElement.pinned || dropElement.hasAttribute('zen-essential'))
|
|
) {
|
|
this.clearDragOverVisuals();
|
|
return;
|
|
}
|
|
if (
|
|
isTab(dropElement) ||
|
|
dropIntoFolder ||
|
|
showIndicatorUnderNewTabButton ||
|
|
dropElement.hasAttribute('split-view-group')
|
|
) {
|
|
if (showIndicatorUnderNewTabButton) {
|
|
rect = window.windowUtils.getBoundsWithoutFlushing(this.#dragShiftableItems.at(-1));
|
|
}
|
|
const indicator = gZenPinnedTabManager.dragIndicator;
|
|
let top = 0;
|
|
threshold =
|
|
Services.prefs.getIntPref('browser.tabs.dragDrop.moveOverThresholdPercent') / 100;
|
|
if (overlapPercent > threshold) {
|
|
top = Math.round(rect.top + rect.height) + 'px';
|
|
dropBefore = false;
|
|
} else {
|
|
top = Math.round(rect.top) + 'px';
|
|
dropBefore = true;
|
|
}
|
|
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');
|
|
this.#removeDragOverBackground();
|
|
if (!isTab(dropElement) && dropElement?.parentElement?.isZenFolder) {
|
|
dropElement = dropElement.parentElement;
|
|
}
|
|
} else if (dropElement.classList.contains('zen-drop-target') && canHightlightGroup) {
|
|
shouldPlayHapticFeedback =
|
|
this.#applyDragOverBackground(dropElement) && !gZenPinnedTabManager._dragIndicator;
|
|
gZenPinnedTabManager.removeTabContainersDragoverClass();
|
|
dropElement = dropElement.parentElement?.labelElement || dropElement;
|
|
}
|
|
|
|
if (shouldPlayHapticFeedback) {
|
|
Services.zen.playHapticFeedback();
|
|
}
|
|
return [dropBefore, dropElement];
|
|
}
|
|
|
|
#getDragImageOffset(event, tab, draggingTabs) {
|
|
if (draggingTabs.length > 1) {
|
|
return {
|
|
offsetX: 18,
|
|
offsetY: 18,
|
|
};
|
|
}
|
|
const rect = tab.getBoundingClientRect();
|
|
return {
|
|
offsetX: event.clientX - rect.left,
|
|
offsetY: event.clientY - rect.top,
|
|
};
|
|
}
|
|
|
|
#animateVerticalPinnedGridDragOver(event) {
|
|
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
|
|
let dragData = draggedTab._dragData;
|
|
let movingTabs = dragData.movingTabs;
|
|
this.clearDragOverVisuals();
|
|
if (
|
|
!draggedTab.hasAttribute('zen-essential') &&
|
|
gBrowser._numZenEssentials >= gZenPinnedTabManager.maxEssentialTabs
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (!this._fakeEssentialTab) {
|
|
const numEssentials = gBrowser._numZenEssentials;
|
|
let pinnedTabs = this._tabbrowserTabs.ariaFocusableItems.slice(0, numEssentials);
|
|
this._fakeEssentialTab = document.createXULElement('vbox');
|
|
this._fakeEssentialTab.elementIndex = numEssentials;
|
|
delete dragData.animDropElementIndex;
|
|
if (!draggedTab.hasAttribute('zen-essential')) {
|
|
event.target.closest('.zen-essentials-container').appendChild(this._fakeEssentialTab);
|
|
gZenWorkspaces.updateTabsContainers();
|
|
pinnedTabs.push(this._fakeEssentialTab);
|
|
}
|
|
this.#makeDragImageEssential(event);
|
|
let tabsPerRow = 0;
|
|
let position = RTL_UI
|
|
? window.windowUtils.getBoundsWithoutFlushing(this._tabbrowserTabs.pinnedTabsContainer)
|
|
.right
|
|
: 0;
|
|
for (let pinnedTab of pinnedTabs) {
|
|
let tabPosition;
|
|
let rect = window.windowUtils.getBoundsWithoutFlushing(pinnedTab);
|
|
if (RTL_UI) {
|
|
tabPosition = rect.right;
|
|
if (tabPosition > position) {
|
|
break;
|
|
}
|
|
} else {
|
|
tabPosition = rect.left;
|
|
if (tabPosition < position) {
|
|
break;
|
|
}
|
|
}
|
|
tabsPerRow++;
|
|
position = tabPosition;
|
|
}
|
|
this.#maxTabsPerRow = tabsPerRow;
|
|
}
|
|
let usingFakeElement = !!this._fakeEssentialTab.parentElement;
|
|
let elementMoving = usingFakeElement ? this._fakeEssentialTab : draggedTab;
|
|
if (usingFakeElement) {
|
|
movingTabs = [this._fakeEssentialTab];
|
|
}
|
|
|
|
let dragDataScreenX = usingFakeElement ? this._fakeEssentialTab.screenX : dragData.screenX;
|
|
let dragDataScreenY = usingFakeElement ? this._fakeEssentialTab.screenY : dragData.screenY;
|
|
|
|
dragData.animLastScreenX ??= dragDataScreenX;
|
|
dragData.animLastScreenY ??= dragDataScreenY;
|
|
|
|
let screenX = event.screenX;
|
|
let screenY = event.screenY;
|
|
|
|
if (screenY == dragData.animLastScreenY && screenX == dragData.animLastScreenX) {
|
|
return;
|
|
}
|
|
|
|
let tabs = this._tabbrowserTabs.visibleTabs.slice(0, gBrowser._numZenEssentials);
|
|
if (usingFakeElement) {
|
|
tabs.push(this._fakeEssentialTab);
|
|
}
|
|
|
|
let directionX = screenX > dragData.animLastScreenX;
|
|
let directionY = screenY > dragData.animLastScreenY;
|
|
dragData.animLastScreenY = screenY;
|
|
dragData.animLastScreenX = screenX;
|
|
|
|
let { width: tabWidth, height: tabHeight } = elementMoving.getBoundingClientRect();
|
|
tabWidth += 4; // Add 6px to account for the gap
|
|
tabHeight += 4;
|
|
let shiftSizeX = tabWidth;
|
|
let shiftSizeY = tabHeight;
|
|
dragData.tabWidth = tabWidth;
|
|
dragData.tabHeight = tabHeight;
|
|
|
|
// Move the dragged tab based on the mouse position.
|
|
let firstTabInRow;
|
|
let lastTabInRow;
|
|
let lastTab = tabs.at(-1);
|
|
if (RTL_UI) {
|
|
firstTabInRow =
|
|
tabs.length >= this.#maxTabsPerRow ? tabs[this.#maxTabsPerRow - 1] : lastTab;
|
|
lastTabInRow = tabs[0];
|
|
} else {
|
|
firstTabInRow = tabs[0];
|
|
lastTabInRow = tabs.length >= this.#maxTabsPerRow ? tabs[this.#maxTabsPerRow - 1] : lastTab;
|
|
}
|
|
let lastMovingTabScreenX = movingTabs.at(-1).screenX;
|
|
let lastMovingTabScreenY = movingTabs.at(-1).screenY;
|
|
let firstMovingTabScreenX = movingTabs[0].screenX;
|
|
let firstMovingTabScreenY = movingTabs[0].screenY;
|
|
let translateX = screenX - dragDataScreenX;
|
|
let translateY = screenY - dragDataScreenY;
|
|
let firstBoundX = firstTabInRow.screenX - firstMovingTabScreenX;
|
|
let firstBoundY = this._tabbrowserTabs.screenY - firstMovingTabScreenY;
|
|
let lastBoundX =
|
|
lastTabInRow.screenX +
|
|
lastTabInRow.getBoundingClientRect().width -
|
|
(lastMovingTabScreenX + tabWidth);
|
|
let lastBoundY = lastTab.screenY - lastMovingTabScreenY;
|
|
translateX = Math.min(Math.max(translateX, firstBoundX), lastBoundX);
|
|
translateY = Math.min(Math.max(translateY, firstBoundY), lastBoundY);
|
|
|
|
// Center the tab under the cursor if the tab is not under the cursor while dragging
|
|
if (
|
|
screen < elementMoving.screenY + translateY ||
|
|
screen > elementMoving.screenY + tabHeight + translateY
|
|
) {
|
|
translateY = screen - elementMoving.screenY - tabHeight / 2;
|
|
}
|
|
|
|
dragData.translateX = translateX;
|
|
dragData.translateY = translateY;
|
|
|
|
// 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 top to bottom, the bottom reference gets activated,
|
|
// otherwise the top 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 == elementMoving);
|
|
let firstTabCenterX = firstMovingTabScreenX + translateX + tabWidth / 2;
|
|
let lastTabCenterX = lastMovingTabScreenX + translateX + tabWidth / 2;
|
|
let tabCenterX = directionX ? lastTabCenterX : firstTabCenterX;
|
|
let firstTabCenterY = firstMovingTabScreenY + translateY + tabHeight / 2;
|
|
let lastTabCenterY = lastMovingTabScreenY + translateY + tabHeight / 2;
|
|
let tabCenterY = directionY ? lastTabCenterY : firstTabCenterY;
|
|
|
|
let shiftNumber = this.#maxTabsPerRow - movingTabs.length;
|
|
|
|
let getTabShift = (tab, dropIndex) => {
|
|
if (tab.elementIndex < elementMoving.elementIndex && tab.elementIndex >= dropIndex) {
|
|
// If tab is at the end of a row, shift back and down
|
|
let tabRow = Math.ceil((tab.elementIndex + 1) / this.#maxTabsPerRow);
|
|
let shiftedTabRow = Math.ceil(
|
|
(tab.elementIndex + 1 + movingTabs.length) / this.#maxTabsPerRow
|
|
);
|
|
if (tab.elementIndex && tabRow != shiftedTabRow) {
|
|
return [RTL_UI ? tabWidth * shiftNumber : -tabWidth * shiftNumber, shiftSizeY];
|
|
}
|
|
return [RTL_UI ? -shiftSizeX : shiftSizeX, 0];
|
|
}
|
|
if (tab.elementIndex > elementMoving.elementIndex && tab.elementIndex < dropIndex) {
|
|
// If tab is not index 0 and at the start of a row, shift across and up
|
|
let tabRow = Math.floor(tab.elementIndex / this.#maxTabsPerRow);
|
|
let shiftedTabRow = Math.floor(
|
|
(tab.elementIndex - movingTabs.length) / this.#maxTabsPerRow
|
|
);
|
|
if (tab.elementIndex && tabRow != shiftedTabRow) {
|
|
return [RTL_UI ? -tabWidth * shiftNumber : tabWidth * shiftNumber, -shiftSizeY];
|
|
}
|
|
return [RTL_UI ? shiftSizeX : -shiftSizeX, 0];
|
|
}
|
|
return [0, 0];
|
|
};
|
|
|
|
let low = 0;
|
|
let high = tabs.length - 1;
|
|
let newIndex = -1;
|
|
let oldIndex = dragData.animDropElementIndex ?? movingTabs[0].elementIndex;
|
|
while (low <= high) {
|
|
let mid = Math.floor((low + high) / 2);
|
|
if (tabs[mid] == elementMoving && ++mid > high) {
|
|
break;
|
|
}
|
|
let [shiftX, shiftY] = getTabShift(tabs[mid], oldIndex);
|
|
screenX = tabs[mid].screenX + shiftX;
|
|
screenY = tabs[mid].screenY + shiftY;
|
|
|
|
if (screenY + tabHeight < tabCenterY) {
|
|
low = mid + 1;
|
|
} else if (screenY > tabCenterY) {
|
|
high = mid - 1;
|
|
} else if (RTL_UI ? screenX + tabWidth < tabCenterX : screenX > tabCenterX) {
|
|
high = mid - 1;
|
|
} else if (RTL_UI ? screenX > tabCenterX : screenX + tabWidth < tabCenterX) {
|
|
low = mid + 1;
|
|
} else {
|
|
newIndex = tabs[mid].elementIndex;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (newIndex >= oldIndex && newIndex < tabs.length) {
|
|
newIndex++;
|
|
}
|
|
|
|
if (newIndex < 0) {
|
|
newIndex = oldIndex;
|
|
}
|
|
|
|
if (newIndex == dragData.animDropElementIndex) {
|
|
return;
|
|
}
|
|
|
|
dragData.animDropElementIndex = newIndex;
|
|
dragData.dropElement = tabs[Math.min(newIndex, tabs.length - 1)];
|
|
dragData.dropBefore = newIndex < tabs.length;
|
|
|
|
// Shift background tabs to leave a gap where the dragged tab
|
|
// would currently be dropped.
|
|
for (let tab of tabs) {
|
|
if (tab != draggedTab) {
|
|
let [shiftX, shiftY] = getTabShift(tab, newIndex);
|
|
tab.style.transform = shiftX || shiftY ? `translate(${shiftX}px, ${shiftY}px)` : '';
|
|
}
|
|
}
|
|
}
|
|
|
|
#maybeClearVerticalPinnedGridDragOver() {
|
|
if (this._fakeEssentialTab) {
|
|
this._fakeEssentialTab.remove();
|
|
delete this._fakeEssentialTab;
|
|
for (let tab of this._tabbrowserTabs.visibleTabs.slice(0, gBrowser._numZenEssentials)) {
|
|
tab.style.transform = '';
|
|
}
|
|
gZenWorkspaces.updateTabsContainers();
|
|
}
|
|
}
|
|
|
|
#makeDragImageEssential(event) {
|
|
const dt = event.dataTransfer;
|
|
const draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
|
|
const dragData = draggedTab._dragData;
|
|
const [wrapper] = this.originalDragImageArgs;
|
|
const tab = wrapper.firstElementChild;
|
|
tab.setAttribute('zen-essential', 'true');
|
|
tab.setAttribute('pinned', 'true');
|
|
tab.setAttribute('selected', 'true');
|
|
const draggedTabRect = window.windowUtils.getBoundsWithoutFlushing(this._fakeEssentialTab);
|
|
tab.style.minWidth = tab.style.maxWidth = wrapper.style.width = draggedTabRect.width + 'px';
|
|
tab.style.minHeight =
|
|
tab.style.maxHeight =
|
|
wrapper.style.height =
|
|
draggedTabRect.height + 'px';
|
|
const offsetY = dragData.offsetY;
|
|
const offsetX = dragData.offsetX;
|
|
// Apply a transform translate to the tab in order to center it within the drag image
|
|
tab.style.transform = `translate(${(54 - offsetX) / 2}px, ${(50 - offsetY) / 2}px)`;
|
|
gZenPinnedTabManager.setEssentialTabIcon(tab);
|
|
dt.updateDragImage(wrapper, -16, -16);
|
|
}
|
|
|
|
#makeDragImageNonEssential(event) {
|
|
const dt = event.dataTransfer;
|
|
const draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
|
|
const wrapper = this.originalDragImageArgs[0];
|
|
const tab = wrapper.firstElementChild;
|
|
tab.style.setProperty('transition', 'none', 'important');
|
|
tab.removeAttribute('zen-essential');
|
|
tab.removeAttribute('pinned');
|
|
tab.style.minWidth = tab.style.maxWidth = '';
|
|
tab.style.minHeight = tab.style.maxHeight = '';
|
|
tab.style.transform = '';
|
|
const rect = window.windowUtils.getBoundsWithoutFlushing(draggedTab);
|
|
wrapper.style.width = rect.width + 'px';
|
|
wrapper.style.height = rect.height + 'px';
|
|
setTimeout(() => {
|
|
tab.style.transition = '';
|
|
dt.updateDragImage(...this.originalDragImageArgs);
|
|
}, 50);
|
|
}
|
|
};
|
|
}
|