tor-browser

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

drag-and-drop.js (96080B)


      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  /**
     14   * The elements in the tab strip from `this.dragAndDropElements` that contain
     15   * logical information are:
     16   *
     17   * - <tab> (.tabbrowser-tab)
     18   * - <tab-group> label element (.tab-group-label)
     19   * - <tab-split-view-wrapper>
     20   *
     21   * The elements in the tab strip that contain the space inside of the <tabs>
     22   * element are:
     23   *
     24   * - <tab> (.tabbrowser-tab)
     25   * - <tab-group> label element wrapper (.tab-group-label-container)
     26   * - <tab-split-view-wrapper>
     27   *
     28   * When working with tab strip items, if you need logical information, you
     29   * can get it directly, e.g. `element.elementIndex` or `element._tPos`. If
     30   * you need spatial information like position or dimensions, then you should
     31   * call this function. For example, `elementToMove(element).getBoundingClientRect()`
     32   * or `elementToMove(element).style.top`.
     33   *
     34   * @param {MozTabbrowserTab|typeof MozTabbrowserTabGroup.labelElement} element
     35   * @returns {MozTabbrowserTab|vbox}
     36   */
     37  const elementToMove = element => {
     38    if (isTab(element) || isSplitViewWrapper(element)) {
     39      return element;
     40    }
     41    if (isTabGroupLabel(element)) {
     42      return element.closest(".tab-group-label-container");
     43    }
     44    throw new Error(`Element "${element.tagName}" is not expected to move`);
     45  };
     46 
     47  window.TabDragAndDrop = class {
     48    #dragTime = 0;
     49    #pinnedDropIndicatorTimeout = null;
     50 
     51    constructor(tabbrowserTabs) {
     52      this._tabbrowserTabs = tabbrowserTabs;
     53    }
     54 
     55    init() {
     56      this._pinnedDropIndicator = document.getElementById(
     57        "pinned-drop-indicator"
     58      );
     59      this._dragToPinPromoCard = document.getElementById(
     60        "drag-to-pin-promo-card"
     61      );
     62      this._tabDropIndicator = this._tabbrowserTabs.querySelector(
     63        ".tab-drop-indicator"
     64      );
     65    }
     66 
     67    // Event handlers
     68 
     69    handle_dragstart(event) {
     70      if (this._tabbrowserTabs._isCustomizing) {
     71        return;
     72      }
     73 
     74      let tab = this._getDragTarget(event);
     75      if (!tab) {
     76        return;
     77      }
     78      if (tab.splitview) {
     79        tab = tab.splitview;
     80      }
     81 
     82      this._tabbrowserTabs.previewPanel?.deactivate(null, { force: true });
     83      this.startTabDrag(event, tab);
     84    }
     85 
     86    handle_dragover(event) {
     87      var dropEffect = this.getDropEffectForTabDrag(event);
     88 
     89      var ind = this._tabDropIndicator;
     90      if (dropEffect == "" || dropEffect == "none") {
     91        ind.hidden = true;
     92        return;
     93      }
     94      event.preventDefault();
     95      event.stopPropagation();
     96 
     97      var arrowScrollbox = this._tabbrowserTabs.arrowScrollbox;
     98 
     99      // autoscroll the tab strip if we drag over the scroll
    100      // buttons, even if we aren't dragging a tab, but then
    101      // return to avoid drawing the drop indicator
    102      var pixelsToScroll = 0;
    103      if (this._tabbrowserTabs.overflowing) {
    104        switch (event.originalTarget) {
    105          case arrowScrollbox._scrollButtonUp:
    106            pixelsToScroll = arrowScrollbox.scrollIncrement * -1;
    107            break;
    108          case arrowScrollbox._scrollButtonDown:
    109            pixelsToScroll = arrowScrollbox.scrollIncrement;
    110            break;
    111        }
    112        if (pixelsToScroll) {
    113          arrowScrollbox.scrollByPixels(
    114            (this._rtlMode ? -1 : 1) * pixelsToScroll,
    115            true
    116          );
    117        }
    118      }
    119 
    120      let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
    121      if (
    122        (dropEffect == "move" || dropEffect == "copy") &&
    123        document == draggedTab.ownerDocument &&
    124        !draggedTab._dragData.fromTabList
    125      ) {
    126        ind.hidden = true;
    127        if (this.#isAnimatingMoveTogetherSelectedTabs()) {
    128          // Wait for moving selected tabs together animation to finish.
    129          return;
    130        }
    131        this.finishMoveTogetherSelectedTabs(draggedTab);
    132        this._updateTabStylesOnDrag(draggedTab, dropEffect);
    133 
    134        if (dropEffect == "move") {
    135          this.#setMovingTabMode(true);
    136 
    137          // Pinned tabs in expanded vertical mode are on a grid format and require
    138          // different logic to drag and drop.
    139          if (this._tabbrowserTabs.isContainerVerticalPinnedGrid(draggedTab)) {
    140            this._animateExpandedPinnedTabMove(event);
    141            return;
    142          }
    143          this._animateTabMove(event);
    144          return;
    145        }
    146      }
    147 
    148      this.finishAnimateTabMove();
    149 
    150      if (dropEffect == "link") {
    151        let target = this._getDragTarget(event, {
    152          ignoreSides: true,
    153        });
    154        if (target) {
    155          if (!this.#dragTime) {
    156            this.#dragTime = Date.now();
    157          }
    158          let overGroupLabel = isTabGroupLabel(target);
    159          if (
    160            Date.now() >=
    161            this.#dragTime +
    162              Services.prefs.getIntPref(
    163                overGroupLabel
    164                  ? "browser.tabs.dragDrop.expandGroup.delayMS"
    165                  : "browser.tabs.dragDrop.selectTab.delayMS"
    166              )
    167          ) {
    168            if (overGroupLabel) {
    169              target.group.collapsed = false;
    170            } else {
    171              this._tabbrowserTabs.selectedItem = target;
    172            }
    173          }
    174          if (isTab(target)) {
    175            // Dropping on the target tab would replace the loaded page rather
    176            // than opening a new tab, so hide the drop indicator.
    177            ind.hidden = true;
    178            return;
    179          }
    180        }
    181      }
    182 
    183      var rect = arrowScrollbox.getBoundingClientRect();
    184      var newMargin;
    185      if (pixelsToScroll) {
    186        // if we are scrolling, put the drop indicator at the edge
    187        // so that it doesn't jump while scrolling
    188        let scrollRect = arrowScrollbox.scrollClientRect;
    189        let minMargin = this._tabbrowserTabs.verticalMode
    190          ? scrollRect.top - rect.top
    191          : scrollRect.left - rect.left;
    192        let maxMargin = this._tabbrowserTabs.verticalMode
    193          ? Math.min(minMargin + scrollRect.height, scrollRect.bottom)
    194          : Math.min(minMargin + scrollRect.width, scrollRect.right);
    195        if (this._rtlMode) {
    196          [minMargin, maxMargin] = [
    197            this._tabbrowserTabs.clientWidth - maxMargin,
    198            this._tabbrowserTabs.clientWidth - minMargin,
    199          ];
    200        }
    201        newMargin = pixelsToScroll > 0 ? maxMargin : minMargin;
    202      } else {
    203        let newIndex = this._getDropIndex(event);
    204        let children = this._tabbrowserTabs.dragAndDropElements;
    205        if (newIndex == children.length) {
    206          let itemRect = children.at(-1).getBoundingClientRect();
    207          if (this._tabbrowserTabs.verticalMode) {
    208            newMargin = itemRect.bottom - rect.top;
    209          } else if (this._rtlMode) {
    210            newMargin = rect.right - itemRect.left;
    211          } else {
    212            newMargin = itemRect.right - rect.left;
    213          }
    214        } else {
    215          let itemRect = children[newIndex].getBoundingClientRect();
    216          if (this._tabbrowserTabs.verticalMode) {
    217            newMargin = rect.top - itemRect.bottom;
    218          } else if (this._rtlMode) {
    219            newMargin = rect.right - itemRect.right;
    220          } else {
    221            newMargin = itemRect.left - rect.left;
    222          }
    223        }
    224      }
    225 
    226      ind.hidden = false;
    227      newMargin += this._tabbrowserTabs.verticalMode
    228        ? ind.clientHeight
    229        : ind.clientWidth / 2;
    230      if (this._rtlMode) {
    231        newMargin *= -1;
    232      }
    233      ind.style.transform = this._tabbrowserTabs.verticalMode
    234        ? "translateY(" + Math.round(newMargin) + "px)"
    235        : "translateX(" + Math.round(newMargin) + "px)";
    236    }
    237 
    238    // eslint-disable-next-line complexity
    239    handle_drop(event) {
    240      var dt = event.dataTransfer;
    241      var dropEffect = dt.dropEffect;
    242      var draggedTab;
    243      let movingTabs;
    244      /** @type {TabMetricsContext} */
    245      const dropMetricsContext = gBrowser.TabMetrics.userTriggeredContext(
    246        gBrowser.TabMetrics.METRIC_SOURCE.DRAG_AND_DROP
    247      );
    248      if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) {
    249        // tab copy or move
    250        draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
    251        // not our drop then
    252        if (!draggedTab) {
    253          return;
    254        }
    255        movingTabs = draggedTab._dragData.movingTabs;
    256        draggedTab.container.tabDragAndDrop.finishMoveTogetherSelectedTabs(
    257          draggedTab
    258        );
    259      }
    260 
    261      if (this._rtlMode) {
    262        // In `startTabDrag` we reverse the moving tabs order to handle
    263        // positioning and animation. For drop, we require the original
    264        // order, so reverse back.
    265        movingTabs?.reverse();
    266      }
    267 
    268      let overPinnedDropIndicator =
    269        this._pinnedDropIndicator.hasAttribute("visible") &&
    270        this._pinnedDropIndicator.hasAttribute("interactive");
    271      this._resetTabsAfterDrop(draggedTab?.ownerDocument);
    272 
    273      this._tabDropIndicator.hidden = true;
    274      event.stopPropagation();
    275      if (draggedTab && dropEffect == "copy") {
    276        let duplicatedDraggedTab;
    277        let duplicatedTabs = [];
    278        let dropTarget =
    279          this._tabbrowserTabs.dragAndDropElements[this._getDropIndex(event)];
    280        for (let tab of movingTabs) {
    281          let duplicatedTab = gBrowser.duplicateTab(tab);
    282          duplicatedTabs.push(duplicatedTab);
    283          if (tab == draggedTab) {
    284            duplicatedDraggedTab = duplicatedTab;
    285          }
    286        }
    287        gBrowser.moveTabsBefore(duplicatedTabs, dropTarget, dropMetricsContext);
    288        if (draggedTab.container != this._tabbrowserTabs || event.shiftKey) {
    289          this._tabbrowserTabs.selectedItem = duplicatedDraggedTab;
    290        }
    291      } else if (draggedTab && draggedTab.container == this._tabbrowserTabs) {
    292        let oldTranslateX = Math.round(draggedTab._dragData.translateX);
    293        let oldTranslateY = Math.round(draggedTab._dragData.translateY);
    294        let tabWidth = Math.round(draggedTab._dragData.tabWidth);
    295        let tabHeight = Math.round(draggedTab._dragData.tabHeight);
    296        let translateOffsetX = oldTranslateX % tabWidth;
    297        let translateOffsetY = oldTranslateY % tabHeight;
    298        let newTranslateX = oldTranslateX - translateOffsetX;
    299        let newTranslateY = oldTranslateY - translateOffsetY;
    300        let isPinned = draggedTab.pinned;
    301        let numPinned = gBrowser.pinnedTabCount;
    302 
    303        if (this._tabbrowserTabs.isContainerVerticalPinnedGrid(draggedTab)) {
    304          // Update both translate axis for pinned vertical expanded tabs
    305          if (oldTranslateX > 0 && translateOffsetX > tabWidth / 2) {
    306            newTranslateX += tabWidth;
    307          } else if (oldTranslateX < 0 && -translateOffsetX > tabWidth / 2) {
    308            newTranslateX -= tabWidth;
    309          }
    310          if (oldTranslateY > 0 && translateOffsetY > tabHeight / 2) {
    311            newTranslateY += tabHeight;
    312          } else if (oldTranslateY < 0 && -translateOffsetY > tabHeight / 2) {
    313            newTranslateY -= tabHeight;
    314          }
    315        } else {
    316          let tabs = this._tabbrowserTabs.dragAndDropElements.slice(
    317            isPinned ? 0 : numPinned,
    318            isPinned ? numPinned : undefined
    319          );
    320          let size = this._tabbrowserTabs.verticalMode ? "height" : "width";
    321          let screenAxis = this._tabbrowserTabs.verticalMode
    322            ? "screenY"
    323            : "screenX";
    324          let tabSize = this._tabbrowserTabs.verticalMode
    325            ? tabHeight
    326            : tabWidth;
    327          let firstTab = tabs[0];
    328          let lastTab = tabs.at(-1);
    329          let lastMovingTabScreen = movingTabs.at(-1)[screenAxis];
    330          let firstMovingTabScreen = movingTabs[0][screenAxis];
    331          let startBound = firstTab[screenAxis] - firstMovingTabScreen;
    332          let endBound =
    333            lastTab[screenAxis] +
    334            window.windowUtils.getBoundsWithoutFlushing(lastTab)[size] -
    335            (lastMovingTabScreen + tabSize);
    336          if (this._tabbrowserTabs.verticalMode) {
    337            newTranslateY = Math.min(
    338              Math.max(oldTranslateY, startBound),
    339              endBound
    340            );
    341          } else {
    342            newTranslateX = RTL_UI
    343              ? Math.min(Math.max(oldTranslateX, endBound), startBound)
    344              : Math.min(Math.max(oldTranslateX, startBound), endBound);
    345          }
    346        }
    347 
    348        let {
    349          dropElement,
    350          dropBefore,
    351          shouldCreateGroupOnDrop,
    352          shouldDropIntoCollapsedTabGroup,
    353          fromTabList,
    354        } = draggedTab._dragData;
    355 
    356        let dropIndex;
    357        let directionForward = false;
    358        if (fromTabList) {
    359          dropIndex = this._getDropIndex(event);
    360          if (dropIndex && dropIndex > movingTabs[0].elementIndex) {
    361            dropIndex--;
    362            directionForward = true;
    363          }
    364        }
    365 
    366        const dragToPinTargets = [
    367          this._tabbrowserTabs.pinnedTabsContainer,
    368          this._dragToPinPromoCard,
    369        ];
    370        let shouldPin =
    371          isTab(draggedTab) &&
    372          !draggedTab.pinned &&
    373          (overPinnedDropIndicator ||
    374            dragToPinTargets.some(el => el.contains(event.target)));
    375        let shouldUnpin =
    376          isTab(draggedTab) &&
    377          draggedTab.pinned &&
    378          this._tabbrowserTabs.arrowScrollbox.contains(event.target);
    379 
    380        let shouldTranslate =
    381          !gReduceMotion &&
    382          !shouldCreateGroupOnDrop &&
    383          !shouldDropIntoCollapsedTabGroup &&
    384          !isTabGroupLabel(draggedTab) &&
    385          !isSplitViewWrapper(draggedTab) &&
    386          !shouldPin &&
    387          !shouldUnpin;
    388        if (this._tabbrowserTabs.isContainerVerticalPinnedGrid(draggedTab)) {
    389          shouldTranslate &&=
    390            (oldTranslateX && oldTranslateX != newTranslateX) ||
    391            (oldTranslateY && oldTranslateY != newTranslateY);
    392        } else if (this._tabbrowserTabs.verticalMode) {
    393          shouldTranslate &&= oldTranslateY && oldTranslateY != newTranslateY;
    394        } else {
    395          shouldTranslate &&= oldTranslateX && oldTranslateX != newTranslateX;
    396        }
    397 
    398        let moveTabs = () => {
    399          if (dropIndex !== undefined) {
    400            for (let tab of movingTabs) {
    401              gBrowser.moveTabTo(
    402                tab,
    403                { elementIndex: dropIndex },
    404                dropMetricsContext
    405              );
    406              if (!directionForward) {
    407                dropIndex++;
    408              }
    409            }
    410          } else if (dropElement && dropBefore) {
    411            gBrowser.moveTabsBefore(
    412              movingTabs,
    413              dropElement,
    414              dropMetricsContext
    415            );
    416          } else if (dropElement && dropBefore != undefined) {
    417            gBrowser.moveTabsAfter(movingTabs, dropElement, dropMetricsContext);
    418          }
    419 
    420          if (isTabGroupLabel(draggedTab)) {
    421            this._setIsDraggingTabGroup(draggedTab.group, false);
    422            this._expandGroupOnDrop(draggedTab);
    423          }
    424        };
    425 
    426        if (shouldPin || shouldUnpin) {
    427          for (let item of movingTabs) {
    428            if (shouldPin) {
    429              gBrowser.pinTab(item, {
    430                telemetrySource:
    431                  gBrowser.TabMetrics.METRIC_SOURCE.DRAG_AND_DROP,
    432              });
    433            } else if (shouldUnpin) {
    434              gBrowser.unpinTab(item);
    435            }
    436          }
    437        }
    438 
    439        if (shouldTranslate) {
    440          let translationPromises = [];
    441          for (let item of movingTabs) {
    442            item = elementToMove(item);
    443            let translationPromise = new Promise(resolve => {
    444              item.toggleAttribute("tabdrop-samewindow", true);
    445              item.style.transform = `translate(${newTranslateX}px, ${newTranslateY}px)`;
    446              let postTransitionCleanup = () => {
    447                item.removeAttribute("tabdrop-samewindow");
    448                resolve();
    449              };
    450              if (gReduceMotion) {
    451                postTransitionCleanup();
    452              } else {
    453                let onTransitionEnd = transitionendEvent => {
    454                  if (
    455                    transitionendEvent.propertyName != "transform" ||
    456                    transitionendEvent.originalTarget != item
    457                  ) {
    458                    return;
    459                  }
    460                  item.removeEventListener("transitionend", onTransitionEnd);
    461 
    462                  postTransitionCleanup();
    463                };
    464                item.addEventListener("transitionend", onTransitionEnd);
    465              }
    466            });
    467            translationPromises.push(translationPromise);
    468          }
    469          Promise.all(translationPromises).then(() => {
    470            this.finishAnimateTabMove();
    471            moveTabs();
    472          });
    473        } else {
    474          this.finishAnimateTabMove();
    475          if (shouldCreateGroupOnDrop) {
    476            // This makes the tab group contents reflect the visual order of
    477            // the tabs right before dropping.
    478            let tabsInGroup = dropBefore
    479              ? [...movingTabs, dropElement]
    480              : [dropElement, ...movingTabs];
    481            gBrowser.addTabGroup(tabsInGroup, {
    482              insertBefore: dropElement,
    483              isUserTriggered: true,
    484              color: draggedTab._dragData.tabGroupCreationColor,
    485              telemetryUserCreateSource: "drag",
    486            });
    487          } else if (
    488            shouldDropIntoCollapsedTabGroup &&
    489            isTabGroupLabel(dropElement) &&
    490            isTab(draggedTab)
    491          ) {
    492            // If the dragged tab is the active tab in a collapsed tab group
    493            // and the user dropped it onto the label of its tab group, leave
    494            // the dragged tab where it was. Otherwise, drop it into the target
    495            // tab group.
    496            if (dropElement.group != draggedTab.group) {
    497              dropElement.group.addTabs(movingTabs, dropMetricsContext);
    498            }
    499          } else {
    500            moveTabs();
    501            this._tabbrowserTabs._notifyBackgroundTab(movingTabs.at(-1));
    502          }
    503        }
    504      } else if (isTabGroupLabel(draggedTab)) {
    505        gBrowser.adoptTabGroup(draggedTab.group, {
    506          elementIndex: this._getDropIndex(event),
    507        });
    508      } else if (isSplitViewWrapper(draggedTab)) {
    509        gBrowser.adoptSplitView(draggedTab, {
    510          elementIndex: this._getDropIndex(event),
    511        });
    512      } else if (draggedTab) {
    513        // Move the tabs into this window. To avoid multiple tab-switches in
    514        // the original window, the selected tab should be adopted last.
    515        const dropIndex = this._getDropIndex(event);
    516        let newIndex = dropIndex;
    517        let selectedTab;
    518        let indexForSelectedTab;
    519        for (let i = 0; i < movingTabs.length; ++i) {
    520          const tab = movingTabs[i];
    521          if (tab.selected) {
    522            selectedTab = tab;
    523            indexForSelectedTab = newIndex;
    524          } else {
    525            const newTab = gBrowser.adoptTab(tab, {
    526              elementIndex: newIndex,
    527              selectTab: tab == draggedTab,
    528            });
    529            if (newTab) {
    530              ++newIndex;
    531            }
    532          }
    533        }
    534        if (selectedTab) {
    535          const newTab = gBrowser.adoptTab(selectedTab, {
    536            elementIndex: indexForSelectedTab,
    537            selectTab: selectedTab == draggedTab,
    538          });
    539          if (newTab) {
    540            ++newIndex;
    541          }
    542        }
    543 
    544        // Restore tab selection
    545        gBrowser.addRangeToMultiSelectedTabs(
    546          this._tabbrowserTabs.dragAndDropElements[dropIndex],
    547          this._tabbrowserTabs.dragAndDropElements[newIndex - 1]
    548        );
    549      } else {
    550        // Pass true to disallow dropping javascript: or data: urls
    551        let links;
    552        try {
    553          links = Services.droppedLinkHandler.dropLinks(event, true);
    554        } catch (ex) {}
    555 
    556        if (!links || links.length === 0) {
    557          return;
    558        }
    559 
    560        let inBackground = Services.prefs.getBoolPref(
    561          "browser.tabs.loadInBackground"
    562        );
    563        if (event.shiftKey) {
    564          inBackground = !inBackground;
    565        }
    566 
    567        let targetTab = this._getDragTarget(event, { ignoreSides: true });
    568        let userContextId =
    569          this._tabbrowserTabs.selectedItem.getAttribute("usercontextid");
    570        let replace = isTab(targetTab);
    571        let newIndex = this._getDropIndex(event);
    572        let urls = links.map(link => link.url);
    573        let policyContainer =
    574          Services.droppedLinkHandler.getPolicyContainer(event);
    575        let triggeringPrincipal =
    576          Services.droppedLinkHandler.getTriggeringPrincipal(event);
    577 
    578        (async () => {
    579          if (
    580            urls.length >=
    581            Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")
    582          ) {
    583            // Sync dialog cannot be used inside drop event handler.
    584            let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs(
    585              urls.length,
    586              window
    587            );
    588            if (!answer) {
    589              return;
    590            }
    591          }
    592 
    593          let nextItem = this._tabbrowserTabs.dragAndDropElements[newIndex];
    594          let tabGroup = isTab(nextItem) && nextItem.group;
    595          gBrowser.loadTabs(urls, {
    596            inBackground,
    597            replace,
    598            allowThirdPartyFixup: true,
    599            targetTab,
    600            elementIndex: newIndex,
    601            tabGroup,
    602            userContextId,
    603            triggeringPrincipal,
    604            policyContainer,
    605          });
    606        })();
    607      }
    608 
    609      if (draggedTab) {
    610        delete draggedTab._dragData;
    611      }
    612    }
    613 
    614    handle_dragend(event) {
    615      var dt = event.dataTransfer;
    616      var draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
    617 
    618      // Prevent this code from running if a tabdrop animation is
    619      // running since calling finishAnimateTabMove would clear
    620      // any CSS transition that is running.
    621      if (draggedTab.hasAttribute("tabdrop-samewindow")) {
    622        return;
    623      }
    624 
    625      this.finishMoveTogetherSelectedTabs(draggedTab);
    626      this.finishAnimateTabMove();
    627      if (isTabGroupLabel(draggedTab)) {
    628        this._setIsDraggingTabGroup(draggedTab.group, false);
    629        this._expandGroupOnDrop(draggedTab);
    630      }
    631      this._resetTabsAfterDrop(draggedTab.ownerDocument);
    632 
    633      if (
    634        dt.mozUserCancelled ||
    635        dt.dropEffect != "none" ||
    636        !Services.prefs.getBoolPref("browser.tabs.allowTabDetach") ||
    637        this._tabbrowserTabs._isCustomizing
    638      ) {
    639        delete draggedTab._dragData;
    640        return;
    641      }
    642 
    643      // Disable detach within the browser toolbox
    644      let [tabAxisPos, tabAxisStart, tabAxisEnd] = this._tabbrowserTabs
    645        .verticalMode
    646        ? [event.screenY, window.screenY, window.screenY + window.outerHeight]
    647        : [event.screenX, window.screenX, window.screenX + window.outerWidth];
    648 
    649      if (tabAxisPos > tabAxisStart && tabAxisPos < tabAxisEnd) {
    650        // also avoid detaching if the tab was dropped too close to
    651        // the tabbar (half a tab)
    652        let rect = window.windowUtils.getBoundsWithoutFlushing(
    653          this._tabbrowserTabs.arrowScrollbox
    654        );
    655        let crossAxisPos = this._tabbrowserTabs.verticalMode
    656          ? event.screenX
    657          : event.screenY;
    658        let crossAxisStart, crossAxisEnd;
    659        if (this._tabbrowserTabs.verticalMode) {
    660          if (
    661            (RTL_UI && this._tabbrowserTabs._sidebarPositionStart) ||
    662            (!RTL_UI && !this._tabbrowserTabs._sidebarPositionStart)
    663          ) {
    664            crossAxisStart =
    665              window.mozInnerScreenX + rect.right - 1.5 * rect.width;
    666            crossAxisEnd = window.screenX + window.outerWidth;
    667          } else {
    668            crossAxisStart = window.screenX;
    669            crossAxisEnd =
    670              window.mozInnerScreenX + rect.left + 1.5 * rect.width;
    671          }
    672        } else {
    673          crossAxisStart = window.screenY;
    674          crossAxisEnd = window.mozInnerScreenY + rect.top + 1.5 * rect.height;
    675        }
    676        if (crossAxisPos > crossAxisStart && crossAxisPos < crossAxisEnd) {
    677          return;
    678        }
    679      }
    680 
    681      // screen.availLeft et. al. only check the screen that this window is on,
    682      // but we want to look at the screen the tab is being dropped onto.
    683      var screen = event.screen;
    684      var availX = {},
    685        availY = {},
    686        availWidth = {},
    687        availHeight = {};
    688      // Get available rect in desktop pixels.
    689      screen.GetAvailRectDisplayPix(availX, availY, availWidth, availHeight);
    690      availX = availX.value;
    691      availY = availY.value;
    692      availWidth = availWidth.value;
    693      availHeight = availHeight.value;
    694 
    695      // Compute the final window size in desktop pixels ensuring that the new
    696      // window entirely fits within `screen`.
    697      let ourCssToDesktopScale =
    698        window.devicePixelRatio / window.desktopToDeviceScale;
    699      let screenCssToDesktopScale =
    700        screen.defaultCSSScaleFactor / screen.contentsScaleFactor;
    701 
    702      // NOTE(emilio): Multiplying the sizes here for screenCssToDesktopScale
    703      // means that we'll try to create a window that has the same amount of CSS
    704      // pixels than our current window, not the same amount of device pixels.
    705      // There are pros and cons of both conversions, though this matches the
    706      // pre-existing intended behavior.
    707      var winWidth = Math.min(
    708        window.outerWidth * screenCssToDesktopScale,
    709        availWidth
    710      );
    711      var winHeight = Math.min(
    712        window.outerHeight * screenCssToDesktopScale,
    713        availHeight
    714      );
    715 
    716      // This is slightly tricky: _dragData.offsetX/Y is an offset in CSS
    717      // pixels. Since we're doing the sizing above based on those, we also need
    718      // to apply the offset with pixels relative to the screen's scale rather
    719      // than our scale.
    720      var left = Math.min(
    721        Math.max(
    722          event.screenX * ourCssToDesktopScale -
    723            draggedTab._dragData.offsetX * screenCssToDesktopScale,
    724          availX
    725        ),
    726        availX + availWidth - winWidth
    727      );
    728      var top = Math.min(
    729        Math.max(
    730          event.screenY * ourCssToDesktopScale -
    731            draggedTab._dragData.offsetY * screenCssToDesktopScale,
    732          availY
    733        ),
    734        availY + availHeight - winHeight
    735      );
    736 
    737      // Convert back left and top to our CSS pixel space.
    738      left /= ourCssToDesktopScale;
    739      top /= ourCssToDesktopScale;
    740 
    741      delete draggedTab._dragData;
    742 
    743      if (gBrowser.tabs.length == 1) {
    744        // resize _before_ move to ensure the window fits the new screen.  if
    745        // the window is too large for its screen, the window manager may do
    746        // automatic repositioning.
    747        //
    748        // Since we're resizing before moving to our new screen, we need to use
    749        // sizes relative to the current screen. If we moved, then resized, then
    750        // we could avoid this special-case and share this with the else branch
    751        // below...
    752        winWidth /= ourCssToDesktopScale;
    753        winHeight /= ourCssToDesktopScale;
    754 
    755        window.resizeTo(winWidth, winHeight);
    756        window.moveTo(left, top);
    757        window.focus();
    758      } else {
    759        // We're opening a new window in a new screen, so make sure to use sizes
    760        // relative to the new screen.
    761        winWidth /= screenCssToDesktopScale;
    762        winHeight /= screenCssToDesktopScale;
    763 
    764        let props = { screenX: left, screenY: top, suppressanimation: 1 };
    765        gBrowser.replaceTabsWithWindow(draggedTab, props);
    766      }
    767      event.stopPropagation();
    768    }
    769 
    770    handle_dragleave(event) {
    771      this.#dragTime = 0;
    772 
    773      // This does not work at all (see bug 458613)
    774      var target = event.relatedTarget;
    775      while (target && target != this._tabbrowserTabs) {
    776        target = target.parentNode;
    777      }
    778      if (target) {
    779        return;
    780      }
    781 
    782      this._tabDropIndicator.hidden = true;
    783      event.stopPropagation();
    784    }
    785 
    786    // Utilities
    787 
    788    get _rtlMode() {
    789      return !this._tabbrowserTabs.verticalMode && RTL_UI;
    790    }
    791 
    792    #setMovingTabMode(movingTab) {
    793      this._tabbrowserTabs.toggleAttribute("movingtab", movingTab);
    794      gNavToolbox.toggleAttribute("movingtab", movingTab);
    795    }
    796 
    797    _getDropIndex(event) {
    798      let item = this._getDragTarget(event);
    799      if (!item) {
    800        return this._tabbrowserTabs.dragAndDropElements.length;
    801      }
    802      let isBeforeMiddle;
    803 
    804      let elementForSize = elementToMove(item);
    805      if (this._tabbrowserTabs.verticalMode) {
    806        let middle =
    807          elementForSize.screenY +
    808          elementForSize.getBoundingClientRect().height / 2;
    809        isBeforeMiddle = event.screenY < middle;
    810      } else {
    811        let middle =
    812          elementForSize.screenX +
    813          elementForSize.getBoundingClientRect().width / 2;
    814        isBeforeMiddle = this._rtlMode
    815          ? event.screenX > middle
    816          : event.screenX < middle;
    817      }
    818      return item.elementIndex + (isBeforeMiddle ? 0 : 1);
    819    }
    820 
    821    /**
    822     * Returns the tab, tab group label or split view wrapper where an event happened,
    823     * it didn't occur on a tab or tab group label.
    824     *
    825     * @param {Event} event
    826     *   The event for which we want to know on which element it happened.
    827     * @param {object} options
    828     * @param {boolean} options.ignoreSides
    829     *   If set to true: events will only be associated with an element if they
    830     *   happened on its central part (from 25% to 75%); if they happened on the
    831     *   left or right sides of the tab, the method will return null.
    832     */
    833    _getDragTarget(event, { ignoreSides = false } = {}) {
    834      let { target } = event;
    835      while (target) {
    836        if (
    837          isTab(target) ||
    838          isTabGroupLabel(target) ||
    839          isSplitViewWrapper(target)
    840        ) {
    841          break;
    842        }
    843        target = target.parentNode;
    844      }
    845      if (target && ignoreSides) {
    846        let { width, height } = target.getBoundingClientRect();
    847        if (
    848          event.screenX < target.screenX + width * 0.25 ||
    849          event.screenX > target.screenX + width * 0.75 ||
    850          ((event.screenY < target.screenY + height * 0.25 ||
    851            event.screenY > target.screenY + height * 0.75) &&
    852            this._tabbrowserTabs.verticalMode)
    853        ) {
    854          return null;
    855        }
    856      }
    857      return target;
    858    }
    859 
    860    #isMovingTab() {
    861      return this._tabbrowserTabs.hasAttribute("movingtab");
    862    }
    863 
    864    // Tab groups
    865 
    866    /**
    867     * When a tab group is being dragged, it fully collapses, even if it
    868     * contains the active tab. Since all of its tabs will become invisible,
    869     * the cache of visible tabs needs to be updated. Similarly, when the user
    870     * stops dragging the tab group, it needs to return to normal, which may
    871     * result in grouped tabs becoming visible again.
    872     *
    873     * @param {MozTabbrowserTabGroup} tabGroup
    874     * @param {boolean} isDragging
    875     */
    876    _setIsDraggingTabGroup(tabGroup, isDragging) {
    877      tabGroup.isBeingDragged = isDragging;
    878      this._tabbrowserTabs._invalidateCachedVisibleTabs();
    879    }
    880 
    881    _expandGroupOnDrop(draggedTab) {
    882      if (
    883        isTabGroupLabel(draggedTab) &&
    884        draggedTab._dragData?.expandGroupOnDrop
    885      ) {
    886        draggedTab.group.collapsed = false;
    887      }
    888    }
    889 
    890    /**
    891     * @param {MozTabbrowserTab|typeof MozTabbrowserTabGroup.labelElement} dropElement
    892     */
    893    _triggerDragOverGrouping(dropElement) {
    894      this._clearDragOverGroupingTimer();
    895 
    896      this._tabbrowserTabs.toggleAttribute("movingtab-group", true);
    897      this._tabbrowserTabs.removeAttribute("movingtab-ungroup");
    898      dropElement.toggleAttribute("dragover-groupTarget", true);
    899    }
    900 
    901    _clearDragOverGroupingTimer() {
    902      if (this._dragOverGroupingTimer) {
    903        clearTimeout(this._dragOverGroupingTimer);
    904        this._dragOverGroupingTimer = 0;
    905      }
    906    }
    907 
    908    _setDragOverGroupColor(groupColorCode) {
    909      if (!groupColorCode) {
    910        this._tabbrowserTabs.style.removeProperty("--dragover-tab-group-color");
    911        this._tabbrowserTabs.style.removeProperty(
    912          "--dragover-tab-group-color-invert"
    913        );
    914        this._tabbrowserTabs.style.removeProperty(
    915          "--dragover-tab-group-color-pale"
    916        );
    917        return;
    918      }
    919 
    920      this._tabbrowserTabs.style.setProperty(
    921        "--dragover-tab-group-color",
    922        `var(--tab-group-color-${groupColorCode})`
    923      );
    924      this._tabbrowserTabs.style.setProperty(
    925        "--dragover-tab-group-color-invert",
    926        `var(--tab-group-color-${groupColorCode}-invert)`
    927      );
    928      this._tabbrowserTabs.style.setProperty(
    929        "--dragover-tab-group-color-pale",
    930        `var(--tab-group-color-${groupColorCode}-pale)`
    931      );
    932    }
    933 
    934    /**
    935     * @param {MozTabbrowserTab|typeof MozTabbrowserTabGroup.labelElement} [element]
    936     */
    937    _resetGroupTarget(element) {
    938      element?.removeAttribute("dragover-groupTarget");
    939    }
    940 
    941    // Drag start
    942 
    943    startTabDrag(event, tab, { fromTabList = false } = {}) {
    944      if (this.expandOnHover) {
    945        // Temporarily disable MousePosTracker while dragging
    946        MousePosTracker.removeListener(document.defaultView.SidebarController);
    947      }
    948      if (this._tabbrowserTabs.isContainerVerticalPinnedGrid(tab)) {
    949        // In expanded vertical mode, the max number of pinned tabs per row is dynamic
    950        // Set this before adjusting dragged tab's position
    951        let pinnedTabs = this._tabbrowserTabs.visibleTabs.slice(
    952          0,
    953          gBrowser.pinnedTabCount
    954        );
    955        let tabsPerRow = 0;
    956        let position = RTL_UI
    957          ? window.windowUtils.getBoundsWithoutFlushing(
    958              this._tabbrowserTabs.pinnedTabsContainer
    959            ).right
    960          : 0;
    961        for (let pinnedTab of pinnedTabs) {
    962          let tabPosition;
    963          let rect = window.windowUtils.getBoundsWithoutFlushing(pinnedTab);
    964          if (RTL_UI) {
    965            tabPosition = rect.right;
    966            if (tabPosition > position) {
    967              break;
    968            }
    969          } else {
    970            tabPosition = rect.left;
    971            if (tabPosition < position) {
    972              break;
    973            }
    974          }
    975          tabsPerRow++;
    976          position = tabPosition;
    977        }
    978        this._maxTabsPerRow = tabsPerRow;
    979      }
    980 
    981      if (tab.multiselected) {
    982        for (let multiselectedTab of gBrowser.selectedTabs.filter(
    983          t => t.pinned != tab.pinned
    984        )) {
    985          gBrowser.removeFromMultiSelectedTabs(multiselectedTab);
    986        }
    987      }
    988 
    989      let dataTransferOrderedTabs;
    990      if (fromTabList || isTabGroupLabel(tab) || isSplitViewWrapper(tab)) {
    991        // Dragging a group label or an item in the all tabs menu doesn't
    992        // change the currently selected tabs, and it's not possible to select
    993        // multiple tabs from the list, thus handle only the dragged tab in
    994        // this case.
    995        dataTransferOrderedTabs = [tab];
    996      } else {
    997        this._tabbrowserTabs.selectedItem = tab;
    998        let selectedTabs = gBrowser.selectedTabs;
    999        let otherSelectedTabs = selectedTabs.filter(
   1000          selectedTab => selectedTab != tab
   1001        );
   1002        dataTransferOrderedTabs = [tab].concat(otherSelectedTabs);
   1003      }
   1004 
   1005      let dt = event.dataTransfer;
   1006      for (let i = 0; i < dataTransferOrderedTabs.length; i++) {
   1007        let dtTab = dataTransferOrderedTabs[i];
   1008        dt.mozSetDataAt(TAB_DROP_TYPE, dtTab, i);
   1009        if (isTab(dtTab)) {
   1010          let dtBrowser = dtTab.linkedBrowser;
   1011 
   1012          // We must not set text/x-moz-url or text/plain data here,
   1013          // otherwise trying to detach the tab by dropping it on the desktop
   1014          // may result in an "internet shortcut"
   1015          dt.mozSetDataAt(
   1016            "text/x-moz-text-internal",
   1017            dtBrowser.currentURI.spec,
   1018            i
   1019          );
   1020        }
   1021      }
   1022 
   1023      // Set the cursor to an arrow during tab drags.
   1024      dt.mozCursor = "default";
   1025 
   1026      // Set the tab as the source of the drag, which ensures we have a stable
   1027      // node to deliver the `dragend` event.  See bug 1345473.
   1028      dt.addElement(tab);
   1029 
   1030      // Create a canvas to which we capture the current tab.
   1031      // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired
   1032      // canvas size (in CSS pixels) to the window's backing resolution in order
   1033      // to get a full-resolution drag image for use on HiDPI displays.
   1034      let scale = window.devicePixelRatio;
   1035      let canvas = this._tabbrowserTabs._dndCanvas;
   1036      if (!canvas) {
   1037        this._tabbrowserTabs._dndCanvas = canvas = document.createElementNS(
   1038          "http://www.w3.org/1999/xhtml",
   1039          "canvas"
   1040        );
   1041        canvas.style.width = "100%";
   1042        canvas.style.height = "100%";
   1043        canvas.mozOpaque = true;
   1044      }
   1045 
   1046      canvas.width = 160 * scale;
   1047      canvas.height = 90 * scale;
   1048      let toDrag = canvas;
   1049      let dragImageOffset = -16;
   1050      let browser = isTab(tab) && tab.linkedBrowser;
   1051      if (isTabGroupLabel(tab)) {
   1052        toDrag = tab;
   1053      } else if (gMultiProcessBrowser) {
   1054        var context = canvas.getContext("2d");
   1055        context.fillStyle = "white";
   1056        context.fillRect(0, 0, canvas.width, canvas.height);
   1057 
   1058        let captureListener;
   1059        let platform = AppConstants.platform;
   1060        // On Windows and Mac we can update the drag image during a drag
   1061        // using updateDragImage. On Linux, we can use a panel.
   1062        if (platform == "win" || platform == "macosx") {
   1063          captureListener = function () {
   1064            dt.updateDragImage(canvas, dragImageOffset, dragImageOffset);
   1065          };
   1066        } else {
   1067          // Create a panel to use it in setDragImage
   1068          // which will tell xul to render a panel that follows
   1069          // the pointer while a dnd session is on.
   1070          if (!this._tabbrowserTabs._dndPanel) {
   1071            this._tabbrowserTabs._dndCanvas = canvas;
   1072            this._tabbrowserTabs._dndPanel = document.createXULElement("panel");
   1073            this._tabbrowserTabs._dndPanel.className = "dragfeedback-tab";
   1074            this._tabbrowserTabs._dndPanel.setAttribute("type", "drag");
   1075            let wrapper = document.createElementNS(
   1076              "http://www.w3.org/1999/xhtml",
   1077              "div"
   1078            );
   1079            wrapper.style.width = "160px";
   1080            wrapper.style.height = "90px";
   1081            wrapper.appendChild(canvas);
   1082            this._tabbrowserTabs._dndPanel.appendChild(wrapper);
   1083            document.documentElement.appendChild(
   1084              this._tabbrowserTabs._dndPanel
   1085            );
   1086          }
   1087          toDrag = this._tabbrowserTabs._dndPanel;
   1088        }
   1089        // PageThumb is async with e10s but that's fine
   1090        // since we can update the image during the dnd.
   1091        PageThumbs.captureToCanvas(browser, canvas)
   1092          .then(captureListener)
   1093          .catch(e => console.error(e));
   1094      } else {
   1095        // For the non e10s case we can just use PageThumbs
   1096        // sync, so let's use the canvas for setDragImage.
   1097        PageThumbs.captureToCanvas(browser, canvas).catch(e =>
   1098          console.error(e)
   1099        );
   1100        dragImageOffset = dragImageOffset * scale;
   1101      }
   1102      dt.setDragImage(toDrag, dragImageOffset, dragImageOffset);
   1103 
   1104      // _dragData.offsetX/Y give the coordinates that the mouse should be
   1105      // positioned relative to the corner of the new window created upon
   1106      // dragend such that the mouse appears to have the same position
   1107      // relative to the corner of the dragged tab.
   1108      let clientPos = ele => {
   1109        const rect = ele.getBoundingClientRect();
   1110        return this._tabbrowserTabs.verticalMode ? rect.top : rect.left;
   1111      };
   1112 
   1113      let tabOffset = clientPos(tab) - clientPos(this._tabbrowserTabs);
   1114 
   1115      let movingTabs = tab.multiselected ? gBrowser.selectedTabs : [tab];
   1116      let movingTabsSet = new Set(movingTabs);
   1117 
   1118      let dropEffect = this.getDropEffectForTabDrag(event);
   1119      let isMovingInTabStrip = !fromTabList && dropEffect == "move";
   1120      let collapseTabGroupDuringDrag =
   1121        isMovingInTabStrip && isTabGroupLabel(tab) && !tab.group.collapsed;
   1122 
   1123      tab._dragData = {
   1124        offsetX: this._tabbrowserTabs.verticalMode
   1125          ? event.screenX - window.screenX
   1126          : event.screenX - window.screenX - tabOffset,
   1127        offsetY: this._tabbrowserTabs.verticalMode
   1128          ? event.screenY - window.screenY - tabOffset
   1129          : event.screenY - window.screenY,
   1130        scrollPos:
   1131          this._tabbrowserTabs.verticalMode && tab.pinned
   1132            ? this._tabbrowserTabs.pinnedTabsContainer.scrollPosition
   1133            : this._tabbrowserTabs.arrowScrollbox.scrollPosition,
   1134        screenX: event.screenX,
   1135        screenY: event.screenY,
   1136        movingTabs,
   1137        movingTabsSet,
   1138        fromTabList,
   1139        tabGroupCreationColor: gBrowser.tabGroupMenu.nextUnusedColor,
   1140        expandGroupOnDrop: collapseTabGroupDuringDrag,
   1141      };
   1142      if (this._rtlMode) {
   1143        // Reverse order to handle positioning in `_updateTabStylesOnDrag`
   1144        // and animation in `_animateTabMove`
   1145        tab._dragData.movingTabs.reverse();
   1146      }
   1147 
   1148      if (isMovingInTabStrip) {
   1149        this.#setMovingTabMode(true);
   1150 
   1151        if (tab.multiselected) {
   1152          this._moveTogetherSelectedTabs(tab);
   1153        } else if (isTabGroupLabel(tab)) {
   1154          this._setIsDraggingTabGroup(tab.group, true);
   1155 
   1156          if (collapseTabGroupDuringDrag) {
   1157            tab.group.collapsed = true;
   1158          }
   1159        }
   1160      }
   1161 
   1162      event.stopPropagation();
   1163 
   1164      if (fromTabList) {
   1165        Glean.browserUiInteraction.allTabsPanelDragstartTabEventCount.add(1);
   1166      }
   1167    }
   1168 
   1169    /* In order to to drag tabs between both the pinned arrowscrollbox (pinned tab container)
   1170      and unpinned arrowscrollbox (tabbrowser-arrowscrollbox), the dragged tabs need to be
   1171      positioned absolutely. This results in a shift in the layout, filling the empty space.
   1172      This function updates the position and widths of elements affected by this layout shift
   1173      when the tab is first selected to be dragged.
   1174    */
   1175    _updateTabStylesOnDrag(tab, dropEffect) {
   1176      let tabStripItemElement = elementToMove(tab);
   1177      tabStripItemElement.style.pointerEvents =
   1178        dropEffect == "copy" ? "auto" : "";
   1179      if (tabStripItemElement.hasAttribute("dragtarget")) {
   1180        return;
   1181      }
   1182      let isPinned = tab.pinned;
   1183      let numPinned = gBrowser.pinnedTabCount;
   1184      let dragAndDropElements = this._tabbrowserTabs.dragAndDropElements;
   1185      let isGrid = this._tabbrowserTabs.isContainerVerticalPinnedGrid(tab);
   1186      let periphery = document.getElementById(
   1187        "tabbrowser-arrowscrollbox-periphery"
   1188      );
   1189 
   1190      if (isPinned && this._tabbrowserTabs.verticalMode) {
   1191        this._tabbrowserTabs.pinnedTabsContainer.setAttribute("dragActive", "");
   1192      }
   1193 
   1194      // Ensure tab containers retain size while tabs are dragged out of the layout
   1195      let pinnedRect = window.windowUtils.getBoundsWithoutFlushing(
   1196        this._tabbrowserTabs.pinnedTabsContainer.scrollbox
   1197      );
   1198      let pinnedContainerRect = window.windowUtils.getBoundsWithoutFlushing(
   1199        this._tabbrowserTabs.pinnedTabsContainer
   1200      );
   1201      let unpinnedRect = window.windowUtils.getBoundsWithoutFlushing(
   1202        this._tabbrowserTabs.arrowScrollbox.scrollbox
   1203      );
   1204      let tabContainerRect = window.windowUtils.getBoundsWithoutFlushing(
   1205        this._tabbrowserTabs
   1206      );
   1207 
   1208      if (this._tabbrowserTabs.pinnedTabsContainer.firstChild) {
   1209        this._tabbrowserTabs.pinnedTabsContainer.scrollbox.style.height =
   1210          pinnedRect.height + "px";
   1211        // Use "minHeight" so as not to interfere with user preferences for height.
   1212        this._tabbrowserTabs.pinnedTabsContainer.style.minHeight =
   1213          pinnedContainerRect.height + "px";
   1214        this._tabbrowserTabs.pinnedTabsContainer.scrollbox.style.width =
   1215          pinnedRect.width + "px";
   1216      }
   1217      this._tabbrowserTabs.arrowScrollbox.scrollbox.style.height =
   1218        unpinnedRect.height + "px";
   1219      this._tabbrowserTabs.arrowScrollbox.scrollbox.style.width =
   1220        unpinnedRect.width + "px";
   1221 
   1222      let { movingTabs, movingTabsSet, expandGroupOnDrop } = tab._dragData;
   1223      /** @type {(MozTabbrowserTab|typeof MozTabbrowserTabGroup.labelElement)[]} */
   1224      let suppressTransitionsFor = [];
   1225      /** @type {Map<MozTabbrowserTab, DOMRect>} */
   1226      const pinnedTabsOrigBounds = new Map();
   1227 
   1228      for (let t of dragAndDropElements) {
   1229        t = elementToMove(t);
   1230        let tabRect = window.windowUtils.getBoundsWithoutFlushing(t);
   1231 
   1232        // record where all the pinned tabs were before we position:absolute the moving tabs
   1233        if (isGrid && t.pinned) {
   1234          pinnedTabsOrigBounds.set(t, tabRect);
   1235        }
   1236        // Prevent flex rules from resizing non dragged tabs while the dragged
   1237        // tabs are positioned absolutely
   1238        if (tabRect.width) {
   1239          t.style.maxWidth = tabRect.width + "px";
   1240        }
   1241        // Prevent non-moving tab strip items from performing any animations
   1242        // at the very beginning of the drag operation; this prevents them
   1243        // from appearing to move while the dragged tabs are positioned absolutely
   1244        let isTabInCollapsingGroup = expandGroupOnDrop && t.group == tab.group;
   1245        if (!movingTabsSet.has(t) && !isTabInCollapsingGroup) {
   1246          t.animationsEnabled = false;
   1247          suppressTransitionsFor.push(t);
   1248        }
   1249      }
   1250 
   1251      if (suppressTransitionsFor.length) {
   1252        window
   1253          .promiseDocumentFlushed(() => {})
   1254          .then(() => {
   1255            window.requestAnimationFrame(() => {
   1256              for (let t of suppressTransitionsFor) {
   1257                t.animationsEnabled = true;
   1258              }
   1259            });
   1260          });
   1261      }
   1262 
   1263      // Use .tab-group-label-container or .tabbrowser-tab for size/position
   1264      // calculations.
   1265      let rect =
   1266        window.windowUtils.getBoundsWithoutFlushing(tabStripItemElement);
   1267      // Vertical tabs live under the #sidebar-main element which gets animated and has a
   1268      // transform style property, making it the containing block for all its descendants.
   1269      // Position:absolute elements need to account for this when updating position using
   1270      // other measurements whose origin is the viewport or documentElement's 0,0
   1271      let movingTabsOffsetX = window.windowUtils.getBoundsWithoutFlushing(
   1272        tabStripItemElement.offsetParent
   1273      ).x;
   1274 
   1275      let movingTabsIndex = movingTabs.findIndex(t => t._tPos == tab._tPos);
   1276      // Update moving tabs absolute position based on original dragged tab position
   1277      // Moving tabs with a lower index are moved before the dragged tab and moving
   1278      // tabs with a higher index are moved after the dragged tab.
   1279      let position = 0;
   1280      // Position moving tabs after dragged tab
   1281      for (let movingTab of movingTabs.slice(movingTabsIndex)) {
   1282        movingTab = elementToMove(movingTab);
   1283        movingTab.style.width = rect.width + "px";
   1284        // "dragtarget" contains the following rules which must only be set AFTER the above
   1285        // elements have been adjusted. {z-index: 3 !important, position: absolute !important}
   1286        movingTab.setAttribute("dragtarget", "");
   1287        if (isTabGroupLabel(tab)) {
   1288          if (this._tabbrowserTabs.verticalMode) {
   1289            movingTab.style.top = rect.top - tabContainerRect.top + "px";
   1290          } else {
   1291            movingTab.style.left = rect.left - movingTabsOffsetX + "px";
   1292            movingTab.style.height = rect.height + "px";
   1293          }
   1294        } else if (isGrid) {
   1295          movingTab.style.top = rect.top - pinnedRect.top + "px";
   1296          movingTab.style.left =
   1297            rect.left - movingTabsOffsetX + position + "px";
   1298          position += rect.width;
   1299        } else if (this._tabbrowserTabs.verticalMode) {
   1300          movingTab.style.top =
   1301            rect.top - tabContainerRect.top + position + "px";
   1302          position += rect.height;
   1303        } else if (this._rtlMode) {
   1304          movingTab.style.left =
   1305            rect.left - movingTabsOffsetX - position + "px";
   1306          position -= rect.width;
   1307        } else {
   1308          movingTab.style.left =
   1309            rect.left - movingTabsOffsetX + position + "px";
   1310          position += rect.width;
   1311        }
   1312      }
   1313      // Reset position so we can next handle moving tabs before the dragged tab
   1314      if (this._tabbrowserTabs.verticalMode) {
   1315        position = -rect.height;
   1316      } else if (this._rtlMode) {
   1317        position = rect.width;
   1318      } else {
   1319        position = -rect.width;
   1320      }
   1321      // Position moving tabs before dragged tab
   1322      for (let movingTab of movingTabs.slice(0, movingTabsIndex).reverse()) {
   1323        movingTab.style.width = rect.width + "px";
   1324        movingTab.setAttribute("dragtarget", "");
   1325        if (this._tabbrowserTabs.verticalMode) {
   1326          movingTab.style.top =
   1327            rect.top - tabContainerRect.top + position + "px";
   1328          position -= rect.height;
   1329        } else if (this._rtlMode) {
   1330          movingTab.style.left =
   1331            rect.left - movingTabsOffsetX - position + "px";
   1332          position += rect.width;
   1333        } else {
   1334          movingTab.style.left =
   1335            rect.left - movingTabsOffsetX + position + "px";
   1336          position -= rect.width;
   1337        }
   1338      }
   1339 
   1340      if (
   1341        !isPinned &&
   1342        this._tabbrowserTabs.arrowScrollbox.hasAttribute("overflowing")
   1343      ) {
   1344        if (this._tabbrowserTabs.verticalMode) {
   1345          periphery.style.marginBlockStart =
   1346            rect.height * movingTabs.length + "px";
   1347        } else {
   1348          periphery.style.marginInlineStart =
   1349            rect.width * movingTabs.length + "px";
   1350        }
   1351      } else if (
   1352        isPinned &&
   1353        this._tabbrowserTabs.pinnedTabsContainer.hasAttribute("overflowing")
   1354      ) {
   1355        let pinnedPeriphery = document.createXULElement("hbox");
   1356        pinnedPeriphery.id = "pinned-tabs-container-periphery";
   1357        pinnedPeriphery.style.width = "100%";
   1358        pinnedPeriphery.style.marginBlockStart =
   1359          (isGrid && numPinned % this._maxTabsPerRow == 1
   1360            ? rect.height
   1361            : rect.height * movingTabs.length) + "px";
   1362        this._tabbrowserTabs.pinnedTabsContainer.appendChild(pinnedPeriphery);
   1363      }
   1364 
   1365      let setElPosition = el => {
   1366        let elRect = window.windowUtils.getBoundsWithoutFlushing(el);
   1367        if (this._tabbrowserTabs.verticalMode && elRect.top > rect.top) {
   1368          el.style.top = movingTabs.length * rect.height + "px";
   1369        } else if (!this._tabbrowserTabs.verticalMode) {
   1370          if (!this._rtlMode && elRect.left > rect.left) {
   1371            el.style.left = movingTabs.length * rect.width + "px";
   1372          } else if (this._rtlMode && elRect.left < rect.left) {
   1373            el.style.left = movingTabs.length * -rect.width + "px";
   1374          }
   1375        }
   1376      };
   1377 
   1378      let setGridElPosition = el => {
   1379        let origBounds = pinnedTabsOrigBounds.get(el);
   1380        if (!origBounds) {
   1381          // No bounds saved for this pinned tab
   1382          return;
   1383        }
   1384        // We use getBoundingClientRect and force a reflow as we need to know their new positions
   1385        // after making the moving tabs position:absolute
   1386        let newBounds = el.getBoundingClientRect();
   1387        let shiftX = origBounds.x - newBounds.x;
   1388        let shiftY = origBounds.y - newBounds.y;
   1389 
   1390        el.style.left = shiftX + "px";
   1391        el.style.top = shiftY + "px";
   1392      };
   1393 
   1394      // Update tabs in the same container as the dragged tabs so as not
   1395      // to fill the space when the dragged tabs become absolute
   1396      for (let t of dragAndDropElements) {
   1397        let tabIsPinned = t.pinned;
   1398        t = elementToMove(t);
   1399        if (!t.hasAttribute("dragtarget")) {
   1400          if (
   1401            (!isPinned && !tabIsPinned) ||
   1402            (tabIsPinned && isPinned && !isGrid)
   1403          ) {
   1404            setElPosition(t);
   1405          } else if (isGrid && tabIsPinned && isPinned) {
   1406            setGridElPosition(t);
   1407          }
   1408        }
   1409      }
   1410 
   1411      if (this._tabbrowserTabs.expandOnHover) {
   1412        // Query the expanded width from sidebar launcher to ensure tabs aren't
   1413        // cut off (Bug 1974037).
   1414        const { SidebarController } = tab.ownerGlobal;
   1415        SidebarController.expandOnHoverComplete.then(async () => {
   1416          const width = await window.promiseDocumentFlushed(
   1417            () => SidebarController.sidebarMain.clientWidth
   1418          );
   1419          requestAnimationFrame(() => {
   1420            for (const t of movingTabs) {
   1421              t.style.width = width + "px";
   1422            }
   1423            // Allow scrollboxes to grow to expanded sidebar width.
   1424            this._tabbrowserTabs.arrowScrollbox.scrollbox.style.width = "";
   1425            this._tabbrowserTabs.pinnedTabsContainer.scrollbox.style.width = "";
   1426          });
   1427        });
   1428      }
   1429 
   1430      // Handle the new tab button filling the space when the dragged tab
   1431      // position becomes absolute
   1432      if (!this._tabbrowserTabs.overflowing && !isPinned) {
   1433        if (this._tabbrowserTabs.verticalMode) {
   1434          periphery.style.top = `${Math.round(movingTabs.length * rect.height)}px`;
   1435        } else if (this._rtlMode) {
   1436          periphery.style.left = `${Math.round(movingTabs.length * -rect.width)}px`;
   1437        } else {
   1438          periphery.style.left = `${Math.round(movingTabs.length * rect.width)}px`;
   1439        }
   1440      }
   1441    }
   1442 
   1443    /**
   1444     * Move together all selected tabs around the tab in param.
   1445     */
   1446    _moveTogetherSelectedTabs(tab) {
   1447      let draggedTabIndex = tab.elementIndex;
   1448      let selectedTabs = gBrowser.selectedTabs;
   1449      if (selectedTabs.some(t => t.pinned != tab.pinned)) {
   1450        throw new Error(
   1451          "Cannot move together a mix of pinned and unpinned tabs."
   1452        );
   1453      }
   1454      let animate = !gReduceMotion;
   1455 
   1456      tab._moveTogetherSelectedTabsData = {
   1457        finished: !animate,
   1458      };
   1459 
   1460      let addAnimationData = (movingTab, isBeforeSelectedTab) => {
   1461        let lowerIndex = Math.min(movingTab.elementIndex, draggedTabIndex) + 1;
   1462        let higherIndex = Math.max(movingTab.elementIndex, draggedTabIndex);
   1463        let middleItems = this._tabbrowserTabs.dragAndDropElements
   1464          .slice(lowerIndex, higherIndex)
   1465          .filter(item => !item.multiselected);
   1466        if (!middleItems.length) {
   1467          // movingTab is already at the right position and thus doesn't need
   1468          // to be animated.
   1469          return;
   1470        }
   1471 
   1472        movingTab._moveTogetherSelectedTabsData = {
   1473          translatePos: 0,
   1474          animate: true,
   1475        };
   1476        movingTab.toggleAttribute("multiselected-move-together", true);
   1477 
   1478        let postTransitionCleanup = () => {
   1479          movingTab._moveTogetherSelectedTabsData.animate = false;
   1480        };
   1481        if (gReduceMotion) {
   1482          postTransitionCleanup();
   1483        } else {
   1484          let onTransitionEnd = transitionendEvent => {
   1485            if (
   1486              transitionendEvent.propertyName != "transform" ||
   1487              transitionendEvent.originalTarget != movingTab
   1488            ) {
   1489              return;
   1490            }
   1491            movingTab.removeEventListener("transitionend", onTransitionEnd);
   1492            postTransitionCleanup();
   1493          };
   1494 
   1495          movingTab.addEventListener("transitionend", onTransitionEnd);
   1496        }
   1497 
   1498        // Add animation data for tabs and tab group labels between movingTab
   1499        // (multiselected tab moving towards the dragged tab) and draggedTab. Those items
   1500        // in the middle should move in the opposite direction of movingTab.
   1501 
   1502        let movingTabSize =
   1503          movingTab.getBoundingClientRect()[
   1504            this._tabbrowserTabs.verticalMode ? "height" : "width"
   1505          ];
   1506 
   1507        for (let middleItem of middleItems) {
   1508          if (isTab(middleItem)) {
   1509            if (middleItem.pinned != movingTab.pinned) {
   1510              // Don't mix pinned and unpinned tabs
   1511              break;
   1512            }
   1513            if (middleItem.multiselected) {
   1514              // Skip because this multiselected tab should
   1515              // be shifted towards the dragged Tab.
   1516              continue;
   1517            }
   1518          }
   1519          middleItem = elementToMove(middleItem);
   1520          let middleItemSize =
   1521            middleItem.getBoundingClientRect()[
   1522              this._tabbrowserTabs.verticalMode ? "height" : "width"
   1523            ];
   1524 
   1525          if (!middleItem._moveTogetherSelectedTabsData?.translatePos) {
   1526            middleItem._moveTogetherSelectedTabsData = { translatePos: 0 };
   1527          }
   1528          movingTab._moveTogetherSelectedTabsData.translatePos +=
   1529            isBeforeSelectedTab ? middleItemSize : -middleItemSize;
   1530          middleItem._moveTogetherSelectedTabsData.translatePos =
   1531            isBeforeSelectedTab ? -movingTabSize : movingTabSize;
   1532 
   1533          middleItem.toggleAttribute("multiselected-move-together", true);
   1534        }
   1535      };
   1536 
   1537      let tabIndex = selectedTabs.indexOf(tab);
   1538 
   1539      // Animate left or top selected tabs
   1540      for (let i = 0; i < tabIndex; i++) {
   1541        let movingTab = selectedTabs[i];
   1542        if (animate) {
   1543          addAnimationData(movingTab, true);
   1544        } else {
   1545          gBrowser.moveTabBefore(movingTab, tab);
   1546        }
   1547      }
   1548 
   1549      // Animate right or bottom selected tabs
   1550      for (let i = selectedTabs.length - 1; i > tabIndex; i--) {
   1551        let movingTab = selectedTabs[i];
   1552        if (animate) {
   1553          addAnimationData(movingTab, false);
   1554        } else {
   1555          gBrowser.moveTabAfter(movingTab, tab);
   1556        }
   1557      }
   1558 
   1559      // Slide the relevant tabs to their new position.
   1560      for (let item of this._tabbrowserTabs.dragAndDropElements) {
   1561        item = elementToMove(item);
   1562        if (item._moveTogetherSelectedTabsData?.translatePos) {
   1563          let translatePos =
   1564            (this._rtlMode ? -1 : 1) *
   1565            item._moveTogetherSelectedTabsData.translatePos;
   1566          item.style.transform = `translate${
   1567            this._tabbrowserTabs.verticalMode ? "Y" : "X"
   1568          }(${translatePos}px)`;
   1569        }
   1570      }
   1571    }
   1572 
   1573    #isAnimatingMoveTogetherSelectedTabs() {
   1574      for (let tab of gBrowser.selectedTabs) {
   1575        if (tab._moveTogetherSelectedTabsData?.animate) {
   1576          return true;
   1577        }
   1578      }
   1579      return false;
   1580    }
   1581 
   1582    finishMoveTogetherSelectedTabs(tab) {
   1583      if (
   1584        !tab._moveTogetherSelectedTabsData ||
   1585        tab._moveTogetherSelectedTabsData.finished
   1586      ) {
   1587        return;
   1588      }
   1589 
   1590      tab._moveTogetherSelectedTabsData.finished = true;
   1591 
   1592      let selectedTabs = gBrowser.selectedTabs;
   1593      let tabIndex = selectedTabs.indexOf(tab);
   1594 
   1595      // Moving left or top tabs
   1596      for (let i = 0; i < tabIndex; i++) {
   1597        gBrowser.moveTabBefore(selectedTabs[i], tab);
   1598      }
   1599 
   1600      // Moving right or bottom tabs
   1601      for (let i = selectedTabs.length - 1; i > tabIndex; i--) {
   1602        gBrowser.moveTabAfter(selectedTabs[i], tab);
   1603      }
   1604 
   1605      for (let item of this._tabbrowserTabs.dragAndDropElements) {
   1606        item = elementToMove(item);
   1607        item.style.transform = "";
   1608        item.removeAttribute("multiselected-move-together");
   1609        delete item._moveTogetherSelectedTabsData;
   1610      }
   1611    }
   1612 
   1613    // Drag over
   1614 
   1615    _animateExpandedPinnedTabMove(event) {
   1616      let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
   1617      let dragData = draggedTab._dragData;
   1618      let movingTabs = dragData.movingTabs;
   1619 
   1620      dragData.animLastScreenX ??= dragData.screenX;
   1621      dragData.animLastScreenY ??= dragData.screenY;
   1622 
   1623      let screenX = event.screenX;
   1624      let screenY = event.screenY;
   1625 
   1626      if (
   1627        screenY == dragData.animLastScreenY &&
   1628        screenX == dragData.animLastScreenX
   1629      ) {
   1630        return;
   1631      }
   1632 
   1633      let tabs = this._tabbrowserTabs.visibleTabs.slice(
   1634        0,
   1635        gBrowser.pinnedTabCount
   1636      );
   1637 
   1638      let directionX = screenX > dragData.animLastScreenX;
   1639      let directionY = screenY > dragData.animLastScreenY;
   1640      dragData.animLastScreenY = screenY;
   1641      dragData.animLastScreenX = screenX;
   1642 
   1643      let { width: tabWidth, height: tabHeight } =
   1644        draggedTab.getBoundingClientRect();
   1645      let shiftSizeX = tabWidth * movingTabs.length;
   1646      let shiftSizeY = tabHeight;
   1647      dragData.tabWidth = tabWidth;
   1648      dragData.tabHeight = tabHeight;
   1649 
   1650      // Move the dragged tab based on the mouse position.
   1651      let firstTabInRow;
   1652      let lastTabInRow;
   1653      let lastTab = tabs.at(-1);
   1654      let periphery = document.getElementById(
   1655        "tabbrowser-arrowscrollbox-periphery"
   1656      );
   1657      if (RTL_UI) {
   1658        firstTabInRow =
   1659          tabs.length >= this._maxTabsPerRow
   1660            ? tabs[this._maxTabsPerRow - 1]
   1661            : lastTab;
   1662        lastTabInRow = tabs[0];
   1663      } else {
   1664        firstTabInRow = tabs[0];
   1665        lastTabInRow =
   1666          tabs.length >= this._maxTabsPerRow
   1667            ? tabs[this._maxTabsPerRow - 1]
   1668            : lastTab;
   1669      }
   1670      let lastMovingTabScreenX = movingTabs.at(-1).screenX;
   1671      let lastMovingTabScreenY = movingTabs.at(-1).screenY;
   1672      let firstMovingTabScreenX = movingTabs[0].screenX;
   1673      let firstMovingTabScreenY = movingTabs[0].screenY;
   1674      let translateX = screenX - dragData.screenX;
   1675      let translateY = screenY - dragData.screenY;
   1676      let firstBoundX = firstTabInRow.screenX - firstMovingTabScreenX;
   1677      let firstBoundY = this._tabbrowserTabs.screenY - firstMovingTabScreenY;
   1678      let lastBoundX =
   1679        lastTabInRow.screenX +
   1680        lastTabInRow.getBoundingClientRect().width -
   1681        (lastMovingTabScreenX + tabWidth);
   1682      let lastBoundY = periphery.screenY - (lastMovingTabScreenY + tabHeight);
   1683      translateX = Math.min(Math.max(translateX, firstBoundX), lastBoundX);
   1684      translateY = Math.min(Math.max(translateY, firstBoundY), lastBoundY);
   1685 
   1686      // Center the tab under the cursor if the tab is not under the cursor while dragging
   1687      if (
   1688        screen < draggedTab.screenY + translateY ||
   1689        screen > draggedTab.screenY + tabHeight + translateY
   1690      ) {
   1691        translateY = screen - draggedTab.screenY - tabHeight / 2;
   1692      }
   1693 
   1694      for (let tab of movingTabs) {
   1695        tab.style.transform = `translate(${translateX}px, ${translateY}px)`;
   1696      }
   1697 
   1698      dragData.translateX = translateX;
   1699      dragData.translateY = translateY;
   1700 
   1701      // Determine what tab we're dragging over.
   1702      // * Single tab dragging: Point of reference is the center of the dragged tab. If that
   1703      //   point touches a background tab, the dragged tab would take that
   1704      //   tab's position when dropped.
   1705      // * Multiple tabs dragging: All dragged tabs are one "giant" tab with two
   1706      //   points of reference (center of tabs on the extremities). When
   1707      //   mouse is moving from top to bottom, the bottom reference gets activated,
   1708      //   otherwise the top reference will be used. Everything else works the same
   1709      //   as single tab dragging.
   1710      // * We're doing a binary search in order to reduce the amount of
   1711      //   tabs we need to check.
   1712 
   1713      tabs = tabs.filter(t => !movingTabs.includes(t) || t == draggedTab);
   1714      let firstTabCenterX = firstMovingTabScreenX + translateX + tabWidth / 2;
   1715      let lastTabCenterX = lastMovingTabScreenX + translateX + tabWidth / 2;
   1716      let tabCenterX = directionX ? lastTabCenterX : firstTabCenterX;
   1717      let firstTabCenterY = firstMovingTabScreenY + translateY + tabHeight / 2;
   1718      let lastTabCenterY = lastMovingTabScreenY + translateY + tabHeight / 2;
   1719      let tabCenterY = directionY ? lastTabCenterY : firstTabCenterY;
   1720 
   1721      let shiftNumber = this._maxTabsPerRow - movingTabs.length;
   1722 
   1723      let getTabShift = (tab, dropIndex) => {
   1724        if (
   1725          tab.elementIndex < draggedTab.elementIndex &&
   1726          tab.elementIndex >= dropIndex
   1727        ) {
   1728          // If tab is at the end of a row, shift back and down
   1729          let tabRow = Math.ceil((tab.elementIndex + 1) / this._maxTabsPerRow);
   1730          let shiftedTabRow = Math.ceil(
   1731            (tab.elementIndex + 1 + movingTabs.length) / this._maxTabsPerRow
   1732          );
   1733          if (tab.elementIndex && tabRow != shiftedTabRow) {
   1734            return [
   1735              RTL_UI ? tabWidth * shiftNumber : -tabWidth * shiftNumber,
   1736              shiftSizeY,
   1737            ];
   1738          }
   1739          return [RTL_UI ? -shiftSizeX : shiftSizeX, 0];
   1740        }
   1741        if (
   1742          tab.elementIndex > draggedTab.elementIndex &&
   1743          tab.elementIndex < dropIndex
   1744        ) {
   1745          // If tab is not index 0 and at the start of a row, shift across and up
   1746          let tabRow = Math.floor(tab.elementIndex / this._maxTabsPerRow);
   1747          let shiftedTabRow = Math.floor(
   1748            (tab.elementIndex - movingTabs.length) / this._maxTabsPerRow
   1749          );
   1750          if (tab.elementIndex && tabRow != shiftedTabRow) {
   1751            return [
   1752              RTL_UI ? -tabWidth * shiftNumber : tabWidth * shiftNumber,
   1753              -shiftSizeY,
   1754            ];
   1755          }
   1756          return [RTL_UI ? shiftSizeX : -shiftSizeX, 0];
   1757        }
   1758        return [0, 0];
   1759      };
   1760 
   1761      let low = 0;
   1762      let high = tabs.length - 1;
   1763      let newIndex = -1;
   1764      let oldIndex =
   1765        dragData.animDropElementIndex ?? movingTabs[0].elementIndex;
   1766      while (low <= high) {
   1767        let mid = Math.floor((low + high) / 2);
   1768        if (tabs[mid] == draggedTab && ++mid > high) {
   1769          break;
   1770        }
   1771        let [shiftX, shiftY] = getTabShift(tabs[mid], oldIndex);
   1772        screenX = tabs[mid].screenX + shiftX;
   1773        screenY = tabs[mid].screenY + shiftY;
   1774 
   1775        if (screenY + tabHeight < tabCenterY) {
   1776          low = mid + 1;
   1777        } else if (screenY > tabCenterY) {
   1778          high = mid - 1;
   1779        } else if (
   1780          RTL_UI ? screenX + tabWidth < tabCenterX : screenX > tabCenterX
   1781        ) {
   1782          high = mid - 1;
   1783        } else if (
   1784          RTL_UI ? screenX > tabCenterX : screenX + tabWidth < tabCenterX
   1785        ) {
   1786          low = mid + 1;
   1787        } else {
   1788          newIndex = tabs[mid].elementIndex;
   1789          break;
   1790        }
   1791      }
   1792 
   1793      if (newIndex >= oldIndex && newIndex < tabs.length) {
   1794        newIndex++;
   1795      }
   1796 
   1797      if (newIndex < 0) {
   1798        newIndex = oldIndex;
   1799      }
   1800 
   1801      if (newIndex == dragData.animDropElementIndex) {
   1802        return;
   1803      }
   1804 
   1805      dragData.animDropElementIndex = newIndex;
   1806      dragData.dropElement = tabs[Math.min(newIndex, tabs.length - 1)];
   1807      dragData.dropBefore = newIndex < tabs.length;
   1808 
   1809      // Shift background tabs to leave a gap where the dragged tab
   1810      // would currently be dropped.
   1811      for (let tab of tabs) {
   1812        if (tab != draggedTab) {
   1813          let [shiftX, shiftY] = getTabShift(tab, newIndex);
   1814          tab.style.transform =
   1815            shiftX || shiftY ? `translate(${shiftX}px, ${shiftY}px)` : "";
   1816        }
   1817      }
   1818    }
   1819 
   1820    // eslint-disable-next-line complexity
   1821    _animateTabMove(event) {
   1822      let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
   1823      let dragData = draggedTab._dragData;
   1824      let movingTabs = dragData.movingTabs;
   1825      let movingTabsSet = dragData.movingTabsSet;
   1826 
   1827      dragData.animLastScreenPos ??= this._tabbrowserTabs.verticalMode
   1828        ? dragData.screenY
   1829        : dragData.screenX;
   1830      let screen = this._tabbrowserTabs.verticalMode
   1831        ? event.screenY
   1832        : event.screenX;
   1833      if (screen == dragData.animLastScreenPos) {
   1834        return;
   1835      }
   1836      let screenForward = screen > dragData.animLastScreenPos;
   1837      dragData.animLastScreenPos = screen;
   1838 
   1839      this._clearDragOverGroupingTimer();
   1840      this.#clearPinnedDropIndicatorTimer();
   1841 
   1842      let isPinned = draggedTab.pinned;
   1843      let numPinned = gBrowser.pinnedTabCount;
   1844      let dragAndDropElements = this._tabbrowserTabs.dragAndDropElements;
   1845      let tabs = dragAndDropElements.slice(
   1846        isPinned ? 0 : numPinned,
   1847        isPinned ? numPinned : undefined
   1848      );
   1849 
   1850      if (this._rtlMode) {
   1851        tabs.reverse();
   1852      }
   1853 
   1854      let bounds = ele => window.windowUtils.getBoundsWithoutFlushing(ele);
   1855      let logicalForward = screenForward != this._rtlMode;
   1856      let screenAxis = this._tabbrowserTabs.verticalMode
   1857        ? "screenY"
   1858        : "screenX";
   1859      let size = this._tabbrowserTabs.verticalMode ? "height" : "width";
   1860      let translateAxis = this._tabbrowserTabs.verticalMode
   1861        ? "translateY"
   1862        : "translateX";
   1863      let { width: tabWidth, height: tabHeight } = bounds(draggedTab);
   1864      let tabSize = this._tabbrowserTabs.verticalMode ? tabHeight : tabWidth;
   1865      let translateX = event.screenX - dragData.screenX;
   1866      let translateY = event.screenY - dragData.screenY;
   1867 
   1868      dragData.tabWidth = tabWidth;
   1869      dragData.tabHeight = tabHeight;
   1870      dragData.translateX = translateX;
   1871      dragData.translateY = translateY;
   1872 
   1873      // Move the dragged tab based on the mouse position.
   1874      let periphery = document.getElementById(
   1875        "tabbrowser-arrowscrollbox-periphery"
   1876      );
   1877      let lastMovingTab = movingTabs.at(-1);
   1878      let firstMovingTab = movingTabs[0];
   1879      let endEdge = ele => ele[screenAxis] + bounds(ele)[size];
   1880      let lastMovingTabScreen = endEdge(lastMovingTab);
   1881      let firstMovingTabScreen = firstMovingTab[screenAxis];
   1882      let shiftSize = lastMovingTabScreen - firstMovingTabScreen;
   1883      let translate = screen - dragData[screenAxis];
   1884 
   1885      // Constrain the range over which the moving tabs can move between the edge of the tabstrip and periphery.
   1886      // Add 1 to periphery so we don't overlap it.
   1887      let startBound = this._rtlMode
   1888        ? endEdge(periphery) + 1 - firstMovingTabScreen
   1889        : this._tabbrowserTabs[screenAxis] - firstMovingTabScreen;
   1890      let endBound = this._rtlMode
   1891        ? endEdge(this._tabbrowserTabs) - lastMovingTabScreen
   1892        : periphery[screenAxis] - 1 - lastMovingTabScreen;
   1893      translate = Math.min(Math.max(translate, startBound), endBound);
   1894 
   1895      // Center the tab under the cursor if the tab is not under the cursor while dragging
   1896      let draggedTabScreenAxis = draggedTab[screenAxis] + translate;
   1897      if (
   1898        (screen < draggedTabScreenAxis ||
   1899          screen > draggedTabScreenAxis + tabSize) &&
   1900        draggedTabScreenAxis + tabSize < endBound &&
   1901        draggedTabScreenAxis > startBound
   1902      ) {
   1903        translate = screen - draggedTab[screenAxis] - tabSize / 2;
   1904        // Ensure, after the above calculation, we are still within bounds
   1905        translate = Math.min(Math.max(translate, startBound), endBound);
   1906      }
   1907 
   1908      if (!gBrowser.pinnedTabCount && !this._dragToPinPromoCard.shouldRender) {
   1909        let pinnedDropIndicatorMargin = parseFloat(
   1910          window.getComputedStyle(this._pinnedDropIndicator).marginInline
   1911        );
   1912        this._checkWithinPinnedContainerBounds({
   1913          firstMovingTabScreen,
   1914          lastMovingTabScreen,
   1915          pinnedTabsStartEdge: this._rtlMode
   1916            ? endEdge(this._tabbrowserTabs.arrowScrollbox) +
   1917              pinnedDropIndicatorMargin
   1918            : this[screenAxis],
   1919          pinnedTabsEndEdge: this._rtlMode
   1920            ? endEdge(this._tabbrowserTabs)
   1921            : this._tabbrowserTabs.arrowScrollbox[screenAxis] -
   1922              pinnedDropIndicatorMargin,
   1923          translate,
   1924          draggedTab,
   1925        });
   1926      }
   1927 
   1928      for (let item of movingTabs) {
   1929        item = elementToMove(item);
   1930        item.style.transform = `${translateAxis}(${translate}px)`;
   1931      }
   1932 
   1933      dragData.translatePos = translate;
   1934 
   1935      tabs = tabs.filter(t => !movingTabsSet.has(t) || t == draggedTab);
   1936 
   1937      /**
   1938       * When the `draggedTab` is just starting to move, the `draggedTab` is in
   1939       * its original location and the `dropElementIndex == draggedTab.elementIndex`.
   1940       * Any tabs or tab group labels passed in as `item` will result in a 0 shift
   1941       * because all of those items should also continue to appear in their original
   1942       * locations.
   1943       *
   1944       * Once the `draggedTab` is more "backward" in the tab strip than its original
   1945       * position, any tabs or tab group labels between the `draggedTab`'s original
   1946       * `elementIndex` and the current `dropElementIndex` should shift "forward"
   1947       * out of the way of the dragging tabs.
   1948       *
   1949       * When the `draggedTab` is more "forward" in the tab strip than its original
   1950       * position, any tabs or tab group labels between the `draggedTab`'s original
   1951       * `elementIndex` and the current `dropElementIndex` should shift "backward"
   1952       * out of the way of the dragging tabs.
   1953       *
   1954       * @param {MozTabbrowserTab|MozTabbrowserTabGroup.label} item
   1955       * @param {number} dropElementIndex
   1956       * @returns {number}
   1957       */
   1958      let getTabShift = (item, dropElementIndex) => {
   1959        if (
   1960          item.elementIndex < draggedTab.elementIndex &&
   1961          item.elementIndex >= dropElementIndex
   1962        ) {
   1963          return this._rtlMode ? -shiftSize : shiftSize;
   1964        }
   1965        if (
   1966          item.elementIndex > draggedTab.elementIndex &&
   1967          item.elementIndex < dropElementIndex
   1968        ) {
   1969          return this._rtlMode ? shiftSize : -shiftSize;
   1970        }
   1971        return 0;
   1972      };
   1973 
   1974      let oldDropElementIndex =
   1975        dragData.animDropElementIndex ?? movingTabs[0].elementIndex;
   1976 
   1977      /**
   1978       * Returns the higher % by which one element overlaps another
   1979       * in the tab strip.
   1980       *
   1981       * When element 1 is further forward in the tab strip:
   1982       *
   1983       *   p1            p2      p1+s1    p2+s2
   1984       *    |             |        |        |
   1985       *    ---------------------------------
   1986       *    ========================
   1987       *               s1
   1988       *                  ===================
   1989       *                           s2
   1990       *                  ==========
   1991       *                   overlap
   1992       *
   1993       * When element 2 is further forward in the tab strip:
   1994       *
   1995       *   p2            p1      p2+s2    p1+s1
   1996       *    |             |        |        |
   1997       *    ---------------------------------
   1998       *    ========================
   1999       *               s2
   2000       *                  ===================
   2001       *                           s1
   2002       *                  ==========
   2003       *                   overlap
   2004       *
   2005       * @param {number} p1
   2006       *   Position (x or y value in screen coordinates) of element 1.
   2007       * @param {number} s1
   2008       *   Size (width or height) of element 1.
   2009       * @param {number} p2
   2010       *   Position (x or y value in screen coordinates) of element 2.
   2011       * @param {number} s2
   2012       *   Size (width or height) of element 1.
   2013       * @returns {number}
   2014       *   Percent between 0.0 and 1.0 (inclusive) of element 1 or element 2
   2015       *   that is overlapped by the other element. If the elements have
   2016       *   different sizes, then this returns the larger overlap percentage.
   2017       */
   2018      function greatestOverlap(p1, s1, p2, s2) {
   2019        let overlapSize;
   2020        if (p1 < p2) {
   2021          // element 1 starts first
   2022          overlapSize = p1 + s1 - p2;
   2023        } else {
   2024          // element 2 starts first
   2025          overlapSize = p2 + s2 - p1;
   2026        }
   2027 
   2028        // No overlap if size is <= 0
   2029        if (overlapSize <= 0) {
   2030          return 0;
   2031        }
   2032 
   2033        // Calculate the overlap fraction from each element's perspective.
   2034        let overlapPercent = Math.max(overlapSize / s1, overlapSize / s2);
   2035 
   2036        return Math.min(overlapPercent, 1);
   2037      }
   2038 
   2039      /**
   2040       * Determine what tab/tab group label we're dragging over.
   2041       *
   2042       * When dragging right or downwards, the reference point for overlap is
   2043       * the right or bottom edge of the most forward moving tab.
   2044       *
   2045       * When dragging left or upwards, the reference point for overlap is the
   2046       * left or top edge of the most backward moving tab.
   2047       *
   2048       * @returns {Element|null}
   2049       *   The tab or tab group label that should be used to visually shift tab
   2050       *   strip elements out of the way of the dragged tab(s) during a drag
   2051       *   operation. Note: this is not used to determine where the dragged
   2052       *   tab(s) will be dropped, it is only used for visual animation at this
   2053       *   time.
   2054       */
   2055      let getOverlappedElement = () => {
   2056        let point =
   2057          (screenForward ? lastMovingTabScreen : firstMovingTabScreen) +
   2058          translate;
   2059        let low = 0;
   2060        let high = tabs.length - 1;
   2061        while (low <= high) {
   2062          let mid = Math.floor((low + high) / 2);
   2063          if (tabs[mid] == draggedTab && ++mid > high) {
   2064            break;
   2065          }
   2066          let element = tabs[mid];
   2067          let elementForSize = elementToMove(element);
   2068          screen =
   2069            elementForSize[screenAxis] +
   2070            getTabShift(element, oldDropElementIndex);
   2071 
   2072          if (screen > point) {
   2073            high = mid - 1;
   2074          } else if (screen + bounds(elementForSize)[size] < point) {
   2075            low = mid + 1;
   2076          } else {
   2077            return element;
   2078          }
   2079        }
   2080        return null;
   2081      };
   2082 
   2083      let dropElement = getOverlappedElement();
   2084 
   2085      let newDropElementIndex;
   2086      if (dropElement) {
   2087        newDropElementIndex = dropElement.elementIndex;
   2088      } else {
   2089        // When the dragged element(s) moves past a tab strip item, the dragged
   2090        // element's leading edge starts dragging over empty space, resulting in
   2091        // no overlapping `dropElement`. In these cases, try to fall back to the
   2092        // previous animation drop element index to avoid unstable animations
   2093        // (tab strip items snapping back and forth to shift out of the way of
   2094        // the dragged element(s)).
   2095        newDropElementIndex = oldDropElementIndex;
   2096 
   2097        // We always want to have a `dropElement` so that we can determine where to
   2098        // logically drop the dragged element(s).
   2099        //
   2100        // It's tempting to set `dropElement` to
   2101        // `this.dragAndDropElements.at(oldDropElementIndex)`, and that is correct
   2102        // for most cases, but there are edge cases:
   2103        //
   2104        // 1) the drop element index range needs to be one larger than the number of
   2105        //    items that can move in the tab strip. The simplest example is when all
   2106        //    tabs are ungrouped and unpinned: for 5 tabs, the drop element index needs
   2107        //    to be able to go from 0 (become the first tab) to 5 (become the last tab).
   2108        //    `this.dragAndDropElements.at(5)` would be `undefined` when dragging to the
   2109        //    end of the tab strip. In this specific case, it works to fall back to
   2110        //    setting the drop element to the last tab.
   2111        //
   2112        // 2) the `elementIndex` values of the tab strip items do not change during
   2113        //    the drag operation. When dragging the last tab or multiple tabs at the end
   2114        //    of the tab strip, having `dropElement` fall back to the last tab makes the
   2115        //    drop element one of the moving tabs. This can have some unexpected behavior
   2116        //    if not careful. Falling back to the last tab that's not moving (instead of
   2117        //    just the last tab) helps ensure that `dropElement` is always a stable target
   2118        //    to drop next to.
   2119        //
   2120        // 3) all of the elements in the tab strip are moving, in which case there can't
   2121        //    be a drop element and it should stay `undefined`.
   2122        //
   2123        // 4) we just started dragging and the `oldDropElementIndex` has its default
   2124        //    valuë of `movingTabs[0].elementIndex`. In this case, the drop element
   2125        //    shouldn't be a moving tab, so keep it `undefined`.
   2126        let lastPossibleDropElement = this._rtlMode
   2127          ? tabs.find(t => t != draggedTab)
   2128          : tabs.findLast(t => t != draggedTab);
   2129        let maxElementIndexForDropElement =
   2130          lastPossibleDropElement?.elementIndex;
   2131        if (Number.isInteger(maxElementIndexForDropElement)) {
   2132          let index = Math.min(
   2133            oldDropElementIndex,
   2134            maxElementIndexForDropElement
   2135          );
   2136          let oldDropElementCandidate =
   2137            this._tabbrowserTabs.dragAndDropElements.at(index);
   2138          if (!movingTabsSet.has(oldDropElementCandidate)) {
   2139            dropElement = oldDropElementCandidate;
   2140          }
   2141        }
   2142      }
   2143 
   2144      let moveOverThreshold;
   2145      let overlapPercent;
   2146      let dropBefore;
   2147      if (dropElement) {
   2148        let dropElementForOverlap = elementToMove(dropElement);
   2149 
   2150        let dropElementScreen = dropElementForOverlap[screenAxis];
   2151        let dropElementPos =
   2152          dropElementScreen + getTabShift(dropElement, oldDropElementIndex);
   2153        let dropElementSize = bounds(dropElementForOverlap)[size];
   2154        let firstMovingTabPos = firstMovingTabScreen + translate;
   2155        overlapPercent = greatestOverlap(
   2156          firstMovingTabPos,
   2157          shiftSize,
   2158          dropElementPos,
   2159          dropElementSize
   2160        );
   2161 
   2162        moveOverThreshold = gBrowser._tabGroupsEnabled
   2163          ? Services.prefs.getIntPref(
   2164              "browser.tabs.dragDrop.moveOverThresholdPercent"
   2165            ) / 100
   2166          : 0.5;
   2167        moveOverThreshold = Math.min(1, Math.max(0, moveOverThreshold));
   2168        let shouldMoveOver = overlapPercent > moveOverThreshold;
   2169        if (logicalForward && shouldMoveOver) {
   2170          newDropElementIndex++;
   2171        } else if (!logicalForward && !shouldMoveOver) {
   2172          newDropElementIndex++;
   2173          if (newDropElementIndex > oldDropElementIndex) {
   2174            // FIXME: Not quite sure what's going on here, but this check
   2175            // prevents jittery back-and-forth movement of background tabs
   2176            // in certain cases.
   2177            newDropElementIndex = oldDropElementIndex;
   2178          }
   2179        }
   2180 
   2181        // Recalculate the overlap with the updated drop index for when the
   2182        // drop element moves over.
   2183        dropElementPos =
   2184          dropElementScreen + getTabShift(dropElement, newDropElementIndex);
   2185        overlapPercent = greatestOverlap(
   2186          firstMovingTabPos,
   2187          shiftSize,
   2188          dropElementPos,
   2189          dropElementSize
   2190        );
   2191        dropBefore = firstMovingTabPos < dropElementPos;
   2192        if (this._rtlMode) {
   2193          dropBefore = !dropBefore;
   2194        }
   2195 
   2196        // If dragging a group over another group, don't make it look like it is
   2197        // possible to drop the dragged group inside the other group.
   2198        if (
   2199          isTabGroupLabel(draggedTab) &&
   2200          dropElement?.group &&
   2201          (!dropElement.group.collapsed ||
   2202            (dropElement.group.collapsed && dropElement.group.hasActiveTab))
   2203        ) {
   2204          let overlappedGroup = dropElement.group;
   2205 
   2206          if (isTabGroupLabel(dropElement)) {
   2207            dropBefore = true;
   2208            newDropElementIndex = dropElement.elementIndex;
   2209          } else {
   2210            dropBefore = false;
   2211            let lastVisibleTabInGroup = overlappedGroup.tabs.findLast(
   2212              tab => tab.visible
   2213            );
   2214            newDropElementIndex = lastVisibleTabInGroup.elementIndex + 1;
   2215          }
   2216 
   2217          dropElement = overlappedGroup;
   2218        }
   2219 
   2220        // Constrain drop direction at the boundary between pinned and
   2221        // unpinned tabs so that they don't mix together.
   2222        let isOutOfBounds = isPinned
   2223          ? dropElement.elementIndex >= numPinned
   2224          : dropElement.elementIndex < numPinned;
   2225        if (isOutOfBounds) {
   2226          // Drop after last pinned tab
   2227          dropElement = this._tabbrowserTabs.dragAndDropElements[numPinned - 1];
   2228          dropBefore = false;
   2229        }
   2230      }
   2231 
   2232      if (
   2233        gBrowser._tabGroupsEnabled &&
   2234        isTab(draggedTab) &&
   2235        !isPinned &&
   2236        (!numPinned || newDropElementIndex >= numPinned)
   2237      ) {
   2238        let dragOverGroupingThreshold = 1 - moveOverThreshold;
   2239        let groupingDelay = Services.prefs.getIntPref(
   2240          "browser.tabs.dragDrop.createGroup.delayMS"
   2241        );
   2242 
   2243        // When dragging tab(s) over an ungrouped tab, signal to the user
   2244        // that dropping the tab(s) will create a new tab group.
   2245        let shouldCreateGroupOnDrop =
   2246          !movingTabsSet.has(dropElement) &&
   2247          isTab(dropElement) &&
   2248          !dropElement?.group &&
   2249          overlapPercent > dragOverGroupingThreshold;
   2250 
   2251        // When dragging tab(s) over a collapsed tab group label, signal to the
   2252        // user that dropping the tab(s) will add them to the group.
   2253        let shouldDropIntoCollapsedTabGroup =
   2254          isTabGroupLabel(dropElement) &&
   2255          dropElement.group.collapsed &&
   2256          overlapPercent > dragOverGroupingThreshold;
   2257 
   2258        if (shouldCreateGroupOnDrop) {
   2259          this._dragOverGroupingTimer = setTimeout(() => {
   2260            this._triggerDragOverGrouping(dropElement);
   2261            dragData.shouldCreateGroupOnDrop = true;
   2262            this._setDragOverGroupColor(dragData.tabGroupCreationColor);
   2263          }, groupingDelay);
   2264        } else if (shouldDropIntoCollapsedTabGroup) {
   2265          this._dragOverGroupingTimer = setTimeout(() => {
   2266            this._triggerDragOverGrouping(dropElement);
   2267            dragData.shouldDropIntoCollapsedTabGroup = true;
   2268            this._setDragOverGroupColor(dropElement.group.color);
   2269          }, groupingDelay);
   2270        } else {
   2271          this._tabbrowserTabs.removeAttribute("movingtab-group");
   2272          this._resetGroupTarget(
   2273            document.querySelector("[dragover-groupTarget]")
   2274          );
   2275 
   2276          delete dragData.shouldCreateGroupOnDrop;
   2277          delete dragData.shouldDropIntoCollapsedTabGroup;
   2278 
   2279          // Default to dropping into `dropElement`'s tab group, if it exists.
   2280          let dropElementGroup = dropElement?.group;
   2281          let colorCode = dropElementGroup?.color;
   2282 
   2283          let lastUnmovingTabInGroup = dropElementGroup?.tabs.findLast(
   2284            t => !movingTabsSet.has(t)
   2285          );
   2286          if (
   2287            isTab(dropElement) &&
   2288            dropElementGroup &&
   2289            dropElement == lastUnmovingTabInGroup &&
   2290            !dropBefore &&
   2291            overlapPercent < dragOverGroupingThreshold
   2292          ) {
   2293            // Dragging tab over the last tab of a tab group, but not enough
   2294            // for it to drop into the tab group. Drop it after the tab group instead.
   2295            dropElement = dropElementGroup;
   2296            colorCode = undefined;
   2297          } else if (isTabGroupLabel(dropElement)) {
   2298            if (dropBefore) {
   2299              // Dropping right before the tab group.
   2300              dropElement = dropElementGroup;
   2301              colorCode = undefined;
   2302            } else if (dropElementGroup.collapsed) {
   2303              // Dropping right after the collapsed tab group.
   2304              dropElement = dropElementGroup;
   2305              colorCode = undefined;
   2306            } else {
   2307              // Dropping right before the first tab in the tab group.
   2308              dropElement = dropElementGroup.tabs[0];
   2309              dropBefore = true;
   2310            }
   2311          }
   2312          this._setDragOverGroupColor(colorCode);
   2313          this._tabbrowserTabs.toggleAttribute(
   2314            "movingtab-addToGroup",
   2315            colorCode
   2316          );
   2317          this._tabbrowserTabs.toggleAttribute("movingtab-ungroup", !colorCode);
   2318        }
   2319      }
   2320 
   2321      if (
   2322        newDropElementIndex == oldDropElementIndex &&
   2323        dropBefore == dragData.dropBefore &&
   2324        dropElement == dragData.dropElement
   2325      ) {
   2326        return;
   2327      }
   2328 
   2329      dragData.dropElement = dropElement;
   2330      dragData.dropBefore = dropBefore;
   2331      dragData.animDropElementIndex = newDropElementIndex;
   2332 
   2333      // Shift background tabs to leave a gap where the dragged tab
   2334      // would currently be dropped.
   2335      for (let item of tabs) {
   2336        if (item == draggedTab) {
   2337          continue;
   2338        }
   2339 
   2340        let shift = getTabShift(item, newDropElementIndex);
   2341        let transform = shift ? `${translateAxis}(${shift}px)` : "";
   2342        item = elementToMove(item);
   2343        item.style.transform = transform;
   2344      }
   2345    }
   2346 
   2347    _checkWithinPinnedContainerBounds({
   2348      firstMovingTabScreen,
   2349      lastMovingTabScreen,
   2350      pinnedTabsStartEdge,
   2351      pinnedTabsEndEdge,
   2352      translate,
   2353      draggedTab,
   2354    }) {
   2355      // Display the pinned drop indicator based on the position of the moving tabs.
   2356      // If the indicator is not yet shown, display once we are within a pinned tab width/height
   2357      // distance.
   2358      let firstMovingTabPosition = firstMovingTabScreen + translate;
   2359      let lastMovingTabPosition = lastMovingTabScreen + translate;
   2360      // Approximation of half pinned tabs width and height in horizontal or grid mode (40) is a sufficient
   2361      // buffer to display the pinned drop indicator slightly before dragging over it. Exact value is
   2362      // not necessary.
   2363      let buffer = 20;
   2364      let inPinnedRange = this._rtlMode
   2365        ? lastMovingTabPosition >= pinnedTabsStartEdge
   2366        : firstMovingTabPosition <= pinnedTabsEndEdge;
   2367      let inVisibleRange = this._rtlMode
   2368        ? lastMovingTabPosition >= pinnedTabsStartEdge - buffer
   2369        : firstMovingTabPosition <= pinnedTabsEndEdge + buffer;
   2370      let isVisible = this._pinnedDropIndicator.hasAttribute("visible");
   2371      let isInteractive = this._pinnedDropIndicator.hasAttribute("interactive");
   2372 
   2373      if (
   2374        this.#pinnedDropIndicatorTimeout &&
   2375        !inPinnedRange &&
   2376        !inVisibleRange &&
   2377        !isVisible &&
   2378        !isInteractive
   2379      ) {
   2380        this.#resetPinnedDropIndicator();
   2381      } else if (
   2382        isTab(draggedTab) &&
   2383        ((inVisibleRange && !isVisible) || (inPinnedRange && !isInteractive))
   2384      ) {
   2385        // On drag into pinned container
   2386        let tabbrowserTabsRect = window.windowUtils.getBoundsWithoutFlushing(
   2387          this._tabbrowserTabs
   2388        );
   2389        if (!this._tabbrowserTabs.verticalMode) {
   2390          // The tabbrowser container expands with the expansion of the
   2391          // drop indicator - prevent that by setting maxWidth first.
   2392          this._tabbrowserTabs.style.maxWidth = tabbrowserTabsRect.width + "px";
   2393        }
   2394        if (isVisible) {
   2395          this._pinnedDropIndicator.setAttribute("interactive", "");
   2396        } else if (!this.#pinnedDropIndicatorTimeout) {
   2397          let interactionDelay = Services.prefs.getIntPref(
   2398            "browser.tabs.dragDrop.pinInteractionCue.delayMS"
   2399          );
   2400          this.#pinnedDropIndicatorTimeout = setTimeout(() => {
   2401            if (this.#isMovingTab()) {
   2402              this._pinnedDropIndicator.setAttribute("visible", "");
   2403              this._pinnedDropIndicator.setAttribute("interactive", "");
   2404            }
   2405          }, interactionDelay);
   2406        }
   2407      } else if (!inPinnedRange) {
   2408        this._pinnedDropIndicator.removeAttribute("interactive");
   2409      }
   2410    }
   2411 
   2412    #clearPinnedDropIndicatorTimer() {
   2413      if (this.#pinnedDropIndicatorTimeout) {
   2414        clearTimeout(this.#pinnedDropIndicatorTimeout);
   2415        this.#pinnedDropIndicatorTimeout = null;
   2416      }
   2417    }
   2418 
   2419    #resetPinnedDropIndicator() {
   2420      this.#clearPinnedDropIndicatorTimer();
   2421      this._pinnedDropIndicator.removeAttribute("visible");
   2422      this._pinnedDropIndicator.removeAttribute("interactive");
   2423    }
   2424 
   2425    finishAnimateTabMove() {
   2426      if (!this.#isMovingTab()) {
   2427        return;
   2428      }
   2429 
   2430      this.#setMovingTabMode(false);
   2431 
   2432      for (let item of this._tabbrowserTabs.dragAndDropElements) {
   2433        this._resetGroupTarget(item);
   2434        item = elementToMove(item);
   2435        item.style.transform = "";
   2436      }
   2437      this._tabbrowserTabs.removeAttribute("movingtab-group");
   2438      this._tabbrowserTabs.removeAttribute("movingtab-ungroup");
   2439      this._tabbrowserTabs.removeAttribute("movingtab-addToGroup");
   2440      this._setDragOverGroupColor(null);
   2441      this._clearDragOverGroupingTimer();
   2442      this.#resetPinnedDropIndicator();
   2443    }
   2444 
   2445    // Drop
   2446 
   2447    // If the tab is dropped in another window, we need to pass in the original window document
   2448    _resetTabsAfterDrop(draggedTabDocument = document) {
   2449      if (this._tabbrowserTabs.expandOnHover) {
   2450        // Re-enable MousePosTracker after dropping
   2451        MousePosTracker.addListener(document.defaultView.SidebarController);
   2452      }
   2453 
   2454      let pinnedDropIndicator = draggedTabDocument.getElementById(
   2455        "pinned-drop-indicator"
   2456      );
   2457      pinnedDropIndicator.removeAttribute("visible");
   2458      pinnedDropIndicator.removeAttribute("interactive");
   2459      draggedTabDocument.ownerGlobal.gBrowser.tabContainer.style.maxWidth = "";
   2460      let allTabs = draggedTabDocument.getElementsByClassName("tabbrowser-tab");
   2461      for (let tab of allTabs) {
   2462        tab.style.width = "";
   2463        tab.style.left = "";
   2464        tab.style.top = "";
   2465        tab.style.maxWidth = "";
   2466        tab.removeAttribute("dragtarget");
   2467      }
   2468      for (let label of draggedTabDocument.getElementsByClassName(
   2469        "tab-group-label-container"
   2470      )) {
   2471        label.style.width = "";
   2472        label.style.height = "";
   2473        label.style.left = "";
   2474        label.style.top = "";
   2475        label.style.maxWidth = "";
   2476        label.removeAttribute("dragtarget");
   2477      }
   2478      let periphery = draggedTabDocument.getElementById(
   2479        "tabbrowser-arrowscrollbox-periphery"
   2480      );
   2481      periphery.style.marginBlockStart = "";
   2482      periphery.style.marginInlineStart = "";
   2483      periphery.style.left = "";
   2484      periphery.style.top = "";
   2485      let pinnedTabsContainer = draggedTabDocument.getElementById(
   2486        "pinned-tabs-container"
   2487      );
   2488      let pinnedPeriphery = draggedTabDocument.getElementById(
   2489        "pinned-tabs-container-periphery"
   2490      );
   2491      pinnedPeriphery && pinnedTabsContainer.removeChild(pinnedPeriphery);
   2492      pinnedTabsContainer.removeAttribute("dragActive");
   2493      pinnedTabsContainer.style.minHeight = "";
   2494      draggedTabDocument.defaultView.SidebarController.updatePinnedTabsHeightOnResize();
   2495      pinnedTabsContainer.scrollbox.style.height = "";
   2496      pinnedTabsContainer.scrollbox.style.width = "";
   2497      let arrowScrollbox = draggedTabDocument.getElementById(
   2498        "tabbrowser-arrowscrollbox"
   2499      );
   2500      arrowScrollbox.scrollbox.style.height = "";
   2501      arrowScrollbox.scrollbox.style.width = "";
   2502      for (let groupLabel of draggedTabDocument.getElementsByClassName(
   2503        "tab-group-label-container"
   2504      )) {
   2505        groupLabel.style.left = "";
   2506        groupLabel.style.top = "";
   2507      }
   2508    }
   2509 
   2510    /**
   2511     * @param {DragEvent} event
   2512     * @returns {typeof DataTransfer.prototype.dropEffect}
   2513     */
   2514    getDropEffectForTabDrag(event) {
   2515      var dt = event.dataTransfer;
   2516 
   2517      let isMovingTab = dt.mozItemCount > 0;
   2518      for (let i = 0; i < dt.mozItemCount; i++) {
   2519        // tabs are always added as the first type
   2520        let types = dt.mozTypesAt(0);
   2521        if (types[0] != TAB_DROP_TYPE) {
   2522          isMovingTab = false;
   2523          break;
   2524        }
   2525      }
   2526 
   2527      if (isMovingTab) {
   2528        let sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
   2529        if (
   2530          (isTab(sourceNode) ||
   2531            isTabGroupLabel(sourceNode) ||
   2532            isSplitViewWrapper(sourceNode)) &&
   2533          sourceNode.ownerGlobal.isChromeWindow &&
   2534          sourceNode.ownerDocument.documentElement.getAttribute("windowtype") ==
   2535            "navigator:browser"
   2536        ) {
   2537          // Do not allow transfering a private tab to a non-private window
   2538          // and vice versa.
   2539          if (
   2540            PrivateBrowsingUtils.isWindowPrivate(window) !=
   2541            PrivateBrowsingUtils.isWindowPrivate(sourceNode.ownerGlobal)
   2542          ) {
   2543            return "none";
   2544          }
   2545 
   2546          if (
   2547            window.gMultiProcessBrowser !=
   2548            sourceNode.ownerGlobal.gMultiProcessBrowser
   2549          ) {
   2550            return "none";
   2551          }
   2552 
   2553          if (
   2554            window.gFissionBrowser != sourceNode.ownerGlobal.gFissionBrowser
   2555          ) {
   2556            return "none";
   2557          }
   2558 
   2559          return dt.dropEffect == "copy" ? "copy" : "move";
   2560        }
   2561      }
   2562 
   2563      if (Services.droppedLinkHandler.canDropLink(event, true)) {
   2564        return "link";
   2565      }
   2566      return "none";
   2567    }
   2568  };
   2569 }