tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

tab-stacking.js (64336B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 // Wrap in a block to prevent leaking to window scope.
      8 {
      9  const isTab = element => gBrowser.isTab(element);
     10  const isTabGroupLabel = element => gBrowser.isTabGroupLabel(element);
     11  const isSplitViewWrapper = element => gBrowser.isSplitViewWrapper(element);
     12  /**
     13   * The elements in the tab strip from `this.dragAndDropElements` that contain
     14   * logical information are:
     15   *
     16   * - <tab> (.tabbrowser-tab)
     17   * - <tab-group> label element (.tab-group-label)
     18   * - <tab-split-view-wrapper>
     19   *
     20   * The elements in the tab strip that contain the space inside of the <tabs>
     21   * element are:
     22   *
     23   * - <tab> (.tabbrowser-tab)
     24   * - <tab-group> label element wrapper (.tab-group-label-container)
     25   * - <tab-split-view-wrapper>
     26   *
     27   * When working with tab strip items, if you need logical information, you
     28   * can get it directly, e.g. `element.elementIndex` or `element._tPos`. If
     29   * you need spatial information like position or dimensions, then you should
     30   * call this function. For example, `elementToMove(element).getBoundingClientRect()`
     31   * or `elementToMove(element).style.top`.
     32   *
     33   * @param {MozTabbrowserTab|typeof MozTabbrowserTabGroup.labelElement} element
     34   * @returns {MozTabbrowserTab|vbox}
     35   */
     36  const elementToMove = element => {
     37    if (isTab(element) || isSplitViewWrapper(element)) {
     38      return element;
     39    }
     40    if (isTabGroupLabel(element)) {
     41      return element.closest(".tab-group-label-container");
     42    }
     43    throw new Error(`Element "${element.tagName}" is not expected to move`);
     44  };
     45 
     46  window.TabStacking = class extends window.TabDragAndDrop {
     47    constructor(tabbrowserTabs) {
     48      super(tabbrowserTabs);
     49    }
     50 
     51    // eslint-disable-next-line complexity
     52    handle_drop(event) {
     53      var dt = event.dataTransfer;
     54      var dropEffect = dt.dropEffect;
     55      var draggedTab;
     56      let movingTabs;
     57      /** @type {TabMetricsContext} */
     58      const dropMetricsContext = gBrowser.TabMetrics.userTriggeredContext(
     59        gBrowser.TabMetrics.METRIC_SOURCE.DRAG_AND_DROP
     60      );
     61      if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) {
     62        // tab copy or move
     63        draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
     64        // not our drop then
     65        if (!draggedTab) {
     66          return;
     67        }
     68        movingTabs = draggedTab._dragData.movingTabs;
     69        draggedTab.container.tabDragAndDrop.finishMoveTogetherSelectedTabs(
     70          draggedTab
     71        );
     72      }
     73 
     74      if (this._rtlMode) {
     75        // In `startTabDrag` we reverse the moving tabs order to handle
     76        // positioning and animation. For drop, we require the original
     77        // order, so reverse back.
     78        movingTabs?.reverse();
     79      }
     80 
     81      let overPinnedDropIndicator =
     82        this._pinnedDropIndicator.hasAttribute("visible") &&
     83        this._pinnedDropIndicator.hasAttribute("interactive");
     84      this._resetTabsAfterDrop(draggedTab?.ownerDocument);
     85 
     86      this._tabDropIndicator.hidden = true;
     87      event.stopPropagation();
     88      if (draggedTab && dropEffect == "copy") {
     89        let duplicatedDraggedTab;
     90        let duplicatedTabs = [];
     91        let dropTarget =
     92          this._tabbrowserTabs.dragAndDropElements[this._getDropIndex(event)];
     93        for (let tab of movingTabs) {
     94          let duplicatedTab = gBrowser.duplicateTab(tab);
     95          duplicatedTabs.push(duplicatedTab);
     96          if (tab == draggedTab) {
     97            duplicatedDraggedTab = duplicatedTab;
     98          }
     99        }
    100        gBrowser.moveTabsBefore(duplicatedTabs, dropTarget, dropMetricsContext);
    101        if (draggedTab.container != this._tabbrowserTabs || event.shiftKey) {
    102          this._tabbrowserTabs.selectedItem = duplicatedDraggedTab;
    103        }
    104      } else if (draggedTab && draggedTab.container == this._tabbrowserTabs) {
    105        let oldTranslateX = Math.round(draggedTab._dragData.translateX);
    106        let oldTranslateY = Math.round(draggedTab._dragData.translateY);
    107        let tabWidth = Math.round(draggedTab._dragData.tabWidth);
    108        let tabHeight = Math.round(draggedTab._dragData.tabHeight);
    109        let translateOffsetX = oldTranslateX % tabWidth;
    110        let translateOffsetY = oldTranslateY % tabHeight;
    111        let newTranslateX = oldTranslateX - translateOffsetX;
    112        let newTranslateY = oldTranslateY - translateOffsetY;
    113        let isPinned = draggedTab.pinned;
    114        let numPinned = gBrowser.pinnedTabCount;
    115        let tabs = this._tabbrowserTabs.dragAndDropElements.slice(
    116          isPinned ? 0 : numPinned,
    117          isPinned ? numPinned : undefined
    118        );
    119 
    120        if (this._tabbrowserTabs.isContainerVerticalPinnedGrid(draggedTab)) {
    121          // Update both translate axis for pinned vertical expanded tabs
    122          if (oldTranslateX > 0 && translateOffsetX > tabWidth / 2) {
    123            newTranslateX += tabWidth;
    124          } else if (oldTranslateX < 0 && -translateOffsetX > tabWidth / 2) {
    125            newTranslateX -= tabWidth;
    126          }
    127          if (oldTranslateY > 0 && translateOffsetY > tabHeight / 2) {
    128            newTranslateY += tabHeight;
    129          } else if (oldTranslateY < 0 && -translateOffsetY > tabHeight / 2) {
    130            newTranslateY -= tabHeight;
    131          }
    132        } else {
    133          let size = this._tabbrowserTabs.verticalMode ? "height" : "width";
    134          let screenAxis = this._tabbrowserTabs.verticalMode
    135            ? "screenY"
    136            : "screenX";
    137          let tabSize = this._tabbrowserTabs.verticalMode
    138            ? tabHeight
    139            : tabWidth;
    140          let firstTab = tabs[0];
    141          let lastTab = tabs.at(-1);
    142          let lastMovingTabScreen = movingTabs.at(-1)[screenAxis];
    143          let firstMovingTabScreen = movingTabs[0][screenAxis];
    144          let startBound = firstTab[screenAxis] - firstMovingTabScreen;
    145          let endBound =
    146            lastTab[screenAxis] +
    147            window.windowUtils.getBoundsWithoutFlushing(lastTab)[size] -
    148            (lastMovingTabScreen + tabSize);
    149          if (this._tabbrowserTabs.verticalMode) {
    150            newTranslateY = Math.min(
    151              Math.max(oldTranslateY, startBound),
    152              endBound
    153            );
    154          } else {
    155            newTranslateX = RTL_UI
    156              ? Math.min(Math.max(oldTranslateX, endBound), startBound)
    157              : Math.min(Math.max(oldTranslateX, startBound), endBound);
    158          }
    159        }
    160 
    161        let {
    162          dropElement,
    163          dropBefore,
    164          shouldCreateGroupOnDrop,
    165          shouldDropIntoCollapsedTabGroup,
    166          fromTabList,
    167        } = draggedTab._dragData;
    168 
    169        let dropIndex;
    170        let directionForward = false;
    171        if (fromTabList) {
    172          dropIndex = this._getDropIndex(event);
    173          if (dropIndex && dropIndex > movingTabs[0].elementIndex) {
    174            dropIndex--;
    175            directionForward = true;
    176          }
    177        } else if (
    178          draggedTab.currentIndex > tabs[tabs.length - 1].currentIndex
    179        ) {
    180          // There is a case where the currentIndex could be greater than the last item's in
    181          // the container. If this is the case, dropIndex needs to be set to the last item's
    182          // elementIndex to ensure the draggedTab/s are dropped in the last position.
    183          dropIndex = tabs[tabs.length - 1].elementIndex;
    184        }
    185 
    186        const dragToPinTargets = [
    187          this._tabbrowserTabs.pinnedTabsContainer,
    188          this._dragToPinPromoCard,
    189        ];
    190        let shouldPin =
    191          isTab(draggedTab) &&
    192          !draggedTab.pinned &&
    193          (overPinnedDropIndicator ||
    194            dragToPinTargets.some(el => el.contains(event.target)));
    195        let shouldUnpin =
    196          isTab(draggedTab) &&
    197          draggedTab.pinned &&
    198          this._tabbrowserTabs.arrowScrollbox.contains(event.target);
    199 
    200        let shouldTranslate =
    201          !gReduceMotion &&
    202          !shouldCreateGroupOnDrop &&
    203          !shouldDropIntoCollapsedTabGroup &&
    204          !isTabGroupLabel(draggedTab) &&
    205          !isSplitViewWrapper(draggedTab) &&
    206          !shouldPin &&
    207          !shouldUnpin;
    208        if (this._tabbrowserTabs.isContainerVerticalPinnedGrid(draggedTab)) {
    209          shouldTranslate &&=
    210            (oldTranslateX && oldTranslateX != newTranslateX) ||
    211            (oldTranslateY && oldTranslateY != newTranslateY);
    212        } else if (this._tabbrowserTabs.verticalMode) {
    213          shouldTranslate &&= oldTranslateY && oldTranslateY != newTranslateY;
    214        } else {
    215          shouldTranslate &&= oldTranslateX && oldTranslateX != newTranslateX;
    216        }
    217 
    218        let moveTabs = () => {
    219          if (dropIndex !== undefined) {
    220            for (let tab of movingTabs) {
    221              gBrowser.moveTabTo(
    222                tab,
    223                { elementIndex: dropIndex },
    224                dropMetricsContext
    225              );
    226              if (!directionForward) {
    227                dropIndex++;
    228              }
    229            }
    230          } else if (dropElement && dropBefore) {
    231            gBrowser.moveTabsBefore(
    232              movingTabs,
    233              dropElement,
    234              dropMetricsContext
    235            );
    236          } else if (dropElement && dropBefore != undefined) {
    237            gBrowser.moveTabsAfter(movingTabs, dropElement, dropMetricsContext);
    238          }
    239 
    240          if (isTabGroupLabel(draggedTab)) {
    241            this._setIsDraggingTabGroup(draggedTab.group, false);
    242            this._expandGroupOnDrop(draggedTab);
    243          }
    244        };
    245 
    246        if (shouldPin || shouldUnpin) {
    247          for (let item of movingTabs) {
    248            if (shouldPin) {
    249              gBrowser.pinTab(item, {
    250                telemetrySource:
    251                  gBrowser.TabMetrics.METRIC_SOURCE.DRAG_AND_DROP,
    252              });
    253            } else if (shouldUnpin) {
    254              gBrowser.unpinTab(item);
    255            }
    256          }
    257        }
    258 
    259        if (shouldTranslate) {
    260          let translationPromises = [];
    261          for (let item of movingTabs) {
    262            item = elementToMove(item);
    263            let translationPromise = new Promise(resolve => {
    264              item.toggleAttribute("tabdrop-samewindow", true);
    265              item.style.transform = `translate(${newTranslateX}px, ${newTranslateY}px)`;
    266              let postTransitionCleanup = () => {
    267                item.removeAttribute("tabdrop-samewindow");
    268                resolve();
    269              };
    270              if (gReduceMotion) {
    271                postTransitionCleanup();
    272              } else {
    273                let onTransitionEnd = transitionendEvent => {
    274                  if (
    275                    transitionendEvent.propertyName != "transform" ||
    276                    transitionendEvent.originalTarget != item
    277                  ) {
    278                    return;
    279                  }
    280                  item.removeEventListener("transitionend", onTransitionEnd);
    281 
    282                  postTransitionCleanup();
    283                };
    284                item.addEventListener("transitionend", onTransitionEnd);
    285              }
    286            });
    287            translationPromises.push(translationPromise);
    288          }
    289          Promise.all(translationPromises).then(() => {
    290            this.finishAnimateTabMove();
    291            moveTabs();
    292          });
    293        } else {
    294          this.finishAnimateTabMove();
    295          if (shouldCreateGroupOnDrop) {
    296            // This makes the tab group contents reflect the visual order of
    297            // the tabs right before dropping.
    298            let tabsInGroup = dropBefore
    299              ? [...movingTabs, dropElement]
    300              : [dropElement, ...movingTabs];
    301            gBrowser.addTabGroup(tabsInGroup, {
    302              insertBefore: dropElement,
    303              isUserTriggered: true,
    304              color: draggedTab._dragData.tabGroupCreationColor,
    305              telemetryUserCreateSource: "drag",
    306            });
    307          } else if (
    308            shouldDropIntoCollapsedTabGroup &&
    309            isTabGroupLabel(dropElement) &&
    310            isTab(draggedTab)
    311          ) {
    312            // If the dragged tab is the active tab in a collapsed tab group
    313            // and the user dropped it onto the label of its tab group, leave
    314            // the dragged tab where it was. Otherwise, drop it into the target
    315            // tab group.
    316            if (dropElement.group != draggedTab.group) {
    317              dropElement.group.addTabs(movingTabs, dropMetricsContext);
    318            }
    319          } else {
    320            moveTabs();
    321            this._tabbrowserTabs._notifyBackgroundTab(movingTabs.at(-1));
    322          }
    323        }
    324      } else if (isTabGroupLabel(draggedTab)) {
    325        gBrowser.adoptTabGroup(draggedTab.group, {
    326          elementIndex: this._getDropIndex(event),
    327        });
    328      } else if (isSplitViewWrapper(draggedTab)) {
    329        gBrowser.adoptSplitView(draggedTab, {
    330          elementIndex: this._getDropIndex(event),
    331        });
    332      } else if (draggedTab) {
    333        // Move the tabs into this window. To avoid multiple tab-switches in
    334        // the original window, the selected tab should be adopted last.
    335        const dropIndex = this._getDropIndex(event);
    336        let newIndex = dropIndex;
    337        let selectedTab;
    338        let indexForSelectedTab;
    339        for (let i = 0; i < movingTabs.length; ++i) {
    340          const tab = movingTabs[i];
    341          if (tab.selected) {
    342            selectedTab = tab;
    343            indexForSelectedTab = newIndex;
    344          } else {
    345            const newTab = gBrowser.adoptTab(tab, {
    346              elementIndex: newIndex,
    347              selectTab: tab == draggedTab,
    348            });
    349            if (newTab) {
    350              ++newIndex;
    351            }
    352          }
    353        }
    354        if (selectedTab) {
    355          const newTab = gBrowser.adoptTab(selectedTab, {
    356            elementIndex: indexForSelectedTab,
    357            selectTab: selectedTab == draggedTab,
    358          });
    359          if (newTab) {
    360            ++newIndex;
    361          }
    362        }
    363 
    364        // Restore tab selection
    365        gBrowser.addRangeToMultiSelectedTabs(
    366          this._tabbrowserTabs.dragAndDropElements[dropIndex],
    367          this._tabbrowserTabs.dragAndDropElements[newIndex - 1]
    368        );
    369      } else {
    370        // Pass true to disallow dropping javascript: or data: urls
    371        let links;
    372        try {
    373          links = Services.droppedLinkHandler.dropLinks(event, true);
    374        } catch (ex) {}
    375 
    376        if (!links || links.length === 0) {
    377          return;
    378        }
    379 
    380        let inBackground = Services.prefs.getBoolPref(
    381          "browser.tabs.loadInBackground"
    382        );
    383        if (event.shiftKey) {
    384          inBackground = !inBackground;
    385        }
    386 
    387        let targetTab = this._getDragTarget(event, { ignoreSides: true });
    388        let userContextId =
    389          this._tabbrowserTabs.selectedItem.getAttribute("usercontextid");
    390        let replace = isTab(targetTab);
    391        let newIndex = this._getDropIndex(event);
    392        let urls = links.map(link => link.url);
    393        let policyContainer =
    394          Services.droppedLinkHandler.getPolicyContainer(event);
    395        let triggeringPrincipal =
    396          Services.droppedLinkHandler.getTriggeringPrincipal(event);
    397 
    398        (async () => {
    399          if (
    400            urls.length >=
    401            Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")
    402          ) {
    403            // Sync dialog cannot be used inside drop event handler.
    404            let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs(
    405              urls.length,
    406              window
    407            );
    408            if (!answer) {
    409              return;
    410            }
    411          }
    412 
    413          let nextItem = this._tabbrowserTabs.dragAndDropElements[newIndex];
    414          let tabGroup = isTab(nextItem) && nextItem.group;
    415          gBrowser.loadTabs(urls, {
    416            inBackground,
    417            replace,
    418            allowThirdPartyFixup: true,
    419            targetTab,
    420            elementIndex: newIndex,
    421            tabGroup,
    422            userContextId,
    423            triggeringPrincipal,
    424            policyContainer,
    425          });
    426        })();
    427      }
    428 
    429      for (let tab of this._tabbrowserTabs.dragAndDropElements) {
    430        delete tab.currentIndex;
    431      }
    432 
    433      if (draggedTab) {
    434        delete draggedTab._dragData;
    435      }
    436    }
    437 
    438    /**
    439     * Move together all selected tabs around the tab in param.
    440     */
    441    _moveTogetherSelectedTabs(tab) {
    442      let selectedTabs = gBrowser.selectedTabs;
    443      let tabIndex = selectedTabs.indexOf(tab);
    444      if (selectedTabs.some(t => t.pinned != tab.pinned)) {
    445        throw new Error(
    446          "Cannot move together a mix of pinned and unpinned tabs."
    447        );
    448      }
    449      let isGrid = this._tabbrowserTabs.isContainerVerticalPinnedGrid(tab);
    450      let animate = !gReduceMotion;
    451 
    452      tab._moveTogetherSelectedTabsData = {
    453        finished: !animate,
    454      };
    455 
    456      tab.toggleAttribute("multiselected-move-together", true);
    457 
    458      let addAnimationData = movingTab => {
    459        movingTab._moveTogetherSelectedTabsData = {
    460          translateX: 0,
    461          translateY: 0,
    462          animate: true,
    463        };
    464        movingTab.toggleAttribute("multiselected-move-together", true);
    465 
    466        let postTransitionCleanup = () => {
    467          movingTab._moveTogetherSelectedTabsData.animate = false;
    468        };
    469        if (gReduceMotion) {
    470          postTransitionCleanup();
    471        } else {
    472          let onTransitionEnd = transitionendEvent => {
    473            if (
    474              transitionendEvent.propertyName != "transform" ||
    475              transitionendEvent.originalTarget != movingTab
    476            ) {
    477              return;
    478            }
    479            movingTab.removeEventListener("transitionend", onTransitionEnd);
    480            postTransitionCleanup();
    481          };
    482 
    483          movingTab.addEventListener("transitionend", onTransitionEnd);
    484        }
    485 
    486        let tabRect = tab.getBoundingClientRect();
    487        let movingTabRect = movingTab.getBoundingClientRect();
    488        movingTab._moveTogetherSelectedTabsData.translateX =
    489          tabRect.x - movingTabRect.x;
    490        movingTab._moveTogetherSelectedTabsData.translateY =
    491          tabRect.y - movingTabRect.y;
    492      };
    493 
    494      let selectedIndices = selectedTabs.map(t => t.elementIndex);
    495      let currentIndex = 0;
    496      let draggedRect = tab.getBoundingClientRect();
    497      let translateX = 0;
    498      let translateY = 0;
    499 
    500      // The currentIndex represents the indexes for all visible tab strip items after the
    501      // selected tabs have moved together. These values make the math in _animateTabMove and
    502      // _animateExpandedPinnedTabMove possible and less prone to edge cases when dragging
    503      // multiple tabs.
    504      for (let unmovingTab of this._tabbrowserTabs.dragAndDropElements) {
    505        if (unmovingTab.multiselected) {
    506          unmovingTab.currentIndex = tab.elementIndex;
    507          // Skip because this multiselected tab should
    508          // be shifted towards the dragged Tab.
    509          continue;
    510        }
    511        if (unmovingTab.elementIndex > selectedIndices[currentIndex]) {
    512          while (
    513            selectedIndices[currentIndex + 1] &&
    514            unmovingTab.elementIndex > selectedIndices[currentIndex + 1]
    515          ) {
    516            let currentRect = selectedTabs
    517              .find(t => t.elementIndex == selectedIndices[currentIndex])
    518              .getBoundingClientRect();
    519            // For everything but the grid, we need to work out the shift required based
    520            // on the size of the tabs being dragged together.
    521            translateY -= currentRect.height;
    522            translateX -= currentRect.width;
    523            currentIndex++;
    524          }
    525 
    526          // Find the new index of the tab once selected tabs have moved together to use
    527          // for positioning and animation
    528          let isAfterDraggedTab =
    529            unmovingTab.elementIndex - currentIndex > tab.elementIndex;
    530          let newIndex = isAfterDraggedTab
    531            ? unmovingTab.elementIndex - currentIndex
    532            : unmovingTab.elementIndex - currentIndex - 1;
    533          let newTranslateX = isAfterDraggedTab
    534            ? translateX
    535            : translateX - draggedRect.width;
    536          let newTranslateY = isAfterDraggedTab
    537            ? translateY
    538            : translateY - draggedRect.height;
    539          unmovingTab.currentIndex = newIndex;
    540          unmovingTab._moveTogetherSelectedTabsData = {
    541            translateX: 0,
    542            translateY: 0,
    543          };
    544          if (isGrid) {
    545            // For the grid, use the position of the tab with the old index to dictate the
    546            // translation needed for the background tab with the new index to move there.
    547            let unmovingTabRect = unmovingTab.getBoundingClientRect();
    548            let oldTabRect =
    549              this._tabbrowserTabs.dragAndDropElements[
    550                newIndex
    551              ].getBoundingClientRect();
    552            unmovingTab._moveTogetherSelectedTabsData.translateX =
    553              oldTabRect.x - unmovingTabRect.x;
    554            unmovingTab._moveTogetherSelectedTabsData.translateY =
    555              oldTabRect.y - unmovingTabRect.y;
    556          } else if (this._tabbrowserTabs.verticalMode) {
    557            unmovingTab._moveTogetherSelectedTabsData.translateY =
    558              newTranslateY;
    559          } else {
    560            unmovingTab._moveTogetherSelectedTabsData.translateX =
    561              newTranslateX;
    562          }
    563        } else {
    564          unmovingTab.currentIndex = unmovingTab.elementIndex;
    565        }
    566      }
    567 
    568      // Animate left or top selected tabs
    569      for (let i = 0; i < tabIndex; i++) {
    570        let movingTab = selectedTabs[i];
    571        addAnimationData(movingTab);
    572      }
    573      // Animate right or bottom selected tabs
    574      for (let i = selectedTabs.length - 1; i > tabIndex; i--) {
    575        let movingTab = selectedTabs[i];
    576        addAnimationData(movingTab);
    577      }
    578 
    579      // Slide the relevant tabs to their new position.
    580      // non-moving tabs adjust for RTL
    581      for (let item of this._tabbrowserTabs.dragAndDropElements) {
    582        if (
    583          !tab._dragData.movingTabsSet.has(item) &&
    584          (item._moveTogetherSelectedTabsData?.translateX ||
    585            item._moveTogetherSelectedTabsData?.translateY) &&
    586          ((item.pinned && tab.pinned) || (!item.pinned && !tab.pinned))
    587        ) {
    588          let element = elementToMove(item);
    589          if (isGrid) {
    590            element.style.transform = `translate(${(this._rtlMode ? -1 : 1) * item._moveTogetherSelectedTabsData.translateX}px, ${item._moveTogetherSelectedTabsData.translateY}px)`;
    591          } else if (this._tabbrowserTabs.verticalMode) {
    592            element.style.transform = `translateY(${item._moveTogetherSelectedTabsData.translateY}px)`;
    593          } else {
    594            element.style.transform = `translateX(${(this._rtlMode ? -1 : 1) * item._moveTogetherSelectedTabsData.translateX}px)`;
    595          }
    596        }
    597      }
    598      // moving tabs don't adjust for RTL
    599      for (let item of selectedTabs) {
    600        if (
    601          item._moveTogetherSelectedTabsData?.translateX ||
    602          item._moveTogetherSelectedTabsData?.translateY
    603        ) {
    604          let element = elementToMove(item);
    605          element.style.transform = `translate(${item._moveTogetherSelectedTabsData.translateX}px, ${item._moveTogetherSelectedTabsData.translateY}px)`;
    606        }
    607      }
    608    }
    609 
    610    finishMoveTogetherSelectedTabs(tab) {
    611      if (
    612        !tab._moveTogetherSelectedTabsData ||
    613        (tab._moveTogetherSelectedTabsData.finished && !gReduceMotion)
    614      ) {
    615        return;
    616      }
    617 
    618      if (tab._moveTogetherSelectedTabsData) {
    619        tab._moveTogetherSelectedTabsData.finished = true;
    620      }
    621 
    622      let selectedTabs = gBrowser.selectedTabs;
    623      let tabIndex = selectedTabs.indexOf(tab);
    624 
    625      // Moving left or top tabs
    626      for (let i = 0; i < tabIndex; i++) {
    627        gBrowser.moveTabBefore(selectedTabs[i], tab);
    628      }
    629 
    630      // Moving right or bottom tabs
    631      for (let i = selectedTabs.length - 1; i > tabIndex; i--) {
    632        gBrowser.moveTabAfter(selectedTabs[i], tab);
    633      }
    634 
    635      for (let item of this._tabbrowserTabs.dragAndDropElements) {
    636        delete item._moveTogetherSelectedTabsData;
    637        item = elementToMove(item);
    638        item.style.transform = "";
    639        item.removeAttribute("multiselected-move-together");
    640      }
    641    }
    642 
    643    /* In order to to drag tabs between both the pinned arrowscrollbox (pinned tab container)
    644      and unpinned arrowscrollbox (tabbrowser-arrowscrollbox), the dragged tabs need to be
    645      positioned absolutely. This results in a shift in the layout, filling the empty space.
    646      This function updates the position and widths of elements affected by this layout shift
    647      when the tab is first selected to be dragged.
    648    */
    649    _updateTabStylesOnDrag(tab, dropEffect) {
    650      let tabStripItemElement = elementToMove(tab);
    651      tabStripItemElement.style.pointerEvents =
    652        dropEffect == "copy" ? "auto" : "";
    653      if (tabStripItemElement.hasAttribute("dragtarget")) {
    654        return;
    655      }
    656      let isPinned = tab.pinned;
    657      let dragAndDropElements = this._tabbrowserTabs.dragAndDropElements;
    658      let isGrid = this._tabbrowserTabs.isContainerVerticalPinnedGrid(tab);
    659      let periphery = document.getElementById(
    660        "tabbrowser-arrowscrollbox-periphery"
    661      );
    662 
    663      if (isPinned && this._tabbrowserTabs.verticalMode) {
    664        this._tabbrowserTabs.pinnedTabsContainer.setAttribute("dragActive", "");
    665      }
    666 
    667      // Ensure tab containers retain size while tabs are dragged out of the layout
    668      let pinnedRect = window.windowUtils.getBoundsWithoutFlushing(
    669        this._tabbrowserTabs.pinnedTabsContainer.scrollbox
    670      );
    671      let pinnedContainerRect = window.windowUtils.getBoundsWithoutFlushing(
    672        this._tabbrowserTabs.pinnedTabsContainer
    673      );
    674      let unpinnedRect = window.windowUtils.getBoundsWithoutFlushing(
    675        this._tabbrowserTabs.arrowScrollbox.scrollbox
    676      );
    677      let tabContainerRect = window.windowUtils.getBoundsWithoutFlushing(
    678        this._tabbrowserTabs
    679      );
    680 
    681      if (this._tabbrowserTabs.pinnedTabsContainer.firstChild) {
    682        this._tabbrowserTabs.pinnedTabsContainer.scrollbox.style.height =
    683          pinnedRect.height + "px";
    684        // Use "minHeight" so as not to interfere with user preferences for height.
    685        this._tabbrowserTabs.pinnedTabsContainer.style.minHeight =
    686          pinnedContainerRect.height + "px";
    687        this._tabbrowserTabs.pinnedTabsContainer.scrollbox.style.width =
    688          pinnedRect.width + "px";
    689      }
    690      this._tabbrowserTabs.arrowScrollbox.scrollbox.style.height =
    691        unpinnedRect.height + "px";
    692      this._tabbrowserTabs.arrowScrollbox.scrollbox.style.width =
    693        unpinnedRect.width + "px";
    694 
    695      let { movingTabs, movingTabsSet, expandGroupOnDrop } = tab._dragData;
    696      /** @type {(MozTabbrowserTab|typeof MozTabbrowserTabGroup.labelElement)[]} */
    697      let suppressTransitionsFor = [];
    698      /** @type {Map<MozTabbrowserTab, DOMRect>} */
    699 
    700      const tabsOrigBounds = new Map();
    701 
    702      for (let t of dragAndDropElements) {
    703        t = elementToMove(t);
    704        let tabRect = window.windowUtils.getBoundsWithoutFlushing(t);
    705 
    706        // record where all the tabs were before we position:absolute the moving tabs
    707        tabsOrigBounds.set(t, tabRect);
    708 
    709        // Prevent flex rules from resizing non dragged tabs while the dragged
    710        // tabs are positioned absolutely
    711        t.style.maxWidth = tabRect.width + "px";
    712        // Prevent non-moving tab strip items from performing any animations
    713        // at the very beginning of the drag operation; this prevents them
    714        // from appearing to move while the dragged tabs are positioned absolutely
    715        let isTabInCollapsingGroup = expandGroupOnDrop && t.group == tab.group;
    716        if (!movingTabsSet.has(t) && !isTabInCollapsingGroup) {
    717          t.style.transition = "none";
    718          suppressTransitionsFor.push(t);
    719        }
    720      }
    721 
    722      if (suppressTransitionsFor.length) {
    723        window
    724          .promiseDocumentFlushed(() => {})
    725          .then(() => {
    726            window.requestAnimationFrame(() => {
    727              for (let t of suppressTransitionsFor) {
    728                t.style.transition = "";
    729              }
    730            });
    731          });
    732      }
    733 
    734      // Use .tab-group-label-container, tab-split-view-wrapper or .tabbrowser-tab for size/position
    735      // calculations.
    736      let rect =
    737        window.windowUtils.getBoundsWithoutFlushing(tabStripItemElement);
    738      // Vertical tabs live under the #sidebar-main element which gets animated and has a
    739      // transform style property, making it the containing block for all its descendants.
    740      // Position:absolute elements need to account for this when updating position using
    741      // other measurements whose origin is the viewport or documentElement's 0,0
    742      let movingTabsOffsetX = window.windowUtils.getBoundsWithoutFlushing(
    743        tabStripItemElement.offsetParent
    744      ).x;
    745 
    746      for (let movingTab of movingTabs) {
    747        movingTab = elementToMove(movingTab);
    748        movingTab.style.width = rect.width + "px";
    749        // "dragtarget" contains the following rules which must only be set AFTER the above
    750        // elements have been adjusted. {z-index: 3 !important, position: absolute !important}
    751        movingTab.setAttribute("dragtarget", "");
    752        if (isTabGroupLabel(tab)) {
    753          if (this._tabbrowserTabs.verticalMode) {
    754            movingTab.style.top = rect.top - tabContainerRect.top + "px";
    755          } else {
    756            movingTab.style.left = rect.left - movingTabsOffsetX + "px";
    757            movingTab.style.height = rect.height + "px";
    758          }
    759        } else if (isGrid) {
    760          movingTab.style.top = rect.top - pinnedRect.top + "px";
    761          movingTab.style.left = rect.left - movingTabsOffsetX + "px";
    762        } else if (this._tabbrowserTabs.verticalMode) {
    763          movingTab.style.top = rect.top - tabContainerRect.top + "px";
    764        } else if (this._rtlMode) {
    765          movingTab.style.left = rect.left - movingTabsOffsetX + "px";
    766        } else {
    767          movingTab.style.left = rect.left - movingTabsOffsetX + "px";
    768        }
    769      }
    770 
    771      if (movingTabs.length == 2) {
    772        tab.setAttribute("small-stack", "");
    773      } else if (movingTabs.length > 2) {
    774        tab.setAttribute("big-stack", "");
    775      }
    776 
    777      if (
    778        !isPinned &&
    779        this._tabbrowserTabs.arrowScrollbox.hasAttribute("overflowing")
    780      ) {
    781        if (this._tabbrowserTabs.verticalMode) {
    782          periphery.style.marginBlockStart = rect.height + "px";
    783        } else {
    784          periphery.style.marginInlineStart = rect.width + "px";
    785        }
    786      } else if (
    787        isPinned &&
    788        this._tabbrowserTabs.pinnedTabsContainer.hasAttribute("overflowing")
    789      ) {
    790        let pinnedPeriphery = document.createXULElement("hbox");
    791        pinnedPeriphery.id = "pinned-tabs-container-periphery";
    792        pinnedPeriphery.style.width = "100%";
    793        pinnedPeriphery.style.marginBlockStart = rect.height + "px";
    794        this._tabbrowserTabs.pinnedTabsContainer.appendChild(pinnedPeriphery);
    795      }
    796 
    797      let setElPosition = el => {
    798        let origBounds = tabsOrigBounds.get(el);
    799        if (this._tabbrowserTabs.verticalMode && origBounds.top > rect.top) {
    800          el.style.top = rect.height + "px";
    801        } else if (!this._tabbrowserTabs.verticalMode) {
    802          if (!this._rtlMode && origBounds.left > rect.left) {
    803            el.style.left = rect.width + "px";
    804          } else if (this._rtlMode && origBounds.left < rect.left) {
    805            el.style.left = -rect.width + "px";
    806          }
    807        }
    808      };
    809 
    810      let setGridElPosition = el => {
    811        let origBounds = tabsOrigBounds.get(el);
    812        if (!origBounds) {
    813          // No bounds saved for this pinned tab
    814          return;
    815        }
    816        // We use getBoundingClientRect and force a reflow as we need to know their new positions
    817        // after making the moving tabs position:absolute
    818        let newBounds = el.getBoundingClientRect();
    819        let shiftX = origBounds.x - newBounds.x;
    820        let shiftY = origBounds.y - newBounds.y;
    821 
    822        el.style.left = shiftX + "px";
    823        el.style.top = shiftY + "px";
    824      };
    825 
    826      // Update tabs in the same container as the dragged tabs so as not
    827      // to fill the space when the dragged tabs become absolute
    828      for (let t of dragAndDropElements) {
    829        let tabIsPinned = t.pinned;
    830        t = elementToMove(t);
    831        if (!t.hasAttribute("dragtarget")) {
    832          if (
    833            (!isPinned && !tabIsPinned) ||
    834            (tabIsPinned && isPinned && !isGrid)
    835          ) {
    836            setElPosition(t);
    837          } else if (isGrid && tabIsPinned && isPinned) {
    838            setGridElPosition(t);
    839          }
    840        }
    841      }
    842 
    843      // Handle the new tab button filling the space when the dragged tab
    844      // position becomes absolute
    845      if (!this._tabbrowserTabs.overflowing && !isPinned) {
    846        if (this._tabbrowserTabs.verticalMode) {
    847          periphery.style.top = `${rect.height}px`;
    848        } else if (this._rtlMode) {
    849          periphery.style.left = `${-rect.width}px`;
    850        } else {
    851          periphery.style.left = `${rect.width}px`;
    852        }
    853      }
    854    }
    855 
    856    // eslint-disable-next-line complexity
    857    _animateTabMove(event) {
    858      let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
    859      let dragData = draggedTab._dragData;
    860      let movingTabs = dragData.movingTabs;
    861      let movingTabsSet = dragData.movingTabsSet;
    862 
    863      dragData.animLastScreenPos ??= this._tabbrowserTabs.verticalMode
    864        ? dragData.screenY
    865        : dragData.screenX;
    866      let screen = this._tabbrowserTabs.verticalMode
    867        ? event.screenY
    868        : event.screenX;
    869      if (screen == dragData.animLastScreenPos) {
    870        return;
    871      }
    872      let screenForward = screen > dragData.animLastScreenPos;
    873      dragData.animLastScreenPos = screen;
    874 
    875      this._clearDragOverGroupingTimer();
    876 
    877      let isPinned = draggedTab.pinned;
    878      let numPinned = gBrowser.pinnedTabCount;
    879      let dragAndDropElements = this._tabbrowserTabs.dragAndDropElements;
    880      let tabs = dragAndDropElements.slice(
    881        isPinned ? 0 : numPinned,
    882        isPinned ? numPinned : undefined
    883      );
    884 
    885      if (this._rtlMode) {
    886        tabs.reverse();
    887      }
    888 
    889      let bounds = ele => window.windowUtils.getBoundsWithoutFlushing(ele);
    890      let logicalForward = screenForward != this._rtlMode;
    891      let screenAxis = this._tabbrowserTabs.verticalMode
    892        ? "screenY"
    893        : "screenX";
    894      let size = this._tabbrowserTabs.verticalMode ? "height" : "width";
    895      let translateAxis = this._tabbrowserTabs.verticalMode
    896        ? "translateY"
    897        : "translateX";
    898      let translateX = event.screenX - dragData.screenX;
    899      let translateY = event.screenY - dragData.screenY;
    900 
    901      // Move the dragged tab based on the mouse position.
    902      let periphery = document.getElementById(
    903        "tabbrowser-arrowscrollbox-periphery"
    904      );
    905      let endEdge = ele => ele[screenAxis] + bounds(ele)[size];
    906      let endScreen = endEdge(draggedTab);
    907      let startScreen = draggedTab[screenAxis];
    908      let { width: tabWidth, height: tabHeight } = bounds(
    909        elementToMove(draggedTab)
    910      );
    911      let tabSize = this._tabbrowserTabs.verticalMode ? tabHeight : tabWidth;
    912      let shiftSize = tabSize;
    913      dragData.tabWidth = tabWidth;
    914      dragData.tabHeight = tabHeight;
    915      dragData.translateX = translateX;
    916      dragData.translateY = translateY;
    917      let translate = screen - dragData[screenAxis];
    918 
    919      // Constrain the range over which the moving tabs can move between the edge of the tabstrip and periphery.
    920      // Add 1 to periphery so we don't overlap it.
    921      let startBound = this._rtlMode
    922        ? endEdge(periphery) + 1 - startScreen
    923        : this._tabbrowserTabs[screenAxis] - startScreen;
    924      let endBound = this._rtlMode
    925        ? endEdge(this._tabbrowserTabs) - endScreen
    926        : periphery[screenAxis] - 1 - endScreen;
    927      translate = Math.min(Math.max(translate, startBound), endBound);
    928 
    929      // Center the tab under the cursor if the tab is not under the cursor while dragging
    930      let draggedTabScreenAxis = draggedTab[screenAxis] + translate;
    931      if (
    932        (screen < draggedTabScreenAxis ||
    933          screen > draggedTabScreenAxis + tabSize) &&
    934        draggedTabScreenAxis + tabSize < endBound &&
    935        draggedTabScreenAxis > startBound
    936      ) {
    937        translate = screen - draggedTab[screenAxis] - tabSize / 2;
    938        // Ensure, after the above calculation, we are still within bounds
    939        translate = Math.min(Math.max(translate, startBound), endBound);
    940      }
    941 
    942      if (!gBrowser.pinnedTabCount && !this._dragToPinPromoCard.shouldRender) {
    943        let pinnedDropIndicatorMargin = parseFloat(
    944          window.getComputedStyle(this._pinnedDropIndicator).marginInline
    945        );
    946        this._checkWithinPinnedContainerBounds({
    947          firstMovingTabScreen: startScreen,
    948          lastMovingTabScreen: endScreen,
    949          pinnedTabsStartEdge: this._rtlMode
    950            ? endEdge(this._tabbrowserTabs.arrowScrollbox) +
    951              pinnedDropIndicatorMargin
    952            : this._tabbrowserTabs[screenAxis],
    953          pinnedTabsEndEdge: this._rtlMode
    954            ? endEdge(this._tabbrowserTabs)
    955            : this._tabbrowserTabs.arrowScrollbox[screenAxis] -
    956              pinnedDropIndicatorMargin,
    957          translate,
    958          draggedTab,
    959        });
    960      }
    961 
    962      for (let item of movingTabs) {
    963        item = elementToMove(item);
    964        item.style.transform = `${translateAxis}(${translate}px)`;
    965      }
    966 
    967      dragData.translatePos = translate;
    968 
    969      tabs = tabs.filter(t => !movingTabsSet.has(t) || t == draggedTab);
    970 
    971      /**
    972       * When the `draggedTab` is just starting to move, the `draggedTab` is in
    973       * its original location and the `dropElementIndex == draggedTab.elementIndex`.
    974       * Any tabs or tab group labels passed in as `item` will result in a 0 shift
    975       * because all of those items should also continue to appear in their original
    976       * locations.
    977       *
    978       * Once the `draggedTab` is more "backward" in the tab strip than its original
    979       * position, any tabs or tab group labels between the `draggedTab`'s original
    980       * `elementIndex` and the current `dropElementIndex` should shift "forward"
    981       * out of the way of the dragging tabs.
    982       *
    983       * When the `draggedTab` is more "forward" in the tab strip than its original
    984       * position, any tabs or tab group labels between the `draggedTab`'s original
    985       * `elementIndex` and the current `dropElementIndex` should shift "backward"
    986       * out of the way of the dragging tabs.
    987       *
    988       * @param {MozTabbrowserTab|MozTabbrowserTabGroup.label} item
    989       * @param {number} dropElementIndex
    990       * @returns {number}
    991       */
    992      let getTabShift = (item, dropElementIndex) => {
    993        if (item?.currentIndex == undefined) {
    994          item.currentIndex = item.elementIndex;
    995        }
    996        if (
    997          item.currentIndex < draggedTab.elementIndex &&
    998          item.currentIndex >= dropElementIndex
    999        ) {
   1000          return this._rtlMode ? -shiftSize : shiftSize;
   1001        }
   1002        if (
   1003          item.currentIndex > draggedTab.elementIndex &&
   1004          item.currentIndex < dropElementIndex
   1005        ) {
   1006          return this._rtlMode ? shiftSize : -shiftSize;
   1007        }
   1008        return 0;
   1009      };
   1010 
   1011      let oldDropElementIndex =
   1012        dragData.animDropElementIndex ?? draggedTab.elementIndex;
   1013 
   1014      /**
   1015       * Returns the higher % by which one element overlaps another
   1016       * in the tab strip.
   1017       *
   1018       * When element 1 is further forward in the tab strip:
   1019       *
   1020       *   p1            p2      p1+s1    p2+s2
   1021       *    |             |        |        |
   1022       *    ---------------------------------
   1023       *    ========================
   1024       *               s1
   1025       *                  ===================
   1026       *                           s2
   1027       *                  ==========
   1028       *                   overlap
   1029       *
   1030       * When element 2 is further forward in the tab strip:
   1031       *
   1032       *   p2            p1      p2+s2    p1+s1
   1033       *    |             |        |        |
   1034       *    ---------------------------------
   1035       *    ========================
   1036       *               s2
   1037       *                  ===================
   1038       *                           s1
   1039       *                  ==========
   1040       *                   overlap
   1041       *
   1042       * @param {number} p1
   1043       *   Position (x or y value in screen coordinates) of element 1.
   1044       * @param {number} s1
   1045       *   Size (width or height) of element 1.
   1046       * @param {number} p2
   1047       *   Position (x or y value in screen coordinates) of element 2.
   1048       * @param {number} s2
   1049       *   Size (width or height) of element 1.
   1050       * @returns {number}
   1051       *   Percent between 0.0 and 1.0 (inclusive) of element 1 or element 2
   1052       *   that is overlapped by the other element. If the elements have
   1053       *   different sizes, then this returns the larger overlap percentage.
   1054       */
   1055      function greatestOverlap(p1, s1, p2, s2) {
   1056        let overlapSize;
   1057        if (p1 < p2) {
   1058          // element 1 starts first
   1059          overlapSize = p1 + s1 - p2;
   1060        } else {
   1061          // element 2 starts first
   1062          overlapSize = p2 + s2 - p1;
   1063        }
   1064 
   1065        // No overlap if size is <= 0
   1066        if (overlapSize <= 0) {
   1067          return 0;
   1068        }
   1069 
   1070        // Calculate the overlap fraction from each element's perspective.
   1071        let overlapPercent = Math.max(overlapSize / s1, overlapSize / s2);
   1072 
   1073        return Math.min(overlapPercent, 1);
   1074      }
   1075 
   1076      /**
   1077       * Determine what tab/tab group label we're dragging over.
   1078       *
   1079       * When dragging right or downwards, the reference point for overlap is
   1080       * the right or bottom edge of the most forward moving tab.
   1081       *
   1082       * When dragging left or upwards, the reference point for overlap is the
   1083       * left or top edge of the most backward moving tab.
   1084       *
   1085       * @returns {Element|null}
   1086       *   The tab or tab group label that should be used to visually shift tab
   1087       *   strip elements out of the way of the dragged tab(s) during a drag
   1088       *   operation. Note: this is not used to determine where the dragged
   1089       *   tab(s) will be dropped, it is only used for visual animation at this
   1090       *   time.
   1091       */
   1092      let getOverlappedElement = () => {
   1093        let point = (screenForward ? endScreen : startScreen) + translate;
   1094        let low = 0;
   1095        let high = tabs.length - 1;
   1096        while (low <= high) {
   1097          let mid = Math.floor((low + high) / 2);
   1098          if (tabs[mid] == draggedTab && ++mid > high) {
   1099            break;
   1100          }
   1101          let element = tabs[mid];
   1102          let elementForSize = elementToMove(element);
   1103          screen =
   1104            elementForSize[screenAxis] +
   1105            getTabShift(element, oldDropElementIndex);
   1106 
   1107          if (screen > point) {
   1108            high = mid - 1;
   1109          } else if (screen + bounds(elementForSize)[size] < point) {
   1110            low = mid + 1;
   1111          } else {
   1112            return element;
   1113          }
   1114        }
   1115        return null;
   1116      };
   1117 
   1118      let dropElement = getOverlappedElement();
   1119 
   1120      let newDropElementIndex;
   1121      if (dropElement) {
   1122        newDropElementIndex =
   1123          dropElement?.currentIndex ?? dropElement.elementIndex;
   1124      } else {
   1125        // When the dragged element(s) moves past a tab strip item, the dragged
   1126        // element's leading edge starts dragging over empty space, resulting in
   1127        // no overlapping `dropElement`. In these cases, try to fall back to the
   1128        // previous animation drop element index to avoid unstable animations
   1129        // (tab strip items snapping back and forth to shift out of the way of
   1130        // the dragged element(s)).
   1131        newDropElementIndex = oldDropElementIndex;
   1132 
   1133        // We always want to have a `dropElement` so that we can determine where to
   1134        // logically drop the dragged element(s).
   1135        //
   1136        // It's tempting to set `dropElement` to
   1137        // `this.dragAndDropElements.at(oldDropElementIndex)`, and that is correct
   1138        // for most cases, but there are edge cases:
   1139        //
   1140        // 1) the drop element index range needs to be one larger than the number of
   1141        //    items that can move in the tab strip. The simplest example is when all
   1142        //    tabs are ungrouped and unpinned: for 5 tabs, the drop element index needs
   1143        //    to be able to go from 0 (become the first tab) to 5 (become the last tab).
   1144        //    `this.dragAndDropElements.at(5)` would be `undefined` when dragging to the
   1145        //    end of the tab strip. In this specific case, it works to fall back to
   1146        //    setting the drop element to the last tab.
   1147        //
   1148        // 2) the `elementIndex` values of the tab strip items do not change during
   1149        //    the drag operation. When dragging the last tab or multiple tabs at the end
   1150        //    of the tab strip, having `dropElement` fall back to the last tab makes the
   1151        //    drop element one of the moving tabs. This can have some unexpected behavior
   1152        //    if not careful. Falling back to the last tab that's not moving (instead of
   1153        //    just the last tab) helps ensure that `dropElement` is always a stable target
   1154        //    to drop next to.
   1155        //
   1156        // 3) all of the elements in the tab strip are moving, in which case there can't
   1157        //    be a drop element and it should stay `undefined`.
   1158        //
   1159        // 4) we just started dragging and the `oldDropElementIndex` has its default
   1160        //    valuë of `movingTabs[0].elementIndex`. In this case, the drop element
   1161        //    shouldn't be a moving tab, so keep it `undefined`.
   1162        let lastPossibleDropElement = this._rtlMode
   1163          ? tabs.find(t => t != draggedTab)
   1164          : tabs.findLast(t => t != draggedTab);
   1165        let maxElementIndexForDropElement =
   1166          lastPossibleDropElement?.currentIndex ??
   1167          lastPossibleDropElement?.elementIndex;
   1168        if (Number.isInteger(maxElementIndexForDropElement)) {
   1169          let index = Math.min(
   1170            oldDropElementIndex,
   1171            maxElementIndexForDropElement
   1172          );
   1173          let oldDropElementCandidate = this._tabbrowserTabs.dragAndDropElements
   1174            .filter(t => !movingTabsSet.has(t) || t == draggedTab)
   1175            .at(index);
   1176          if (!movingTabsSet.has(oldDropElementCandidate)) {
   1177            dropElement = oldDropElementCandidate;
   1178          }
   1179        }
   1180      }
   1181 
   1182      let moveOverThreshold;
   1183      let overlapPercent;
   1184      let dropBefore;
   1185      if (dropElement) {
   1186        let dropElementForOverlap = elementToMove(dropElement);
   1187 
   1188        let dropElementScreen = dropElementForOverlap[screenAxis];
   1189        let dropElementPos =
   1190          dropElementScreen + getTabShift(dropElement, oldDropElementIndex);
   1191        let dropElementSize = bounds(dropElementForOverlap)[size];
   1192        let firstMovingTabPos = startScreen + translate;
   1193        overlapPercent = greatestOverlap(
   1194          firstMovingTabPos,
   1195          shiftSize,
   1196          dropElementPos,
   1197          dropElementSize
   1198        );
   1199 
   1200        moveOverThreshold = gBrowser._tabGroupsEnabled
   1201          ? Services.prefs.getIntPref(
   1202              "browser.tabs.dragDrop.moveOverThresholdPercent"
   1203            ) / 100
   1204          : 0.5;
   1205        moveOverThreshold = Math.min(1, Math.max(0, moveOverThreshold));
   1206        let shouldMoveOver = overlapPercent > moveOverThreshold;
   1207        if (logicalForward && shouldMoveOver) {
   1208          newDropElementIndex++;
   1209        } else if (!logicalForward && !shouldMoveOver) {
   1210          newDropElementIndex++;
   1211          if (newDropElementIndex > oldDropElementIndex) {
   1212            // FIXME: Not quite sure what's going on here, but this check
   1213            // prevents jittery back-and-forth movement of background tabs
   1214            // in certain cases.
   1215            newDropElementIndex = oldDropElementIndex;
   1216          }
   1217        }
   1218 
   1219        // Recalculate the overlap with the updated drop index for when the
   1220        // drop element moves over.
   1221        dropElementPos =
   1222          dropElementScreen + getTabShift(dropElement, newDropElementIndex);
   1223        overlapPercent = greatestOverlap(
   1224          firstMovingTabPos,
   1225          shiftSize,
   1226          dropElementPos,
   1227          dropElementSize
   1228        );
   1229        dropBefore = firstMovingTabPos < dropElementPos;
   1230        if (this._rtlMode) {
   1231          dropBefore = !dropBefore;
   1232        }
   1233 
   1234        // If dragging a group over another group, don't make it look like it is
   1235        // possible to drop the dragged group inside the other group.
   1236        if (
   1237          isTabGroupLabel(draggedTab) &&
   1238          dropElement?.group &&
   1239          (!dropElement.group.collapsed ||
   1240            (dropElement.group.collapsed && dropElement.group.hasActiveTab))
   1241        ) {
   1242          let overlappedGroup = dropElement.group;
   1243 
   1244          if (isTabGroupLabel(dropElement)) {
   1245            dropBefore = true;
   1246            newDropElementIndex =
   1247              dropElement?.currentIndex ?? dropElement.elementIndex;
   1248          } else {
   1249            dropBefore = false;
   1250            let lastVisibleTabInGroup =
   1251              overlappedGroup.tabsAndSplitViews.findLast(ele => ele.visible);
   1252            newDropElementIndex =
   1253              (lastVisibleTabInGroup?.currentIndex ??
   1254                lastVisibleTabInGroup.elementIndex) + 1;
   1255          }
   1256 
   1257          dropElement = overlappedGroup;
   1258        }
   1259 
   1260        // Constrain drop direction at the boundary between pinned and
   1261        // unpinned tabs so that they don't mix together.
   1262        let isOutOfBounds = isPinned
   1263          ? dropElement.elementIndex >= numPinned
   1264          : dropElement.elementIndex < numPinned;
   1265        if (isOutOfBounds) {
   1266          // Drop after last pinned tab
   1267          dropElement = this._tabbrowserTabs.dragAndDropElements[numPinned - 1];
   1268          dropBefore = false;
   1269        }
   1270      }
   1271 
   1272      if (
   1273        gBrowser._tabGroupsEnabled &&
   1274        isTab(draggedTab) &&
   1275        !isPinned &&
   1276        (!numPinned || newDropElementIndex >= numPinned)
   1277      ) {
   1278        let dragOverGroupingThreshold = 1 - moveOverThreshold;
   1279        let groupingDelay = Services.prefs.getIntPref(
   1280          "browser.tabs.dragDrop.createGroup.delayMS"
   1281        );
   1282 
   1283        // When dragging tab(s) over an ungrouped tab, signal to the user
   1284        // that dropping the tab(s) will create a new tab group.
   1285        let shouldCreateGroupOnDrop =
   1286          !movingTabsSet.has(dropElement) &&
   1287          isTab(dropElement) &&
   1288          !dropElement?.group &&
   1289          overlapPercent > dragOverGroupingThreshold;
   1290 
   1291        // When dragging tab(s) over a collapsed tab group label, signal to the
   1292        // user that dropping the tab(s) will add them to the group.
   1293        let shouldDropIntoCollapsedTabGroup =
   1294          isTabGroupLabel(dropElement) &&
   1295          dropElement.group.collapsed &&
   1296          overlapPercent > dragOverGroupingThreshold;
   1297 
   1298        if (shouldCreateGroupOnDrop) {
   1299          this._dragOverGroupingTimer = setTimeout(() => {
   1300            this._triggerDragOverGrouping(dropElement);
   1301            dragData.shouldCreateGroupOnDrop = true;
   1302            this._setDragOverGroupColor(dragData.tabGroupCreationColor);
   1303          }, groupingDelay);
   1304        } else if (shouldDropIntoCollapsedTabGroup) {
   1305          this._dragOverGroupingTimer = setTimeout(() => {
   1306            this._triggerDragOverGrouping(dropElement);
   1307            dragData.shouldDropIntoCollapsedTabGroup = true;
   1308            this._setDragOverGroupColor(dropElement.group.color);
   1309          }, groupingDelay);
   1310        } else {
   1311          this._tabbrowserTabs.removeAttribute("movingtab-group");
   1312          this._resetGroupTarget(
   1313            document.querySelector("[dragover-groupTarget]")
   1314          );
   1315 
   1316          delete dragData.shouldCreateGroupOnDrop;
   1317          delete dragData.shouldDropIntoCollapsedTabGroup;
   1318 
   1319          // Default to dropping into `dropElement`'s tab group, if it exists.
   1320          let dropElementGroup = dropElement?.group;
   1321          let colorCode = dropElementGroup?.color;
   1322 
   1323          let lastUnmovingTabInGroup = dropElementGroup?.tabs.findLast(
   1324            t => !movingTabsSet.has(t)
   1325          );
   1326          if (
   1327            isTab(dropElement) &&
   1328            dropElementGroup &&
   1329            dropElement == lastUnmovingTabInGroup &&
   1330            !dropBefore &&
   1331            overlapPercent < dragOverGroupingThreshold
   1332          ) {
   1333            // Dragging tab over the last tab of a tab group, but not enough
   1334            // for it to drop into the tab group. Drop it after the tab group instead.
   1335            dropElement = dropElementGroup;
   1336            colorCode = undefined;
   1337          } else if (isTabGroupLabel(dropElement)) {
   1338            if (dropBefore) {
   1339              // Dropping right before the tab group.
   1340              dropElement = dropElementGroup;
   1341              colorCode = undefined;
   1342            } else if (dropElementGroup.collapsed) {
   1343              // Dropping right after the collapsed tab group.
   1344              dropElement = dropElementGroup;
   1345              colorCode = undefined;
   1346            } else {
   1347              // Dropping right before the first tab in the tab group.
   1348              dropElement = dropElementGroup.tabs[0];
   1349              dropBefore = true;
   1350            }
   1351          }
   1352          this._setDragOverGroupColor(colorCode);
   1353          this._tabbrowserTabs.toggleAttribute(
   1354            "movingtab-addToGroup",
   1355            colorCode
   1356          );
   1357          this._tabbrowserTabs.toggleAttribute("movingtab-ungroup", !colorCode);
   1358        }
   1359      }
   1360 
   1361      if (
   1362        newDropElementIndex == oldDropElementIndex &&
   1363        dropBefore == dragData.dropBefore &&
   1364        dropElement == dragData.dropElement
   1365      ) {
   1366        return;
   1367      }
   1368 
   1369      dragData.dropElement = dropElement;
   1370      dragData.dropBefore = dropBefore;
   1371      dragData.animDropElementIndex = newDropElementIndex;
   1372 
   1373      // Shift background tabs to leave a gap where the dragged tab
   1374      // would currently be dropped.
   1375      for (let item of tabs) {
   1376        if (item == draggedTab) {
   1377          continue;
   1378        }
   1379        let shift = getTabShift(item, newDropElementIndex);
   1380        let transform = shift ? `${translateAxis}(${shift}px)` : "";
   1381        item = elementToMove(item);
   1382        item.style.transform = transform;
   1383      }
   1384    }
   1385 
   1386    _animateExpandedPinnedTabMove(event) {
   1387      let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
   1388      let dragData = draggedTab._dragData;
   1389      let movingTabs = dragData.movingTabs;
   1390 
   1391      dragData.animLastScreenX ??= dragData.screenX;
   1392      dragData.animLastScreenY ??= dragData.screenY;
   1393 
   1394      let screenX = event.screenX;
   1395      let screenY = event.screenY;
   1396 
   1397      if (
   1398        screenY == dragData.animLastScreenY &&
   1399        screenX == dragData.animLastScreenX
   1400      ) {
   1401        return;
   1402      }
   1403 
   1404      let tabs = this._tabbrowserTabs.visibleTabs.slice(
   1405        0,
   1406        gBrowser.pinnedTabCount
   1407      );
   1408 
   1409      dragData.animLastScreenY = screenY;
   1410      dragData.animLastScreenX = screenX;
   1411 
   1412      let { width: tabWidth, height: tabHeight } =
   1413        draggedTab.getBoundingClientRect();
   1414      let shiftSizeX = tabWidth;
   1415      let shiftSizeY = tabHeight;
   1416      dragData.tabWidth = tabWidth;
   1417      dragData.tabHeight = tabHeight;
   1418 
   1419      // Move the dragged tab based on the mouse position.
   1420      let periphery = document.getElementById(
   1421        "tabbrowser-arrowscrollbox-periphery"
   1422      );
   1423      let endScreenX = draggedTab.screenX + tabWidth;
   1424      let endScreenY = draggedTab.screenY + tabHeight;
   1425      let startScreenX = draggedTab.screenX;
   1426      let startScreenY = draggedTab.screenY;
   1427      let translateX = screenX - dragData.screenX;
   1428      let translateY = screenY - dragData.screenY;
   1429      let startBoundX = this._tabbrowserTabs.screenX - startScreenX;
   1430      let startBoundY = this._tabbrowserTabs.screenY - startScreenY;
   1431      let endBoundX =
   1432        this._tabbrowserTabs.screenX +
   1433        window.windowUtils.getBoundsWithoutFlushing(this._tabbrowserTabs)
   1434          .width -
   1435        endScreenX;
   1436      let endBoundY = periphery.screenY - endScreenY;
   1437      translateX = Math.min(Math.max(translateX, startBoundX), endBoundX);
   1438      translateY = Math.min(Math.max(translateY, startBoundY), endBoundY);
   1439 
   1440      // Center the tab under the cursor if the tab is not under the cursor while dragging
   1441      if (
   1442        screen < draggedTab.screenY + translateY ||
   1443        screen > draggedTab.screenY + tabHeight + translateY
   1444      ) {
   1445        translateY = screen - draggedTab.screenY - tabHeight / 2;
   1446      }
   1447 
   1448      for (let tab of movingTabs) {
   1449        tab.style.transform = `translate(${translateX}px, ${translateY}px)`;
   1450      }
   1451 
   1452      dragData.translateX = translateX;
   1453      dragData.translateY = translateY;
   1454 
   1455      // Determine what tab we're dragging over.
   1456      // * Single tab dragging: Point of reference is the center of the dragged tab. If that
   1457      //   point touches a background tab, the dragged tab would take that
   1458      //   tab's position when dropped.
   1459      // * Multiple tabs dragging: Tabs are stacked, so we can still use the above
   1460      //   point of reference, the center of the dragged tab.
   1461      // * We're doing a binary search in order to reduce the amount of
   1462      //   tabs we need to check.
   1463 
   1464      tabs = tabs.filter(t => !movingTabs.includes(t) || t == draggedTab);
   1465      let tabCenterX = startScreenX + translateX + tabWidth / 2;
   1466      let tabCenterY = startScreenY + translateY + tabHeight / 2;
   1467 
   1468      let shiftNumber = this._maxTabsPerRow - 1;
   1469 
   1470      let getTabShift = (tab, dropIndex) => {
   1471        if (tab?.currentIndex == undefined) {
   1472          tab.currentIndex = tab.elementIndex;
   1473        }
   1474        if (
   1475          tab.currentIndex < draggedTab.elementIndex &&
   1476          tab.currentIndex >= dropIndex
   1477        ) {
   1478          // If tab is at the end of a row, shift back and down
   1479          let tabRow = Math.ceil((tab.currentIndex + 1) / this._maxTabsPerRow);
   1480          let shiftedTabRow = Math.ceil(
   1481            (tab.currentIndex + 2) / this._maxTabsPerRow
   1482          );
   1483          if (tab.currentIndex && tabRow != shiftedTabRow) {
   1484            return [
   1485              RTL_UI ? tabWidth * shiftNumber : -tabWidth * shiftNumber,
   1486              shiftSizeY,
   1487            ];
   1488          }
   1489          return [RTL_UI ? -shiftSizeX : shiftSizeX, 0];
   1490        }
   1491        if (
   1492          tab.currentIndex > draggedTab.elementIndex &&
   1493          tab.currentIndex < dropIndex
   1494        ) {
   1495          // If tab is not index 0 and at the start of a row, shift across and up
   1496          let tabRow = Math.floor(tab.currentIndex / this._maxTabsPerRow);
   1497          let shiftedTabRow = Math.floor(
   1498            (tab.currentIndex - 1) / this._maxTabsPerRow
   1499          );
   1500          if (tab.currentIndex && tabRow != shiftedTabRow) {
   1501            return [
   1502              RTL_UI ? -tabWidth * shiftNumber : tabWidth * shiftNumber,
   1503              -shiftSizeY,
   1504            ];
   1505          }
   1506          return [RTL_UI ? shiftSizeX : -shiftSizeX, 0];
   1507        }
   1508        return [0, 0];
   1509      };
   1510 
   1511      let low = 0;
   1512      let high = tabs.length - 1;
   1513      let newIndex = -1;
   1514      let oldIndex = dragData.animDropElementIndex ?? draggedTab.elementIndex;
   1515 
   1516      while (low <= high) {
   1517        let mid = Math.floor((low + high) / 2);
   1518        if (tabs[mid] == draggedTab && ++mid > high) {
   1519          break;
   1520        }
   1521        let [shiftX, shiftY] = getTabShift(tabs[mid], oldIndex);
   1522        screenX = tabs[mid].screenX + shiftX;
   1523        screenY = tabs[mid].screenY + shiftY;
   1524 
   1525        if (screenY + tabHeight < tabCenterY) {
   1526          low = mid + 1;
   1527        } else if (screenY > tabCenterY) {
   1528          high = mid - 1;
   1529        } else if (
   1530          RTL_UI ? screenX + tabWidth < tabCenterX : screenX > tabCenterX
   1531        ) {
   1532          high = mid - 1;
   1533        } else if (
   1534          RTL_UI ? screenX > tabCenterX : screenX + tabWidth < tabCenterX
   1535        ) {
   1536          low = mid + 1;
   1537        } else {
   1538          newIndex = tabs[mid].currentIndex;
   1539          break;
   1540        }
   1541      }
   1542 
   1543      if (newIndex >= oldIndex && newIndex < tabs.length) {
   1544        newIndex++;
   1545      }
   1546 
   1547      if (newIndex < 0) {
   1548        newIndex = oldIndex;
   1549      }
   1550 
   1551      if (newIndex == dragData.animDropElementIndex) {
   1552        return;
   1553      }
   1554 
   1555      dragData.animDropElementIndex = newIndex;
   1556      dragData.dropElement = tabs[Math.min(newIndex, tabs.length - 1)];
   1557      dragData.dropBefore = newIndex < tabs.length;
   1558 
   1559      // Shift background tabs to leave a gap where the dragged tab
   1560      // would currently be dropped.
   1561      for (let tab of tabs) {
   1562        if (tab != draggedTab) {
   1563          let [shiftX, shiftY] = getTabShift(tab, newIndex);
   1564          tab.style.transform =
   1565            shiftX || shiftY ? `translate(${shiftX}px, ${shiftY}px)` : "";
   1566        }
   1567      }
   1568    }
   1569 
   1570    // If the tab is dropped in another window, we need to pass in the original window document
   1571    _resetTabsAfterDrop(draggedTabDocument = document) {
   1572      if (this._tabbrowserTabs.expandOnHover) {
   1573        // Re-enable MousePosTracker after dropping
   1574        MousePosTracker.addListener(document.defaultView.SidebarController);
   1575      }
   1576 
   1577      let pinnedDropIndicator = draggedTabDocument.getElementById(
   1578        "pinned-drop-indicator"
   1579      );
   1580      let draggedTabContainer =
   1581        draggedTabDocument.ownerGlobal.gBrowser.tabContainer;
   1582      pinnedDropIndicator.removeAttribute("visible");
   1583      pinnedDropIndicator.removeAttribute("interactive");
   1584      draggedTabContainer.style.maxWidth = "";
   1585      let allTabs = draggedTabDocument.getElementsByClassName("tabbrowser-tab");
   1586      for (let tab of allTabs) {
   1587        tab.style.width = "";
   1588        tab.style.left = "";
   1589        tab.style.top = "";
   1590        tab.style.maxWidth = "";
   1591        tab.style.pointerEvents = "";
   1592        tab.removeAttribute("dragtarget");
   1593        tab.removeAttribute("small-stack");
   1594        tab.removeAttribute("big-stack");
   1595      }
   1596      for (let label of draggedTabDocument.getElementsByClassName(
   1597        "tab-group-label-container"
   1598      )) {
   1599        label.style.width = "";
   1600        label.style.maxWidth = "";
   1601        label.style.height = "";
   1602        label.style.left = "";
   1603        label.style.top = "";
   1604        label.style.pointerEvents = "";
   1605        label.removeAttribute("dragtarget");
   1606      }
   1607      for (let label of draggedTabContainer.getElementsByClassName(
   1608        "tab-group-label"
   1609      )) {
   1610        delete label.currentIndex;
   1611      }
   1612      let periphery = draggedTabDocument.getElementById(
   1613        "tabbrowser-arrowscrollbox-periphery"
   1614      );
   1615      periphery.style.marginBlockStart = "";
   1616      periphery.style.marginInlineStart = "";
   1617      periphery.style.left = "";
   1618      periphery.style.top = "";
   1619      let pinnedTabsContainer = draggedTabDocument.getElementById(
   1620        "pinned-tabs-container"
   1621      );
   1622      let pinnedPeriphery = draggedTabDocument.getElementById(
   1623        "pinned-tabs-container-periphery"
   1624      );
   1625      pinnedPeriphery && pinnedTabsContainer.removeChild(pinnedPeriphery);
   1626      pinnedTabsContainer.removeAttribute("dragActive");
   1627      pinnedTabsContainer.style.minHeight = "";
   1628      draggedTabDocument.defaultView.SidebarController.updatePinnedTabsHeightOnResize();
   1629      pinnedTabsContainer.scrollbox.style.height = "";
   1630      pinnedTabsContainer.scrollbox.style.width = "";
   1631      let arrowScrollbox = draggedTabDocument.getElementById(
   1632        "tabbrowser-arrowscrollbox"
   1633      );
   1634      arrowScrollbox.scrollbox.style.height = "";
   1635      arrowScrollbox.scrollbox.style.width = "";
   1636      for (let groupLabel of draggedTabContainer.getElementsByClassName(
   1637        "tab-group-label-container"
   1638      )) {
   1639        groupLabel.style.left = "";
   1640        groupLabel.style.top = "";
   1641      }
   1642      for (let splitviewWrapper of draggedTabContainer.getElementsByTagName(
   1643        "tab-split-view-wrapper"
   1644      )) {
   1645        splitviewWrapper.style.width = "";
   1646        splitviewWrapper.style.maxWidth = "";
   1647        splitviewWrapper.style.height = "";
   1648        splitviewWrapper.style.left = "";
   1649        splitviewWrapper.style.top = "";
   1650        splitviewWrapper.style.pointerEvents = "";
   1651        splitviewWrapper.removeAttribute("dragtarget");
   1652      }
   1653    }
   1654  };
   1655 }