tabs.js (57308B)
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 // This is loaded into all browser windows. Wrap in a block to prevent 8 // leaking to window scope. 9 { 10 const DIRECTION_BACKWARD = -1; 11 const DIRECTION_FORWARD = 1; 12 13 const isTab = element => gBrowser.isTab(element); 14 const isTabGroup = element => gBrowser.isTabGroup(element); 15 const isTabGroupLabel = element => gBrowser.isTabGroupLabel(element); 16 const isSplitViewWrapper = element => gBrowser.isSplitViewWrapper(element); 17 18 class MozTabbrowserTabs extends MozElements.TabsBase { 19 static observedAttributes = ["orient"]; 20 21 #mustUpdateTabMinHeight = false; 22 #tabMinHeight = 36; 23 #animatingGroups = new Set(); 24 25 constructor() { 26 super(); 27 28 this.addEventListener("TabSelect", this); 29 this.addEventListener("TabClose", this); 30 this.addEventListener("TabAttrModified", this); 31 this.addEventListener("TabHide", this); 32 this.addEventListener("TabShow", this); 33 this.addEventListener("TabHoverStart", this); 34 this.addEventListener("TabHoverEnd", this); 35 this.addEventListener("TabGroupLabelHoverStart", this); 36 this.addEventListener("TabGroupLabelHoverEnd", this); 37 // Capture collapse/expand early so we mark animating groups before 38 // overflow/underflow handlers run. 39 this.addEventListener("TabGroupExpand", this, true); 40 this.addEventListener("TabGroupCollapse", this, true); 41 this.addEventListener("TabGroupAnimationComplete", this); 42 this.addEventListener("TabGroupCreate", this); 43 this.addEventListener("TabGroupRemoved", this); 44 this.addEventListener("SplitViewCreated", this); 45 this.addEventListener("SplitViewRemoved", this); 46 this.addEventListener("transitionend", this); 47 this.addEventListener("dblclick", this); 48 this.addEventListener("click", this); 49 this.addEventListener("click", this, true); 50 this.addEventListener("keydown", this, { mozSystemGroup: true }); 51 this.addEventListener("mouseleave", this); 52 this.addEventListener("focusin", this); 53 this.addEventListener("focusout", this); 54 this.addEventListener("contextmenu", this); 55 this.addEventListener("dragstart", this); 56 this.addEventListener("dragover", this); 57 this.addEventListener("drop", this); 58 this.addEventListener("dragend", this); 59 this.addEventListener("dragleave", this); 60 } 61 62 init() { 63 this.startupTime = Services.startup.getStartupInfo().start.getTime(); 64 65 this.arrowScrollbox = document.getElementById( 66 "tabbrowser-arrowscrollbox" 67 ); 68 this.arrowScrollbox.addEventListener("wheel", this, true); 69 this.arrowScrollbox.addEventListener("underflow", this); 70 this.arrowScrollbox.addEventListener("overflow", this); 71 this.pinnedTabsContainer = document.getElementById( 72 "pinned-tabs-container" 73 ); 74 this.pinnedTabsContainer.setAttribute( 75 "orient", 76 this.getAttribute("orient") 77 ); 78 79 // Override arrowscrollbox.js method, since our scrollbox's children are 80 // inherited from the scrollbox binding parent (this). 81 this.arrowScrollbox._getScrollableElements = () => { 82 return this.ariaFocusableItems.reduce((elements, item) => { 83 if (this.arrowScrollbox._canScrollToElement(item)) { 84 elements.push(item); 85 if ( 86 isTab(item) && 87 item.group && 88 item.group.collapsed && 89 item.selected 90 ) { 91 // overflow container is scrollable, but not in focus order 92 elements.push(item.group.overflowContainer); 93 } 94 } 95 return elements; 96 }, []); 97 }; 98 this.arrowScrollbox._canScrollToElement = element => { 99 if (isTab(element)) { 100 return !element.pinned; 101 } 102 return true; 103 }; 104 105 // Override for performance reasons. This is the size of a single element 106 // that can be scrolled when using mouse wheel scrolling. If we don't do 107 // this then arrowscrollbox computes this value by calling 108 // _getScrollableElements and dividing the box size by that number. 109 // However in the tabstrip case we already know the answer to this as, 110 // when we're overflowing, it is always the same as the tab min width or 111 // height. For tab group labels, the number won't exactly match, but 112 // that shouldn't be a problem in practice since the arrowscrollbox 113 // stops at element bounds when finishing scrolling. 114 Object.defineProperty(this.arrowScrollbox, "lineScrollAmount", { 115 get: () => 116 this.verticalMode ? this.#tabMinHeight : this._tabMinWidthPref, 117 }); 118 119 this.baseConnect(); 120 121 this._blockDblClick = false; 122 this._closeButtonsUpdatePending = false; 123 this._closingTabsSpacer = this.querySelector(".closing-tabs-spacer"); 124 this._tabDefaultMaxWidth = NaN; 125 this._lastTabClosedByMouse = false; 126 this._hasTabTempMaxWidth = false; 127 this._scrollButtonWidth = 0; 128 this._animateElement = this.arrowScrollbox; 129 this._tabClipWidth = Services.prefs.getIntPref( 130 "browser.tabs.tabClipWidth" 131 ); 132 this._hiddenSoundPlayingTabs = new Set(); 133 this.previewPanel = null; 134 135 this.allTabs[0].label = this.emptyTabTitle; 136 137 // Hide the secondary text for locales where it is unsupported due to size constraints. 138 const language = Services.locale.appLocaleAsBCP47; 139 const unsupportedLocales = Services.prefs.getCharPref( 140 "browser.tabs.secondaryTextUnsupportedLocales" 141 ); 142 this.toggleAttribute( 143 "secondarytext-unsupported", 144 unsupportedLocales.split(",").includes(language.split("-")[0]) 145 ); 146 147 this.newTabButton.setAttribute( 148 "aria-label", 149 DynamicShortcutTooltip.getText("tabs-newtab-button") 150 ); 151 152 let handleResize = () => { 153 this._updateCloseButtons(); 154 this._handleTabSelect(true); 155 }; 156 window.addEventListener("resize", handleResize); 157 this._fullscreenMutationObserver = new MutationObserver(handleResize); 158 this._fullscreenMutationObserver.observe(document.documentElement, { 159 attributeFilter: ["inFullscreen", "inDOMFullscreen"], 160 }); 161 162 this.boundObserve = (...args) => this.observe(...args); 163 Services.prefs.addObserver("privacy.userContext", this.boundObserve); 164 this.observe(null, "nsPref:changed", "privacy.userContext.enabled"); 165 166 document 167 .getElementById("vertical-tabs-newtab-button") 168 .addEventListener("keypress", this); 169 document 170 .getElementById("tabs-newtab-button") 171 .addEventListener("keypress", this); 172 173 XPCOMUtils.defineLazyPreferenceGetter( 174 this, 175 "_tabMinWidthPref", 176 "browser.tabs.tabMinWidth", 177 null, 178 (pref, prevValue, newValue) => this.#updateTabMinWidth(newValue), 179 newValue => { 180 const LIMIT = 50; 181 return Math.max(newValue, LIMIT); 182 } 183 ); 184 this.#updateTabMinWidth(this._tabMinWidthPref); 185 this.#updateTabMinHeight(); 186 187 CustomizableUI.addListener(this); 188 this._updateNewTabVisibility(); 189 190 XPCOMUtils.defineLazyPreferenceGetter( 191 this, 192 "_closeTabByDblclick", 193 "browser.tabs.closeTabByDblclick", 194 false 195 ); 196 197 XPCOMUtils.defineLazyPreferenceGetter( 198 this, 199 "_sidebarVisibility", 200 "sidebar.visibility", 201 "always-show" 202 ); 203 204 XPCOMUtils.defineLazyPreferenceGetter( 205 this, 206 "_sidebarPositionStart", 207 "sidebar.position_start", 208 true 209 ); 210 211 if (gMultiProcessBrowser) { 212 this.tabbox.tabpanels.setAttribute("async", "true"); 213 } 214 215 XPCOMUtils.defineLazyPreferenceGetter( 216 this, 217 "_showTabHoverPreview", 218 "browser.tabs.hoverPreview.enabled", 219 false 220 ); 221 XPCOMUtils.defineLazyPreferenceGetter( 222 this, 223 "_showTabGroupHoverPreview", 224 "browser.tabs.groups.hoverPreview.enabled", 225 false 226 ); 227 228 this.tooltip = "tabbrowser-tab-tooltip"; 229 230 Services.prefs.addObserver( 231 "browser.tabs.dragDrop.multiselectStacking", 232 this.boundObserve 233 ); 234 this.observe( 235 null, 236 "nsPref:changed", 237 "browser.tabs.dragDrop.multiselectStacking" 238 ); 239 } 240 241 #initializeDragAndDrop() { 242 this.tabDragAndDrop = Services.prefs.getBoolPref( 243 "browser.tabs.dragDrop.multiselectStacking", 244 true 245 ) 246 ? new window.TabStacking(this) 247 : new window.TabDragAndDrop(this); 248 this.tabDragAndDrop.init(); 249 } 250 251 attributeChangedCallback(name, oldValue, newValue) { 252 if (name == "orient") { 253 // reset this attribute so we don't have incorrect styling for vertical tabs 254 this.removeAttribute("overflow"); 255 this.#updateTabMinWidth(); 256 this.#updateTabMinHeight(); 257 this.pinnedTabsContainer?.setAttribute("orient", newValue); 258 } 259 super.attributeChangedCallback(name, oldValue, newValue); 260 } 261 262 // Event handlers 263 264 handleEvent(aEvent) { 265 switch (aEvent.type) { 266 case "mouseout": { 267 // If the "related target" (the node to which the pointer went) is not 268 // a child of the current document, the mouse just left the window. 269 let relatedTarget = aEvent.relatedTarget; 270 if (relatedTarget && relatedTarget.ownerDocument == document) { 271 break; 272 } 273 } 274 // fall through 275 case "mousemove": 276 if ( 277 document.getElementById("tabContextMenu").state != "open" && 278 !this.#isMovingTab() 279 ) { 280 this._unlockTabSizing(); 281 } 282 break; 283 case "mouseleave": 284 this.previewPanel?.deactivate(); 285 break; 286 default: { 287 let methodName = `on_${aEvent.type}`; 288 if (methodName in this) { 289 this[methodName](aEvent); 290 } else { 291 throw new Error(`Unexpected event ${aEvent.type}`); 292 } 293 } 294 } 295 } 296 297 /** 298 * @param {CustomEvent} event 299 */ 300 on_TabSelect(event) { 301 const { 302 target: newTab, 303 detail: { previousTab }, 304 } = event; 305 306 // In some cases (e.g. by selecting a tab in a collapsed tab group), 307 // changing the selected tab may cause a tab to appear/disappear. 308 if (previousTab.group?.collapsed || newTab.group?.collapsed) { 309 this._invalidateCachedVisibleTabs(); 310 } 311 this._handleTabSelect(); 312 } 313 314 on_TabClose(event) { 315 this._hiddenSoundPlayingStatusChanged(event.target, { closed: true }); 316 } 317 318 on_TabAttrModified(event) { 319 if ( 320 event.detail.changed.includes("soundplaying") && 321 !event.target.visible 322 ) { 323 this._hiddenSoundPlayingStatusChanged(event.target); 324 } 325 if ( 326 event.detail.changed.includes("soundplaying") || 327 event.detail.changed.includes("muted") || 328 event.detail.changed.includes("activemedia-blocked") 329 ) { 330 this.updateTabSoundLabel(event.target); 331 } 332 } 333 334 on_TabHide(event) { 335 if (event.target.soundPlaying) { 336 this._hiddenSoundPlayingStatusChanged(event.target); 337 } 338 } 339 340 on_TabShow(event) { 341 if (event.target.soundPlaying) { 342 this._hiddenSoundPlayingStatusChanged(event.target); 343 } 344 } 345 346 on_TabHoverStart(event) { 347 if (!this._showTabHoverPreview) { 348 return; 349 } 350 this.ensureTabPreviewPanelLoaded(); 351 this.previewPanel.activate(event.target); 352 } 353 354 on_TabHoverEnd(event) { 355 this.previewPanel?.deactivate(event.target); 356 } 357 358 cancelTabGroupPreview() { 359 this.previewPanel?.panelOpener.clear(); 360 } 361 362 showTabGroupPreview(group) { 363 if (!this._showTabGroupHoverPreview) { 364 return; 365 } 366 this.ensureTabPreviewPanelLoaded(); 367 this.previewPanel.activate(group); 368 } 369 370 on_TabGroupLabelHoverStart(event) { 371 this.showTabGroupPreview(event.target.group); 372 } 373 374 on_TabGroupLabelHoverEnd(event) { 375 this.previewPanel?.deactivate(event.target.group); 376 } 377 378 on_TabGroupExpand(event) { 379 this._invalidateCachedVisibleTabs(); 380 this.#animatingGroups.add(event.target.id); 381 } 382 383 on_TabGroupCollapse(event) { 384 this._invalidateCachedVisibleTabs(); 385 this._unlockTabSizing(); 386 this.#animatingGroups.add(event.target.id); 387 } 388 389 on_TabGroupAnimationComplete(event) { 390 // Delay clearing the animating flag so overflow/underflow handlers 391 // triggered by the size change can observe it and skip auto-scroll. 392 window.requestAnimationFrame(() => { 393 this.#animatingGroups.delete(event.target.id); 394 }); 395 } 396 397 on_TabGroupCreate() { 398 this._invalidateCachedTabs(); 399 } 400 401 on_TabGroupRemoved() { 402 this._invalidateCachedTabs(); 403 } 404 405 on_SplitViewCreated() { 406 this._invalidateCachedTabs(); 407 } 408 409 on_SplitViewRemoved() { 410 this._invalidateCachedTabs(); 411 } 412 413 /** 414 * @param {TransitionEvent} event 415 */ 416 on_transitionend(event) { 417 if (event.propertyName != "max-width") { 418 return; 419 } 420 421 let tab = event.target?.closest("tab"); 422 423 if (!tab) { 424 return; 425 } 426 427 if (tab.hasAttribute("fadein")) { 428 if (tab._fullyOpen) { 429 this._updateCloseButtons(); 430 } else { 431 this._handleNewTab(tab); 432 } 433 } else if (tab.closing) { 434 gBrowser._endRemoveTab(tab); 435 } 436 437 let evt = new CustomEvent("TabAnimationEnd", { bubbles: true }); 438 tab.dispatchEvent(evt); 439 } 440 441 on_dblclick(event) { 442 // When the tabbar has an unified appearance with the titlebar 443 // and menubar, a double-click in it should have the same behavior 444 // as double-clicking the titlebar 445 if (CustomTitlebar.enabled && !this.verticalMode) { 446 return; 447 } 448 449 // Make sure it is the primary button, we are hitting our arrowscrollbox, 450 // and we're not hitting the scroll buttons. 451 if ( 452 event.button != 0 || 453 event.target != this.arrowScrollbox || 454 event.composedTarget.localName == "toolbarbutton" 455 ) { 456 return; 457 } 458 459 if (!this._blockDblClick) { 460 BrowserCommands.openTab(); 461 } 462 463 event.preventDefault(); 464 } 465 466 on_click(event) { 467 if (event.eventPhase == Event.CAPTURING_PHASE && event.button == 0) { 468 /* Catches extra clicks meant for the in-tab close button. 469 * Placed here to avoid leaking (a temporary handler added from the 470 * in-tab close button binding would close over the tab and leak it 471 * until the handler itself was removed). (bug 897751) 472 * 473 * The only sequence in which a second click event (i.e. dblclik) 474 * can be dispatched on an in-tab close button is when it is shown 475 * after the first click (i.e. the first click event was dispatched 476 * on the tab). This happens when we show the close button only on 477 * the active tab. (bug 352021) 478 * The only sequence in which a third click event can be dispatched 479 * on an in-tab close button is when the tab was opened with a 480 * double click on the tabbar. (bug 378344) 481 * In both cases, it is most likely that the close button area has 482 * been accidentally clicked, therefore we do not close the tab. 483 * 484 * We don't want to ignore processing of more than one click event, 485 * though, since the user might actually be repeatedly clicking to 486 * close many tabs at once. 487 */ 488 let target = event.originalTarget; 489 if (target.classList.contains("tab-close-button")) { 490 // We preemptively set this to allow the closing-multiple-tabs- 491 // in-a-row case. 492 if (this._blockDblClick) { 493 target._ignoredCloseButtonClicks = true; 494 } else if (event.detail > 1 && !target._ignoredCloseButtonClicks) { 495 target._ignoredCloseButtonClicks = true; 496 event.stopPropagation(); 497 return; 498 } else { 499 // Reset the "ignored click" flag 500 target._ignoredCloseButtonClicks = false; 501 } 502 } 503 504 /* Protects from close-tab-button errant doubleclick: 505 * Since we're removing the event target, if the user 506 * double-clicks the button, the dblclick event will be dispatched 507 * with the tabbar as its event target (and explicit/originalTarget), 508 * which treats that as a mouse gesture for opening a new tab. 509 * In this context, we're manually blocking the dblclick event. 510 */ 511 if (this._blockDblClick) { 512 if (!("_clickedTabBarOnce" in this)) { 513 this._clickedTabBarOnce = true; 514 return; 515 } 516 delete this._clickedTabBarOnce; 517 this._blockDblClick = false; 518 } 519 } else if ( 520 event.eventPhase == Event.BUBBLING_PHASE && 521 event.button == 1 522 ) { 523 let tab = event.target?.closest("tab"); 524 if (tab) { 525 if (tab.multiselected) { 526 gBrowser.removeMultiSelectedTabs(); 527 } else { 528 gBrowser.removeTab(tab, { 529 animate: true, 530 triggeringEvent: event, 531 }); 532 } 533 } else if (isTabGroupLabel(event.target)) { 534 event.target.group.saveAndClose(); 535 } else if ( 536 event.originalTarget.closest("scrollbox") && 537 !Services.prefs.getBoolPref( 538 "widget.gtk.titlebar-action-middle-click-enabled" 539 ) 540 ) { 541 // Check whether the click 542 // was dispatched on the open space of it. 543 let visibleTabs = this.visibleTabs; 544 let lastTab = visibleTabs.at(-1); 545 let winUtils = window.windowUtils; 546 let endOfTab = 547 winUtils.getBoundsWithoutFlushing(lastTab)[ 548 (this.verticalMode && "bottom") || 549 (this.#rtlMode ? "left" : "right") 550 ]; 551 if ( 552 (this.verticalMode && event.clientY > endOfTab) || 553 (!this.verticalMode && 554 (this.#rtlMode 555 ? event.clientX < endOfTab 556 : event.clientX > endOfTab)) 557 ) { 558 BrowserCommands.openTab(); 559 } 560 } else { 561 return; 562 } 563 564 event.preventDefault(); 565 event.stopPropagation(); 566 } 567 } 568 569 on_keydown(event) { 570 let { altKey, shiftKey } = event; 571 let [accel, nonAccel] = 572 AppConstants.platform == "macosx" 573 ? [event.metaKey, event.ctrlKey] 574 : [event.ctrlKey, event.metaKey]; 575 576 let keyComboForFocusedElement = 577 !accel && !shiftKey && !altKey && !nonAccel; 578 let keyComboForMove = accel && shiftKey && !altKey && !nonAccel; 579 let keyComboForFocus = accel && !shiftKey && !altKey && !nonAccel; 580 581 if (!keyComboForFocusedElement && !keyComboForMove && !keyComboForFocus) { 582 return; 583 } 584 585 if (keyComboForFocusedElement) { 586 let ariaFocusedItem = this.ariaFocusedItem; 587 if (isTabGroupLabel(ariaFocusedItem)) { 588 switch (event.keyCode) { 589 case KeyEvent.DOM_VK_SPACE: 590 case KeyEvent.DOM_VK_RETURN: { 591 ariaFocusedItem.click(); 592 event.preventDefault(); 593 } 594 } 595 } 596 } else if (keyComboForMove) { 597 switch (event.keyCode) { 598 case KeyEvent.DOM_VK_UP: 599 gBrowser.moveTabBackward(); 600 break; 601 case KeyEvent.DOM_VK_DOWN: 602 gBrowser.moveTabForward(); 603 break; 604 case KeyEvent.DOM_VK_RIGHT: 605 if (RTL_UI) { 606 gBrowser.moveTabBackward(); 607 } else { 608 gBrowser.moveTabForward(); 609 } 610 break; 611 case KeyEvent.DOM_VK_LEFT: 612 if (RTL_UI) { 613 gBrowser.moveTabForward(); 614 } else { 615 gBrowser.moveTabBackward(); 616 } 617 break; 618 case KeyEvent.DOM_VK_HOME: 619 gBrowser.moveTabToStart(); 620 break; 621 case KeyEvent.DOM_VK_END: 622 gBrowser.moveTabToEnd(); 623 break; 624 default: 625 // Consume the keydown event for the above keyboard 626 // shortcuts only. 627 return; 628 } 629 630 event.preventDefault(); 631 } else if (keyComboForFocus) { 632 switch (event.keyCode) { 633 case KeyEvent.DOM_VK_UP: 634 this.#advanceFocus(DIRECTION_BACKWARD); 635 break; 636 case KeyEvent.DOM_VK_DOWN: 637 this.#advanceFocus(DIRECTION_FORWARD); 638 break; 639 case KeyEvent.DOM_VK_RIGHT: 640 if (RTL_UI) { 641 this.#advanceFocus(DIRECTION_BACKWARD); 642 } else { 643 this.#advanceFocus(DIRECTION_FORWARD); 644 } 645 break; 646 case KeyEvent.DOM_VK_LEFT: 647 if (RTL_UI) { 648 this.#advanceFocus(DIRECTION_FORWARD); 649 } else { 650 this.#advanceFocus(DIRECTION_BACKWARD); 651 } 652 break; 653 case KeyEvent.DOM_VK_HOME: 654 this.ariaFocusedItem = this.ariaFocusableItems.at(0); 655 break; 656 case KeyEvent.DOM_VK_END: 657 this.ariaFocusedItem = this.ariaFocusableItems.at(-1); 658 break; 659 case KeyEvent.DOM_VK_SPACE: { 660 let ariaFocusedItem = this.ariaFocusedItem; 661 if (isTab(ariaFocusedItem)) { 662 if (ariaFocusedItem.multiselected) { 663 gBrowser.removeFromMultiSelectedTabs(ariaFocusedItem); 664 } else { 665 gBrowser.addToMultiSelectedTabs(ariaFocusedItem); 666 } 667 } 668 break; 669 } 670 default: 671 // Consume the keydown event for the above keyboard 672 // shortcuts only. 673 return; 674 } 675 676 event.preventDefault(); 677 } 678 } 679 680 /** 681 * @param {FocusEvent} event 682 */ 683 on_focusin(event) { 684 if (event.target == this.selectedItem) { 685 this.tablistHasFocus = true; 686 if (!this.ariaFocusedItem) { 687 // If the active tab is receiving focus and there isn't a keyboard 688 // focus target yet, set the keyboard focus target to the active 689 // tab. Do not override the keyboard-focused item if the user 690 // already set a keyboard focus. 691 this.ariaFocusedItem = this.selectedItem; 692 } 693 } 694 let focusReturnedFromGroupPanel = event.relatedTarget?.classList.contains( 695 "group-preview-button" 696 ); 697 if ( 698 !focusReturnedFromGroupPanel && 699 this.tablistHasFocus && 700 isTabGroupLabel(this.ariaFocusedItem) 701 ) { 702 this.showTabGroupPreview(this.ariaFocusedItem.group); 703 } 704 } 705 706 /** 707 * @param {FocusEvent} event 708 */ 709 on_focusout(event) { 710 this.cancelTabGroupPreview(); 711 if (event.target == this.selectedItem) { 712 this.tablistHasFocus = false; 713 } 714 } 715 716 on_keypress(event) { 717 if (event.defaultPrevented) { 718 return; 719 } 720 if (event.key == " " || event.key == "Enter") { 721 event.preventDefault(); 722 event.target.click(); 723 } 724 } 725 726 on_dragstart(event) { 727 this.tabDragAndDrop.handle_dragstart(event); 728 } 729 730 on_dragover(event) { 731 this.tabDragAndDrop.handle_dragover(event); 732 } 733 734 on_drop(event) { 735 this.tabDragAndDrop.handle_drop(event); 736 } 737 738 on_dragend(event) { 739 this.tabDragAndDrop.handle_dragend(event); 740 } 741 742 on_dragleave(event) { 743 this.tabDragAndDrop.handle_dragleave(event); 744 } 745 746 on_wheel(event) { 747 if ( 748 Services.prefs.getBoolPref("toolkit.tabbox.switchByScrolling", false) 749 ) { 750 event.stopImmediatePropagation(); 751 } 752 } 753 754 on_overflow(event) { 755 // Ignore overflow events from nested scrollable elements 756 if (event.target != this.arrowScrollbox) { 757 return; 758 } 759 760 this.toggleAttribute("overflow", true); 761 this._updateCloseButtons(); 762 763 if (!this.#animatingGroups.size) { 764 this._handleTabSelect(true); 765 } 766 767 document 768 .getElementById("tab-preview-panel") 769 ?.setAttribute("rolluponmousewheel", true); 770 } 771 772 on_underflow(event) { 773 // Ignore underflow events: 774 // - from nested scrollable elements 775 // - corresponding to an overflow event that we ignored 776 if (event.target != this.arrowScrollbox || !this.overflowing) { 777 return; 778 } 779 780 this.removeAttribute("overflow"); 781 782 if (this._lastTabClosedByMouse) { 783 this._expandSpacerBy(this._scrollButtonWidth); 784 } 785 786 for (let tab of gBrowser._removingTabs) { 787 gBrowser.removeTab(tab); 788 } 789 790 this._updateCloseButtons(); 791 792 document 793 .getElementById("tab-preview-panel") 794 ?.removeAttribute("rolluponmousewheel"); 795 } 796 797 on_contextmenu(event) { 798 // When pressing the context menu key (as opposed to right-clicking) 799 // while a tab group label has aria focus (as opposed to DOM focus), 800 // open the tab group context menu as if the label had DOM focus. 801 // The button property is used to differentiate between key and mouse. 802 if (event.button == 0 && isTabGroupLabel(this.ariaFocusedItem)) { 803 gBrowser.tabGroupMenu.openEditModal(this.ariaFocusedItem.group); 804 event.preventDefault(); 805 } 806 } 807 808 // Utilities 809 810 get emptyTabTitle() { 811 // Normal tab title is used also in the permanent private browsing mode. 812 const l10nId = 813 PrivateBrowsingUtils.isWindowPrivate(window) && 814 !Services.prefs.getBoolPref("browser.privatebrowsing.autostart") 815 ? "tabbrowser-empty-private-tab-title" 816 : "tabbrowser-empty-tab-title"; 817 return gBrowser.tabLocalization.formatValueSync(l10nId); 818 } 819 820 get tabbox() { 821 return document.getElementById("tabbrowser-tabbox"); 822 } 823 824 get newTabButton() { 825 return this.querySelector("#tabs-newtab-button"); 826 } 827 828 get verticalMode() { 829 return this.getAttribute("orient") == "vertical"; 830 } 831 832 get expandOnHover() { 833 return this._sidebarVisibility == "expand-on-hover"; 834 } 835 836 get #rtlMode() { 837 return !this.verticalMode && RTL_UI; 838 } 839 840 get overflowing() { 841 return this.hasAttribute("overflow"); 842 } 843 844 #allTabs; 845 get allTabs() { 846 if (this.#allTabs) { 847 return this.#allTabs; 848 } 849 // Remove temporary periphery element added at drag start. 850 let pinnedChildren = Array.from(this.pinnedTabsContainer.children); 851 if (pinnedChildren?.at(-1)?.id == "pinned-tabs-container-periphery") { 852 pinnedChildren.pop(); 853 } 854 let unpinnedChildren = Array.from(this.arrowScrollbox.children); 855 // remove arrowScrollbox periphery element. 856 unpinnedChildren.pop(); 857 858 // explode tab groups and split view wrappers 859 // Iterate backwards over the array to preserve indices while we modify 860 // things in place 861 for (let i = unpinnedChildren.length - 1; i >= 0; i--) { 862 if ( 863 unpinnedChildren[i].tagName == "tab-group" || 864 unpinnedChildren[i].tagName == "tab-split-view-wrapper" 865 ) { 866 unpinnedChildren.splice(i, 1, ...unpinnedChildren[i].tabs); 867 } 868 } 869 870 this.#allTabs = [...pinnedChildren, ...unpinnedChildren]; 871 return this.#allTabs; 872 } 873 874 get allGroups() { 875 let children = Array.from(this.arrowScrollbox.children); 876 return children.filter(node => node.tagName == "tab-group"); 877 } 878 879 /** 880 * Returns all tabs in the current window, including hidden tabs and tabs 881 * in collapsed groups, but excluding closing tabs and the Firefox View tab. 882 */ 883 get openTabs() { 884 if (!this.#openTabs) { 885 this.#openTabs = this.allTabs.filter(tab => tab.isOpen); 886 } 887 return this.#openTabs; 888 } 889 #openTabs; 890 891 /** 892 * Same as `openTabs` but excluding hidden tabs. 893 */ 894 get nonHiddenTabs() { 895 if (!this.#nonHiddenTabs) { 896 this.#nonHiddenTabs = this.openTabs.filter(tab => !tab.hidden); 897 } 898 return this.#nonHiddenTabs; 899 } 900 #nonHiddenTabs; 901 902 /** 903 * Same as `openTabs` but excluding hidden tabs and tabs in collapsed groups. 904 */ 905 get visibleTabs() { 906 if (!this.#visibleTabs) { 907 this.#visibleTabs = this.openTabs.filter(tab => tab.visible); 908 } 909 return this.#visibleTabs; 910 } 911 #visibleTabs; 912 913 /** 914 * @returns {boolean} true if the keyboard focus is on the active tab 915 */ 916 get tablistHasFocus() { 917 return this.hasAttribute("tablist-has-focus"); 918 } 919 920 /** 921 * @param {boolean} hasFocus true if the keyboard focus is on the active tab 922 */ 923 set tablistHasFocus(hasFocus) { 924 this.toggleAttribute("tablist-has-focus", hasFocus); 925 } 926 927 /** @typedef {MozTabbrowserTab|MozTextLabel} FocusableItem */ 928 929 /** @type {FocusableItem[]} */ 930 #focusableItems; 931 932 /** @type {dragAndDropElements[]} */ 933 #dragAndDropElements; 934 935 /** 936 * @returns {FocusableItem[]} 937 * @override 938 */ 939 get ariaFocusableItems() { 940 if (this.#focusableItems) { 941 return this.#focusableItems; 942 } 943 944 let unpinnedChildren = Array.from(this.arrowScrollbox.children); 945 let pinnedChildren = Array.from(this.pinnedTabsContainer.children); 946 947 let focusableItems = []; 948 for (let child of pinnedChildren) { 949 if (isTab(child)) { 950 focusableItems.push(child); 951 } 952 } 953 for (let child of unpinnedChildren) { 954 if (isTab(child) && child.visible) { 955 focusableItems.push(child); 956 } else if (isTabGroup(child)) { 957 focusableItems.push(child.labelElement); 958 959 let visibleTabsInGroup = child.tabs.filter(tab => tab.visible); 960 focusableItems.push(...visibleTabsInGroup); 961 } else if (child.tagName == "tab-split-view-wrapper") { 962 let visibleTabsInSplitView = child.tabs.filter(tab => tab.visible); 963 focusableItems.push(...visibleTabsInSplitView); 964 } 965 } 966 967 this.#focusableItems = focusableItems; 968 969 return this.#focusableItems; 970 } 971 972 /** 973 * @returns {dragAndDropElements[]} 974 * Representation of every drag and drop element including tabs, tab group labels and split view wrapper. 975 * We keep this separate from ariaFocusableItems because not every element for drag n'drop also needs to be 976 * focusable (ex, we don't want the splitview container to be focusable, only its children). 977 */ 978 get dragAndDropElements() { 979 if (this.#dragAndDropElements) { 980 return this.#dragAndDropElements; 981 } 982 983 let elementIndex = 0; 984 let dragAndDropElements = []; 985 let unpinnedChildren = Array.from(this.arrowScrollbox.children); 986 let pinnedChildren = Array.from(this.pinnedTabsContainer.children); 987 988 for (let child of [...pinnedChildren, ...unpinnedChildren]) { 989 if ( 990 !( 991 (isTab(child) && child.visible) || 992 isTabGroup(child) || 993 isSplitViewWrapper(child) 994 ) 995 ) { 996 continue; 997 } 998 999 if (isTabGroup(child)) { 1000 child.labelElement.elementIndex = elementIndex++; 1001 dragAndDropElements.push(child.labelElement); 1002 1003 let tabsAndSplitViews = child.tabsAndSplitViews.filter( 1004 node => node.visible 1005 ); 1006 tabsAndSplitViews.forEach(ele => { 1007 ele.elementIndex = elementIndex++; 1008 }); 1009 dragAndDropElements.push(...tabsAndSplitViews); 1010 } else { 1011 child.elementIndex = elementIndex++; 1012 dragAndDropElements.push(child); 1013 } 1014 } 1015 1016 this.#dragAndDropElements = dragAndDropElements; 1017 return this.#dragAndDropElements; 1018 } 1019 1020 /** 1021 * Moves the ARIA focus in the tab strip left or right, as appropriate, to 1022 * the next tab or tab group label. 1023 * 1024 * @param {-1|1} direction 1025 */ 1026 #advanceFocus(direction) { 1027 let currentIndex = this.ariaFocusableItems.indexOf(this.ariaFocusedItem); 1028 let newIndex = currentIndex + direction; 1029 1030 // Clamp the index so that the focus stops at the edges of the tab strip 1031 newIndex = Math.min( 1032 this.ariaFocusableItems.length - 1, 1033 Math.max(0, newIndex) 1034 ); 1035 1036 let itemToFocus = this.ariaFocusableItems[newIndex]; 1037 this.ariaFocusedItem = itemToFocus; 1038 1039 // If the newly-focused item is a tab group label and the group is collapsed, 1040 // proactively show the tab group preview 1041 if (isTabGroupLabel(this.ariaFocusedItem)) { 1042 this.showTabGroupPreview(this.ariaFocusedItem.group); 1043 } 1044 } 1045 1046 _invalidateCachedTabs() { 1047 this.#allTabs = null; 1048 this._invalidateCachedVisibleTabs(); 1049 } 1050 1051 _invalidateCachedVisibleTabs() { 1052 this.#openTabs = null; 1053 this.#nonHiddenTabs = null; 1054 this.#visibleTabs = null; 1055 // Focusable items must also be visible, but they do not depend on 1056 // this.#visibleTabs, so changes to visible tabs need to also invalidate 1057 // the focusable items and dragAndDropElements cache. 1058 this.#focusableItems = null; 1059 this.#dragAndDropElements = null; 1060 } 1061 1062 #isMovingTab() { 1063 return this.hasAttribute("movingtab"); 1064 } 1065 1066 isContainerVerticalPinnedGrid(tab) { 1067 return ( 1068 tab.pinned && 1069 this.verticalMode && 1070 this.hasAttribute("expanded") && 1071 !this.expandOnHover 1072 ); 1073 } 1074 1075 /** 1076 * Changes the selected tab or tab group label on the tab strip 1077 * relative to the ARIA-focused tab strip element or the active tab. This 1078 * is intended for traversing the tab strip visually, e.g by using keyboard 1079 * arrows. For cases where keyboard shortcuts or other logic should only 1080 * select tabs (and never tab group labels), see `advanceSelectedTab`. 1081 * 1082 * @override 1083 * @param {-1|1} direction 1084 * @param {boolean} shouldWrap 1085 */ 1086 advanceSelectedItem(aDir, aWrap) { 1087 let groupPanel = this.previewPanel?.tabGroupPanel; 1088 if (groupPanel && groupPanel.isActive) { 1089 // if the group panel is open, it should receive keyboard focus here 1090 // instead of moving to the next item in the tabstrip. 1091 groupPanel.focusPanel(aDir); 1092 return; 1093 } 1094 1095 // cancel any pending group popup since we expect to deselect the label 1096 this.cancelTabGroupPreview(); 1097 1098 let { ariaFocusableItems, ariaFocusedIndex } = this; 1099 1100 // Advance relative to the ARIA-focused item if set, otherwise advance 1101 // relative to the active tab. 1102 let currentItemIndex = 1103 ariaFocusedIndex >= 0 1104 ? ariaFocusedIndex 1105 : ariaFocusableItems.indexOf(this.selectedItem); 1106 1107 let newItemIndex = currentItemIndex + aDir; 1108 1109 if (aWrap) { 1110 if (newItemIndex >= ariaFocusableItems.length) { 1111 newItemIndex = 0; 1112 } else if (newItemIndex < 0) { 1113 newItemIndex = ariaFocusableItems.length - 1; 1114 } 1115 } else { 1116 newItemIndex = Math.min( 1117 ariaFocusableItems.length - 1, 1118 Math.max(0, newItemIndex) 1119 ); 1120 } 1121 1122 if (currentItemIndex == newItemIndex) { 1123 return; 1124 } 1125 1126 // If the next item is a tab, select it. If the next item is a tab group 1127 // label, keep the active tab selected and just set ARIA focus on the tab 1128 // group label. 1129 let newItem = ariaFocusableItems[newItemIndex]; 1130 if (isTab(newItem)) { 1131 this._selectNewTab(newItem, aDir, aWrap); 1132 } 1133 this.ariaFocusedItem = newItem; 1134 1135 // If the newly-focused item is a tab group label and the group is collapsed, 1136 // proactively show the tab group preview 1137 if (isTabGroupLabel(this.ariaFocusedItem)) { 1138 this.showTabGroupPreview(this.ariaFocusedItem.group); 1139 } 1140 } 1141 1142 ensureTabPreviewPanelLoaded() { 1143 if (!this.previewPanel) { 1144 const TabHoverPanelSet = ChromeUtils.importESModule( 1145 "chrome://browser/content/tabbrowser/tab-hover-preview.mjs" 1146 ).default; 1147 this.previewPanel = new TabHoverPanelSet(window); 1148 } 1149 } 1150 1151 appendChild(tab) { 1152 return this.insertBefore(tab, null); 1153 } 1154 1155 insertBefore(tab, node) { 1156 if (!this.arrowScrollbox) { 1157 throw new Error("Shouldn't call this without arrowscrollbox"); 1158 } 1159 1160 if (node == null) { 1161 // We have a container for non-tab elements at the end of the scrollbox. 1162 node = this.arrowScrollbox.lastChild; 1163 } 1164 1165 node.before(tab); 1166 1167 if (this.#mustUpdateTabMinHeight) { 1168 this.#updateTabMinHeight(); 1169 } 1170 } 1171 1172 #updateTabMinWidth(val) { 1173 this.style.setProperty( 1174 "--tab-min-width-pref", 1175 (val ?? this._tabMinWidthPref) + "px" 1176 ); 1177 } 1178 1179 #updateTabMinHeight() { 1180 if (!this.verticalMode || !window.toolbar.visible) { 1181 this.#mustUpdateTabMinHeight = false; 1182 return; 1183 } 1184 1185 // Find at least one tab we can scroll to. 1186 let firstScrollableTab = this.visibleTabs.find( 1187 this.arrowScrollbox._canScrollToElement 1188 ); 1189 1190 if (!firstScrollableTab) { 1191 // If not, we're in a pickle. We should never get here except if we 1192 // also don't use the outcome of this work (because there's nothing to 1193 // scroll so we don't care about the scrollbox size). 1194 // So just set a flag so we re-run once we do have a new tab. 1195 this.#mustUpdateTabMinHeight = true; 1196 return; 1197 } 1198 1199 let { height } = 1200 window.windowUtils.getBoundsWithoutFlushing(firstScrollableTab); 1201 1202 // Use the current known height or a sane default. 1203 this.#tabMinHeight = height || 36; 1204 1205 // The height we got may be incorrect if a flush is pending so re-check it after 1206 // a flush completes. 1207 window 1208 .promiseDocumentFlushed(() => {}) 1209 .then( 1210 () => { 1211 height = 1212 window.windowUtils.getBoundsWithoutFlushing( 1213 firstScrollableTab 1214 ).height; 1215 1216 if (height) { 1217 this.#tabMinHeight = height; 1218 } 1219 }, 1220 () => { 1221 /* ignore errors */ 1222 } 1223 ); 1224 } 1225 1226 get _isCustomizing() { 1227 return document.documentElement.hasAttribute("customizing"); 1228 } 1229 1230 // This overrides the TabsBase _selectNewTab method so that we can 1231 // potentially interrupt keyboard tab switching when sharing the 1232 // window or screen. 1233 _selectNewTab(aNewTab, aFallbackDir, aWrap) { 1234 if (!gSharedTabWarning.willShowSharedTabWarning(aNewTab)) { 1235 super._selectNewTab(aNewTab, aFallbackDir, aWrap); 1236 } 1237 } 1238 1239 observe(aSubject, aTopic, aData) { 1240 switch (aTopic) { 1241 case "nsPref:changed": { 1242 if (aData == "browser.tabs.dragDrop.multiselectStacking") { 1243 this.#initializeDragAndDrop(); 1244 } 1245 // This is has to deal with changes in 1246 // privacy.userContext.enabled and 1247 // privacy.userContext.newTabContainerOnLeftClick.enabled. 1248 let containersEnabled = 1249 Services.prefs.getBoolPref("privacy.userContext.enabled") && 1250 !PrivateBrowsingUtils.isWindowPrivate(window); 1251 1252 // This pref won't change so often, so just recreate the menu. 1253 const newTabLeftClickOpensContainersMenu = Services.prefs.getBoolPref( 1254 "privacy.userContext.newTabContainerOnLeftClick.enabled" 1255 ); 1256 1257 // There are separate "new tab" buttons for horizontal tabs toolbar, vertical tabs and 1258 // for when the tab strip is overflowed (which is shared by vertical and horizontal tabs); 1259 // Attach the long click popup to all of them. 1260 const newTab = document.getElementById("new-tab-button"); 1261 const newTab2 = this.newTabButton; 1262 const newTabVertical = document.getElementById( 1263 "vertical-tabs-newtab-button" 1264 ); 1265 1266 for (let parent of [newTab, newTab2, newTabVertical]) { 1267 if (!parent) { 1268 continue; 1269 } 1270 1271 parent.removeAttribute("type"); 1272 if (parent.menupopup) { 1273 parent.menupopup.remove(); 1274 } 1275 1276 if (containersEnabled) { 1277 parent.setAttribute("context", "new-tab-button-popup"); 1278 1279 let popup = document 1280 .getElementById("new-tab-button-popup") 1281 .cloneNode(true); 1282 popup.removeAttribute("id"); 1283 popup.className = "new-tab-popup"; 1284 popup.setAttribute("position", "after_end"); 1285 popup.addEventListener("popupshowing", CreateContainerTabMenu); 1286 parent.prepend(popup); 1287 parent.setAttribute("type", "menu"); 1288 // Update tooltip text 1289 DynamicShortcutTooltip.nodeToTooltipMap[parent.id] = 1290 newTabLeftClickOpensContainersMenu 1291 ? "newTabAlwaysContainer.tooltip" 1292 : "newTabContainer.tooltip"; 1293 } else { 1294 DynamicShortcutTooltip.nodeToTooltipMap[parent.id] = 1295 "newTabButton.tooltip"; 1296 parent.removeAttribute("context", "new-tab-button-popup"); 1297 } 1298 // evict from tooltip cache 1299 DynamicShortcutTooltip.cache.delete(parent.id); 1300 1301 // If containers and press-hold container menu are both used, 1302 // add to gClickAndHoldListenersOnElement; otherwise, remove. 1303 if (containersEnabled && !newTabLeftClickOpensContainersMenu) { 1304 gClickAndHoldListenersOnElement.add(parent); 1305 } else { 1306 gClickAndHoldListenersOnElement.remove(parent); 1307 } 1308 } 1309 1310 break; 1311 } 1312 } 1313 } 1314 1315 _updateCloseButtons() { 1316 if (this.overflowing) { 1317 // Tabs are at their minimum widths. 1318 this.setAttribute("closebuttons", "activetab"); 1319 return; 1320 } 1321 1322 if (this._closeButtonsUpdatePending) { 1323 return; 1324 } 1325 this._closeButtonsUpdatePending = true; 1326 1327 // Wait until after the next paint to get current layout data from 1328 // getBoundsWithoutFlushing. 1329 window.requestAnimationFrame(() => { 1330 window.requestAnimationFrame(() => { 1331 this._closeButtonsUpdatePending = false; 1332 1333 // The scrollbox may have started overflowing since we checked 1334 // overflow earlier, so check again. 1335 if (this.overflowing) { 1336 this.setAttribute("closebuttons", "activetab"); 1337 return; 1338 } 1339 1340 // Check if tab widths are below the threshold where we want to 1341 // remove close buttons from background tabs so that people don't 1342 // accidentally close tabs by selecting them. 1343 let rect = ele => { 1344 return window.windowUtils.getBoundsWithoutFlushing(ele); 1345 }; 1346 let tab = this.visibleTabs[gBrowser.pinnedTabCount]; 1347 if (tab && rect(tab).width <= this._tabClipWidth) { 1348 this.setAttribute("closebuttons", "activetab"); 1349 } else { 1350 this.removeAttribute("closebuttons"); 1351 } 1352 }); 1353 }); 1354 } 1355 1356 /** 1357 * @param {boolean} [aInstant] 1358 */ 1359 _handleTabSelect(aInstant) { 1360 let selectedTab = this.selectedItem; 1361 this.#ensureTabIsVisible(selectedTab, aInstant); 1362 1363 selectedTab._notselectedsinceload = false; 1364 } 1365 1366 /** 1367 * @param {MozTabbrowserTab} tab 1368 * @param {boolean} [shouldScrollInstantly=false] 1369 */ 1370 #ensureTabIsVisible(tab, shouldScrollInstantly = false) { 1371 let arrowScrollbox = tab.closest("arrowscrollbox"); 1372 if (arrowScrollbox?.overflowing) { 1373 arrowScrollbox.ensureElementIsVisible(tab, shouldScrollInstantly); 1374 } 1375 } 1376 1377 /** 1378 * Try to keep the active tab's close button under the mouse cursor 1379 */ 1380 _lockTabSizing(aClosingTab, aTabWidth) { 1381 if (this.verticalMode) { 1382 return; 1383 } 1384 1385 let tabs = this.visibleTabs; 1386 let numPinned = gBrowser.pinnedTabCount; 1387 1388 if (tabs.length <= numPinned) { 1389 // There are no unpinned tabs left. 1390 return; 1391 } 1392 1393 let isEndTab = aClosingTab && aClosingTab._tPos > tabs.at(-1)._tPos; 1394 1395 if (!this._tabDefaultMaxWidth) { 1396 this._tabDefaultMaxWidth = parseFloat( 1397 window.getComputedStyle(tabs[numPinned]).maxWidth 1398 ); 1399 } 1400 this._lastTabClosedByMouse = true; 1401 this._scrollButtonWidth = window.windowUtils.getBoundsWithoutFlushing( 1402 this.arrowScrollbox._scrollButtonDown 1403 ).width; 1404 if (aTabWidth === undefined) { 1405 aTabWidth = window.windowUtils.getBoundsWithoutFlushing( 1406 tabs[numPinned] 1407 ).width; 1408 } 1409 1410 if (this.overflowing) { 1411 // Don't need to do anything if we're in overflow mode and aren't scrolled 1412 // all the way to the right, or if we're closing the last tab. 1413 if (isEndTab || !this.arrowScrollbox.hasAttribute("scrolledtoend")) { 1414 return; 1415 } 1416 // If the tab has an owner that will become the active tab, the owner will 1417 // be to the left of it, so we actually want the left tab to slide over. 1418 // This can't be done as easily in non-overflow mode, so we don't bother. 1419 if (aClosingTab?.owner) { 1420 return; 1421 } 1422 this._expandSpacerBy(aTabWidth); 1423 } /* non-overflow mode */ else { 1424 if (isEndTab && !this._hasTabTempMaxWidth) { 1425 // Locking is neither in effect nor needed, so let tabs expand normally. 1426 return; 1427 } 1428 // Force tabs to stay the same width, unless we're closing the last tab, 1429 // which case we need to let them expand just enough so that the overall 1430 // tabbar width is the same. 1431 if (isEndTab) { 1432 let numNormalTabs = tabs.length - numPinned; 1433 aTabWidth = (aTabWidth * (numNormalTabs + 1)) / numNormalTabs; 1434 if (aTabWidth > this._tabDefaultMaxWidth) { 1435 aTabWidth = this._tabDefaultMaxWidth; 1436 } 1437 } 1438 aTabWidth += "px"; 1439 let tabsToReset = []; 1440 for (let i = numPinned; i < tabs.length; i++) { 1441 let tab = tabs[i]; 1442 tab.style.setProperty("max-width", aTabWidth, "important"); 1443 if (!isEndTab) { 1444 // keep tabs the same width 1445 tab.animationsEnabled = false; 1446 tabsToReset.push(tab); 1447 } 1448 } 1449 1450 if (tabsToReset.length) { 1451 window 1452 .promiseDocumentFlushed(() => {}) 1453 .then(() => { 1454 window.requestAnimationFrame(() => { 1455 for (let tab of tabsToReset) { 1456 tab.animationsEnabled = true; 1457 } 1458 }); 1459 }); 1460 } 1461 1462 this._hasTabTempMaxWidth = true; 1463 gBrowser.addEventListener("mousemove", this); 1464 window.addEventListener("mouseout", this); 1465 } 1466 } 1467 1468 _expandSpacerBy(pixels) { 1469 let spacer = this._closingTabsSpacer; 1470 spacer.style.width = parseFloat(spacer.style.width) + pixels + "px"; 1471 this.toggleAttribute("using-closing-tabs-spacer", true); 1472 gBrowser.addEventListener("mousemove", this); 1473 window.addEventListener("mouseout", this); 1474 } 1475 1476 _unlockTabSizing() { 1477 gBrowser.removeEventListener("mousemove", this); 1478 window.removeEventListener("mouseout", this); 1479 1480 if (this._hasTabTempMaxWidth) { 1481 this._hasTabTempMaxWidth = false; 1482 // Only visible tabs have their sizes locked, but those visible tabs 1483 // could become invisible before being unlocked (e.g. by being inside 1484 // of a collapsing tab group), so it's better to reset all tabs. 1485 let tabs = this.allTabs; 1486 for (let i = 0; i < tabs.length; i++) { 1487 tabs[i].style.maxWidth = ""; 1488 } 1489 } 1490 1491 if (this.hasAttribute("using-closing-tabs-spacer")) { 1492 this.removeAttribute("using-closing-tabs-spacer"); 1493 this._closingTabsSpacer.style.width = 0; 1494 } 1495 } 1496 1497 uiDensityChanged() { 1498 this._updateCloseButtons(); 1499 this.#updateTabMinHeight(); 1500 this._handleTabSelect(true); 1501 } 1502 1503 _notifyBackgroundTab(aTab) { 1504 if (aTab.pinned || !aTab.visible || !this.overflowing) { 1505 return; 1506 } 1507 1508 this._lastTabToScrollIntoView = aTab; 1509 if (!this._backgroundTabScrollPromise) { 1510 this._backgroundTabScrollPromise = window 1511 .promiseDocumentFlushed(() => { 1512 let lastTabRect = 1513 this._lastTabToScrollIntoView.getBoundingClientRect(); 1514 let selectedTab = this.selectedItem; 1515 if (selectedTab.pinned) { 1516 selectedTab = null; 1517 } else { 1518 selectedTab = selectedTab.getBoundingClientRect(); 1519 selectedTab = { 1520 left: selectedTab.left, 1521 right: selectedTab.right, 1522 top: selectedTab.top, 1523 bottom: selectedTab.bottom, 1524 }; 1525 } 1526 return [ 1527 this._lastTabToScrollIntoView, 1528 this.arrowScrollbox.scrollClientRect, 1529 lastTabRect, 1530 selectedTab, 1531 ]; 1532 }) 1533 .then(([tabToScrollIntoView, scrollRect, tabRect, selectedRect]) => { 1534 // First off, remove the promise so we can re-enter if necessary. 1535 delete this._backgroundTabScrollPromise; 1536 // Then, if the layout info isn't for the last-scrolled-to-tab, re-run 1537 // the code above to get layout info for *that* tab, and don't do 1538 // anything here, as we really just want to run this for the last-opened tab. 1539 if (this._lastTabToScrollIntoView != tabToScrollIntoView) { 1540 this._notifyBackgroundTab(this._lastTabToScrollIntoView); 1541 return; 1542 } 1543 delete this._lastTabToScrollIntoView; 1544 // Is the new tab already completely visible? 1545 if ( 1546 this.verticalMode 1547 ? scrollRect.top <= tabRect.top && 1548 tabRect.bottom <= scrollRect.bottom 1549 : scrollRect.left <= tabRect.left && 1550 tabRect.right <= scrollRect.right 1551 ) { 1552 return; 1553 } 1554 1555 if (this.arrowScrollbox.smoothScroll) { 1556 // Can we make both the new tab and the selected tab completely visible? 1557 if ( 1558 !selectedRect || 1559 (this.verticalMode 1560 ? Math.max( 1561 tabRect.bottom - selectedRect.top, 1562 selectedRect.bottom - tabRect.top 1563 ) <= scrollRect.height 1564 : Math.max( 1565 tabRect.right - selectedRect.left, 1566 selectedRect.right - tabRect.left 1567 ) <= scrollRect.width) 1568 ) { 1569 this.#ensureTabIsVisible(tabToScrollIntoView); 1570 return; 1571 } 1572 1573 let scrollPixels; 1574 if (this.verticalMode) { 1575 scrollPixels = tabRect.top - selectedRect.top; 1576 } else if (this.#rtlMode) { 1577 scrollPixels = selectedRect.right - scrollRect.right; 1578 } else { 1579 scrollPixels = selectedRect.left - scrollRect.left; 1580 } 1581 this.arrowScrollbox.scrollByPixels(scrollPixels); 1582 } 1583 1584 if (!this._animateElement.hasAttribute("highlight")) { 1585 this._animateElement.toggleAttribute("highlight", true); 1586 setTimeout( 1587 function (ele) { 1588 ele.removeAttribute("highlight"); 1589 }, 1590 150, 1591 this._animateElement 1592 ); 1593 } 1594 }); 1595 } 1596 } 1597 1598 _handleNewTab(tab) { 1599 if (tab.container != this) { 1600 return; 1601 } 1602 tab._fullyOpen = true; 1603 gBrowser.tabAnimationsInProgress--; 1604 1605 this._updateCloseButtons(); 1606 1607 if (tab.hasAttribute("selected")) { 1608 this._handleTabSelect(); 1609 } else if (!tab.hasAttribute("skipbackgroundnotify")) { 1610 this._notifyBackgroundTab(tab); 1611 } 1612 1613 // If this browser isn't lazy (indicating it's probably created by 1614 // session restore), preload the next about:newtab if we don't 1615 // already have a preloaded browser. 1616 if (tab.linkedPanel) { 1617 NewTabPagePreloading.maybeCreatePreloadedBrowser(window); 1618 } 1619 1620 if (UserInteraction.running("browser.tabs.opening", window)) { 1621 UserInteraction.finish("browser.tabs.opening", window); 1622 } 1623 } 1624 1625 _canAdvanceToTab(aTab) { 1626 return !aTab.closing; 1627 } 1628 1629 /** 1630 * Returns the panel associated with a tab if it has a connected browser 1631 * and/or it is the selected tab. 1632 * For background lazy browsers, this will return null. 1633 */ 1634 getRelatedElement(aTab) { 1635 if (!aTab) { 1636 return null; 1637 } 1638 1639 // Cannot access gBrowser before it's initialized. 1640 if (!gBrowser._initialized) { 1641 return this.tabbox.tabpanels.firstElementChild; 1642 } 1643 1644 // If the tab's browser is lazy, we need to `_insertBrowser` in order 1645 // to have a linkedPanel. This will also serve to bind the browser 1646 // and make it ready to use. We only do this if the tab is selected 1647 // because otherwise, callers might end up unintentionally binding the 1648 // browser for lazy background tabs. 1649 if (!aTab.linkedPanel) { 1650 if (!aTab.selected) { 1651 return null; 1652 } 1653 gBrowser._insertBrowser(aTab); 1654 } 1655 return document.getElementById(aTab.linkedPanel); 1656 } 1657 1658 _updateNewTabVisibility() { 1659 // Helper functions to help deal with customize mode wrapping some items 1660 let wrap = n => 1661 n.parentNode.localName == "toolbarpaletteitem" ? n.parentNode : n; 1662 let unwrap = n => 1663 n && n.localName == "toolbarpaletteitem" ? n.firstElementChild : n; 1664 1665 // Starting from the tabs element, find the next sibling that: 1666 // - isn't hidden; and 1667 // - isn't the all-tabs button. 1668 // If it's the new tab button, consider the new tab button adjacent to the tabs. 1669 // If the new tab button is marked as adjacent and the tabstrip doesn't 1670 // overflow, we'll display the 'new tab' button inline in the tabstrip. 1671 // In all other cases, the separate new tab button is displayed in its 1672 // customized location. 1673 let sib = this; 1674 do { 1675 sib = unwrap(wrap(sib).nextElementSibling); 1676 } while (sib && (sib.hidden || sib.id == "alltabs-button")); 1677 1678 this.toggleAttribute( 1679 "hasadjacentnewtabbutton", 1680 sib && sib.id == "new-tab-button" 1681 ); 1682 } 1683 1684 onWidgetAfterDOMChange(aNode, aNextNode, aContainer) { 1685 if ( 1686 aContainer.ownerDocument == document && 1687 aContainer.id == "TabsToolbar-customization-target" 1688 ) { 1689 this._updateNewTabVisibility(); 1690 } 1691 } 1692 1693 onAreaNodeRegistered(aArea, aContainer) { 1694 if (aContainer.ownerDocument == document && aArea == "TabsToolbar") { 1695 this._updateNewTabVisibility(); 1696 } 1697 } 1698 1699 onAreaReset(aArea, aContainer) { 1700 this.onAreaNodeRegistered(aArea, aContainer); 1701 } 1702 1703 _hiddenSoundPlayingStatusChanged(tab, opts) { 1704 let closed = opts && opts.closed; 1705 if (!closed && tab.soundPlaying && !tab.visible) { 1706 this._hiddenSoundPlayingTabs.add(tab); 1707 this.toggleAttribute("hiddensoundplaying", true); 1708 } else { 1709 this._hiddenSoundPlayingTabs.delete(tab); 1710 if (this._hiddenSoundPlayingTabs.size == 0) { 1711 this.removeAttribute("hiddensoundplaying"); 1712 } 1713 } 1714 } 1715 1716 destroy() { 1717 if (this.boundObserve) { 1718 Services.prefs.removeObserver("privacy.userContext", this.boundObserve); 1719 Services.prefs.removeObserver( 1720 "browser.tabs.dragDrop.multiselectStacking", 1721 this.boundObserve 1722 ); 1723 } 1724 CustomizableUI.removeListener(this); 1725 } 1726 1727 updateTabSoundLabel(tab) { 1728 // Add aria-label for inline audio button 1729 const [unmute, mute, unblock] = 1730 gBrowser.tabLocalization.formatMessagesSync([ 1731 "tabbrowser-unmute-tab-audio-aria-label", 1732 "tabbrowser-mute-tab-audio-aria-label", 1733 "tabbrowser-unblock-tab-audio-aria-label", 1734 ]); 1735 if (tab.audioButton) { 1736 if (tab.hasAttribute("muted") || tab.hasAttribute("soundplaying")) { 1737 let ariaLabel; 1738 tab.linkedBrowser.audioMuted 1739 ? (ariaLabel = unmute.attributes[0].value) 1740 : (ariaLabel = mute.attributes[0].value); 1741 tab.audioButton.setAttribute("aria-label", ariaLabel); 1742 } else if (tab.hasAttribute("activemedia-blocked")) { 1743 tab.audioButton.setAttribute( 1744 "aria-label", 1745 unblock.attributes[0].value 1746 ); 1747 } 1748 } 1749 } 1750 } 1751 1752 customElements.define("tabbrowser-tabs", MozTabbrowserTabs, { 1753 extends: "tabs", 1754 }); 1755 }