tor-browser

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

TabsList.sys.mjs (25896B)


      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 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  PanelMultiView:
      9    "moz-src:///browser/components/customizableui/PanelMultiView.sys.mjs",
     10  TabMetrics: "moz-src:///browser/components/tabbrowser/TabMetrics.sys.mjs",
     11 });
     12 
     13 const TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
     14 
     15 const ROW_VARIANT_TAB = "tab";
     16 const ROW_VARIANT_TAB_GROUP = "tab-group";
     17 
     18 function setAttributes(element, attrs) {
     19  for (let [name, value] of Object.entries(attrs)) {
     20    if (value) {
     21      element.setAttribute(name, value);
     22    } else {
     23      element.removeAttribute(name);
     24    }
     25  }
     26 }
     27 
     28 /**
     29 * @param {Element} element
     30 *   One row (`toolbaritem`) of this tab list or one of its descendent
     31 *   elements, e.g. a `toolbarbutton`.
     32 * @returns {MozTabbrowserTab|undefined}
     33 */
     34 function getTabFromRow(element) {
     35  return element.closest("toolbaritem")?._tab;
     36 }
     37 
     38 /**
     39 * @param {Element} element
     40 *   One row (`toolbaritem`) of this tab list or one of its descendent
     41 *   elements, e.g. a `toolbarbutton`.
     42 * @returns {MozTabbrowserTabGroup|undefined}
     43 */
     44 function getTabGroupFromRow(element) {
     45  return element.closest("toolbaritem")?._tabGroup;
     46 }
     47 
     48 /**
     49 * @param {Element} element
     50 *   One row (`toolbaritem`) of this tab list or one of its descendent
     51 *   elements, e.g. a `toolbarbutton`.
     52 * @returns {"tab"|"tab-group"|undefined}
     53 */
     54 function getRowVariant(element) {
     55  return element.closest("toolbaritem")?.getAttribute("row-variant");
     56 }
     57 
     58 class TabsListBase {
     59  /** @returns {Promise<void>} */
     60  get domRefreshComplete() {
     61    return this.#domRefreshPromise ?? Promise.resolve();
     62  }
     63 
     64  /** @type {Promise<void>|undefined} */
     65  #domRefreshPromise;
     66 
     67  /** @type {Map<MozTabbrowserTab, XulToolbarItem>} */
     68  tabToElement = new Map();
     69 
     70  /**
     71   * @param {object} opts
     72   * @param {string} opts.className
     73   * @param {function(MozTabbrowserTab):boolean} opts.filterFn
     74   * @param {Element} opts.containerNode
     75   * @param {Element} [opts.dropIndicator=null]
     76   * @param {boolean} opts.onlyHiddenTabs
     77   */
     78  constructor({
     79    className,
     80    filterFn,
     81    containerNode,
     82    dropIndicator = null,
     83    onlyHiddenTabs,
     84  }) {
     85    /** @type {string} */
     86    this.className = className;
     87    /** @type {function(MozTabbrowserTab):boolean} */
     88    this.filterFn = onlyHiddenTabs
     89      ? tab => filterFn(tab) && tab.hidden
     90      : filterFn;
     91    /** @type {Element} */
     92    this.containerNode = containerNode;
     93    /** @type {Element|null} */
     94    this.dropIndicator = dropIndicator;
     95 
     96    if (this.dropIndicator) {
     97      /** @type {XulToolbarItem|null} */
     98      this.dropTargetRow = null;
     99      /** @type {-1|0} */
    100      this.dropTargetDirection = 0;
    101    }
    102 
    103    /** @type {Document} */
    104    this.doc = containerNode.ownerDocument;
    105    /** @type {Tabbrowser} */
    106    this.gBrowser = this.doc.defaultView.gBrowser;
    107    /** @type {boolean} */
    108    this.listenersRegistered = false;
    109    /** @type {boolean} */
    110    this.onlyHiddenTabs = onlyHiddenTabs;
    111  }
    112 
    113  /** @returns {MapIterator<XulToolbarItem>} */
    114  get rows() {
    115    return this.tabToElement.values();
    116  }
    117 
    118  handleEvent(event) {
    119    switch (event.type) {
    120      case "TabAttrModified":
    121        this._tabAttrModified(event.target);
    122        break;
    123      case "TabClose":
    124        this._tabClose(event.target);
    125        break;
    126      case "TabGroupCollapse":
    127      case "TabGroupExpand":
    128      case "TabGroupCreate":
    129      case "TabGroupRemoved":
    130      case "TabGrouped":
    131      case "TabGroupMoved":
    132      case "TabUngrouped":
    133        this._refreshDOM();
    134        break;
    135      case "TabMove":
    136        this._moveTab(event.target);
    137        break;
    138      case "TabPinned":
    139        if (!this.filterFn(event.target)) {
    140          this._tabClose(event.target);
    141        }
    142        break;
    143      case "command":
    144        this.#handleCommand(event);
    145        break;
    146      case "dragstart":
    147        this._onDragStart(event);
    148        break;
    149      case "dragover":
    150        this._onDragOver(event);
    151        break;
    152      case "dragleave":
    153        this._onDragLeave(event);
    154        break;
    155      case "dragend":
    156        this._onDragEnd(event);
    157        break;
    158      case "drop":
    159        this._onDrop(event);
    160        break;
    161      case "click":
    162        this._onClick(event);
    163        break;
    164    }
    165  }
    166 
    167  /**
    168   * @param {XULCommandEvent} event
    169   */
    170  #handleCommand(event) {
    171    if (event.target.classList.contains("all-tabs-mute-button")) {
    172      getTabFromRow(event.target)?.toggleMuteAudio();
    173    } else if (event.target.classList.contains("all-tabs-close-button")) {
    174      const tab = getTabFromRow(event.target);
    175      if (tab) {
    176        this.gBrowser.removeTab(
    177          tab,
    178          lazy.TabMetrics.userTriggeredContext(
    179            lazy.TabMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU
    180          )
    181        );
    182      }
    183    } else {
    184      const rowVariant = getRowVariant(event.target);
    185      if (rowVariant == ROW_VARIANT_TAB) {
    186        const tab = getTabFromRow(event.target);
    187        if (tab) {
    188          this._selectTab(tab);
    189        }
    190      } else if (rowVariant == ROW_VARIANT_TAB_GROUP) {
    191        getTabGroupFromRow(event.target)?.select();
    192      }
    193    }
    194  }
    195 
    196  _selectTab(tab) {
    197    if (this.gBrowser.selectedTab != tab) {
    198      this.gBrowser.selectedTab = tab;
    199    } else {
    200      this.gBrowser.tabContainer._handleTabSelect();
    201    }
    202  }
    203 
    204  /*
    205   * Populate the popup with menuitems and setup the listeners.
    206   */
    207  _populate() {
    208    this._populateDOM();
    209    this._setupListeners();
    210  }
    211 
    212  _populateDOM() {
    213    let fragment = this.doc.createDocumentFragment();
    214    let currentGroupId;
    215 
    216    for (let tab of this.gBrowser.tabs) {
    217      if (this.filterFn(tab)) {
    218        if (tab.group && tab.group.id != currentGroupId) {
    219          fragment.appendChild(this._createGroupRow(tab.group));
    220          currentGroupId = tab.group.id;
    221        }
    222 
    223        let tabHiddenByGroup = tab.group?.collapsed && !tab.selected;
    224        if (!tabHiddenByGroup || this.onlyHiddenTabs) {
    225          // Don't show tabs in collapsed tab groups in the main tabs list.
    226          // However, in the hidden tabs lists, do show hidden tabs even if
    227          // they belong to collapsed tab groups.
    228          fragment.appendChild(this._createRow(tab));
    229        }
    230      }
    231    }
    232 
    233    this._addElement(fragment);
    234  }
    235 
    236  _addElement(elementOrFragment) {
    237    this.containerNode.appendChild(elementOrFragment);
    238  }
    239 
    240  /*
    241   * Remove the menuitems from the DOM, cleanup internal state and listeners.
    242   */
    243  _cleanup() {
    244    this._cleanupDOM();
    245    this._cleanupListeners();
    246    this._clearDropTarget();
    247  }
    248 
    249  _cleanupDOM() {
    250    this.containerNode
    251      .querySelectorAll(":scope toolbaritem")
    252      .forEach(node => node.remove());
    253    this.tabToElement = new Map();
    254  }
    255 
    256  _refreshDOM() {
    257    if (!this.#domRefreshPromise) {
    258      this.#domRefreshPromise = new Promise(resolve => {
    259        this.containerNode.ownerGlobal.requestAnimationFrame(() => {
    260          if (this.#domRefreshPromise) {
    261            if (this.listenersRegistered) {
    262              // Only re-render the menu DOM if the menu is still open.
    263              this._cleanupDOM();
    264              this._populateDOM();
    265            }
    266            resolve();
    267            this.#domRefreshPromise = undefined;
    268          }
    269        });
    270      });
    271    }
    272  }
    273 
    274  _setupListeners() {
    275    this.listenersRegistered = true;
    276 
    277    this.gBrowser.tabContainer.addEventListener("TabAttrModified", this);
    278    this.gBrowser.tabContainer.addEventListener("TabClose", this);
    279    this.gBrowser.tabContainer.addEventListener("TabMove", this);
    280    this.gBrowser.tabContainer.addEventListener("TabPinned", this);
    281    this.gBrowser.tabContainer.addEventListener("TabGroupCollapse", this);
    282    this.gBrowser.tabContainer.addEventListener("TabGroupExpand", this);
    283    this.gBrowser.tabContainer.addEventListener("TabGroupCreate", this);
    284    this.gBrowser.tabContainer.addEventListener("TabGroupRemoved", this);
    285    this.gBrowser.tabContainer.addEventListener("TabGroupMoved", this);
    286    this.gBrowser.tabContainer.addEventListener("TabGrouped", this);
    287    this.gBrowser.tabContainer.addEventListener("TabUngrouped", this);
    288 
    289    this.containerNode.addEventListener("click", this);
    290    this.containerNode.addEventListener("command", this);
    291 
    292    if (this.dropIndicator) {
    293      this.containerNode.addEventListener("dragstart", this);
    294      this.containerNode.addEventListener("dragover", this);
    295      this.containerNode.addEventListener("dragleave", this);
    296      this.containerNode.addEventListener("dragend", this);
    297      this.containerNode.addEventListener("drop", this);
    298    }
    299  }
    300 
    301  _cleanupListeners() {
    302    this.gBrowser.tabContainer.removeEventListener("TabAttrModified", this);
    303    this.gBrowser.tabContainer.removeEventListener("TabClose", this);
    304    this.gBrowser.tabContainer.removeEventListener("TabMove", this);
    305    this.gBrowser.tabContainer.removeEventListener("TabPinned", this);
    306    this.gBrowser.tabContainer.removeEventListener("TabGroupCollapse", this);
    307    this.gBrowser.tabContainer.removeEventListener("TabGroupExpand", this);
    308    this.gBrowser.tabContainer.removeEventListener("TabGroupCreate", this);
    309    this.gBrowser.tabContainer.removeEventListener("TabGroupRemoved", this);
    310    this.gBrowser.tabContainer.removeEventListener("TabGroupMoved", this);
    311    this.gBrowser.tabContainer.removeEventListener("TabGrouped", this);
    312    this.gBrowser.tabContainer.removeEventListener("TabUngrouped", this);
    313 
    314    this.containerNode.removeEventListener("click", this);
    315    this.containerNode.removeEventListener("command", this);
    316 
    317    if (this.dropIndicator) {
    318      this.containerNode.removeEventListener("dragstart", this);
    319      this.containerNode.removeEventListener("dragover", this);
    320      this.containerNode.removeEventListener("dragleave", this);
    321      this.containerNode.removeEventListener("dragend", this);
    322      this.containerNode.removeEventListener("drop", this);
    323    }
    324 
    325    this.listenersRegistered = false;
    326  }
    327 
    328  /**
    329   * @param {MozTabbrowserTab} tab
    330   */
    331  _tabAttrModified(tab) {
    332    let item = this.tabToElement.get(tab);
    333    if (item) {
    334      if (!this.filterFn(tab)) {
    335        // The tab no longer matches our criteria, remove it.
    336        this._removeItem(item, tab);
    337      } else {
    338        this._setRowAttributes(item, tab);
    339      }
    340    } else if (this.filterFn(tab)) {
    341      // The tab now matches our criteria, add a row for it.
    342      this._addTab(tab);
    343    }
    344  }
    345 
    346  /**
    347   * @param {MozTabbrowserTab} tab
    348   */
    349  _moveTab(tab) {
    350    let item = this.tabToElement.get(tab);
    351    if (item) {
    352      this._removeItem(item, tab);
    353      this._addTab(tab);
    354    }
    355  }
    356 
    357  /**
    358   * @param {MozTabbrowserTab} tab
    359   */
    360  _addTab(newTab) {
    361    if (!this.filterFn(newTab)) {
    362      return;
    363    }
    364    if (newTab.group?.collapsed && !this.onlyHiddenTabs) {
    365      return;
    366    }
    367 
    368    let newRow = this._createRow(newTab);
    369    let nextTab = this.gBrowser.tabContainer.findNextTab(newTab, {
    370      filter: this.filterFn,
    371    });
    372    if (!nextTab) {
    373      // If there's no next tab then append the new row to the end of the menu.
    374      this._addElement(newRow);
    375    } else if (!newTab.group && nextTab.group) {
    376      // newTab should not go right before nextTab because then it would
    377      // appear to be inside the tab group; instead, put newTab before
    378      // nextTab's tab group's row menu item.
    379      // Should be equivalent to `.insertBefore(newRow, nextRow.previousSiblingElement)`
    380      // but this is more explicit about inserting before the nextTab's tab group's
    381      // row menu item.
    382      let nextTabTabGroupRow = this.containerNode.querySelector(
    383        `:scope [tab-group-id="${nextTab.group.id}"]`
    384      );
    385      this.containerNode.insertBefore(newRow, nextTabTabGroupRow);
    386    } else {
    387      let nextRow = this.tabToElement.get(nextTab);
    388      if (!nextRow) {
    389        // If for some reason the next tab has no item in this menu already,
    390        // just add this new tab's menu item to the end.
    391        this._addElement(newRow);
    392      } else {
    393        this.containerNode.insertBefore(newRow, nextRow);
    394      }
    395    }
    396  }
    397 
    398  _tabClose(tab) {
    399    let item = this.tabToElement.get(tab);
    400    if (item) {
    401      this._removeItem(item, tab);
    402    }
    403  }
    404 
    405  _removeItem(item, tab) {
    406    this.tabToElement.delete(tab);
    407    item.remove();
    408    // If removing this grouped tab results in there being no more tabs from
    409    // this tab group in the menu list, then also remove the tab group label
    410    // menu item. This is only relevant right now in tabs lists that only show
    411    // hidden tabs. For the normal tabs list, removing the last tab in a group
    412    // will also remove the tab group, which re-renders the whole tabs list
    413    // with the side-effect of removing the tab group label menu item.
    414    if (
    415      tab.group &&
    416      !this.tabToElement.keys().some(t => t.group == tab.group)
    417    ) {
    418      this.containerNode
    419        .querySelector(`:scope [tab-group-id="${tab.group.id}"]`)
    420        ?.remove();
    421    }
    422  }
    423 }
    424 
    425 const TABS_PANEL_EVENTS = {
    426  show: "ViewShowing",
    427  hide: "PanelMultiViewHidden",
    428 };
    429 
    430 export class TabsPanel extends TabsListBase {
    431  /**
    432   * @param {object} opts
    433   * @param {string} opts.className
    434   * @param {function(MozTabbrowserTab):boolean} opts.filterFn
    435   * @param {Element} opts.containerNode
    436   * @param {Element} [opts.dropIndicator=null]
    437   * @param {Element} opts.view
    438   * @param {boolean} opts.onlyHiddenTabs
    439   */
    440  constructor(opts) {
    441    super({
    442      ...opts,
    443      containerNode: opts.containerNode || opts.view.firstElementChild,
    444    });
    445    this.view = opts.view;
    446    this.view.addEventListener(TABS_PANEL_EVENTS.show, this);
    447    this.panelMultiView = null;
    448  }
    449 
    450  handleEvent(event) {
    451    switch (event.type) {
    452      case TABS_PANEL_EVENTS.hide:
    453        if (event.target == this.panelMultiView) {
    454          this._cleanup();
    455          this.panelMultiView = null;
    456        }
    457        break;
    458      case TABS_PANEL_EVENTS.show:
    459        if (!this.listenersRegistered && event.target == this.view) {
    460          this.panelMultiView = this.view.panelMultiView;
    461          this._populate(event);
    462          this.gBrowser.translateTabContextMenu();
    463        }
    464        break;
    465      default:
    466        super.handleEvent(event);
    467        break;
    468    }
    469  }
    470 
    471  _populate(event) {
    472    super._populate(event);
    473 
    474    // The loading throbber can't be set until the toolbarbutton is rendered,
    475    // so set the image attributes again now that the elements are in the DOM.
    476    for (let row of this.rows) {
    477      // Ensure this isn't a group label
    478      if (getRowVariant(row) == ROW_VARIANT_TAB) {
    479        this._setImageAttributes(row, getTabFromRow(row));
    480      }
    481    }
    482  }
    483 
    484  _selectTab(tab) {
    485    super._selectTab(tab);
    486    lazy.PanelMultiView.hidePopup(this.view.closest("panel"));
    487  }
    488 
    489  _setupListeners() {
    490    super._setupListeners();
    491    this.panelMultiView.addEventListener(TABS_PANEL_EVENTS.hide, this);
    492  }
    493 
    494  _cleanupListeners() {
    495    super._cleanupListeners();
    496    this.panelMultiView.removeEventListener(TABS_PANEL_EVENTS.hide, this);
    497  }
    498 
    499  /**
    500   * @param {MozTabbrowserTab} tab
    501   * @returns {XULElement}
    502   */
    503  _createRow(tab) {
    504    let { doc } = this;
    505    let row = doc.createXULElement("toolbaritem");
    506    row.setAttribute("class", "all-tabs-item");
    507    if (this.className) {
    508      row.classList.add(this.className);
    509    }
    510    row.setAttribute("context", "tabContextMenu");
    511    row.setAttribute("row-variant", ROW_VARIANT_TAB);
    512 
    513    /**
    514     * Setting a new property `XulToolbarItem._tab` on the row elements
    515     * for internal use by this module only.
    516     *
    517     * @see getTabFromRow
    518     */
    519    row._tab = tab;
    520    this.tabToElement.set(tab, row);
    521 
    522    let button = doc.createXULElement("toolbarbutton");
    523    button.setAttribute(
    524      "class",
    525      "all-tabs-button subviewbutton subviewbutton-iconic"
    526    );
    527    button.setAttribute("flex", "1");
    528    button.setAttribute("crop", "end");
    529 
    530    /**
    531     * Setting a new property `MozToolbarbutton.tab` on the buttons
    532     * to support tab context menu integration.
    533     *
    534     * @see TabContextMenu.updateContextMenu
    535     */
    536    button.tab = tab;
    537 
    538    if (tab.userContextId) {
    539      tab.classList.forEach(property => {
    540        if (property.startsWith("identity-color")) {
    541          button.classList.add(property);
    542          button.classList.add("all-tabs-container-indicator");
    543        }
    544      });
    545    }
    546 
    547    if (tab.group) {
    548      row.classList.add("grouped");
    549    }
    550 
    551    row.appendChild(button);
    552 
    553    let muteButton = doc.createXULElement("toolbarbutton");
    554    muteButton.classList.add(
    555      "all-tabs-mute-button",
    556      "all-tabs-secondary-button",
    557      "subviewbutton"
    558    );
    559    muteButton.setAttribute("closemenu", "none");
    560    row.appendChild(muteButton);
    561 
    562    if (!tab.pinned) {
    563      let closeButton = doc.createXULElement("toolbarbutton");
    564      closeButton.classList.add(
    565        "all-tabs-close-button",
    566        "all-tabs-secondary-button",
    567        "subviewbutton"
    568      );
    569      closeButton.setAttribute("closemenu", "none");
    570      doc.l10n.setAttributes(closeButton, "tabbrowser-manager-close-tab");
    571      row.appendChild(closeButton);
    572    }
    573 
    574    this._setRowAttributes(row, tab);
    575 
    576    return row;
    577  }
    578 
    579  /**
    580   * @param {MozTabbrowserTabGroup} group
    581   * @returns {XULElement}
    582   */
    583  _createGroupRow(group) {
    584    let { doc } = this;
    585    let row = doc.createXULElement("toolbaritem");
    586    row.setAttribute("class", "all-tabs-item all-tabs-group-item");
    587    row.setAttribute("row-variant", ROW_VARIANT_TAB_GROUP);
    588    row.setAttribute("tab-group-id", group.id);
    589    /**
    590     * Setting a new property `XulToolbarItem._tabGroup` on the row elements
    591     * for internal use by this module only.
    592     *
    593     * @see getTabGroupFromRow
    594     */
    595    row._tabGroup = group;
    596 
    597    row.style.setProperty(
    598      "--tab-group-color",
    599      `var(--tab-group-color-${group.color})`
    600    );
    601    row.style.setProperty(
    602      "--tab-group-color-invert",
    603      `var(--tab-group-color-${group.color}-invert)`
    604    );
    605    row.style.setProperty(
    606      "--tab-group-color-pale",
    607      `var(--tab-group-color-${group.color}-pale)`
    608    );
    609 
    610    let button = doc.createXULElement("toolbarbutton");
    611    button.setAttribute("context", "open-tab-group-context-menu");
    612    button.classList.add(
    613      "all-tabs-button",
    614      "all-tabs-group-button",
    615      "subviewbutton",
    616      "subviewbutton-iconic",
    617      "tab-group-icon"
    618    );
    619    if (group.collapsed) {
    620      button.classList.add("tab-group-icon-collapsed");
    621    }
    622    button.setAttribute("flex", "1");
    623    button.setAttribute("crop", "end");
    624 
    625    let setName = tabGroupName => {
    626      doc.l10n.setAttributes(
    627        button,
    628        "tabbrowser-manager-current-window-tab-group",
    629        { tabGroupName }
    630      );
    631    };
    632 
    633    if (group.label) {
    634      setName(group.label);
    635    } else {
    636      doc.l10n
    637        .formatValues([{ id: "tab-group-name-default" }])
    638        .then(([msg]) => {
    639          setName(msg);
    640        });
    641    }
    642    row.appendChild(button);
    643    return row;
    644  }
    645 
    646  /**
    647   * @param {XulToolbarItem} row
    648   * @param {MozTabbrowserTab} tab
    649   */
    650  _setRowAttributes(row, tab) {
    651    setAttributes(row, { selected: tab.selected });
    652 
    653    let tooltiptext = this.gBrowser.getTabTooltip(tab);
    654    let busy = tab.getAttribute("busy");
    655    let button = row.firstElementChild;
    656    setAttributes(button, {
    657      busy,
    658      label: tab.label,
    659      tooltiptext,
    660      image: !busy && tab.getAttribute("image"),
    661    });
    662 
    663    this._setImageAttributes(row, tab);
    664 
    665    let muteButton = row.querySelector(".all-tabs-mute-button");
    666    let muteButtonTooltipString = tab.muted
    667      ? "tabbrowser-manager-unmute-tab"
    668      : "tabbrowser-manager-mute-tab";
    669    this.doc.l10n.setAttributes(muteButton, muteButtonTooltipString);
    670 
    671    setAttributes(muteButton, {
    672      muted: tab.muted,
    673      soundplaying: tab.soundPlaying,
    674      hidden: !(tab.muted || tab.soundPlaying),
    675    });
    676  }
    677 
    678  /**
    679   * @param {XulToolbarItem} row
    680   * @param {MozTabbrowserTab} tab
    681   */
    682  _setImageAttributes(row, tab) {
    683    let button = row.firstElementChild;
    684    let image = button.icon;
    685 
    686    if (image) {
    687      let busy = tab.getAttribute("busy");
    688      let progress = tab.getAttribute("progress");
    689      setAttributes(image, { busy, progress });
    690      if (busy) {
    691        image.classList.add("tab-throbber-tabslist");
    692      } else {
    693        image.classList.remove("tab-throbber-tabslist");
    694      }
    695    }
    696  }
    697 
    698  /**
    699   * @param {DragEvent} event
    700   */
    701  _onDragStart(event) {
    702    const row = this._getTargetRowFromEvent(event);
    703    if (!row) {
    704      return;
    705    }
    706 
    707    const elementToDrag =
    708      getRowVariant(row) == ROW_VARIANT_TAB_GROUP
    709        ? getTabGroupFromRow(row).labelElement
    710        : getTabFromRow(row);
    711 
    712    this.gBrowser.tabContainer.tabDragAndDrop.startTabDrag(
    713      event,
    714      elementToDrag,
    715      {
    716        fromTabList: true,
    717      }
    718    );
    719  }
    720 
    721  /**
    722   * @param {DragEvent} event
    723   * @returns {XulToolbarItem|undefined}
    724   */
    725  _getTargetRowFromEvent(event) {
    726    return event.target.closest("toolbaritem");
    727  }
    728 
    729  /**
    730   * @param {DragEvent} event
    731   * @returns {boolean}
    732   */
    733  _isMovingTabs(event) {
    734    var effects =
    735      this.gBrowser.tabContainer.tabDragAndDrop.getDropEffectForTabDrag(event);
    736    return effects == "move";
    737  }
    738 
    739  /**
    740   * @param {DragEvent} event
    741   */
    742  _onDragOver(event) {
    743    if (!this._isMovingTabs(event)) {
    744      return;
    745    }
    746 
    747    if (!this._updateDropTarget(event)) {
    748      return;
    749    }
    750 
    751    event.preventDefault();
    752    event.stopPropagation();
    753  }
    754 
    755  /**
    756   * @param {XulToolbarItem} row
    757   * @returns {number}
    758   */
    759  _getRowIndex(row) {
    760    return Array.prototype.indexOf.call(this.containerNode.children, row);
    761  }
    762 
    763  /**
    764   * @param {DragEvent} event
    765   */
    766  _onDrop(event) {
    767    if (!this._isMovingTabs(event)) {
    768      return;
    769    }
    770 
    771    if (!this._updateDropTarget(event)) {
    772      return;
    773    }
    774 
    775    event.preventDefault();
    776    event.stopPropagation();
    777 
    778    let draggedElement = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
    779    let targetElement =
    780      getRowVariant(this.dropTargetRow) == ROW_VARIANT_TAB_GROUP
    781        ? getTabGroupFromRow(this.dropTargetRow).labelElement
    782        : getTabFromRow(this.dropTargetRow);
    783 
    784    if (draggedElement === targetElement) {
    785      this._clearDropTarget();
    786      return;
    787    }
    788 
    789    // NOTE: Given the list is opened only when the window is focused,
    790    //       we don't have to check `draggedTab.container`.
    791    const metricsContext = {
    792      isUserTriggered: true,
    793      telemetrySource: lazy.TabMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU,
    794    };
    795    if (this.dropTargetDirection == -1) {
    796      this.gBrowser.moveTabBefore(
    797        draggedElement,
    798        targetElement,
    799        metricsContext
    800      );
    801    } else {
    802      this.gBrowser.moveTabAfter(draggedElement, targetElement, metricsContext);
    803    }
    804 
    805    this._clearDropTarget();
    806  }
    807 
    808  /**
    809   * @param {DragEvent} event
    810   */
    811  _onDragLeave(event) {
    812    if (!this._isMovingTabs(event)) {
    813      return;
    814    }
    815 
    816    let target = event.relatedTarget;
    817    while (target && target != this.containerNode) {
    818      target = target.parentNode;
    819    }
    820    if (target) {
    821      return;
    822    }
    823 
    824    this._clearDropTarget();
    825  }
    826 
    827  /**
    828   * @param {DragEvent} event
    829   */
    830  _onDragEnd(event) {
    831    if (!this._isMovingTabs(event)) {
    832      return;
    833    }
    834 
    835    this._clearDropTarget();
    836  }
    837 
    838  /**
    839   * @param {DragEvent} event
    840   * @returns {boolean}
    841   */
    842  _updateDropTarget(event) {
    843    const row = this._getTargetRowFromEvent(event);
    844    if (!row) {
    845      return false;
    846    }
    847 
    848    const rect = row.getBoundingClientRect();
    849    const index = this._getRowIndex(row);
    850    if (index === -1) {
    851      return false;
    852    }
    853 
    854    const threshold = rect.height * 0.5;
    855    if (event.clientY < rect.top + threshold) {
    856      this._setDropTarget(row, -1);
    857    } else {
    858      this._setDropTarget(row, 0);
    859    }
    860 
    861    return true;
    862  }
    863 
    864  /**
    865   * @param {XulToolbarItem} row
    866   * @param {-1|0} direction
    867   */
    868  _setDropTarget(row, direction) {
    869    this.dropTargetRow = row;
    870    this.dropTargetDirection = direction;
    871 
    872    const holder = this.dropIndicator.parentNode;
    873    const holderOffset = holder.getBoundingClientRect().top;
    874 
    875    // Set top to before/after the target row.
    876    let top;
    877    if (this.dropTargetDirection === -1) {
    878      if (this.dropTargetRow.previousSibling) {
    879        const rect = this.dropTargetRow.previousSibling.getBoundingClientRect();
    880        top = rect.top + rect.height;
    881      } else {
    882        const rect = this.dropTargetRow.getBoundingClientRect();
    883        top = rect.top;
    884      }
    885    } else {
    886      const rect = this.dropTargetRow.getBoundingClientRect();
    887      top = rect.top + rect.height;
    888    }
    889 
    890    // Avoid overflowing the sub view body.
    891    const indicatorHeight = 12;
    892    const subViewBody = holder.parentNode;
    893    const subViewBodyRect = subViewBody.getBoundingClientRect();
    894    top = Math.min(top, subViewBodyRect.bottom - indicatorHeight);
    895 
    896    this.dropIndicator.style.top = `${top - holderOffset - 12}px`;
    897    this.dropIndicator.collapsed = false;
    898  }
    899 
    900  _clearDropTarget() {
    901    if (this.dropTargetRow) {
    902      this.dropTargetRow = null;
    903    }
    904 
    905    if (this.dropIndicator) {
    906      this.dropIndicator.style.top = `0px`;
    907      this.dropIndicator.collapsed = true;
    908    }
    909  }
    910 
    911  /**
    912   * @param {MouseEvent} event
    913   */
    914  _onClick(event) {
    915    if (event.button == 1) {
    916      const row = this._getTargetRowFromEvent(event);
    917      if (!row) {
    918        return;
    919      }
    920 
    921      const rowVariant = getRowVariant(row);
    922 
    923      if (rowVariant == ROW_VARIANT_TAB) {
    924        const tab = getTabFromRow(row);
    925        this.gBrowser.removeTab(tab, {
    926          telemetrySource: lazy.TabMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU,
    927          animate: true,
    928        });
    929      } else if (rowVariant == ROW_VARIANT_TAB_GROUP) {
    930        getTabGroupFromRow(row)?.saveAndClose({ isUserTriggered: true });
    931      }
    932    }
    933  }
    934 }