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 }